Are Manchester United fans more critical of their team than opposing fans about theirs?

This was the (too ambitious) question I intended to answer when I began this piece but having discovered some of the difficulties in accessing bulk, multi-period Twitter data I have scaled it back. I certainly intend to revisit the bigger question using Twitter scraping solutions that have since become available, but for now I’ll restrict myself to the more achievable question:

Is there evidence in tweets from football fans during matches that one fanbase is more critical than another?

I was born and grew up in Manchester, England. I live in the US now and I’m forced to follow the English Premier League remotely. I track my Twitter feed during games to see the opinions of other fans. One thing I noticed was the propensity of Manchester United fans to expect heroics from the team on a weekly basis and I wondered whether it was any different for fans of other clubs. One way to tackle this is to examine the sentiment of opposing fans watching the same football match but necessarily wanting different outcomes: their own team to win. As we will see, this method is very far from experimental conditions, but the approach does give a better sense of the art of the possible.


First, I load the packages I’ll use in R. Tidyverse and tidytext will help me format the data and prepare my analysis. The twitteR and ROauth packages are what I will use to create my sentiment data for the football teams in question. If you do not have the pacman package installed, run the line install.packages(‘pacman’) in the console.

Full documentation for how to use both those Twitter-related packages can be found here in the user vignette.

#Loading the relevant packages
pacman::p_load(tidyverse, twitteR, ROAuth, tidytext, psych,lubridate, SentimentAnalysis, scales, RCurl, tm, grid, wrapr, broom)

Next, I setup my Twitter authentication information so I can start accessing the relevant API. You will need a Twitter account to do this. Full details are included the vignette linked above.

Data acquisition

I download a set of relevant tweets from the Twitter API for Manchester United’s game against Yeovil Town in the FA Cup played on January 26, 2018. I leave the date range broad (all of January 26) so that I can later refine the inquiry to tweets an hour before the match starts and shortly after it ends to capture pre-match expectations and post-game sentiment for both sets of fans.

mufc_tweets1 <- searchTwitteR("manchesterunited", n=2000, lang="en", since="2018-01-26", until = "2018-01-26") %>% 
  strip_retweets(strip_manual = TRUE, strip_mt = TRUE) # ...remove manual retweets that are labeled 'RT'.
mufc_tweets2 <- searchTwitteR("#mufc", n=2000, lang="en", since="2018-01-26", until = "2018-01-26") %>% 
  strip_retweets(strip_manual = TRUE, strip_mt = TRUE) # ...remove manual retweets that are labeled 'RT'.
mufc_tweets3 <- searchTwitteR("#manu", n=2000, lang="en", since="2018-01-26", until = "2018-01-26") %>% 
  strip_retweets(strip_manual = TRUE, strip_mt = TRUE) # ...remove manual retweets that are labeled 'RT'.

mufc_tweets <- merge.list(mufc_tweets1, mufc_tweets2, mufc_tweets3) # I merge the three Twitter lists into a single list before conversion into a dataframe using the merge.list function from the RCurl package.

My intent is to find tweets from Manchester United fans. This is an imperfect approach that will suffice for now. While I certainly access fan tweets, the use of football club hashtags by no means guarantees that the ‘tweeter’ is a supporter of Manchester United. Frequently, commercial tweets or rival fans use club hashtags to attract the attention of ’real fans. Let’s look at a few examples from the combined list we just created:

This is certainly a useful example of a fan tweet for sentiment analysis purposes. It conveys fan satisfaction with the final result.

## [1] "kartikkapadia: 4-0. Time well spent #ManchesterUnited"

This tweet before the game starts is designed to attract the attention of Manchester United and other football fans by using the ‘#ManchesterUnited’ hashtag along with a media hashtag. It is problematic because it uses the word ‘killing’, which will be picked up in our sentiment analysis as a negative sentiment associated with Manchester United fans. With more time we could create a custom stopwords dictionary that removes common media soundbites, or we might remove tweets entirely that contain media hashtags or media-related Twitter handles embedded within them.

## [1] "FTouchsoccer: Giant killing tonight? Yes please #JaredBird\n\n#Yeovil v #ManchesterUnited #FS1"

I convert the output to a data frame, which is a more intuitive format for me to work with:

#convert output to a data frame
mufc_tweets_df <- twListToDF(mufc_tweets)
dim(mufc_tweets_df) # examine dimensions, 1,106 instances with 19 variables

