Dear reader, before moving to the aggregation, you should know that I have upgraded my analytical tool set from JAGS 3.4.0 to JAGS 4.0.1. The good news is that the aggregation models run about twice as fast as they did previously. Also, I have fixed a couple of trivial coding glitches in the Dirichlet models which became evident with the new JAGS (updated code here).
The Morgan Poll continues its statistical hagiography of the Turnbull government. With preferences distributed on the basis of the previous election, this is the third 55 to 45 from Morgan. Plugging these numbers into my standard aggregation model we get an estimated two-party preferred voting intention for the Coalition of 53.3 per cent.
Just for the fun of it, I thought I would see what difference it would make if we treated the post Malcolm Turnbull polls as a new series from Morgan. The results follow.
In this last chart, it is interesting to note the change in systemic polling drift from the House of Morgan before and after the leadership of Malcolm Turnbull.
Turning to the Dirichlet model of primary votes, we have ...
And across all the models.
Showing posts with label JAGS. Show all posts
Showing posts with label JAGS. Show all posts
Monday, November 2, 2015
Monday, October 26, 2015
Model updates
I have spent a little time refreshing and updating the statistical models I use on this site. I have also updated the Bayesian Aggregation information page, with the current version of the JAGS model code for each of the four models I run.
What's changed:
The annotated TPP results follow. We will start with the sum-to-zero TPP model. This chart uses the TPP estimates from the polling houses. This is the simplest model, and the one I regularly report.
The next chart is the anchored TPP model. It should be noted that prior to the 2013 election, the pollsters would have made their TPP estimate based on preference flows in 2010. Since the 2013 election, the pollsters would have used preference flows at the 2013 election.
The next charts are derived from the primary vote estimates from the polling houses. The first chart is based on the 2013 preference flows. The second chart is based on the 2010 preference flows. There is a percentage point in the different preference flows.
The final chart is derived from the primary votes, anchored to the 2013 election result (using 2013 preference flows).
The four charts based on 2013 preference flows can be seen side-by-side as follows:
As an aside, the two anchored charts suggest that the 2013 change from Gillard to Rudd may have saved Labor up to 3.5 percentage points at the 2013 election. It would have saved quite a bit of the furniture.
What's changed:
- With all of the models, I have changed the way in which I code a discontinuity associated with a change of leadership.
- With the anchored models, I have added a discontinuity for the Gillard to Rudd change of leadership and for the Rudd to Shorten change.
- Finally, with the sum-to-zero, primary-vote model I have added a two-party preferred (TPP) estimate based on preference flows in 2010.
The annotated TPP results follow. We will start with the sum-to-zero TPP model. This chart uses the TPP estimates from the polling houses. This is the simplest model, and the one I regularly report.
The next chart is the anchored TPP model. It should be noted that prior to the 2013 election, the pollsters would have made their TPP estimate based on preference flows in 2010. Since the 2013 election, the pollsters would have used preference flows at the 2013 election.
The next charts are derived from the primary vote estimates from the polling houses. The first chart is based on the 2013 preference flows. The second chart is based on the 2010 preference flows. There is a percentage point in the different preference flows.
| Year | From the Greens (%) | From other Parties(%) |
|---|---|---|
| 2010 | 21.16 | 58.26 |
| 2013 | 16.97 | 53.30 |
The final chart is derived from the primary votes, anchored to the 2013 election result (using 2013 preference flows).
The four charts based on 2013 preference flows can be seen side-by-side as follows:
As an aside, the two anchored charts suggest that the 2013 change from Gillard to Rudd may have saved Labor up to 3.5 percentage points at the 2013 election. It would have saved quite a bit of the furniture.
Sunday, July 12, 2015
Should I junk JAGS? Is Stan the man?
There are a number of analytical tools that enable statisticians to solve Bayesian hierarchical models.
For some time I have been using JAGS, which uses Gibbs sampling in its MCMC algorithm.
Because it is so bloody cold outside, I thought I would give Stan a try. Stan uses Hamiltonian Monte Carlo sampling in its MCMC algorithm. I am using Stan version 2.6.3.0 with the interface to Stan from Python's pystan.
Proponents of Stan claim that it has replaced JAGS as the state-of-the-art, black-box MCMC method. Their criticism of JAGS is that Gibbs sampling fails to converge with high posterior correlation. They argue that CPU time and and memory usage under Stan scales much better with model complexity. As models become larger, JAGS chokes.
The first challenge I faced was getting pystan to work. In the end I deleted my entire python set-up, and reinstalled Anaconda 2.3.0 for OSX from Continuum Analytics. From there it was quick step at the shell prompt:
Using Stan required me to refactor the model a little. There was the obvious: Stan uses semi-colons to end statements. Some of the functions have been renamed (for example, JAGS' dnorm() becomes Stan's normal()). Some of the functions take different arguments (dnorm() is passed a precession value, while normal() is passed the standard deviation. Comments in Stan begin with a double-slash, whereas in JAGS they begin with a hash (although Stan will accept hashed comments as well). Stan required me to divide the model into a number of different code blocks.
A bigger challenge was how to code the sum-to-zero constraint I place on house effects. In JAGS, I encode it as follows:
In Stan, I needed a different approach using transformed parameters. But I did not need the for-loop, as Stan allows vector-arithmetic in its statements.
I also ran into all sorts of troubles with uniform distributions generating run-time errors. I am not sure whether this was a result of my poor model specification, something else, or something I should just ignore. In the end, I replaced the problematic uniform distributions with weakly informative (fairly dispersed) normal distributions. Anyway, after a few hours I had a Stan model for aggregating two-party-preferred poll results that was working.
The results for the hidden voting intention generated with Stan are quite similar to those with JAGS. In the next two charts, we have the Stan model first and then the JAGS model. The JAGS model was changed to mirror Stan as much as possible (i.e. I replaced the same uniform distributions in JAGS that I had replaced in Stan).
We also have similar results with the relative house effects. Again, the Stan result precedes JAGS in the following charts. But those of you with a close eye will note that these results differ slightly from the earlier results that came from the JAGS model which used a uniform distribution as a prior for house effects. There is a useful lesson here on the importance of model specification. Also it is far too easy to read too much into results that differ by a few tenths of a percentage point. The reversal of Newspoll and ReachTEL in these charts comes down to hundredths of a percentage point (which is bugger all).
So what does the Stan model look like?
The revised JAGS code is as follows. The code I have changed for this post is commented out.
As for the question: will I now junk JAGS and move to Stan? I will need to think about that a whole lot more before I make any changes. I love the progress feedback that Stan gives as it processes the samples. I think Stan might be marginally faster. But model specification in Stan is far more fiddly.
For some time I have been using JAGS, which uses Gibbs sampling in its MCMC algorithm.
Because it is so bloody cold outside, I thought I would give Stan a try. Stan uses Hamiltonian Monte Carlo sampling in its MCMC algorithm. I am using Stan version 2.6.3.0 with the interface to Stan from Python's pystan.
Proponents of Stan claim that it has replaced JAGS as the state-of-the-art, black-box MCMC method. Their criticism of JAGS is that Gibbs sampling fails to converge with high posterior correlation. They argue that CPU time and and memory usage under Stan scales much better with model complexity. As models become larger, JAGS chokes.
The first challenge I faced was getting pystan to work. In the end I deleted my entire python set-up, and reinstalled Anaconda 2.3.0 for OSX from Continuum Analytics. From there it was quick step at the shell prompt:
conda install pystan
Using Stan required me to refactor the model a little. There was the obvious: Stan uses semi-colons to end statements. Some of the functions have been renamed (for example, JAGS' dnorm() becomes Stan's normal()). Some of the functions take different arguments (dnorm() is passed a precession value, while normal() is passed the standard deviation. Comments in Stan begin with a double-slash, whereas in JAGS they begin with a hash (although Stan will accept hashed comments as well). Stan required me to divide the model into a number of different code blocks.
A bigger challenge was how to code the sum-to-zero constraint I place on house effects. In JAGS, I encode it as follows:
for (i in 2:n_houses) {
houseEffect[i] ~ dnorm(0, pow(0.1, -2))
}
houseEffect[1] <- -sum( houseEffect[2:n_houses] )
In Stan, I needed a different approach using transformed parameters. But I did not need the for-loop, as Stan allows vector-arithmetic in its statements.
pHouseEffects ~ normal(0, 0.1); // weakly informative parameter
houseEffect <- pHouseEffects - mean(pHouseEffects); // sum to zero transformed
I also ran into all sorts of troubles with uniform distributions generating run-time errors. I am not sure whether this was a result of my poor model specification, something else, or something I should just ignore. In the end, I replaced the problematic uniform distributions with weakly informative (fairly dispersed) normal distributions. Anyway, after a few hours I had a Stan model for aggregating two-party-preferred poll results that was working.
The results for the hidden voting intention generated with Stan are quite similar to those with JAGS. In the next two charts, we have the Stan model first and then the JAGS model. The JAGS model was changed to mirror Stan as much as possible (i.e. I replaced the same uniform distributions in JAGS that I had replaced in Stan).
We also have similar results with the relative house effects. Again, the Stan result precedes JAGS in the following charts. But those of you with a close eye will note that these results differ slightly from the earlier results that came from the JAGS model which used a uniform distribution as a prior for house effects. There is a useful lesson here on the importance of model specification. Also it is far too easy to read too much into results that differ by a few tenths of a percentage point. The reversal of Newspoll and ReachTEL in these charts comes down to hundredths of a percentage point (which is bugger all).
So what does the Stan model look like?
data {
// data size
int<lower=1> n_polls;
int<lower=1> n_span;
int<lower=1> n_houses;
// poll data
real<lower=0,upper=1> y[n_polls];
real<lower=0> sampleSigma[n_polls];
int<lower=1> house[n_polls];
int<lower=1> day[n_polls];
}
parameters {
real<lower=0,upper=1> hidden_voting_intention[n_span];
vector[n_houses] pHouseEffects;
real<lower=0,upper=0.01> sigma;
}
transformed parameters {
vector[n_houses] houseEffect;
houseEffect <- pHouseEffects - mean(pHouseEffects); // sum to zero
}
model{
// -- house effects model
pHouseEffects ~ normal(0, 0.1); // weakly informative
// -- temporal model
sigma ~ uniform(0, 0.01);
hidden_voting_intention[1] ~ normal(0.5, 0.1);
for(i in 2:n_span) {
hidden_voting_intention[i] ~ normal(hidden_voting_intention[i-1], sigma);
}
// -- observational model
for(poll in 1:n_polls) {
y[poll] ~ normal(houseEffect[house[poll]] + hidden_voting_intention[day[poll]], sampleSigma[poll]);
}
}
The revised JAGS code is as follows. The code I have changed for this post is commented out.
model {
## developed from Simon Jackman's hidden Markov model
## - note: poll results are analysed as a value between 0.0 and 1.0
## -- observational model
for(poll in 1:n_polls) { # for each observed poll result ...
yhat[poll] <- houseEffect[house[poll]] + hidden_voting_intention[day[poll]]
y[poll] ~ dnorm(yhat[poll], samplePrecision[poll]) # distribution
}
## -- temporal model
for(i in 2:n_span) { # for each day under analysis, except the first ...
# today's national TPP voting intention looks much like yesterday's
hidden_voting_intention[i] ~ dnorm(hidden_voting_intention[i-1], walkPrecision)
}
# day 1 estimate of TPP between 20% and 80% - a weakly informative prior
#hidden_voting_intention[1] ~ dunif(0.2, 0.8)
hidden_voting_intention[1] ~ dnorm(0.5, pow(0.1, -2))
# day-to-day change in TPP has a standard deviation between 0 and 1
# percentage points
sigmaWalk ~ dunif(0, 0.01)
walkPrecision <- pow(sigmaWalk, -2)
## -- house effects model
#for(i in 2:n_houses) { # for each polling house, except the first ...
# # assume house effect is somewhere in the range -15 to +15 percentage points.
# houseEffect[i] ~ dunif(-0.15, 0.15)
#}
for (i in 2:n_houses) {
houseEffect[i] ~ dnorm(0, pow(0.1, -2))
}
# sum to zero constraint applied to the first polling house ...
houseEffect[1] <- -sum( houseEffect[2:n_houses] )
}
As for the question: will I now junk JAGS and move to Stan? I will need to think about that a whole lot more before I make any changes. I love the progress feedback that Stan gives as it processes the samples. I think Stan might be marginally faster. But model specification in Stan is far more fiddly.
Update
I have now timed them. Stan is slower.
Labels:
aggregated polls,
JAGS,
Stan,
wonkish
Monday, June 8, 2015
Pollster preference flows
I was wondering whether the difference between my TPP aggregations from the TPP polling and the primary vote polling was an artifact of the preference flows that the pollsters were applying to the primary vote estimates they had derived.
This is a question for a simple multiple regression against the formula:
In English, the Coalition's two-party preferred vote-share estimate comprises the Coalition primary vote, plus a proportion of the Greens' primary vote ( α ), and a proportion of the other parties' primary vote ( β ). In this equation, α and β are both values between 0 and 1 (on the continuum of no flow of preferences through to 100% flow). I decided to solve this regression using a simple Bayesian model, as follows.
I undertook the analysis for each polling house, using their polling data since the last Federal election, with the following results.
On both charts, I have marked with a vertical gray line the preference flow I use in my models (0.1697 for the Greens and 0.533 for other parties). I used Antony Green's earlier work to set my preference flows within my models.
The gray line falls within the 95% credibility interval for each of the polling houses. Therefore, I cannot argue that any of the polling houses are using different preference flows from the one I am using. If the pollsters are using different preference flows, this test did not demonstrate that.
This is a question for a simple multiple regression against the formula:
TPP_estimate = coalition_pv +
α green_pv +
β other_pv
In English, the Coalition's two-party preferred vote-share estimate comprises the Coalition primary vote, plus a proportion of the Greens' primary vote ( α ), and a proportion of the other parties' primary vote ( β ). In this equation, α and β are both values between 0 and 1 (on the continuum of no flow of preferences through to 100% flow). I decided to solve this regression using a simple Bayesian model, as follows.
model {
## -- preference flows
for(poll in 1:NUMPOLLS) { # for each poll result - rows
yhat[poll] <- pv_coalition[poll] +
(alpha * pv_greens[poll]) +
(beta * pv_other[poll])
y[poll] ~ dnorm(yhat[poll], tau)
}
## priors
alpha ~ dunif(0.0, 1.0)
beta ~ dunif(0.0, 1.0)
sigma ~ dunif(0.001, 0.1)
tau <- pow(sigma, -2)
}
I undertook the analysis for each polling house, using their polling data since the last Federal election, with the following results.
On both charts, I have marked with a vertical gray line the preference flow I use in my models (0.1697 for the Greens and 0.533 for other parties). I used Antony Green's earlier work to set my preference flows within my models.
The gray line falls within the 95% credibility interval for each of the polling houses. Therefore, I cannot argue that any of the polling houses are using different preference flows from the one I am using. If the pollsters are using different preference flows, this test did not demonstrate that.
Update
I have re-ran this analysis with uninformative, rather than weakly-informative priors. The charts and model have been updated. The width of the credibility intervals for Nielsen and Ipsos are unsurprising, as they have 7 and 6 observations respectively.
Labels:
Bayes,
JAGS,
preferences,
wonkish
Sunday, June 7, 2015
Charting the collapse of the Palmer United Party
There was a time when the Palmer United Party (PUP) looked like a force to be reckoned with. It commanded 6.5 per cent of the primary vote, a seat in the House of Representatives, and Senate seats for Queensland, Tasmania and Western Australia. But since then, the PUP share of the primary vote has fallen dramatically.
For those who are interested in these things, I aggregated the Palmer primary vote polls using a Beta-walk model. The Beta-walk model follows.
I did not include Palmer United in my primary vote models because Newspoll does not publish a primary vote estimate for Palmer United. Newspoll includes Palmer United in its other parties count.
For those who are interested in these things, I aggregated the Palmer primary vote polls using a Beta-walk model. The Beta-walk model follows.
model {
#### -- observational model
for(poll in 1:NUMPOLLS) { # for each poll result - rows
adjusted_poll[poll] <- walk[pollDay[poll]] + houseEffect[house[poll]]
palmerVotes[poll] ~ dbin(adjusted_poll[poll], n[poll])
}
#### -- temporal model (a daily walk where this today is much like yesterday)
tightness <- 50000 # tightness of fit parameter
for(day in 2:PERIOD) { # rows
binomial[day] <- walk[day-1] * tightness
walk[day] ~ dbeta(binomial[day], tightness - binomial[day])
}
## -- weakly informative priors for first day in the temporal model
alpha ~ dunif(1, 1500)
walk[1] ~ dbeta(alpha, 10000-alpha)
#### -- sum-to-zero constraints on house effects
for(house in 2:HOUSECOUNT) { # for each house ...
houseEffect[house] ~ dnorm(0, pow(0.1, -2))
}
houseEffect[1] <- -sum(houseEffect[2:HOUSECOUNT])
}
I did not include Palmer United in my primary vote models because Newspoll does not publish a primary vote estimate for Palmer United. Newspoll includes Palmer United in its other parties count.
Labels:
Beta,
JAGS,
Palmer,
primary vote,
wonkish
Saturday, June 6, 2015
Dirichlet-walk hidden Markov model
Forgive me, but I am going to talk technical for a bit. I have been playing with a latent Dirichlet process hidden Markov model for aggregating opinion polls of primary voting intention. But before we get there, let's locate this approach.
The broad description for the method of poll aggregation I use on this site is the hidden Markov model (HMM). These models are analogous to the state-space models developed in the 1960s (beginning with the Kalman Filter). Hidden Markov models are one subset of Bayesian hierarchical models.
Using a HMM, I model the population voting intention (which cannot be observed directly - it is "hidden") as a series of states (either daily or weekly, depending on the model). Each state is directly dependent on the previous state and a probability distribution linking the states. Collectively, these links form a Markov chain or process. The model is informed by irregular and noisy data from the selected polling houses.
Solving the model necessitates integration over a series of complex multidimensional probability distributions. The definite integral is typically impossible to solve algebraically. But it can be solved using a numerical method based on Markov chains and random numbers known as Markov Chain Monte Carlo (MCMC) integration. I use a free software product called JAGS to solve the model.
The two poll aggregation models I had developed previously were dynamic linear models. In both models, the probability distribution linking the hidden states in the Markov model was the normal or Gaussian distribution: the bell-curve of high school statistics. Similarly, the probability distribution linking the hidden states with the polling observations was also the normal distribution. It is this dual independent use of the normal distribution that makes these models: dynamic linear models.
For the model based on the univariate Coalition two-party preferred poll results, the dynamic linear model works a treat. However, the multivariate primary vote model was far more difficult to construct. Quite some effort went into constraining the primary vote shares so that they always summed to one. It resulted in a large (and very slow) directed acyclic graph.
To address this complexity problem, I wondered whether a Dirichlet distribution could be used. Named for Peter Gustav Lejeune Dirichlet, the distribution is pronounced dirik-lay. The Dirichlet distribution has a number of useful features: First it is a multivariate distribution. Second, the output from the distribution always sums to one. It sounded ideal for modelling a time series of dynamically changing, continuous primary vote proportions (all on the unit interval).
In 2013, Emil Aas Stoltenberg wrote a paper on Bayesian Forecasting of Election Results in Multiparty Systems. In that paper he explored a Dirichlet-Multinomial process for the hidden Markov model. His model was coded in Python.
The Coalition TPP estimate from the this model over 6-months, and over the period since the previous election is not dissimilar to the output from the two dynamic linear models.
Turning to the model, rather than estimate the poll result, I use the multinomial distribution to estimate the number of people in each poll sample who expressed a preference for each of the parties. This is a very different approach to my other models. So that you can see it, I will include the R-code where I set up the input data for the model.
It should go without saying that the usual caveats apply. This is new code, and may include bugs. You will note that I am testing a few options in different places (see comments).
The input for the 6-month model was as follows:
Second, I have removed the dmulti() - the multinomial distribution step - in the temporal model and replaced it with simple arithmetic to calculate the multinomial. This makes the model a simpler Dirichlet-walk.
The broad description for the method of poll aggregation I use on this site is the hidden Markov model (HMM). These models are analogous to the state-space models developed in the 1960s (beginning with the Kalman Filter). Hidden Markov models are one subset of Bayesian hierarchical models.
Using a HMM, I model the population voting intention (which cannot be observed directly - it is "hidden") as a series of states (either daily or weekly, depending on the model). Each state is directly dependent on the previous state and a probability distribution linking the states. Collectively, these links form a Markov chain or process. The model is informed by irregular and noisy data from the selected polling houses.
Solving the model necessitates integration over a series of complex multidimensional probability distributions. The definite integral is typically impossible to solve algebraically. But it can be solved using a numerical method based on Markov chains and random numbers known as Markov Chain Monte Carlo (MCMC) integration. I use a free software product called JAGS to solve the model.
The two poll aggregation models I had developed previously were dynamic linear models. In both models, the probability distribution linking the hidden states in the Markov model was the normal or Gaussian distribution: the bell-curve of high school statistics. Similarly, the probability distribution linking the hidden states with the polling observations was also the normal distribution. It is this dual independent use of the normal distribution that makes these models: dynamic linear models.
For the model based on the univariate Coalition two-party preferred poll results, the dynamic linear model works a treat. However, the multivariate primary vote model was far more difficult to construct. Quite some effort went into constraining the primary vote shares so that they always summed to one. It resulted in a large (and very slow) directed acyclic graph.
To address this complexity problem, I wondered whether a Dirichlet distribution could be used. Named for Peter Gustav Lejeune Dirichlet, the distribution is pronounced dirik-lay. The Dirichlet distribution has a number of useful features: First it is a multivariate distribution. Second, the output from the distribution always sums to one. It sounded ideal for modelling a time series of dynamically changing, continuous primary vote proportions (all on the unit interval).
In 2013, Emil Aas Stoltenberg wrote a paper on Bayesian Forecasting of Election Results in Multiparty Systems. In that paper he explored a Dirichlet-Multinomial process for the hidden Markov model. His model was coded in Python.
The Coalition TPP estimate from the this model over 6-months, and over the period since the previous election is not dissimilar to the output from the two dynamic linear models.
Turning to the model, rather than estimate the poll result, I use the multinomial distribution to estimate the number of people in each poll sample who expressed a preference for each of the parties. This is a very different approach to my other models. So that you can see it, I will include the R-code where I set up the input data for the model.
It should go without saying that the usual caveats apply. This is new code, and may include bugs. You will note that I am testing a few options in different places (see comments).
df <- read.csv(args[6], header=TRUE)
df <- df[order(df$Week), ]
NUMPOLLS <- nrow(df)
PERIOD <- max(df$Week)
HOUSECOUNT <- length(levels(df$House)) # a factor
HOUSENAMES <- levels(df$House)
PARTYNAMES <- c('Coalition', 'Labor', 'Greens', 'Other')
PARTIES <- length(PARTYNAMES)
primaryVotes <- df[PARTYNAMES] * df$Sample
primaryVotes <- sapply(primaryVotes, function(x) round(x,0))
day0 <- min(as.Date(df$Date)) - 1
## Assume Coalition gets preferences as follows:
## - 16.97% of the Green vote [was 16.97 in 2013 and 21.16 in 2010]
## - 53.3% of the Other vote [was 53.3 in 2013 and 58.26 in 2010]
## See: Antony Green - http://blogs.abc.net.au/antonygreen/2013/11/
## preference-flows-at-the-2013-federal-election.html
preference_flows <- c(1.0, 0.0, 0.1697, 0.533)
data = list(PERIOD = PERIOD,
HOUSECOUNT = HOUSECOUNT,
NUMPOLLS = NUMPOLLS,
PARTIES = PARTIES,
primaryVotes = primaryVotes,
pollWeek = df$Week,
house = as.integer(df$House),
# manage rounding issues with df$Sample ...
n = rowSums(primaryVotes),
preference_flows = preference_flows
)
print(data)
# ----- JAGS model ...
library(rjags)
model <- "
model {
#### -- observational model
for(poll in 1:NUMPOLLS) { # for each poll result - rows
adjusted_poll[poll, 1:PARTIES] <- walk[pollWeek[poll], 1:PARTIES] +
houseEffect[house[poll], 1:PARTIES]
primaryVotes[poll, 1:PARTIES] ~ dmulti(adjusted_poll[poll, 1:PARTIES], n[poll])
}
#### -- temporal model (a weekly walk where this week is much like last week)
tightness <- 10000 # KLUDGE: tightness of fit parameter selected by trial and error
for(week in 2:PERIOD) {
# Note: use math not a distribution to generate the multinomial ...
multinomial[week, 1:PARTIES] <- walk[week-1, 1:PARTIES] * tightness
walk[week, 1:PARTIES] ~ ddirch(multinomial[week, 1:PARTIES])
}
## -- weakly informative priors for first week in the temporal model
for (party in 1:2) { # for each major party
alpha[party] ~ dunif(250, 600) # majors between 25% and 60%
}
for (party in 3:PARTIES) { # for each minor party
alpha[party] ~ dunif(10, 250) # minors between 1% and 25%
}
walk[1, 1:PARTIES] ~ ddirch(alpha[])
## -- estimate a Coalition TPP from the primary votes
for(week in 1:PERIOD) {
CoalitionTPP[week] <- sum(walk[week, 1:PARTIES] *
preference_flows[1:PARTIES])
}
#### -- sum-to-zero constraints on house effects
for (party in 2:PARTIES) { # for each party ...
# house effects across houses sum to zero
# NOTE: ALL MUST SUM TO ZERO
houseEffect[1, party] <- -sum( houseEffect[2:HOUSECOUNT, party] )
}
for(house in 1:HOUSECOUNT) { # for each house ...
# house effects across the parties sum to zero
houseEffect[house, 1] <- -sum( houseEffect[house, 2:PARTIES] )
}
# but note, we do not apply a double constraint to houseEffect[1, 1]
monitorHouseEffectOneSumParties <- sum(houseEffect[1, 1:PARTIES])
monitorHouseEffectOneSumHouses <- sum(houseEffect[1:HOUSECOUNT, 1])
## -- vague normal priors for house effects - centred on zero
for (party in 2:PARTIES) { # for each party (cols)
for(house in 2:HOUSECOUNT) { # (rows)
houseEffect[house, party] ~ dnorm(0, pow(0.1, -2))
}
}
}
"
jags <- jags.model(textConnection(model),
data = data,
n.chains=4,
n.adapt=n_adapt
)
The input for the 6-month model was as follows:
$PERIOD
[1] 26
$HOUSECOUNT
[1] 5
$NUMPOLLS
[1] 35
$PARTIES
[1] 4
$primaryVotes
Coalition Labor Greens Other
[1,] 532 574 154 140
[2,] 560 518 168 154
[3,] 350 410 115 125
[4,] 439 450 139 127
[5,] 385 385 95 135
[6,] 375 395 120 110
[7,] 1465 1483 417 325
[8,] 504 602 154 140
[9,] 532 560 154 154
[10,] 504 602 154 140
[11,] 355 415 120 110
[12,] 412 483 141 141
[13,] 1345 1450 392 312
[14,] 375 405 100 120
[15,] 448 448 142 142
[16,] 588 504 168 140
[17,] 390 380 115 115
[18,] 441 453 139 128
[19,] 380 400 110 110
[20,] 471 425 126 126
[21,] 957 979 278 205
[22,] 405 360 125 110
[23,] 546 532 182 126
[24,] 471 413 126 138
[25,] 385 380 120 115
[26,] 1008 995 301 228
[27,] 400 375 115 110
[28,] 457 410 141 164
[29,] 690 656 185 151
[30,] 603 491 182 126
[31,] 415 355 125 105
[32,] 464 429 139 128
[33,] 1307 1218 385 273
[34,] 410 370 130 90
[35,] 479 433 152 105
$pollWeek
[1] 1 1 2 2 6 8 8 9 9 10 10 10 10 12 12 13 14 14 16 16 17 18 19 19 20
[26] 21 22 22 24 24 24 24 24 26 26
$house
[1] 1 2 3 4 3 3 5 1 2 1 3 4 5 3 4 2 3 4 3 4 5 3 2 4 3 5 3 4 1 2 3 4 5 3 4
$n
[1] 1400 1400 1000 1155 1000 1000 3690 1400 1400 1400 1000 1177 3499 1000 1180
[16] 1400 1000 1161 1000 1148 2419 1000 1386 1148 1000 2532 1000 1172 1682 1402
[31] 1000 1160 3183 1000 1169
$preference_flows
[1] 1.0000 0.0000 0.1697 0.5330
Update
This page has been updated. Updates were the result of further analysis, which identified glitches with the original model. The tightness of fit parameter in the temporal model was not resolving. I have returned to specifying the tightness of fit parameter in the model.Second, I have removed the dmulti() - the multinomial distribution step - in the temporal model and replaced it with simple arithmetic to calculate the multinomial. This makes the model a simpler Dirichlet-walk.
Friday, May 29, 2015
Refactored Bayesian model
Today, I updated the technicals page for the 2016 election. This is the page where I explain the Bayesian model I use. The page includes additional commentary on the JAGS model, which explains how the various elements of the model interact.
In the process, I re-examined the model I had been using. I became concerned that the prior I had been using for the house effect was not sufficiently uninformative. I had been using a normal prior, centred on zero, with a standard deviation of five percentage points. I have changed this to a uniform prior between -15 and +15 percentage points. I would expect house effects to be in the range of -2 to +2 percentage points, so the 30 point uniform range should be uninformative.
The change has had little impact on the analysis, so my fears were probably unfounded. Nonetheless, I have retained the uniform prior, as it is clearly less informative than the normal prior.
The other change I have made to the charts is that I now include an extra band in the Bayesian output to indicate the middle 99 per cent of samples. Previously, I had only indicated the median sample with a line, and the ranges for the middle 95, 80 and 50 percent of the samples with increasingly darker shading.
Let's look at the outcome. For the three month analysis, the median estimate of voting intention is unchanged. The first chart is the revised chart, the second chart is the earlier analysis (from here).
Turning now to house effects. The first chart is the updated analysis. The second chart is the earlier analysis. The only significant difference is the extra band in the first chart, that indicates the range for the middle 99 per cent of samples.
These changes in the second decimal place for the medians are very small, and should be ignored.
In the process, I re-examined the model I had been using. I became concerned that the prior I had been using for the house effect was not sufficiently uninformative. I had been using a normal prior, centred on zero, with a standard deviation of five percentage points. I have changed this to a uniform prior between -15 and +15 percentage points. I would expect house effects to be in the range of -2 to +2 percentage points, so the 30 point uniform range should be uninformative.
The change has had little impact on the analysis, so my fears were probably unfounded. Nonetheless, I have retained the uniform prior, as it is clearly less informative than the normal prior.
The other change I have made to the charts is that I now include an extra band in the Bayesian output to indicate the middle 99 per cent of samples. Previously, I had only indicated the median sample with a line, and the ranges for the middle 95, 80 and 50 percent of the samples with increasingly darker shading.
Let's look at the outcome. For the three month analysis, the median estimate of voting intention is unchanged. The first chart is the revised chart, the second chart is the earlier analysis (from here).
Turning now to house effects. The first chart is the updated analysis. The second chart is the earlier analysis. The only significant difference is the extra band in the first chart, that indicates the range for the middle 99 per cent of samples.
These changes in the second decimal place for the medians are very small, and should be ignored.
Thursday, May 21, 2015
Data and code for election 2016
I have made most of my data and code base available on Google Drive. Please note, this is my live code base, which I play with quite a bit. So, there will be times when it is broken or in some stage of being edited.
What I have not made available is the Excel spreadsheets into which I initially place my data. These live in the (hidden) raw-data directory. However, the collated data for the Bayesian model lives in the intermediate directory, visible from the above link.
The program that collates and organises the spreadsheets into a single CSV input file for the Bayesian analysis is TPP-step1.py. There are two intermediate input files (at the moment): TPP-3-stage1.csv and TPP-all-stage1.csv. The first of these input files covers the most recent three months. The second of these input files is for all polls since the 2013 election.
The Bayesian model itself lives in the file TPP-step2.R.
The code for producing the plots lives in TPP-step3.py.
The files that begin with the letter 'z' are bash shell scripts.
The most recent set of charts live in the graphs directory. I don't keep historical charts.
There are a handful of helper programs that live in the bin directory.
There are a couple files that I am working on in respect of a primary votes model. This is still a long way from finished.
If you see an error in my code or data, please drop me a line (comments below or email address in right hand column), and let me know. I can only improve with your help.
What I have not made available is the Excel spreadsheets into which I initially place my data. These live in the (hidden) raw-data directory. However, the collated data for the Bayesian model lives in the intermediate directory, visible from the above link.
The program that collates and organises the spreadsheets into a single CSV input file for the Bayesian analysis is TPP-step1.py. There are two intermediate input files (at the moment): TPP-3-stage1.csv and TPP-all-stage1.csv. The first of these input files covers the most recent three months. The second of these input files is for all polls since the 2013 election.
The Bayesian model itself lives in the file TPP-step2.R.
The code for producing the plots lives in TPP-step3.py.
The files that begin with the letter 'z' are bash shell scripts.
The most recent set of charts live in the graphs directory. I don't keep historical charts.
There are a handful of helper programs that live in the bin directory.
There are a couple files that I am working on in respect of a primary votes model. This is still a long way from finished.
If you see an error in my code or data, please drop me a line (comments below or email address in right hand column), and let me know. I can only improve with your help.
Wednesday, July 31, 2013
Further explorations in non-linearity
On the weekend I began exploring a non-linear model for aggregating polling. My test case produced nice looking graphs; but the results were in some large part an artifact of the priors I had chosen for the model (not good).
In the comments to that post, I suggested that part of the problem may have been how I had defined the model.
I have now redefined the model to use the midpoint for each polling house (defined as the (min+max)/2 for that house). The use of house-specific mid-points acknowledges that the raw x scores include the alpha house effect I am trying to estimate. The revised model is:
The priors to the beta effect have been completely freed-up, so that they are uninformative. As a result, this model works better internally than the previous model.
However, I have added a constraint such that the aggregation assumes the "fizziness" factors also sum to zero. This may be a bollocks assumption; and will need some further analysis.
So - with the caveat that this is still very early exploratory analysis - the results of this new model follow.
On a plain reading, the above charts suggest that the current Rudd effect may actually be better for Labor than other aggregations have found. However, I am not convinced this is anything more than an artifact of the model. I am particularly concerned about the inclusion of data from polling houses that do not have data points across the highs and lows of the Gillard period, and across the Rudd and Gillard periods. Such data don't fully occupy the model, which can result in artifacts.
If we limit the analysis to data from Nielsen and Newspoll for a comparison using data that does fully occupy the model, we get the following.
Because I am committed to presenting poll analysis in a transparent and unbiased fashion, the next two code snippets show how I package up the data for the model (in R), and the model itself (in JAGS).
In the comments to that post, I suggested that part of the problem may have been how I had defined the model.
Thinking about this some more, I think the problem was in this statement: << beta-for-pollster * (poll-result - minimum-poll-result) >>. Rather than the minimum, it should be a central tendency of some type (mid-point, mean, median, etc.).
I have now redefined the model to use the midpoint for each polling house (defined as the (min+max)/2 for that house). The use of house-specific mid-points acknowledges that the raw x scores include the alpha house effect I am trying to estimate. The revised model is:
house-effect-for-pollster = alpha-for-pollster + beta-for-pollster * (poll-result - pollster-mid-point)
The priors to the beta effect have been completely freed-up, so that they are uninformative. As a result, this model works better internally than the previous model.
However, I have added a constraint such that the aggregation assumes the "fizziness" factors also sum to zero. This may be a bollocks assumption; and will need some further analysis.
So - with the caveat that this is still very early exploratory analysis - the results of this new model follow.
On a plain reading, the above charts suggest that the current Rudd effect may actually be better for Labor than other aggregations have found. However, I am not convinced this is anything more than an artifact of the model. I am particularly concerned about the inclusion of data from polling houses that do not have data points across the highs and lows of the Gillard period, and across the Rudd and Gillard periods. Such data don't fully occupy the model, which can result in artifacts.
If we limit the analysis to data from Nielsen and Newspoll for a comparison using data that does fully occupy the model, we get the following.
Because I am committed to presenting poll analysis in a transparent and unbiased fashion, the next two code snippets show how I package up the data for the model (in R), and the model itself (in JAGS).
# - prepare the data ...
data$y <- data[ , y] / 100 # vote share between 0-1
data$variance <- data$y * (1 - data$y) / data$Sample #pq/n
data$standardError <- sqrt(data$variance)
data$samplePrecision <- 1 / data$variance
HOUSES <- levels(data$House)
HOUSECOUNT <- length(levels(data$House))
NUMPOLLS <- length(data$y)
cat('Number of polls: '); print(NUMPOLLS)
midpoints <- rep(NA, HOUSECOUNT)
for(i in seq_len(HOUSECOUNT)) {
midpoints[i] <- (max(data[data$House==HOUSES[i], 'y']) +
min(data[data$House==HOUSES[i], 'y'])) / 2
cat('Midpoints: '); cat(i); cat(' '); cat(HOUSES[i]); cat(': '); print(midpoints[i])
}
# - remember these dots
dotsFile <- paste('./files/', fPrefix, 'original.csv', sep='')
write.csv(data, file=dotsFile)
# - manage dates
day0 <- min(data$Date) - 1 # walk starts from earliest date
if(endDate == TODAY)
endDate <- max(data$Date)
PERIOD <- as.numeric(endDate - day0) # length of walk in days
DISCOUNTINUITYDAY <- as.numeric(as.Date(discontinuity) - day0)
cat('Discontinuity: '); print(DISCOUNTINUITYDAY)
tPrefix <- paste(format(day0+1, '%d-%b-%Y'), ' to ', format(endDate, '%d-%b-%Y'), sep='')
data$day <- as.numeric(data$Date - day0)
# - do the MCMC thing ...
parameters <- list(PERIOD = PERIOD,
HOUSECOUNT = HOUSECOUNT,
NUMPOLLS = NUMPOLLS,
DISCOUNTINUITYDAY = DISCOUNTINUITYDAY,
NEWSPOLL = which(levels(data$House) == 'Newspoll'),
y = data$y,
x = data$y,
day = data$day, house = as.integer(data$House),
samplePrecision = data$samplePrecision,
midpoints=midpoints
)
jags <- jags.model(textConnection(model),
data=parameters,
n.chains=4,
n.adapt=n.adapt
)
# - burn in
update(jags, n.iter=n.update) # burn-in the chains
# - capture results
jags.capture <- c('walk', 'alpha', 'beta', 'discontinuityValue')
coda.samples <- coda.samples(jags, jags.capture, n.iter=n.iter, thin=n.thin)
coda.matrix <- as.matrix(coda.samples)
model {
## Derived from Simon Jackman's original model
## -- observational model
for(poll in 1:NUMPOLLS) {
# note: x and y are the original polling series
houseEffect[poll] <- alpha[house[poll]] +
beta[house[poll]]*(x[poll]-midpoints[house[poll]])
mu[poll] <- walk[day[poll]] + houseEffect[poll]
y[poll] ~ dnorm(mu[poll], samplePrecision[poll])
}
## -- temporal model
for(i in 2:PERIOD) { # for each day under analysis ...
day2DayAdj[i] <- ifelse(i==DISCOUNTINUITYDAY,
walk[i-1]+discontinuityValue, walk[i-1])
walk[i] ~ dnorm(day2DayAdj[i], walkPrecision)
}
sigmaWalk ~ dunif(0, 0.01) ## uniform prior on std. dev.
walkPrecision <- pow(sigmaWalk, -2) ## for the day-to-day random walk
walk[1] ~ dunif(0.4, 0.6) ## uninformative prior
discontinuityValue ~ dunif(-0.2, 0.2) ## uninformative prior
## -- house effects model
for(i in 2:HOUSECOUNT) { ## vague normal priors for house effects
alpha[i] ~ dunif(-0.1,0.1)
beta[i] ~ dunif(-1,1)
}
alpha[NEWSPOLL] <- -sum(alpha[2:HOUSECOUNT]) ## sum to zero
beta[NEWSPOLL] <- -sum(beta[2:HOUSECOUNT]) ## sum to zero
}
Labels:
aggregated polls,
JAGS,
methodology,
R,
wonkish
Sunday, July 28, 2013
Exploring a non-linear house effects model
I have spent a couple of hours today exploring a non-linear house effects model. I have been troubled by the "fizziness" of individual polling houses. When the population voting intention moves to labor, some polling houses are much more sensitive to this movement than others. There appears to be a consistency to this tendency between polling houses (ie. some polling houses appear consistently over-sensitive to these movements, and other houses appear consistently under-sensitive).
One of the reasons I limit my weekly aggregation data to six months is to avoid problems from this non-linearity. If I can better model this non-linearity, I might be able to model longer time-sequences of data.
In today's exploration, I have modeled the non-linearity with a degree-1 polynomial for each pollster. In simplified terms, the house-effect for each pollster is given by the following equation.
In this model, the constant alpha is analogous to house-effect equation from my previous model. The value for beta indicates the extent to which a polling house is more or less sensitive to a population shift in voting intention. Returning to the term I introduced above, beta is my measure of fizziness.
I have used a sum to zero constraint across polling houses to anchor the alpha value. I have assigned Newspoll=0 to anchor the beta value. The part of this approach I am least comfortable with is the selection of priors for the beta value. These are neither uninformed nor vague. They significantly influence the final result. Clearly, some more thinking is needed here.
The initial experimental results follow (using all of the polling data since the previous election).
The code for the non-linear model follows.
One of the reasons I limit my weekly aggregation data to six months is to avoid problems from this non-linearity. If I can better model this non-linearity, I might be able to model longer time-sequences of data.
In today's exploration, I have modeled the non-linearity with a degree-1 polynomial for each pollster. In simplified terms, the house-effect for each pollster is given by the following equation.
house-effect-for-pollster = alpha-for-pollster + beta-for-pollster * (poll-result - minimum-poll-result)
In this model, the constant alpha is analogous to house-effect equation from my previous model. The value for beta indicates the extent to which a polling house is more or less sensitive to a population shift in voting intention. Returning to the term I introduced above, beta is my measure of fizziness.
I have used a sum to zero constraint across polling houses to anchor the alpha value. I have assigned Newspoll=0 to anchor the beta value. The part of this approach I am least comfortable with is the selection of priors for the beta value. These are neither uninformed nor vague. They significantly influence the final result. Clearly, some more thinking is needed here.
The initial experimental results follow (using all of the polling data since the previous election).
The code for the non-linear model follows.
model {
## Developed on the base of Simon Jackman's original model
## -- observational model
for(poll in 1:NUMPOLLS) {
# note: x and y are the original polling series
houseEffect[poll] <- alpha[house[poll]] + beta[house[poll]]*(x[poll]-min(x))
mu[poll] <- walk[day[poll]] + houseEffect[poll]
y[poll] ~ dnorm(mu[poll], samplePrecision[poll])
}
## -- temporal model
for(i in 2:PERIOD) {
day2DayAdj[i] <- ifelse(i==DISCOUNTINUITYDAY,
walk[i-1]+discontinuityValue, walk[i-1])
walk[i] ~ dnorm(day2DayAdj[i], walkPrecision)
}
sigmaWalk ~ dunif(0, 0.01) ## uniform prior on std. dev.
walkPrecision <- pow(sigmaWalk, -2) ## for the day-to-day random walk
walk[1] ~ dunif(0.4, 0.6) ## vague prior
discontinuityValue ~ dunif(-0.2, 0.2) ## uninformative prior
## -- house effects model
for(i in 2:HOUSECOUNT) {
alpha[i] ~ dunif(-0.1,0.1) ## vague prior
beta[i] ~ dunif(-0.1,0.1) ## could be problematic!!
}
alpha[NEWSPOLL] <- -sum(alpha[2:HOUSECOUNT]) ## sum to zero
beta[NEWSPOLL] <- 0 ## Newspoll as benchmark on non-linearity
}
Saturday, July 27, 2013
How much was Kevin Rudd worth?
I was a little surprised when I saw Simon Jackman suggest that Kevin Rudd had moved the two-party preferred voting intention by seven percentage points in Labor's favour. It was not consistent with my own analysis and only one pollster (Morgan) has data that supports a seven point movement. Data from all the remaining pollsters suggest the "Rudd Effect" was less than seven percentage points.
Now don't get me wrong, I have enormous respect for Professor Jackman. I purchased and read his 600 page text, Bayesian Analysis for Social Sciences. It is a tour de force on Bayesian statistics. I cannot recommend this book enough. His understanding and knowledge in this area far surpasses my own. Unashamedly, I have used Jackman's approach as the basis for my own aggregation efforts.
However, I suspect he has not noticed that the data since the second ascension of Keven Rudd violates a number of the linear assumptions implicit in his model. In particular, some of the house effects before and after Kevin are radically different. I blogged on this under the rubric: When models fail us. As I noted previously, the violation of the underpinning assumptions results in the model producing incorrect results.
Revisiting the discontinuity model I initially used following Rudd's restoration, I have treated the Morgan, Galaxy and Essential data before and after the restoration as different series. I have also centred the aggregation on the assumption that the house effects for Newspoll and Nielsen sum to zero (this may turn out to be problematic, but it is sufficient for the moment). Notwithstanding, some remaining doubts, I think this approach overcomes many of the problems my earlier discontinuity model had. I will cut to the results before reviewing the R and JAGS code.
The key finding is that Kevin was worth 5.6 percentage points in Labor's two party preferred vote share.
Turning to the house effects, we can see some of the variability in the pre-Rudd (PR) and after-Rudd (AR) values.
The revised model follows. In the first code block is the R code for managing the Morgan sample size and for separating the relevant polls into pre-Rudd (PR) and after-Rudd (AR) series. The second code block has the JAGS code. (As an aside, I have been playing with Stan lately, and might make a switch down the track).
Now don't get me wrong, I have enormous respect for Professor Jackman. I purchased and read his 600 page text, Bayesian Analysis for Social Sciences. It is a tour de force on Bayesian statistics. I cannot recommend this book enough. His understanding and knowledge in this area far surpasses my own. Unashamedly, I have used Jackman's approach as the basis for my own aggregation efforts.
However, I suspect he has not noticed that the data since the second ascension of Keven Rudd violates a number of the linear assumptions implicit in his model. In particular, some of the house effects before and after Kevin are radically different. I blogged on this under the rubric: When models fail us. As I noted previously, the violation of the underpinning assumptions results in the model producing incorrect results.
Revisiting the discontinuity model I initially used following Rudd's restoration, I have treated the Morgan, Galaxy and Essential data before and after the restoration as different series. I have also centred the aggregation on the assumption that the house effects for Newspoll and Nielsen sum to zero (this may turn out to be problematic, but it is sufficient for the moment). Notwithstanding, some remaining doubts, I think this approach overcomes many of the problems my earlier discontinuity model had. I will cut to the results before reviewing the R and JAGS code.
The key finding is that Kevin was worth 5.6 percentage points in Labor's two party preferred vote share.
Turning to the house effects, we can see some of the variability in the pre-Rudd (PR) and after-Rudd (AR) values.
The revised model follows. In the first code block is the R code for managing the Morgan sample size and for separating the relevant polls into pre-Rudd (PR) and after-Rudd (AR) series. The second code block has the JAGS code. (As an aside, I have been playing with Stan lately, and might make a switch down the track).
# fudge sample size for Morgan multi - adjustment for observed over-dispersion
output.data[output.data[, 'House'] == 'Morgan multi', 'Sample'] <- 1000
# treat before and after for Morgan, Galaxy and Essential as different series
output.data$House <- paste(as.character(output.data$House),
ifelse(as.character(output.data$House) %in% c('Essential', 'Morgan multi', 'Galaxy'),
ifelse(output.data[, 'Date'] >= as.Date(discontinuity), ' AR', ' PR'), ''),
sep='')
l <- levels(factor(output.data$House))
n <- which(l == 'Newspoll')
l[n] <- l[1]
l[1] <- 'Newspoll' # Newspoll is House number one in the factor ...
output.data$House <- factor(output.data$House, levels=l)
model {
## Based on Simon Jackman's original model
## -- observational model
for(poll in 1:NUMPOLLS) {
y[poll] ~ dnorm(walk[day[poll]] + houseEffect[house[poll]], samplePrecision[poll])
}
## -- temporal model
for(i in 2:PERIOD) { # for each day under analysis ...
day2DayAdj[i] <- ifelse(i==DISCOUNTINUITYDAY, walk[i-1]+discontinuityValue, walk[i-1])
walk[i] ~ dnorm(day2DayAdj[i], walkPrecision)
}
sigmaWalk ~ dunif(0, 0.01) ## uniform prior on std. dev.
walkPrecision <- pow(sigmaWalk, -2) ## for the day-to-day random walk
walk[1] ~ dunif(0.01, 0.99) ## uninformative prior
discontinuityValue ~ dunif(-0.2, 0.2) ## uninformative prior
## -- sum-to-zero constraint on house effects
for(i in 2:HOUSECOUNT) { ## vague normal priors for house effects
houseEffect[i] ~ dnorm(0, pow(0.1, -2))
}
#houseEffect[NEWSPOLL] <- -sum(houseEffect[2:HOUSECOUNT]) ## all sum to zero
houseEffect[NEWSPOLL] <- -houseEffect[NIELSEN] ## Newspoll and Nielsen sum to zero
#houseEffect[NEWSPOLL] <- 0 ## centred on Newspoll as zero
}
Labels:
aggregated polls,
JAGS,
methodology,
R,
wonkish
Sunday, December 30, 2012
Footy tipping with some help from Bayes
Christmas is the season for frivolous pursuits. For the fun of it, I thought I would adapt the Bayesian model I use to pool the polls to see how it would fare against the bookmakers in predicting NRL footy outcomes for the 2012 season.
The model I tested was very simple. It assumed that the score difference between two teams can be explained by two parameters. The first is a home game advantage parameter for each team. The second is a parameter for the strength of each team. These team strength parameters are allowed to evolve from round to round. This model can be expressed roughly as follows.
The JAGS code for this model is as follows.
I tested the model with this data for seasons 2011 and 2012. For each round in 2012 (prior to the finals), I picked the team the JAGS code and the team the bookmakers thought most likely to win. I did not consider draws. While I estimated the probability of a draw from the JAGS samples, I only picked the maximum from the probabilities of a home win versus an away win. For the JAGS prediction, I simulated each round 10,000 times. For the Bookmaker prediction I converted their odds to probabilities which I adjusted for the bookmaker's overround so that the sum of the home-win, away-win and draw probabilities was one.
The end result (for such a simple model) was very close. Over the course of 2012, the JAGS model picked the winning team 121 times. The bookmakers (or more accurately, the punters collectively) got it right 122 times.
The challenge now is to refine the model and make it better than the bookmakers.
The model I tested was very simple. It assumed that the score difference between two teams can be explained by two parameters. The first is a home game advantage parameter for each team. The second is a parameter for the strength of each team. These team strength parameters are allowed to evolve from round to round. This model can be expressed roughly as follows.
(home_score - away_score) = home_team_advantage + home_strength - away_strength
team_strength_in_round ~ normal(team_strength_in_prev_round, team_standard_deviation)
The JAGS code for this model is as follows.
model {
# observational model
for( i in 1:N_GAMES ) {
score_diff[i] <- homeAdvantage[Home_Team[i]] +
(strength[Round[i], Home_Team[i]] - strength[ Round[i], Away_Team[i] ])
Home_Win_Margin[i] ~ dnorm(score_diff[i], consistencyPrec)
}
# temporal model
for( round in 2:N_ROUNDS ) {
for( team in 1:N_TEAMS ) {
strength[round, team] ~ dnorm(strength[(round-1), team], strongWalkPrec[team])
}
}
# predictive model
for( i in N_FROM:N_GAMES ) {
prediction[i-N_FROM+1] <- score_diff[i]
}
# priors
consistencySD ~ dunif(0.0001,100) # vague prior - positive
consistencyPrec <- pow(consistencySD, -2)
for( team in 1:N_TEAMS ) {
strength[1, team] ~ dnorm(0, pow(100, -2)) # vague prior
homeAdvantage[team] ~ dnorm(0, pow(10, -2)) # vague prior
strongWalkSD[team] ~ dunif(0.0001,4) # vague prior - positive
strongWalkPrec[team] <- pow(strongWalkSD[team], -2)
}
}
I tested the model with this data for seasons 2011 and 2012. For each round in 2012 (prior to the finals), I picked the team the JAGS code and the team the bookmakers thought most likely to win. I did not consider draws. While I estimated the probability of a draw from the JAGS samples, I only picked the maximum from the probabilities of a home win versus an away win. For the JAGS prediction, I simulated each round 10,000 times. For the Bookmaker prediction I converted their odds to probabilities which I adjusted for the bookmaker's overround so that the sum of the home-win, away-win and draw probabilities was one.
The end result (for such a simple model) was very close. Over the course of 2012, the JAGS model picked the winning team 121 times. The bookmakers (or more accurately, the punters collectively) got it right 122 times.
The challenge now is to refine the model and make it better than the bookmakers.
Sunday, December 9, 2012
More on house effects over time
Early last decade, Simon Jackman published his Bayesian approach to poll aggregation. It allowed the house effects (systemic biases) of a polling house to be calibrated (either absolutely in terms of a known election outcome, or relatively against the average of all the other polling houses).
Jackman's approach was two-fold. He theorised that voting intention typically did not change much day-to-day (although his model allows for occasional larger movement in public opinion). On most days, the voting intention of the public is much the same as it was on the previous day. In his model, he identified the most likely path that voting intention took each and every day through the period under analysis. This day-to-day track of likely voting intention then must line up (as best it can) with the published polls as they occurred during this period. To help the modeled day-to-day walk of public opinion line up with the published polls, Jackman's approach assumed that each polling house had a systemic bias which is normally distributed around a constant number of percentage points above or below the actual population's voting intention.
Jackman's approach works brilliantly over the short run. In the next chart, which is based on a 100,000 simulation of possible walks that satisfies the constraints in the model, we pick out the median pathway for each day over the last six months. The result is a reasonably smooth curve. While I have not labeled the end point in the median series, it was 47.8 per cent.
However, over longer periods, Jackman's model is less effective. The problem is the assumption that the distribution of house effects remains constant over time. This is not the case. In the next chart, we apply the same 100,000 simulation approach as above, but to the data since the last election. The end point for this chart is 47.7 per cent.
It looks like the estimated population voting intention line is more choppy (because the constantly distributed house effects element of the model is contributing less to the analysis over the longer run). Previously I noted that over the last three years, Essential's house effect has moved around somewhat in comparison to the other polling houses.
All of this got me wondering whether it was possible to design a model that identified this movement in house effects over time - on (say) a six month rolling average basis. My idea was to take the median line from Jackman's model and use it to benchmark the polling houses. I also wondered whether I could then use the newly identified time-varying house-effect to better identify the underlying population voting intention.
The first step of taking a six month rolling average against the original Jackman line was simple as can be seen in the next chart (noting this is a 10,000 run simulation).
However, designing a model where the fixed and variable sides of the model informed each other proved more challenging than I had anticipated (in part because the JAGS program requires the specification of a directed acyclic graph). At first, I could not find an easy way for the fixed effect side of the model to inform variable effects side of the model and for the variable effects side to inform the fixed effects side, without the whole model becoming a cyclical graph.
When I finally solved the problem, a really nice chart for population voting intention popped out the other end (after 2.5 hours of computer time for the 100,000 run simulation).
Also, the six-monthly moving average for the house effects (which is measured against the line) looked a touch smoother (but this may be the result of a 100,000 run versus a 10,000 run for the earlier chart).
This leads me to another observation. A number of other blogs interested in poll aggregation ignore or down-weight the Morgan face-to-face poll series. I have been asked why I use it.
I use the Morgan face to face series because it is fairly consistent in respect of the other polls. It is a bit like comparing a watch that is consistently five minutes slow with a watch that is sometimes a minute or two fast and at other times a minute or two slow, but which moves randomly between theses two states. A watch that is consistently slow is more informative once it has been benchmarked than a watch that might be closer to the actual time, but whose behaviour around the actual time is random. In short, I think the people who ignore or down-play this Morgan series are not taking advantage of really useful information.
Back to the model: All of the volatility ended up in the variable effects daily walk, which is substantially influenced by the outliers.
For the nerds: My JAGS code for this is a bit more complicated than for earlier models. The variables y and y2 are the polling observations over the period (the series are identical - this is how I ensured the graph was acyclical). The observations are ordered in date order. The lower and upper variables map the range of the six-month centred window for estimating the variable effects against the fixed effects (this is calculated in R before handing to JAGS for the MCMC simulation). The lines marked with a triple $ sign are the lines that allow the fixed and variable elements of the model to inform each other.
I suspect this is more complicated than it needs to be; any help in simplifying the approach would be appreciated.
Jackman's approach was two-fold. He theorised that voting intention typically did not change much day-to-day (although his model allows for occasional larger movement in public opinion). On most days, the voting intention of the public is much the same as it was on the previous day. In his model, he identified the most likely path that voting intention took each and every day through the period under analysis. This day-to-day track of likely voting intention then must line up (as best it can) with the published polls as they occurred during this period. To help the modeled day-to-day walk of public opinion line up with the published polls, Jackman's approach assumed that each polling house had a systemic bias which is normally distributed around a constant number of percentage points above or below the actual population's voting intention.
Jackman's approach works brilliantly over the short run. In the next chart, which is based on a 100,000 simulation of possible walks that satisfies the constraints in the model, we pick out the median pathway for each day over the last six months. The result is a reasonably smooth curve. While I have not labeled the end point in the median series, it was 47.8 per cent.
However, over longer periods, Jackman's model is less effective. The problem is the assumption that the distribution of house effects remains constant over time. This is not the case. In the next chart, we apply the same 100,000 simulation approach as above, but to the data since the last election. The end point for this chart is 47.7 per cent.
It looks like the estimated population voting intention line is more choppy (because the constantly distributed house effects element of the model is contributing less to the analysis over the longer run). Previously I noted that over the last three years, Essential's house effect has moved around somewhat in comparison to the other polling houses.
All of this got me wondering whether it was possible to design a model that identified this movement in house effects over time - on (say) a six month rolling average basis. My idea was to take the median line from Jackman's model and use it to benchmark the polling houses. I also wondered whether I could then use the newly identified time-varying house-effect to better identify the underlying population voting intention.
The first step of taking a six month rolling average against the original Jackman line was simple as can be seen in the next chart (noting this is a 10,000 run simulation).
However, designing a model where the fixed and variable sides of the model informed each other proved more challenging than I had anticipated (in part because the JAGS program requires the specification of a directed acyclic graph). At first, I could not find an easy way for the fixed effect side of the model to inform variable effects side of the model and for the variable effects side to inform the fixed effects side, without the whole model becoming a cyclical graph.
When I finally solved the problem, a really nice chart for population voting intention popped out the other end (after 2.5 hours of computer time for the 100,000 run simulation).
Also, the six-monthly moving average for the house effects (which is measured against the line) looked a touch smoother (but this may be the result of a 100,000 run versus a 10,000 run for the earlier chart).
This leads me to another observation. A number of other blogs interested in poll aggregation ignore or down-weight the Morgan face-to-face poll series. I have been asked why I use it.
I use the Morgan face to face series because it is fairly consistent in respect of the other polls. It is a bit like comparing a watch that is consistently five minutes slow with a watch that is sometimes a minute or two fast and at other times a minute or two slow, but which moves randomly between theses two states. A watch that is consistently slow is more informative once it has been benchmarked than a watch that might be closer to the actual time, but whose behaviour around the actual time is random. In short, I think the people who ignore or down-play this Morgan series are not taking advantage of really useful information.
Back to the model: All of the volatility ended up in the variable effects daily walk, which is substantially influenced by the outliers.
For the nerds: My JAGS code for this is a bit more complicated than for earlier models. The variables y and y2 are the polling observations over the period (the series are identical - this is how I ensured the graph was acyclical). The observations are ordered in date order. The lower and upper variables map the range of the six-month centred window for estimating the variable effects against the fixed effects (this is calculated in R before handing to JAGS for the MCMC simulation). The lines marked with a triple $ sign are the lines that allow the fixed and variable elements of the model to inform each other.
model {
## -- temporal model for voting intention (VI)
for(i in 2:PERIOD) { # for each day under analysis ...
VI[i] ~ dnorm(VI[i-1], walkVIPrecision) # fixed effects walk
VI2[i] ~ dnorm(VI2[i-1], walkVIPrecision2) # $$$
}
## -- initial fixed house-effects observational model
for(i in 1:NUMPOLLS) { # for each poll result ...
roundingEffect[i] ~ dunif(-houseRounding[i], houseRounding[i])
yhat[i] <- houseEffects[ house[i] ] + VI[ day[i] ] + roundingEffect[i] ## system
y[i] ~ dnorm(yhat[i], samplePrecision[i]) ## distribution
}
## -- variable effects 6-month window adjusted observational model
for(i in 1:NUMPOLLS) { # for each poll result ...
count[i] <- sum(house[ lower[i]:upper[i] ] == house[i])
adjHouseEffects[i] <- sum( (y[ lower[i]:upper[i] ] - VI[ day[i] ]) *
(house[ lower[i]:upper[i] ] == house[i]) ) / count[i]
roundingEffect2[i] ~ dunif(-houseRounding[i], houseRounding[i]) # $$$
yhat2[i] <- adjHouseEffects[i] + VI2[ day[i] ] + roundingEffect2[i] # $$$
y2[i] ~ dnorm(yhat2[i], samplePrecision[i]) # $$$
}
## -- point-in-time sum-to-zero constraint on constant house effects
houseEffects[1] <- -sum( houseEffects[2:HOUSECOUNT] )
## -- priors
for(i in 2:HOUSECOUNT) { ## vague normal priors for house effects
houseEffects[i] ~ dnorm(0, pow(0.1, -2))
}
sigmaWalkVI ~ dunif(0, 0.01) ## uniform prior on std. dev.
walkVIPrecision <- pow(sigmaWalkVI, -2) ## for the day-to-day random walk
VI[1] ~ dunif(0.4, 0.6) ## initialisation of the voting intention daily walk
sigmaWalkVI2 ~ dunif(0, 0.01) ## $$$
walkVIPrecision2 <- pow(sigmaWalkVI2, -2) ## $$$
VI2[1] ~ dunif(0.4, 0.6) ## $$$
}
I suspect this is more complicated than it needs to be; any help in simplifying the approach would be appreciated.
Subscribe to:
Posts (Atom)

















































