Visualisatie baansprint wedstrijd

Vorig jaar rond deze tijd vond er op het Velodrome van Amsterdam een wedstrijd plaats die ik mede georganiseerd had: de Summer Track Meeting. Voor deze sprintwedstrijd had ik een heel nieuw wedstrijdconcept bedacht die niet eerder op de baan toegepast was. Het idee was eigenlijk heel simpel en vast al eerder bij andere sporten gebruikt. De wedstrijd bestond uit een kwalificatie en een aantal sprintronden. In elke sprintronde rijden de renners wedstrijden één tegen één (tenzij er een oneven aantal deelnemers is, dan rijdt er één groep van 3 renners). Na de kwalificatie en na elke sprintronde wordt een nieuwe ranking opgemaakt. De ranking wordt opgemaakt op basis van de uitslag van de sprintronde met de volgende regels:

  • Als de lager geklasseerde wint, dan neem deze de positie van de hoger geklasseerde in en schuift de hoger geklasseerde (en de deelnemers tussen de twee sprinters) één positie naar beneden.
  • Als de hoger geklasseerde wint behouden beide hun positie.

Welke renners tegen elkaar rijden in een heat werd bepaald op basis van vooraf bepaalde schema’s en de positie in de ranking, bijvoorbeeld positie 1 tegen 2, 3 tegen 4, enz. Of 1-3, 2-4, 5-7 enz. of 1-4, 2-5, 3-6 enz.

Het doel van dit nieuwe wedstrijdconcept is om meer dynamiek in de positie van de renners in de ranking te brengen en om renners meer gelegenheid te geven om in de ranking te stijgen (of te dalen, wat ze liever niet willen).

Om inzicht te krijgen of dat ook echt het geval is geweest heb ik op basis van de rankings die na elke sprintronde gemaakt zijn de onderstaand visualisatie gemaakt:
Visualisatie Uitslagen

Je krijgt zo een goed beeld van de bewegelijkheid van de renners in het toernooi. Elke keer als een lijn omhoog loopt heeft een lager geklasseerde renner gewonnen van een hoger geklasseerde renner. Je ziet dus dat tussen ronde 1 en 2 en ronde 3 en 4 er niet veel verschuivingen plaatsgevonden hebben. Je ziet ook dat er wat groepen zijn die bij elkaar in de buurt blijven zitten.

Welke renners tegen elkaar gereden hebben is te zien in de onderstaande visualisatie:

Pouleindeling

 

 

 

 

 

 

 

 

 

 

Een combinatie van de bovenstaande twee zien er als volgt uit. Persoonlijk vind ik deze visualisatie wat druk worden.

Wedstrijdbeweging incl poules

 

 

 

 

 

 

 

 

 

Een visualisatie om veel informatie op weinig ruimte te tonen is middels zogeheten sparklines. Om een compleet overzicht van de wedstrijd te hebben, heb ik sparklines gebruikt om inzicht te krijgen in de volgende zaken:

  • Positie: was de renner de hoger geklasseerde (donker blauw) of lager geklasseerde (licht blauw) was in de poule. Dit moet eerlijk verdeeld zijn (2 om 3, 3 om 2)
  • Resultaat: heeft de renner gewonnen (groen) of verloren (rood).
  • Prestatie: de lager geklasseerde renner wint (gouden ster) of de hoger geklasseerde renner verliest (rode cirkel).

Wedstrijdoverzicht

 

 

 

 

 

 

 

 

 

 

 

Al met al was het een bijzonder geslaagde en succesvolle wedstrijd. Het toegepaste systeem werkte en heeft dynamiek gebracht in de wedstrijd. De visualisaties heb ik achteraf gemaakt maar ik denk dat deze ook zeker waarde hebben tijdens de wedstrijd als informatievoorziening richting de renners en begeleiders.

Animatie van vergrijzing in Nederland

De vergrijzing in Nederland is altijd een onderwerp van gesprek in de politiek, zeker als het gaat om de kosten van de zorg. De term vergrijzing is een aspect van een verandering in de bevolkingssamenstelling. Deze term wordt gebruikt om aan te geven dat het aandeel van ouderen in de bevolking stijgt en daardoor een stijging van de gemiddelde leeftijd veroorzaakt. De vergrijzing wordt zichtbaar in de vorm van de zogeheten bevolkingspiramide. Hierin wordt het aantal personen per leeftijdsgroep en geslacht zichtbaar gemaakt.

Op basis van gegevens die beschikbaar zijn bij het CBS heb ik voor de jaren 1988 tot en met 2013 een bevolkingspiramide gemaakt. De leeftijden zijn samengevoegd in buckets van 5 jaar om het aantal staven in de grafiek te beperken.
Nadat ik de 25 grafieken gemaakt had merkte ik dat ik er steeds volgordelijk doorheen klikte om de beweging van de staven te kunnen zien. Dit gaf mij veel meer inzicht dat de plaatjes op zich. Daarom heb ik de onderstaande animatie gemaakt om de vergrijzing zichtbaar te maken.

