tiistai 5. heinäkuuta 2016

Party poopers - Predicting political affiliations

A few weeks ago I made a post in the Finnish version of the blog utilising Finnish National Broadcasting Company's "voting aid application" data. It consists of a variety of questions on political issues candidates were asked to answer, mixed with background data. In the linked article, I employed various clustering methods to group the candidates to more "natural" political parties based on their ideologies as manifested by their answers to the questionnaire.

To borrow terminology from machine learning community, clustering is an example of so-called "unsupervised learning", a case where data is unlabeled and the result is purely based on patterns in data. In constrast, this post concerns itself with classification which is a prime example of "supervised learning" where we know the kind of problem we wish the algorithm to answer beforehand (ie., there is an output/dependent variable we are specially interested in and hope to explain using input/indepentent variables)

To be more exact, I will train a support vector machine (SVM) model and attempt to predict the party affiliation of a given respondent based on his/her policy statements in the questionnaire. Support vector machines are a staple and an age-old favourite of data science community and one of the most used algorithms for classification tasks. I will not delve into details of the algorithm, not least because my personal understanding is hardly crystal clear. But the bread and butter of the approach is to find planes in a vector space that best separate data points from each other. To try to illustrate it in more simple terms, we can imagine a classic 2-dimensional space to which we draw a line separating two groups of observations from each other. The line is drawn as such that the data points belonging to one label are optimally above the line while other group is below. This is then generalized to multidimensional spaces and hyperplanes. Here is a good article delving a bit deeper into the theory so I won't make too much of a fool of myself.

If we assume that one of the main criticisms leveled at modern party politics to be correct, namely that their ideological convergence makes them more or less indistinguishable from one another, predicting the correct party should not be too easy a task. If parties are ideologically alike then their candidates should show no greater between-groups than within-groups variance. And classifying the party of respondent correctly should be liable to many errors. Let's find out whether all parties are created equal!

For this post we will use library e1071 for the SVM algorithm and caret for a simple, straightforward method to partitioning data to test and train sets. And dplyr. Never leave home without dplyr! Dplyr's older brother plyr is also used to recode levels of factor variable.

First steps of any data analysis project are naturally loading up the data and making required data cleaning and transformation operations. The code below accomplishes some of the routine tasks: loading up the data stored in a csv-file, dealing with missing values, filtering out the rows with observations belonging to parties outside the parliament and picking only desired columns from the hundreds the dataset has to offer. Finally full party names are recoded to their abbreviations. (note, the data set does not exactly contain what the code here suggests, Finnish variable names and values have been translated to English for consistency in this example code.

df <- read.csv("C:/Users/User/Desktop/User/Data/electiondata.csv", header=TRUE,
                  stringsAsFactors=TRUE,sep = ";", na.strings = "",encoding="UTF-8")


x <- as.data.frame(table(df$party))
x <- arrange(x, desc(Freq))
x <- x[c(1:7,11),]
z <- as.character(x$Var1)
 

df <- filter(df, party %in%  z )
df <- df[c(1:64,40:259)] 
df <- df[, -grep("comment",colnames(df))]
df[is.na(df)] <- "pass the question"
 df$party <- revalue(df$party, c("Kansallinen Kokoomus"="KOK","Perussuomalaiset"="PS","Suomen Keskusta"="KESK","Suomen Kristillisdemokraatit (KD)"="KD",
                            "Suomen ruotsalainen kansanpuolue"="RKP","Suomen Sosialidemokraattinen Puolue"="SDP","Vasemmistoliitto"="VAS",
                            "Vihreä liitto"="VIHR"))


Next thing on the to-do list is to split the data to test and train sets. Models should always be tested for accuracy outside their natural habitat, ie. the data they are trained with. This is to ensure that the model can cope with previously unseen data, rather than just imitating the quirks and unique patterns of it's training data not encountered elsewhere. For this task, createDataPartition-function of caret package is a most useful aide. For svm-function of the e1071 package I prefer to split dependent variable from independent variables to a vector of it's own but this is not strictly necessary. Independent factor variables are transformed to numeric ones. Missing values caused by coercing factors to numeric (concerns questions with yes/no-answers) are dealt with.

theSplit <- createDataPartition(df$party,times=1, p=0.20, list=FALSE)
train <- df[-theSplit,]
test <- df[theSplit,]
trainlabels <- as.character(train$party)
traindata <- train[c(2:46)]
traindata <- as.data.frame(lapply(traindata, function(x) as.numeric(x)))
traindata[is.na(traindata)] <- 3

testlabels <- as.character(test$party)
testdata <- test[c(2:46)]
testdata <- as.data.frame(lapply(testdata, function(x) as.numeric(x)))
testdata[is.na(testdata)] <- 3

With data cleaned and modified and test and training sets ready to fire, it's time to do the cool stuff! As is customary, the cool stuff is done only in couple simple lines of code. First training the model with training set, then using the model to predict values in the test set. Finally we check the results by merging predicted labels with actual labels in test set and creating a contingency table.

model <- svm(x=traindata, y=trainlabels, type = "C-classification")
preds <- predict(model, testdata)


results <- merge(testlabels, preds, by="row.names")
table(results$x, results$y)

And the following is the resulting contingency table (actual labels on rows, predicted on columns:

      KD KESK KOK PS RKP SDP VAS VIHR
  KD   24    4   3  3   1   1   1    2
  KESK  8   21   5  5   0   4   1    0
  KOK   2    5  27  3   0   2   4    0
  PS    4    2   2 30   0   2   3    0
  RKP   0    3   1  1  11   0   4    1
  SDP   0    6   2  2   0  27   7    0
  VAS   2    0   0  0   0   4  36    2
  VIHR  0    1   1  0   1   0   6   33

The SVM model obviously did pretty well. With each respective party, the vast majority of respondents were labeled correctly. And of the misses, a big portion went to parties ideologically close to correct answer. For example for the Green Party (VIHR), of the 9 misclassified observations 6 were classified as members of the Left Alliance (VAS). The odd one out is the mostly right-wing liberal Swedish People's Party (RKP) with 4 out 8 misclassification going under Left Alliance but the explanation propably lies in the respective parties' liberal take to moral questions. Also rather curiously 4 members of the right-wing National Coalition (KOK) were labeled Left Alliance members whereas none were assigned the label of the Green Party, once derided as the "National Coalition's garden department.

So based on this experiment, the political affiliation of a random questionnaire respondent can be predicted with reasonable accuracy. The news of parties' death have been greatly exaggerated.