I examine the column names to get a better sense of the variables in the data. The data is fairly intuitive. We are most interested in the ‘text’ variable as this contains the subject matter for our sentiment analysis.

glimpse(mufc_tweets_df) # lists variable names, types and key stats
## Observations: 1,106
## Variables: 19
## $ text            <chr> "Yeovil 0-4 Manchester United - Full Post Matc...
## $ favorited       <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS...
## $ favoriteCount   <dbl> 0, 0, 2, 1, 3, 0, 0, 4, 0, 0, 6, 0, 0, 3, 2, 3...
## $ replyToSN       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ created         <dttm> 2018-01-26 23:57:53, 2018-01-26 23:57:24, 201...
## $ truncated       <lgl> FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, FALSE,...
## $ replyToSID      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ id              <chr> "957039924506656769", "957039800577593344", "9...
## $ replyToUID      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ statusSource    <chr> "<a href=\"\...
## $ screenName      <chr> "BradleyHarrin20", "FanBoyos", "manutdnewsonly...
## $ retweetCount    <dbl> 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1...
## $ isRetweet       <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS...
## $ retweeted       <lgl> FALSE, FALSE, FALSE, FALSE, FALSE, FALSE, FALS...
## $ longitude       <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ latitude        <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ location        <chr> "Billericay, East", "", "", "B...
## $ language        <chr> "en", "en-gb", "en", "en", "en", "en", "en-gb"...
## $ profileImageURL <chr> "

I clean the text field of special characters and gambling references. When I first wrote this code I did not implement it as a function; when I realized I would need to reuse the approach multiple times I wrap it to make it faster for future use.