Animatie

Het wordt in de animatie duidelijk dat het ‘brede’ deel van de bevolking, dat deel waar ten opzicht van de andere leeftijdscategorieën de meeste mensen in zitten, langzaam naar boven verplaatst. Dit is de (toekomstige) vergrijzing. Het deel eronder is stabiel, maar wel lager in aantal dan er boven.

1988-2013

Als we 1988 en 2013 naast elkaar zetten dan wordt het verschil dat over de 25 tussenliggende jaren is ontstaan nog duidelijker. Niet alleen is zichtbaar dat de leeftijdsgroep van 15 tot en met 45 jaar precies 25 jaar verschoven is, ook de oudere leeftijdscategorieën (70+) bevatten in 2013 meer mensen dan in 1988. In de jongere leeftijdscategorieën lijkt een kleine afname zichtbaar.

(Om de mannen en vrouwen in de grafiek naast elkaar te kunnen tonen zijn de aantallen van vrouwen negatieve waarden. In het echt zijn dit uiteraard de overeenkomende positieve waarden)

Verdeling van rijkdom in Nederland

Enige tijd geleden kwam ik het volgende filmpje tegen op youtube: https://www.youtube.com/watch?v=QPKKQnijnsM

Het filmpje is een movie infographic waarin verteld wordt hoe ongelijk het vermogen in Amerika verdeeld. Het komt er op neer dat ruim 80% van het vermogen in handen in van 20% van de mensen. Bekijk het filmpje vooral voor het hele verhaal en om de onderstaande uitwerking te begrijpen.

Het verbaasde me niets dat het vermogen zo ongelijk verdeeld is in Amerika. Hoe vaak hoor je niet verhalen over belachelijk hoge salarissen en bonussen. Of de beurswaarde van de grote organisaties als Apple, Google of Facebook. En de armoede in de grotere steden van de States. Logisch dat het vermogen zo oneerlijk verdeeld is. Maar ik dacht ook: hoe zou dat in Nederland zijn?

Om daar achter te komen ben ik op zoek gegaan naar Nederlandse gegevens om de grafieken uit het filmpje te kunnen reproduceren. Bij het CBS stellen ze gegevens over de samenstelling van het vermogen beschikbaar, wel op minder gedetailleerd niveau dan het filmpje toont. Maar gedetailleerd genoeg om de volgende presentatie te kunnen maken.

Verdeling vermogen 1

Ik was verbaasd toen ik het zag. De grafiek wijkt nauwelijks af van die van het filmpje. In Nederland is ruim 80% van het vermogen in handen van 20% van de mensen. Ook de armste 20% is net als in het filmpje nauwelijks zichtbaar (er staat nog een donkerblauwe lijn naast het lichtblauwe vlak).

Ook de verdeling van het vermogen als staafdiagram lijkt erg op die van het filmpje. De echte extremen zoals in het filmpje ontbreken vanwege het gebrek aan detail. Als de CBS gegevens meer details zouden bevatten dan wordt de rijkste één procent vast ook wat beter zichtbaar alhoewel  de echte extremen (mensen als Bill Gates, Warren Buffet, Mark Zuckerberg) voorbehouden zijn aan Amerika.

Verdeling vermogen 2

Enige tijd geleden is een artikel over de verdeling van rijkdom in Nederland  gepubliceerd: Verdeling welvaart steeds oneerlijker: “De vijf rijkste families van Nederland hebben bijna evenveel vermogen als de 3,2 miljoen armste Nederlanders samen.”

 

Not my piece of the pie

Degene die mij een beetje kennen weten dat ik geen liefhebber ben van taartdiagrammen. Het is een visualisatie die moeilijk te lezen is en daarom weinig meerwaarde biedt. Gisteren werd tijdens de uitzending van de NOS een prachtig voorbeeld hiervan getoond:
CAaQ1o2UQAASzpD.jpg large

In dit voorbeeld heb je voldoende aan de tabel en is de taartdiagram volledig overbodig. Probeer maar eens het volgende: Dek met je hand het linker deel van de visualisatie af (de tabel) en probeer informatie te vergaren. Dek vervolgens met je hand het rechter deel af (de taartdiagram) en probeer informatie te vergaren.

Het grote probleem met een taartdiagram is dat de verhoudingen van de verschillende taartpunten moeilijk te onderscheiden is. Dit wordt alleen maar lastiger wanneer het ook nog een 3D weergave is omdat je dan moet compenseren voor een kanteling (die overigens minimaal is in dit geval, maar er wel is). Over smaak valt niet te twisten en in dit geval is de gekozen kleurstelling zeker niet mijn smaak. Er is in ieder geval geen rekening gehouden met de kleurenblinden onder ons.

