Let's get back to the age-value relationship from my last post. I did some more plotting to see on which position this inversed U-shaped relationship is strongest. Please note, that I use a dataframe called eu.players throughout this post, which holds downloaded football player information from transfermarkt.de.

But first, let us get back to the original graph.
(click to enlarge)

As you can see, very young players are not worth a lot of money, then the quadratic function peaks at 26 years (I limited the y-axis because very worthy players would cause the plot to be unreadable). Then it falls off again. The dashed line in the plot is regression line from a simple linear regression model I introduced in my last post.

Now, let us have a look at the age distributions for the different positions (no player market values are included in these plots, yet).

library(RColorBrewer)
library(plotrix)
hist.list <- list(eu.players[eu.players$pos2 == "Goal", "age"],
  eu.players[eu.players$pos2 == "Def", "age"],
  eu.players[eu.players$pos2 == "Midf", "age"],
  eu.players[eu.players$pos2 == "Forw", "age"])
multhist(hist.list, beside = F, freq = F,
  col = brewer.pal(4, "Paired"), border = "#00000000",
  main = "Age by Position")
legend(x = "topright",
  legend = c("Goal", "Defence", "Midfield", "Forward"),
  fill = brewer.pal(4, "Paired"),
  box.col = "#00000000", border = "#00000000")



(click to enlarge)

We can visualize this distribution in many other different ways. Let us use density plots, now.

plot(density(eu.players[eu.players$pos2 == "Goal", "age"]),
  ylim = c(0,0.09), col = brewer.pal(4, "Paired")[1],
  lwd = 5, main = "Age density by position",
  xlab = "Age", bty = "n")
lines(density(eu.players[eu.players$pos2 == "Def", "age"]),
  col = brewer.pal(4, "Paired")[2], lwd = 5)
lines(density(eu.players[eu.players$pos2 == "Midf", "age"]),
  col = brewer.pal(4, "Paired")[3], lwd = 5)
lines(density(eu.players[eu.players$pos2 == "Forw", "age"]),
  col = brewer.pal(4, "Paired")[4], lwd = 5)
legend(x = "topright",
  legend = c("Goal", "Defence", "Midfield", "Forward"),
  col = brewer.pal(4, "Paired"),
  box.col = "#00000000", border = "#00000000",
  lty = "solid", lwd = 5)


(click to enlarge)

And finally some box plots.

boxplot(age ~ pos2, data = eu.players,
  col = brewer.pal(4, "Paired"), boxcol = "#00000000",
  notch = T, pch = 4, ylab = "Age", xlab = "Position",
  names = c("Goal", "Defence", "Midfield", "Forward"),
  main = "Age by Position")

(click to enlarge)

Each of these plot types visualizes the same information. We can see that goalies are a little older overall and the age distribution is much "flatter" for goalies. You can see this quite clearly in the stacked histogram and the density graphs. Just as a side note: The outlier (marked by a cross) for defence players is Javier Zanetti (39 years old), playing for Inter Mailand. The two outliers for midfielders are Ryan Giggs (age 39) and Paul Scholes (age 38), both playing - of course - for Manchester United.

With the following boxplot, we'll have a look at the different age distributions in the five major european championships.

boxplot(age ~ league, data = eu.players,
  col = brewer.pal(4, "Paired"),
  boxcol = "#00000000", notch = T, pch = 4,
  ylab = "Age", xlab = "Championship",
  names = c("DE", "FR", "UK", "ES", "IT"),
  bty = "n", main = "Age by Championship")


(click to enlarge)

Obviously, the german Bundesliga has the youngest players. Only one player is older than 36 years - and counts as an outlier: Oka Nikolov, goalkeeper with Eintracht Frankfurt. He wouldn't count as an outlier in the Premier League. Taken the whole Premier League together, Giggs and Scholes are not outliers anymore, but Brad Friedel, goalie with the Spurs, is.

So, we learned that age distributions are neither equal across positions nor championships. But what about the age / value relationship mentioned above?

What we'll do: We will plot lowess lines (see ?lowess for details) which minimize deviations in a specific span of data, allowing for changing fits as x-values increase. Let's first compare age-value-relationships for different positions.

First, we extract the data for every position and get rid of NAs.

goal <- eu.players[eu.players$pos2 == "Goal" &
  !is.na(eu.players$val.mill) &
  !is.na(eu.players$age),]
def <- eu.players[eu.players$pos2 == "Def" &
  !is.na(eu.players$val.mill) &
  !is.na(eu.players$age),]
midf <- eu.players[eu.players$pos2 == "Midf" &
  !is.na(eu.players$val.mill) &
  !is.na(eu.players$age),]
forw <- eu.players[eu.players$pos2 == "Forw" &
  !is.na(eu.players$val.mill) &
  !is.na(eu.players$age),]

Now, we plot the lowess lines for each position sub-dataset, all in the same plotting window.

plot(goal$age, goal$val.mill, bty = "n",
  type = "n"ylim = c(0,3.5), xlim = c(16, 42),
  main = "Age by Value smoothers, divided by Position",
  xlab = "Age", ylab = "Value")
lines(lowess(goal$age, goal$val.mill), lwd = 4,
  col = brewer.pal(4, "Paired")[1])
lines(lowess(def$age, def$val.mill), lwd = 4,
  col = brewer.pal(4, "Paired")[2])
lines(lowess(midf$age, midf$val.mill), lwd = 4,
  col = brewer.pal(4, "Paired")[3])
lines(lowess(forw$age, forw$val.mill), lwd = 4,
  col = brewer.pal(4, "Paired")[4])