cleantweet <- function(x) { #creates the function in R
  data("stop_words") # stop words dict as a data frame from the tidytext package.
  stopwords <- as.vector(stop_words$word) # use Tidytext stopwords dictionary
  x$text <- removeWords(x$text, stopwords)
  replace_reg <-
  "[A-Za-z\\d]+|http://[A-Za-z\\d]+|&amp;|&lt;|&gt;|RT|https|#039;|\n|[^\x01-\x7F]|�|  " # This is a good start but can be refined.
  x <-
  x %>%  # regular expressions (regex) useful for cleaning twitter data
  mutate(text = str_replace_all(text, replace_reg, "")) %>%
  !str_detect(text, "LIVE") | # Regex specific to sports tweets that are riddled with gambling advertisements
  !str_detect(text, "Betfair") |
  !str_detect(text, "Stream") |
  !str_detect(text, "Betting") |
  !str_detect(text, "betting") |
  !str_detect(text, "stream")

mufc_tweets_df <- cleantweet(mufc_tweets_df) # run the function on the Twitter dataframe

I convert the relevant text column to a text vector so it can be used by the tm package’s removeWords function. I used this because I want to leave the tweets whole, and not disaggregate using unnest_tokens() in ‘tidytext’.

I run sentiment analysis on the cleaned text field, again using a function so that I can repeat this analysis quickly in the future.

mufc_data <- mufc_tweets_df %>%
  select(text, favoriteCount, retweetCount, created, location) # Select the fields I will use for the sentiment analysis and for plotting the results 
  sentiment_tw <- function(x) {
  vec <- as.vector(x$text)
  sentiment <- analyzeSentiment(vec)
  sentiment$SentimentQDAP <- as.numeric(sentiment$SentimentQDAP)
  x <- cbind(x, sentiment$SentimentQDAP)
  x <- rename(x, sentiment = `sentiment$SentimentQDAP`)
  mufc_data <- sentiment_tw(mufc_data) # running the sentiment function on our Twitter data

Now I use posix strings that approximate pre- and post-game time periods to help me ‘zoom in’ on the sentiment I want to examine.

preko <- as.POSIXlt("2018-01-26 19:45:00")
postgame <- as.POSIXlt("2018-01-26 22:00:00")
int <- interval(preko, postgame) #Creates an interval object using the lubridate package 
mufc_game <- mufc_data[mufc_data$created %within% int,] #This reduces my data to tweets within the specified interval
row.names(mufc_game) <- 1:nrow(mufc_game) # renumber the rows consecutively from 1 so that the goal times are easier to find later for the sentiment plot.

We reexamine the data after these changes:

glimpse(mufc_game) # lists variable names, types and key stats
## Observations: 414
## Variables: 6
## $ text          <chr> "Angel Gomes,! #ManchesterUnited", "Glory ! Glor...
## $ favoriteCount <dbl> 1, 0, 0, 0, 46, 0, 2, 1, 1, 3, 0, 0, 0, 0, 2, 0,...
## $ retweetCount  <dbl> 0, 0, 0, 0, 35, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0,...
## $ created       <dttm> 2018-01-26 21:59:56, 2018-01-26 21:59:39, 2018-...
## $ location      <chr> "Megamind Concept Gh|@ManUtd ", "Kota Kinabalu, ...
## $ sentiment     <dbl> 0.33333333, 0.66666667, 0.00000000, 0.09090909, ...

Now I plot the sentiment results on a timeline with ggplot. I’m using the grid package’s textGrob and annotation_custom functions to add major events to the plot, e.g. when the game starts and ends, and when each goal is scored. These events are inserted using approximate times based on tweets about the events. The plot itself is a heatmap of 2-dimensional bin counts using geom_bin2d. The color scheme is from Manchester United’s official team colors. I take the same approach with Yeovil Town. I have only displayed a selection of the grob text to prevent it overwhelming the code chunk.

grob1 <- grobTree(textGrob("Happy\nTweets", x=0.46,  y=0.90, hjust=0,
  gp=gpar(col="gray50", fontsize = 16, fontfamily = "Arial", fontface = "bold")))

grob2 <- grobTree(textGrob("Angry\nTweets", x=0.46,  y=0.1, hjust=0,
  gp=gpar(col="gray50", fontsize = 16, fontfamily = "Arial", fontface = "bold")))

ggplot(data = mufc_game, aes(x = as.POSIXct(created), y = sentiment)) +
  geom_bin2d() +
  labs(x = "\nTime", y = "Sentiment", title = "Figure 1. Manchester United Twitter fan sentiment", subtitle = "Yeovil Town F.C. versus Manchester United F.C.\nFA Cup Round 4, Jan 26 2018\n", fill = "Tweet\ncount", caption = "Analysis: Tim Raiswell. Source: Twitter.") +
  scale_fill_gradient(low = "#DA020E", high = "black", space = "Lab",
  na.value = "grey50", guide = "colourbar") +
  atheme +
  annotation_custom(grob1) +
  annotation_custom(grob2) +
  annotation_custom(grob3) +
  annotation_custom(grob4) +
  annotation_custom(grob5) +
  annotation_custom(grob6) +
  annotation_custom(grobk) +
  annotation_custom(grobf) +
  geom_hline(yintercept = 0, color = "black", size = 2) +
  geom_vline(xintercept = mufc_game$created[173], color = "#DA020E") +
  geom_vline(xintercept = mufc_game$created[130], color = "#DA020E") +
  geom_vline(xintercept = mufc_game$created[66], color = "#DA020E") +
  geom_vline(xintercept = mufc_game$created[45], color = "#DA020E") +
  geom_vline(xintercept = mufc_game$created[340], color = "#DA020E", size = 1.8, alpha = 0.5) +
  geom_vline(xintercept = mufc_game$created[44], color = "#DA020E", size = 1.8, alpha = 0.5)


Figures 1 and 2 show Manchester United and Yeovil fan sentiment before, during and after the match. Both sets of fans seem equally neutral heading into the event. After the game starts, Yeovil fan sentiment in Figure 2 shows greater variation. Relatively high positive and negative scores for tweets precede the first (Manchester United) goal, scored just before half-time in the 41st minute by Marcus Rashford. After the goal, as we might expect, Yeovil fan tweets seem to be more consistently angry (negative sentiment). Interestingly, Manchester United fan tweets in Figure 1 - where we might expect elation - are somewhat neutral. There are several potential reasons for this:

  1. Manchester United is playing a lower league club in Yeovil. The team’s fans expect the team to win the game handily. A goal as delayed as this prompts relief but not necessarily elation.
  2. Related to item #1, Manchester United fans are less likely to take to Twitter to celebrate a goal against a team from a lower league.
  3. Our techniques for capturing a robust sample of fan sentiment on Twitter are limited.

As the game progresses and as it becomes clearer that Manchester United are going to win, Yeovil fan sentiment lightens. In Figure 2, there is an observable drop in negative sentiment as the full time whistle approaches. The same cannot be said of Manchester United fans in Figure 1. United fans seem to become frustrated as their team creates chances even though four of these chances resulted in goals. I am narrativizing here; a sloppy method. I watched the game and felt these sentiments myself. Let’s check on the content of the tweets to see if this storyline is somewhat justified.

This tweet expresses frustration with a refereeing decision towards the end of the game.

## [1] "Awwwww. Offside. Why @juanmata8 smile? #MUSCbean #ManchesterUnited#mufc#RedArmy#keepgrinding"

Excuse the language. This tweet expresses frustration with Manchester United player Jesse Lingard.

## [1] "Oi fucking hell @JesseLingard #ManchesterUnited"

And while this Twitter user is frustrated with a television pundit, the sentiment is indicative of general Manchester United fan sentiment at this point in the game.

## [1] "Martin keown giving sounessrun bitter pundit #ManchesterUnited #YEOMUN #football"

So, despite the fact that Manchester United is winning, some of the team’s fans are unhappy. Why? To answer that question in full would require several blogs, and months - maybe years - of sentiment analysis. Like Yankees fans in baseball, Manchester United fans have very high expectations. Since the retirement of the club’s talismanic, and record-breaking manager Sir Alex Ferguson in 2014, the club and its fans have endured relative failure compared to the heady successes of the 1990s and early 2000s. The tribal nature of European football fans, and the boiling pot that is football fandom on Twitter combine into a weekly groan-zone as fans desperately try to assure themselves that the glory days will return, and attack anything and anyone that suggests they won’t.

Let’s take a look at the aggregate sentiment for the game in two ways. First we’ll plot a histogram of fan sentiment that will help us to compare the teams side by side. And then we’ll run a t-test to see if the differences in fan sentiment are statistically significant.

ggplot() +
  geom_histogram(data = ytfc_game, aes(sentiment), fill = "#377D22", alpha = 0.7, binwidth = 0.06) +
  geom_histogram(data = mufc_game, aes(sentiment), fill = "#DA020E", alpha = 0.7, binwidth = 0.06) +
  scale_y_continuous(limits = c(0, 60)) +
  atheme +
  labs(x = "Sentiment", y = "Count", title = "Figure 3. Histogram of Manchester United and Yeovil Town fan Twitter sentiment", subtitle = "Yeovil Town F.C. versus Manchester United F.C.\nFA Cup Round 4, Jan 26 2018\n", caption = "Analysis: Tim Raiswell. Source: Twitter.")

The mean sentiment of Manchester United fans for the game period is 0.024; for Yeovil fans it is 0.036. Does this answer our question above? Are United fans more critical of their team and less likely to enjoy a game, even if they win? Sentiment scores for both sets of fans fail a Shapiro-Wilk normality test, so we use a non-parametric, two-sample Wilcoxon rank test.

tidy(wilcox.test(mufc_game$sentiment, ytfc_game$sentiment, alternative = "two.sided"))
##   statistic   p.value                                            method
## 1    133469 0.0107798 Wilcoxon rank sum test with continuity correction
##   alternative
## 1   two.sided

The p-value of the Wilcox test is 0.01078, lower than the alpha of 0.05 (95% significance level), which indicates that the differences in sentiment between fans are significant. But Figure 3 tells a more nuanced story. First, I have limited the Y (count) axis to 60. The majority of tweets we captured are neutral, hence the gap in the middle of the chart. This filter is so we can see the extreme sentiment values (angry to the left, happy to the right). We see that Yeovil fans on Twitter actually convey a broader range of emotions, both positive and negative. But the most negative sentiments for from Yeovil fans; and the most positive sentiments are from United fans.


Yeovil Town fans express more positive sentiment than Manchester United fans on Twitter for the game time period despite getting beat by four goals to nil. There is more than one factor at play that gets us to this result. First, the data is incomplete as I outline above. Second, Yeovil fans likely never expected to win the game; as such, just playing against a Premier League club and getting this far in the competition likely buoyed their mood. Third, Manchester United fans are harder to please than Yeovil fans.

The comparison of an English Premier League fanbase with the fanbase of a club from a lower league is unfair and bad science in some respects, but the analysis does yield interesting results that I’d like to test in the future with sentiment analysis of multiple games and teams. Other interesting approaches might involve lagging sentiment against club events like games won and lost, or goals scored and conceded, to see if various fanbases react differently, and whether supporting a storied club with an expectant fanbase is really a cause for joy at all.