Maar wat is een taartdiagram eigenlijk? Een taartdiagram is eigenlijk gewoon een gestapelde staafdiagram die rond gebogen is (een circulair coördinaat systeem).
gestapeldestaaf
Op zichzelf staand levert deze vorm niet veel meerwaarde ten opzichte van een taartdiagram, dezelfde bezwaren blijven bestaan. Pas als je waarden over meerdere perioden gaat vergelijken ontstaat er inzicht omdat er een beweging van de gegevens zichtbaar wordt (zie onderstaand voorbeeld met wat extra verzonnen gegevens).
gestapeldestaaf2
(Dit is overigens ook gelijk de enige goed werkende variant van de gestapelde staaf diagram: met categorische variabelen en genormaliseerd tot 100%)

Als je de huidige stand van zaken wilt tonen (niet meerdere perioden) en de verhouding tussen de verschillende categorieën duidelijk wilt maken dan kun je mijn inziens het beste een staafdiagram nemen. Door deze te kantelen en te sorteren (niet doen als de variabelen zelf een onderlinge volgorde hebben!) wordt direct duidelijk welke categorie het beste scoort. Door labels toe te voegen kan de as weggelaten worden omdat deze informatie overbodig is.
rank

Analyse van publicatie criminaliteitscijfers

Vandaag viel mijn oog op een tweet van de VVD. In deze tweet werd en grafieken getoond van dalende criminaliteitscijfers in de afgelopen jaren. De reacties op de tweet waren dat de gegevens nogal suggestief gepresenteerd werden. Het gaat om de volgende tweet:

Deze reacties komen voort uit één van de basisprincipes bij het maken van grafieken: Begin de assen indien mogelijk bij het nulpunt. Als je dit namelijk niet doet loop je het risico dat de volatiliteit van de gegevens uitvergroot wordt. Dit principe is echter geen wet, maar een uitgangspunt waar zorgvuldig mee omgegaan moet worden. Heeft de VVD in zijn tweet nu terecht of onterecht afgeweken van dit principe? Hadden ze een alternatief kunnen gebruiken?

Hieronder staat de grafiek nagemaakt zoals in de tweet getoond.

vvd tweet 1

Eerst maar eens duidelijk maken hoe je de perceptie van de gegevens kunt veranderen door het principe wel of niet toe te passen. Hieronder zijn de gebruikte gegevens nogmaals getoond, maar nu startend bij het nulpunt. Het wordt duidelijk dat de daling waar in het bericht naar verwezen wordt een stuk minder zichtbaar is.

vvd tweet 2

Overigens is de daling ook met een grafiek startend in het nulpunt te versterken door de x-as te verkleinen. Zo zie je dat je als maker van een grafiek met hele simpele technieken al invloed uit kunt oefenen op de perceptie van de gegevens.

vvd tweet 3

 

 

 

 

 

 

 

 

Terug naar de grafiek in de tweet. Wat netjes gedaan is, is dat de schaal van elke afzonderlijk grafiek gelijk is aan de ander, de maatstreepjes staan om de 500 eenheden. Op deze wijze is het mogelijk om de grafieken ook ten opzichte van elkaar te beschouwen. Het effect van het niet starten in het nulpunt valt nu ook deels weg, omdat de volatiliteit van de gegevens tussen de grafieken hierdoor gelijk blijft.

Als de maker van deze grafiek de boel echt had willen belazeren dan had hij er wel zoiets van gemaakt (geen nul-punt, ongelijke schaal):

vvd tweet 4

Gezien het beperkte effect van niet bij nul starten en het gelijk houden van de schaal van de y-as representeert de grafiek die in de tweet opgenomen is naar mijn mening nog voldoende de werkelijkheid en is er alleen voor de puristen die vasthouden aan het principe iets aan te merken op de grafiek. Dat gezegd hebbende is er voor een politieke partij natuurlijk alles aan gelegen om te voorkomen dat ze beschuldigd worden van een suggestieve voorstelling van zaken. Gelukkig zijn verschillende alternatieven te bedenken om dezelfde boodschap over te brengen. Zo is het mogelijk om niet de absolute getallen van de relatieve getallen te laten zien, met andere woorden de procentuele verandering van de gegevens tussen periodes. De absolute aantallen zijn hier niet meer zichtbaar, maar omdat het doel is de afname te laten zien is dat niet erg.

vvd tweet 5

Iets moelijker te begrijpen maar een goede manier om verschillende gegevens ten opzichte van elkaar te kunnen beschouwen is via index cijfers. Met een index wordt de verandering van gegevens berekend ten aanzien van een vooraf gekozen startpunt. In dit geval is het startpunt voor de drie metingen gelijk aan elkaar, te weten 2010, maar dit hoeft niet. Als je bijvoorbeeld wilt analyseren wat het effect is van de koers van het Apple aandeel na de introductie van verschillende gadgets (iPhone, iPad), dan is het startpunt per gadget de introductiedatum en wordt de verandering berekend over gelijke intervallen na dit startpunt. Een ander voorbeeld is het meten van de verandering van verkoopcijfers in relatie tot de start van reclamecampagnes gedurende het jaar. In het onderstaande voorbeeld zijn de gegevens omgezet naar een index ten opzichte van de startwaarde in 2010.

vvd tweet 6