legend(x = "topright",
  legend = c("Goal", "Defence", "Midfield", "Forward"),
  col = brewer.pal(4, "Paired"),
  box.col = "#00000000", border = "#00000000",
  lty = "solid", lwd = 5)

(click to enlarge)

Several things can be derived from the graph:
  • The sharpness of the inversed U-curve is very pronounced for forwards and midfielder, slightly less pronounced for defenders and least pronounced for goalies.
  • Goalies "peak" later. For every field player, the peak ist aorund 26 years. Even defenders don't have to be more experienced to be more valuable. Goalies, however, peak at around 30 years and are more valuable than field players in later years (from 35 to 40).
To see if the age-value relationships differ between the championships around Europe, I also divided the dataset in subdatasets for each championship. Here is the code for the respective plot:
plot(buli$age, buli$val.mill, bty = "n",
  type = "n", ylim = c(0,5.5), xlim = c(16, 42),
  main = "Age by Value smoothers, divided by Championship",
  xlab = "Age", ylab = "Value")
lines(lowess(buli$age, buli$val.mill), lwd = 4,
  col = brewer.pal(5, palette)[1])
lines(lowess(ligue1$age, ligue1$val.mill), lwd = 4,
  col = brewer.pal(5, palette)[2])
lines(lowess(preml$age, preml$val.mill), lwd = 4,
  col = brewer.pal(5, palette)[3])
lines(lowess(primd$age, primd$val.mill), lwd = 4,
  col = brewer.pal(5, palette)[4])
lines(lowess(serA$age, serA$val.mill), lwd = 4,
  col = brewer.pal(5, palette)[5])
legend(x = "topright",
  legend = c("Bundesliga", "Ligue 1",
    "Premier League", "Primera DivisiĆ³n", "Serie A"),
  col = brewer.pal(5, palette), box.col = "#00000000",
  border = "#00000000", lty = "solid", lwd = 5)
(click to enlarge)

As for the different positions, there are quite distinct relationships for the different championships - especially regarding the Premier League which peaks much higher. Presumably, this is only the case because the mean value of players is also highest in the Premier League, meaning that the curve has more space to peak. In the Serie A (Italy), the peak is slightly moved to the right, in the Bundesliga slightly to the left. These championships seem to have differing preferences in terms of the age of their players. In France (Ligue 1), the relationship is least pronounced. However, this does not mean that older players are more valuable (as we saw for goalies).

Well, enough football for now. I'll see if I come back to this dataset some other time... also, feel free to propose football-related analyses...


0

Add a comment

Hi all, this is just an announcement.

I am moving Rcrastinate to a blogdown-based solution and am therefore leaving blogger.com. If you're interested in the new setup and how you could do the same yourself, please check out the all shiny and new Rcrastinate over at

http://rcrastinate.rbind.io/

In my first post over there, I am giving a short summary on how I started the whole thing. I hope that the new Rcrastinate is also integrated into R-bloggers soon.

Thanks for being here, see you over there.

Alright, seems like this is developing into a blog where I am increasingly investigating my own music listening habits.

Recently, I've come across the analyzelastfm package by Sebastian Wolf. I used it to download my complete listening history from Last.FM for the last ten years. That's a complete dataset from 2009 to 2018 with exactly 65,356 "scrobbles" (which is the word Last.FM uses to describe one instance of a playback of a song).
3

Giddy up, giddy it up

Wanna move into a fool's gold room

With my pulse on the animal jewels

Of the rules that you choose to use to get loose

With the luminous moves

Bored of these limits, let me get, let me get it like

Wow!

When it comes to surreal lyrics and videos, I'm always thinking of Beck. Above, I cited the beginning of the song "Wow" from his latest album "Colors" which has received rather mixed reviews. In this post, I want to show you what I have done with Spotify's API.

Click here for the interactive visualization

If you're interested in the visualisation of networks or graphs, you might've heard of the great package "visNetwork". I think it's a really great package and I love playing around with it. The scenarios of graph-based analyses are many and diverse: whenever you can describe your data in terms of "outgoing" and "receiving" entities, a graph-based analysis and/or visualisation is possible.
12

Here is some updated R code from my previous post. It doesn't throw any warnings when importing tracks with and without heart rate information. Also, it is easier to distinguish types of tracks now (e.g., when you want to plot runs and rides separately). Another thing I changed: You get very basic information on the track when you click on it (currently the name of the track and the total length).

Have fun and leave a comment if you have any questions.
3

So, Strava's heatmap made quite a stir the last few weeks. I decided to give it a try myself. I wanted to create some kind of "personal heatmap" of my runs, using Strava's API. Also, combining the data with Leaflet maps allows us to make use of the beautiful map tiles supported by Leaflet and to zoom and move the maps around - with the runs on it, of course.

So, let's get started. First, you will need an access token for Strava's API.

I've been using the ggplot2 package a lot recently. When creating a legend or tick marks on the axes, ggplot2 uses the levels of a character or factor vector. Most of the time, I am working with coded variables that use some abbreviation of the "true" meaning (e.g. "f" for female and "m" for male or single characters for some single character for a location: "S" for Stuttgart and "M" for Mannheim).

In my plots, I don't want these codes but the full name of the level.

It's been a while since I had the opportunity to post something on music. Let's get back to that.

I got my hands on some song lyrics by a range of artists. (I have an R script to download all lyrics for a given artist from a lyrics website.
4

Lately, I got the chance to play around with Shiny and Leaflet a lot - and it is really fun! So I decided to catch up on an old post of mine and build a Shiny application where you can upload your own GPX files and plot them directly in the browser.

Of course, you will need some GPX file to try it out. You can get an example file here (you gonna need to save it in a .gpx file with a text editor, though). Also, the Shiny application will always plot the first track saved in a GPX file.
9
Loading