Het nadeel van een indexcijfer is dat het een lastig te begrijpen getal is (een genormaliseerde afwijking van een startpunt). Het is wel een goede manier om gegevens in relatie tot elkaar te brengen. Een cumulatieve procentuele afwijking ten opzichte van het startpunt levert dezelfde grafiek op, maar dan met het startpunt op nul in plaats van 100.

Ik hoop dat ik heb laten zien dat met de keuze voor een visualisatie invloed uitgeoefend kan worden op de perceptie van de kijker, dat het een bewuste keuze moet zijn om een principe wel of net toe te passen en dat er verschillende alternatieve zijn om dezelfde boodschap over te brengen.

Wat mij meer verbaast is dat er geen opmerkingen zijn gemaakt over het onjuiste gebruik van de lijngrafiek. Er mag een lijn tussen twee meetpunten getrokken worden als de meetwaarde tussen de punten grofweg deze lijn gevolgd heeft. In dit geval is er elk jaar opnieuw gestart met tellen, bij nul, en is het dus niet juist om de meetpunten met een lijn te verbinden. In dit geval is het beter gebruik te maken van staafdiagrammen.

Alternative for wattage graph

Last week Guido Vroemen tweeted the following graph about the wattages needed to break the UCI Hour record. As you might have noticed I am a bit of a cycling enthusiast.

I see several problems with this visualization. First of all it is difficult to read. The differences in wattages are very small so it is very hard to see the actual values on the y-axis. It is also not clear which cyclist is which color. You really need to carefully look at the colors and legend to find out. And I am not a fan of this kind of color scheme, it is too messy for me.

So, I made an alternative. It is just a few minutes of work to copy the data and then you can play around with different visuals. The purpose of the graph is not to show a trend or an order but to show which cyclist had what wattage. To make it easier to see this I changed the orientation. This way the viewer can ‘read’ the graph from left to right. I added labels to show the values. I could have removes the axis above because it is redundant to the labels but in this case I left it in. The bars are still in to be able to compare the wattages, different colors are no longer necessary, nor is a legend. I changed the color of Thomas Dekker because his value is a prediction. The final result with a very graceful retweet and thumbs up from Guido:

Vermogenopzeeniveau

 

 

 

 

 

 

 

Also had a bit of a laugh with this prediction:

time_needed

Visualizing UCI Hourrecord attempt

Last year the UCI (international cycling union) changed the rules for the World Hourrecord. This change sparked a lot of interest from great riders in the cycling community. The goal of the hourrecord is to ride the farthest distance in one hour. This is usually done on a Velodrome, a wooden track of (normally) 250m.
Since the rule change there have been several attempts at the records, including that of Jack Bobridge. After his attempt I got a hold of his laptimes and I thought I could make a nice visualization with it.

The dataset contains the lapnumber, lapname and laptime. Knowing the track length (250m) I could then add variables like average laptime and average speed. Knowing the current records (51852m) I could add other interesting variables like the average laptime for the record and required laptimes. This last one is particularity interesting, more on that later.

hourrecord <- 51852
lapdistance <- 250

allhourdata <- allhourdata %>% mutate(Actual.laptimes.cummulative = cumsum(Actual.laptimes)) %>%
        mutate(Average.actual.laptimes = Actual.laptimes.cummulative/Id) %>%
        mutate(Target.laptimes=(60*60/(hourrecord/lapdistance))) %>%
        mutate(Target.laptimes.cummulative = cumsum(Target.laptimes)) %>%
        mutate(Difference = Actual.laptimes - Target.laptimes) %>%
        mutate(Difference.cummulative = cumsum(Difference)) %>%
        mutate(Projected.distance = (Id*lapdistance)/Actual.laptimes.cummulative*3.6) %>%
        mutate(Required.laptimes.for.record =
                       ((60*60)-Actual.laptimes.cummulative)/((hourrecord-(lapdistance*Id))/lapdistance)) %>%
        mutate(Required.speed = 250/Required.laptimes.for.record*3.6) %>%
        mutate(laptime.to.slow = ifelse(Actual.laptimes < Required.laptimes.for.record, 0, Actual.laptimes))

I started of designing a static visualization but soon realized an animation would far better suit the dataset and the activity the rider performed. I needed to make a loop to create a plot for each lap. I used two variables so I could generate just a few plots when needed.

begin_ani <- 1
end_ani <- nrow(allhourdata)

for (i in begin_ani:end_ani){
    ## Code below comes here
}

The first part of the loop was to select the data to create one plot for that specific lap. I wanted to show the laptimes he had done up until that lap and show the status of the recordattempt in that lap. I wanted to show the following metrics:

  • Points for each laptime
  • A line representing the current record
  • A line representing the current average laptime/speed
  • A line representing the laptimes/speed needed for the remaining laps to beat the record

To do that I filtered the dataset to get the data up until that lap (for the points) and selected the variables for that lap and create new data frames to display the lines. The color coding I added to be able to show if he was on track to beat the record( green) or not (red).

var <- allhourdata[i,]
hourdata <- allhourdata %>% filter(Id <= i)

avg_color <- ifelse(var[,10]<hourrecord/1000, "#800000", "#006600")
hline_avg <- data.frame(Id=1:206, avg=as.numeric(var[,5]))
hline_req <- data.frame(Id=1:206, req=as.numeric(var[,11]))
hline_rec <- data.frame(Id=1:206, rec=as.numeric(var[,6]))

Now the interesting part, creating the plot. I’m a big fan of ggplot2 because of the countless possibilities to customize. I did borrow some of the layout from examples on the internet.

Step 1: Setting up the plot with basic themes, colors and margins.

gg <- ggplot()

gg <- gg + theme_bw() +
        theme(panel.background=element_rect(fill="#F0F0F0")) +
        theme(plot.background=element_rect(fill="#F0F0F0")) +
        theme(panel.border=element_rect(colour="#F0F0F0")) +
        theme(panel.grid.major=element_line(colour="#D0D0D0",size=.5)) 

gg <- gg + ggtitle(paste("Hourrecord attempt Jack Bobridge: ", as.character(var[,2]))) +
        theme(plot.title=element_text(face="bold",hjust=-.08,vjust=2,colour="#3C3C3C",size=20)) +
        theme(legend.position="none") +
        ylab("Lap time") + xlab("Lap") +
        theme(axis.text.x=element_text(size=11,colour="#535353",face="bold")) +
        theme(axis.text.y=element_text(size=11,colour="#535353",face="bold")) +
        theme(axis.title.y=element_text(size=11,colour="#535353",face="bold",vjust=1.5)) +
        theme(axis.title.x=element_text(size=11,colour="#535353",face="bold",vjust=-.5))

gg <- gg + theme(plot.margin = unit(c(1.5, 1.5, 1, 1), "cm"))

Step 2: Adding the laptimes. I also added an annotation with the laptime.

## Laptimes
gg <- gg + geom_point(data=hourdata, aes(x=Id, y=Actual.laptimes), colour="#000099", size=3) +
        annotate("text",x=as.numeric(var[,1])+2,y=as.numeric(var[,3]),label=round(as.numeric(var[,3]),digits=3),colour="#000099", hjust = 0)

Step 3: Adding the lines. Each lines is coded separately.

## Required Laptimes
gg <- gg + geom_line(data=hline_req, aes(x=Id, y=req), colour=avg_color, size=1, alpha=1/2) +
        annotate("text",x=206+2,y=as.numeric(var[,11]),label=round(var[,12],digits=3),colour=avg_color, hjust = 0, alpha=1/2) +
        annotate("text",x=0,y=as.numeric(var[,11]),label=round(var[,11],digits=3),colour=avg_color, hjust = 1, alpha=1/2) +
        annotate("text",x=206,y=as.numeric(var[,11])+0.075,label="Required for record",colour=avg_color, hjust = 1, alpha=1/2)

## Current Record line
gg <- gg + geom_line(data=hline_rec, aes(x=Id, y=rec),colour="#909090", size=1, linetype=2) +
        annotate("text",x=206+2,y=as.numeric(var[,6]),label="51.852",colour="#909090", hjust = 0) +
        annotate("text",x=0,y=as.numeric(var[,6]),label=round(var[,6],digits=3),colour="#909090", hjust = 1)

## Average speed/laptime line
gg <- gg + geom_line(data=hline_avg, aes(x=Id, y=avg),colour=avg_color, size=1, alpha=1/2) +
        annotate("text",x=206+2,y=as.numeric(var[,5]),label=round(var[,10],digits=3),colour=avg_color, hjust = 0, alpha=1/2) +
        annotate("text",x=0,y=as.numeric(var[,5]),label=round(var[,5],digits=3),colour=avg_color, hjust = 1, alpha=1/2) +
        annotate("text",x=1,y=as.numeric(var[,5])+0.075,label="Average",colour=avg_color, hjust = 0, alpha=1/2)

Step 4: Changing the scale of the plot. In other words, zooming in on the range where most of the laptimes are. I’ve zoomed in to -5 laps to create some room for the laptimes.

gg <- gg + scale_x_continuous(minor_breaks=0,breaks=seq(0,200,50),limits=c(-5,216)) +
        scale_y_continuous(minor_breaks=0,breaks=seq(16, 18.5, 0.5)) + #,limits=c(16, 18.5)) +
        coord_cartesian(ylim=c(15.9, 18.6)) + ## zoom only, no cutoff data
        theme(axis.ticks=element_blank()) 

Step 5: Show and save the laptimes.

print(gg)
ggsave(file=paste("pr",i,".jpg", sep=""))

Step 6: Here you could add code to create the animated GIF. I tried it but ran out of memory on my PC, so I did it by hand.

files = sprintf('pr%d.png', begin_ani:end_ani)
im.convert(files, output = 'hourrecord.gif')

The result is the animated gif below. I’m pleased with the result (I ‘m sure the code can be better).
The general opinion after the record attempt was that it was a close call. But in the animation you can see that it all lost at 3/4 of the hour. The important variable in this is the required laptime, that is what he needs to ride to make it to the record. He does get to that laptime from around 100 laps. He did a great effort and was probably seriously hurting and that is what people saw and probably what made them think he was still able to make it until a few minutes before the end. But the data shows that was not the case.

Update 9-2-2015: Yesterday another attempt was made by Rohan Dennis. He made it and set a new World Record wth a distance of 52.491 meters.

You can find the dataset and code on GitHub.

Multivariate regression in R

I found a very nice article on the internet that explained how to develop a multivariate regression model. In the article a dataset for several hundred 2005 used General Motors (GM) cars is supplied which is the basis for the step by step explanation. I think this is a good way to learn about regression models. I thought it was a good idea to try to reproduce the result, intercepts, slopes and plots in the article using R.

This (my) article will just show how to reproduce the results in the for-mentioned article using R. I have also added two other ways to get the best fit using the power of R.

The article uses 4 equations to develop the model into the best fit.

  • Equation 1: Based on Mileage alone
  • Equation 2: Based on Mileage, Cylinder, Doors, Cruise, Sound, Leather
  • Equation 3: Based on Mileage, Buick, Cadillac, Chev, Pontiac, SAAB predicting the log of price
  • Equation 4: Based on Mileage, Liter, Doors, Cruise, Sound, Leather, Buick, Cadillac, Chev, Pontiac, SAAB, Conv, Hatchback, Sedan predicting the log of price

Before starting modelling the regressions with R first we need to read the data and setup the required libraries.

library("tidyr")
library("dplyr")

kuiper <- read.csv("kuiper.csv", sep=";", dec=",")

Equation 1

The first equation is simple and just to explore:

model <- lm(Price ~ Mileage, data=kuiper)

Result:

## (Intercept) 24764.5590
## Mileage        -0.1725
## ---
## Multiple R-squared:  0.0205, Adjusted R-squared:  0.0192 

To create the plot of the model use this code:

plot(model$residuals, col="blue", type="l")
points(model$residuals, col="red", type="p", pch=20)
abline(h=0)

unnamed-chunk-4-1

Equation 2
Still a quite simple model, just with more predictors.

model_acc <- lm(Price ~ Mileage + Cylinder +
                Doors + Cruise + Sound + Leather, data=kuiper)
## (Intercept)  7323.1643
## Mileage        -0.1705
## Cylinder     3200.1246
## Doors       -1463.3991
## Cruise       6205.5113
## Sound       -2024.4007
## Leather      3327.1433
## ---
## Multiple R-squared:  0.446,  Adjusted R-squared:  0.442 

If we plot the model we get 4 plots which explain different aspects of the model.

par(mfrow=c(2,2))
plot(model_acc)
par(mfrow=c(1,1))

unnamed-chunk-8-1

Fot the histogram and distribution of the residuals we need different plots.

hist(model_acc$residuals, breaks=30)

plot(model_acc$residuals, col="blue", type="l")
points(model_acc$residuals, col="red", type="p", pch=20)
abline(h=0)

unnamed-chunk-9-1

unnamed-chunk-10-1

Equation 3

For this equation we first need to alter the dataset to add the factor variable Make as boolean variables and take the log of price.

kuiper$id <- 1:nrow(kuiper)
make <- as.data.frame(table(kuiper$id, kuiper$Make))
make <- make %>% spread(Var2, Freq) %>% rename(id = Var1)
kuiper <- merge(kuiper, make, by="id")
kuiper <- select(kuiper, -Make)

kuiper$tPrice <- log10(kuiper$Price)
model_make <- lm(tPrice ~ Mileage + Liter +
             Buick + Cadillac + Chevrolet +
             Pontiac + SAAB, data=kuiper)
## (Intercept)  3.979911512
## Mileage     -0.000003476
## Liter        0.099724553
## Buick        0.039968616
## Cadillac     0.249302587
## Chevrolet   -0.009372280
## Pontiac      0.013613268
## SAAB         0.345305253
## ---
## Multiple R-squared:  0.917,  Adjusted R-squared:  0.916 

Same plot as before.

par(mfrow=c(2,2))
plot(model_make)
par(mfrow=c(1,1))

unnamed-chunk-13-1

Histogram and residuals.

hist(model_make$residuals, breaks=30)
plot(model_make$residuals, col="blue", type="l")
points(model_make$residuals, col="red", type="p", pch=20)
abline(h=0)

unnamed-chunk-14-1

unnamed-chunk-15-1

Equation 4
For equation 4 the dataset needs to be altered for the Type variable.

type <- as.data.frame(table(kuiper$id, kuiper$Type))
type <- type %>% spread(Var2, Freq) %>% rename(id = Var1)
kuiper <- merge(kuiper, type, by="id")
kuiper <- select(kuiper, -Type)
model_all <- lm(tPrice ~ Mileage + Liter + Doors +
                Cruise + Sound + Leather + Buick +
                Cadillac + Chevrolet + Pontiac + SAAB +
                Convertible + Hatchback + Sedan, data=kuiper)
## (Intercept)  3.91810880
## Mileage     -0.00000358
## Liter        0.09576214
## Doors        0.03352658
## Cruise       0.00751699
## Sound        0.00522264
## Leather      0.00625998
## Buick        0.04165259
## Cadillac     0.23303437
## Chevrolet   -0.01331520
## Pontiac     -0.00042065
## SAAB         0.28109769
## Convertible  0.13781907
## Hatchback   -0.08898857
## Sedan       -0.07114898
## ---
## Multiple R-squared:  0.952,  Adjusted R-squared:  0.951 

Again the same plots.

par(mfrow=c(2,2))
plot(model_all)
par(mfrow=c(1,1))

hist(model_all$residuals, breaks=30)
plot(model_all$residuals, col="blue", type="l")
points(model_all$residuals, col="red", type="p", pch=20)
abline(h=0)

unnamed-chunk-18-1

unnamed-chunk-19-1

unnamed-chunk-20-1

Let R find the best model

R has the Step function. You can supply two models and R steps through the model to get to the best fit. I’ve removed the variables I don’t want to use for predicting.

kuiper <- kuiper %>% select(-Price, -id, -Model, -Trim)

model <- lm(tPrice ~ Mileage, data=kuiper)
model_all <- lm(tPrice ~ ., data=kuiper)
bestfit <- step(model, scope=list(lower=model, upper=model_all), direction="forward")

Result:

## (Intercept)  4.006511863
## Mileage     -0.000003579
## Liter        0.109135503
## SAAB         0.276731601
## Cadillac     0.241903489
## Convertible  0.142481573
## Wagon        0.069803273
## Buick        0.041279545
## Hatchback   -0.013685317
## Chevrolet   -0.014330497
## Cylinder    -0.012359839
## Cruise       0.008355646
## Leather      0.005336807
## Sound        0.004812547
## ---
## Multiple R-squared:  0.952,  Adjusted R-squared:  0.952 

This model is slightly different and produces almost the same R-squared.

Machine learning

Using the Caret package we can use machine learning to predict the best model on the dataset.

library("caret")

ml_model <- train(tPrice ~ ., data=kuiper, method="lm")
## (Intercept)  3.93932861
## Mileage     -0.00000358
## Cylinder    -0.01224029
## Liter        0.10880574
## Doors        0.03377855
## Cruise       0.00808055
## Sound        0.00462693
## Leather      0.00517194
## Buick        0.04375913
## Cadillac     0.24448881
## Chevrolet   -0.01264695
## Pontiac      0.00261046
## SAAB         0.27901919
## Saturn               NA
## Convertible  0.14078278
## Coupe                NA
## Hatchback   -0.08269889
## Sedan       -0.06948186
## Wagon                NA
## ---
## Multiple R-squared:  0.952,  Adjusted R-squared:  0.951 

Remarks

I would like to add that normally you would not use the complete dataset to fit a model for prediction. You would want to split the dataset into a training and testset to be able to cross validate your model to prevent over fitting of the model.

Predictive Analytics with R

I did a project for a data scientist training on Coursera. Using machine learning techniques in R our goal was to predict which exercise subjects did using sensor data.

Using devices such as Jawbone UpNike FuelBand, and Fitbit it is now possible to collect a large amount of data about personal activity relatively inexpensively. These type of devices are part of the quantified self movement – a group of enthusiasts who take measurements about themselves regularly to improve their health, to find patterns in their behavior, or because they are tech geeks. One thing that people regularly do is quantify how much of a particular activity they do, but they rarely quantify how well they do it. In this project, your goal will be to use data from accelerometers on the belt, forearm, arm, and dumbell of 6 participants. They were asked to perform barbell lifts correctly and incorrectly in 5 different ways:

  • exactly according to the specification (Class A)
  • throwing the elbows to the front (Class B)
  • lifting the dumbbell only halfway (Class C)
  • lowering the dumbbell only halfway (Class D)
  • throwing the hips to the front (Class E)

After creating a model the goal was to predict how 20 subjects performed the barbell lift. If you use the predict function R gives you (in this case) 20 answers (A to E). (bestfit is the model, pml.submission is the dataset with 20 subjects)

final_predictions <- predict(bestfit, pml.submission)
final_predictions
##  [1] B A B A A E D B A A B C B A E E A B B B
## Levels: A B C D E

But what R does is give you the answer with the highest probability. Because it calculates a probability for each class for every subject. If you add type=”prob” to your predict call you get the probability for each class. I’ve used ggplot2 to visualize the probability distribution for the classes. It clearly shows that the model prediction varies over the subjects.

predprob <- predict(bestfit, pml.submission, type = "prob")
predprob$testcase <- 1:nrow(predprob)
predprob <- gather(predprob, "class", "prob", 1:5)
ggplot(predprob, aes(testcase, class)) +
        geom_tile(aes(fill = prob), colour = "white") +
        geom_text(aes(fill = prob, label = round(prob, 2)), size=3, colour="grey25") +
        scale_fill_gradient(low = "white", high = "red") +
        scale_x_discrete(expand = c(0, 0)) +
        scale_y_discrete(expand = c(0, 0))


It clearly shows that the model prediction varies over the subjects. For a full report of my project see GitHub.

Ledengroei visualisatie – een analyse

Enige tijd geleden las ik een artikel in het blad Fietssport van de Nederlandse Toerfiets Unie (NTFU) waarin zij trots vertelde over de groei die hun ledenaantal de afgelopen jaren door had gemaakt. Om dit verhaal te ondersteunen was de onderstaande grafiek aan het artikel toegevoegd.

blad fiets

Voor het gemak heb ik hem even nagemaakt in Excel. Ik heb de waarden geschat en in een tabel gezet. Nadat ik vervolgens een lijngrafiek ervan maakte kwam exact de grafiek tevoorschijn die in het magazine stond.

Origineel

In eerste oogopslag is er nogal wat mis met deze grafiek wat hem lastig te lezen maakt:

  • De legenda is omgekeerd ten opzichte van de lijnen in de grafiek
  • De verticale as begint niet bij 0 (later meer hierover)
  • Er staat 1 t/m 12 als labels bij de horizontale as

En dit zijn puur esthetische problemen. Welk bericht wil de schrijver eigenlijk overbrengen. 35% stijging staat erbij vermeld, maar hoe zie ik dat terug in de grafiek? Deze grafiek ondersteund zijn verhaal wat mij betreft niet echt, of de schrijver maakt er op z’n minst een zoekplaatje van.

Dus tijd om wat verbeteringen aan te brengen aan de visualisatie. Eerste de esthetische problemen aanpakken.

  • Excel biedt beperkte mogelijkheden om de legenda aan te passen en ik vond geen geschikte plaats rondom de grafiek die direct duidelijk maakte welke lijn overeenkomt met welk jaar. In plaats daarvan heb ik daarom de het jaartal als label bij de lijn gezet.
  • Het is goed gebruik om de assen bij 0 te laten beginnen, maar in dit geval vind ik dat het wegneemt van de leesbaarheid als je dat doet, de lijnen komen in dat geval veel te dicht bij elkaar te staan. We starten de as dus gewoon op 35000.
  • Een simpele verbetering die de leesbaarheid vergroot is niet de nummers van de maanden te tonen, maar de namen of afkortingen.
  • De datapunten heb ik veranderd zodat deze boven de maatstreepjes staan. Dit is een heel subtiel verschil maar dit komt omdat ik er vanuit ga dat de waarde die getoond wordt de eindstand van de maand is (en daarmee de beginstand van de volgende maand). Als je de waarde tussen de maatstreepjes zet dan wordt de suggestie gewekt dat het om een gemiddelde waarde gaat, wat ik onwaarschijnlijk beschouw.
  • De kleuren kun je ook nog afstemmen op een eventuele styleguide, maar deze laat ik nu zo.

Origineel verbeterd

Bovenstaande aanpassingen maken de grafiek al wat leesbaarder. Maar ondersteund het het verhaal van de schrijver? Natuurlijk is te zien dat over de jaren heen het aantal leden toeneemt, maar is dat wel duidelijk zichtbaar op deze manier? Stel dat we een visualisatie kiezen die gebruikelijk is bij het presenteren van de koers van een aandeel?

Als koers

Leuk, anders, maar nog niet een goede ondersteuning van het verhaal wat mij betreft. Een volgende stap om de leesbaarheid te vergroten is het weglaten van details. Waarom zouden we aantallen per maand laten zien als we geïnteresseerd zijn in de groei die in een aantal jaren heeft plaatsgevonden? Begrijp me niet verkeerd, de details bevatten interessante inzichten, maar niet om de groei te laten zien. Vandaar de volgende presentatie:

Minder detail

 

 

 

 

 

 

Met dezelfde informatie (de ledenaantallen) zijn nog andere metrics te maken, bijvoorbeeld de absolute en procentuele groei per jaar. Hierin wordt zichtbaar dat er een gestage groei van de groei is.

toename procentueel

 

 

 

 

 

 

Met deze informatie is er nog een ander soort visualisatie te maken om de groei van het ledenaantal weer te geven. We plussen en minnen het aantal leden ten opzichte van een startaantal.

Gestapeld

 

 

 

 

 

 

 

 

Als laatste de volgende visualisatie. De details van de maanden die we net weggelaten hebben tonen wel een mooi patroon wat elk jaar terug komt en wat zichtbaar wordt als we de gemiddelde groei per maand visualiseren.

ledengroei patroon

Ik heb geprobeerd te laten zien dat er diverse mogelijkheden zijn met de cijfers in de grafiek. De keuze voor een visualisatie en het detail aan informatie wat getoond moet worden is heel erg afhankelijk van het verhaal dat je probeert te vertellen.

(overigens heb ik de geclaimde groei van 35% niet kunnen vinden in de onderliggende gegevens…)