Political Toxicity in Tweets
Example of using a Spanish classification model on Hugging Face, built by Newtral's NLP team, that scores how much "political toxicity" a tweet generates.
Tool
Introduction
“Wandering around” Hugging Face and looking at classification models in Spanish, I came across a model created by the NLP team at Newtral that assigns a tweet a score for the level of “political toxicity” that the tweet generates. After “discovering” this model, I couldn’t resist applying it to the corpus of tweets by Spanish politicians that I have been maintaining and updating daily since May 2019 up to the date of this article, and which I use for text classification examples in my Natural Language Processing Master’s classes.
The corpus in question collects the tweets of several accounts of Spanish politicians and labels these tweets with the political party they belong to. Since I use this corpus for teaching purposes, I ‘only’ collect tweets from the main political parties represented in the lower house: PSOE, PP, Vox, Unidas Podemos and Ciudadanos (listed in order of representation in parliament).
Warning
VERY VERY VERY IMPORTANT: This article has no political aim and takes no partisan stance; it does not take sides for or against any political party, it does not aim to harm or benefit any party, and of course it does not aim to harm or benefit any politician in particular. In fact, the corpus used does not identify the Twitter account that publishes each tweet, although it is obvious that it wouldn’t be hard to identify it, but we don’t do that in this article. In short, the aim is not to hurt anyone’s feelings. The purpose of this article is to show a possible use case of NATURAL LANGUAGE PROCESSING (NLP) using an already trained model and running it with the Hugging Face library (transformers) on a corpus that is used for teaching purposes.Results
So with this corpus and the political toxicity model, I went on to predict the toxicity of each tweet, getting the following as the 5 most toxic tweets(*):
(*)At the end of the article I show in an appendix the 30 tweets that the model considers “most toxic”.
At first glance it may seem surprising that 4 of the 5 tweets the model considers most toxic were written by politicians from Ciudadanos, but if we analyze the toxicity scores aggregated by political party, we see that the tweets posted by Vox politicians have the highest average toxicity score:
If we visualize the distribution of toxicity by quartiles in a box plot, we can see that Vox tweets tend to have higher toxicity scores than the rest of the tweets:
Conclusions
- Regarding the toxicity scores of the tweets, it seems clear that the model detects fairly well the tweets that are offensive, insulting, etc., in short, toxic. We can see this when reading the 5 tweets the model considers the most toxic (and also when reading the 30 most toxic at the end of the article): without getting into whether one is more toxic than another, we can see that they are all tweets with quite “destructive” messages.
- Before getting the toxicity scores of the tweets, we could expect Vox to be the political party with the highest toxicity score, and that is certainly what happened, with average toxicity values considerably higher than the rest of the political parties.
- Excluding Vox from the analysis, we can see that the two right-wing political parties (Ciudadanos and PP) have higher average toxicity values than the left-wing parties (Unidas Podemos and PSOE). This makes you think the model may have been trained on a biased corpus. What do I mean by this? If we read the documentation of the corpus used to train the model, it says that it used tweets written in 2021 by Spanish politicians in the Congress of Deputies and that these were scored (by human annotators) with a certain degree of toxicity. If the people who read and scored the tweets had certain ideological biases (for example, if they “leaned left”), they may have given higher scores to tweets by right-wing politicians than to tweets posted by left-wing politicians. This could be an explanation (not the only one) for the bias we see in toxicity levels between left-wing and right-wing parties.
- Finally, we notice the low toxicity levels obtained by PSOE. The most plausible explanation would be that it is the party in government (the corpus has tweets from May 2019 up to the publication date of this article), and it seems logical that the party in government and its politicians should keep a much more moderate and friendly tone in their tweets than the politicians in the opposition.
Implementing the tweet toxicity score
1.- Setting up the environment and libraries
To run the whole toxicity prediction process, I used an Anaconda virtual environment with Python 3.7. The following libraries were specifically installed in this environment:
- opendatasets: A library that lets us (among other things) download Kaggle datasets (with a username and API KEY).
- tweet-preprocesor: This library lets us clean tweets, removing links, mentions, hashtags, emoticons, etc.
- transformers: Hugging Face library for importing and using already trained models (mainly NLP).
- tqdm: A library I use to show a progress bar for processes (inside a for loop)
With the Conda virtual environment activated, we install these libraries in the Notebook itself that we’ll use for toxicity prediction and analysis:
!pip install opendatasets==0.1.22 --user
!pip install tweet-preprocessor==0.6.0 --user
!pip install transformers==4.17.0 --user
!pip install tqdm --user
2.- Reading the tweets
We are going to download the corpus (dataset) directly from Kaggle. To do so, using the “opendatasets” library, we’ll download the dataset with the “download()” function, passing it the Kaggle URL where the dataset is. When you run this command, it will ask you for your Kaggle username (you must be registered on Kaggle to download it) and then it will ask for an API KEY, which you can download from your Kaggle profile, as shown in the following image:
The code that downloads the corpus (saving it in the project’s file system) and loads it into a pandas dataframe would be the following:
import pandas as pd
import opendatasets as od
# Download the dataset from Kaggle
od.download('https://www.kaggle.com/datasets/ricardomoya/tweets-poltica-espaa', force=True)
# Load the tweets into a dataframe
df = pd.read_table('./tweets-poltica-espaa/tweets_politica_kaggle.csv',
sep='\t',
header=0,
engine='python')
print('\nNúmero de Tweets: {}'.format(df.shape[0]))
df.sample(5)
Next we show the percentage of tweets from each political party in the corpus, with the following code snippet:
import matplotlib.pyplot as plt
%matplotlib inline
# Count of tweets per political party
df_agg = df.groupby('partido')['tweet'].count().reset_index()
# Draw the results in a pie chart
colors={'psoe':'#E74C3C', 'pp':'#3498DB', 'vox':'#58D68D', 'podemos':'#8E44AD', 'ciudadanos':'#E67E22'}
plt.figure(figsize=(12, 6))
plt.pie(x=df_agg['tweet'].values,
labels=df_agg['partido'].values,
colors=[colors[key] for key in df_agg['partido'].values],
autopct='%1.2f%%')
plt.show()
3.- Preprocessing and cleaning the tweets
Although this step isn’t strictly necessary for predicting a tweet’s toxicity, it’s a good idea to do a little cleaning of the tweets, removing text that doesn’t provide relevant information about how toxic a tweet is. In this case, using the “tweet-preprocesor” library and configuring it to remove URLs, mentions, hashtags, and emoticons (among others) from the tweets, we’ll strip all that text from each one, so that we’re left with a cleaner tweet to pass to the model and it has less text to process, which will reduce prediction times. Below is the code that does this cleaning, saving the “clean” tweet in a new column (clean_tweet) of the pandas dataframe.
import preprocessor as p
import unidecode
from tqdm import tqdm
# Configure tweet-preprocessor to remove from the tweet: URLs, mentions, emoticons, etc.
p.set_options(p.OPT.URL, p.OPT.MENTION, p.OPT.HASHTAG, p.OPT.EMOJI, p.OPT.SMILEY, p.OPT.ESCAPE_CHAR)
# Clean the tweets
for i in tqdm(range(df.shape[0])):
# Clean the tweet with tweet-preprocessor and convert it to lowercase
df.loc[i, 'clean_tweet'] = (p.clean(unidecode.unidecode(df.iloc[i]['tweet'].lower())))
df[['partido', 'timestamp', 'tweet', 'clean_tweet']].sample(5)
4.- Predicting the toxicity of the tweets
To predict the toxicity of a tweet, we first need to import the Hugging Face model created by the Newtral team. This is as easy as:
from transformers import pipeline
# Import the model
toxicity_scorer = pipeline("text-classification",
model="Newtral/xlm-r-finetuned-toxic-political-tweets-es")
Once we have the model imported (in the “toxicity_score” object), using it is as simple as passing the text to predict to the model object:
toxicity_scorer("TEXTO CON EL TWEET PARA SU PUNTUACIÓN", return_all_scores=True)
This will return a data structure with the “Toxic” and “Very_Toxic” scores (LABEL_0 and LABEL_1) of the tweet we passed in.
Next we show the code snippet in which we pass the “clean” tweet to the object that holds the model and save in the dataframe (in 3 new columns) the 2 toxicity values the model returns (“Toxic” and “Very_Toxic”) plus a new column with the sum of the 2 scores. IMPORTANT: this process is very computationally expensive and can take several hours to run, depending on your computer’s specs:
from transformers import pipeline
# Import the model
toxicity_scorer = pipeline("text-classification",
model="Newtral/xlm-r-finetuned-toxic-political-tweets-es")
# Get the scores of each of the tweets
for i in tqdm(range(df.shape[0])):
score = toxicity_scorer(df.iloc[i]['clean_tweet'], return_all_scores=True)
df.loc[i, 'toxic'] = score[0][0]['score']
df.loc[i, 'very_toxic'] = score[0][1]['score']
df.loc[i, 'toxic_score'] = score[0][0]['score'] + score[0][1]['score']
5.- Ranking of the most toxic tweets
With the tweets already scored, we sort them from most to least toxic and show the 10 most toxic tweets.
from datetime import datetime
# Sort the tweets by toxicity from highest to lowest
df_rank = df.sort_values('toxic_score', ascending=False).reset_index()
# Show the 10 most toxic tweets according to the model
TOP = 10
for i in range(TOP):
print('{}º Posición:'.format(i+1))
print('\tPartido Político: {}'.format(df_rank.iloc[i]['partido'].upper()))
print('\tFecha: {}'.format(datetime.fromtimestamp(int(df_rank.iloc[i]['timestamp'])).strftime('%d-%m-%Y')))
print('\tScore: {:.6f}'.format(df_rank.iloc[i]['toxic_score']))
print('\tTweet: {}\n'.format(df_rank.iloc[i]['tweet']))
print('----------------------------------------------------------------------------')
The output of this code snippet is not shown, since the 5 tweets considered most toxic are shown at the beginning of the article and the 30 most toxic tweets at the end of the article.
6.- Charts with the distributions and toxicity of the tweets by political party
Finally, we do a simple toxicity analysis of the tweets posted by members of the different political parties, aggregating the scores. First we calculate the average toxicity values:
df_agg = (df_rank
.groupby('partido')['toxic_score']
.mean()
.reset_index()
.sort_values('toxic_score', ascending=False))
The same values shown in a bar chart:
import matplotlib.pyplot as plt
import seaborn as sns
fig, ax = plt.subplots(figsize=(12, 6))
sns.barplot(x="partido",
y="toxic_score",
data=df_rank,
order=df_agg['partido'],
palette=colors)
ax.set_title('Toxicidad Media de lo Tweets por Partido Político')
ax.set_ylabel('Score Toxicidad (Media)')
ax.set_xlabel('Partido Político')
ax.grid()
Quartile distribution of the toxicity scores:
fig, ax = plt.subplots(figsize=(12, 6))
sns.boxplot(x="partido",
y="toxic_score",
data=df_rank,
order=df_agg['partido'],
palette=colors)
ax.set_title('Distribución Toxicidad de lo Tweets por Partido Político')
ax.set_ylabel('Score Toxicidad (Media)')
ax.set_xlabel('Partido Político')
APPENDIX: 30 Tweets with the highest toxicity score
1º Posición:
Partido Político: CIUDADANOS
Fecha: 20-06-2019
Score: 1.991903
Tweet: Que es como decir que el castellano es la lengua de las putas y los camellos. Una nueva ocurrencia de la fanática que quemó la Constitución en TV3.Estáis rociando Cataluña de gasolina desde hace décadas, Empar. Un día saltará una chispa. https://t.co/uKuIjpYkka
2º Posición:
Partido Político: VOX
Fecha: 03-12-2019
Score: 1.991774
Tweet: Lamentable es que dejárais a los separatistas dar un golpe de Estado y sentárais las bases para el segundo. Tuit ridiculo. ¿Pero no eramos fachas irredentos? Sois trileros y desleales, y la gente lo sabe.¿Todo para azuzar el miedo? Si tenéis miedo, apartaos, ¡que vamos! https://t.co/SBDNmZ53Ce
3º Posición:
Partido Político: CIUDADANOS
Fecha: 03-05-2021
Score: 1.991752
Tweet: Para ser un auténtico oprimido tienes que tener la nómina de un conseller y una corte de aduladores colocados en todo tipo de chiringuitos.Por dinero y poder, Junts y ERC son capaces de gobernar juntos sin ni tan siquiera dirigirse la palabra.https://t.co/8D1zAPyWil
4º Posición:
Partido Político: CIUDADANOS
Fecha: 04-01-2020
Score: 1.991741
Tweet: A ver si se entera @NestorRego que no puede hablar en esa tribuna en nombre de todos los gallegos. Los gallegos somos gente de bien, y no Kamikaces desnortados que se vendan por 1 €. Lástima la ingenuidad de este @obloque creyéndose la palabra del infame de Sánchez y su pacto https://t.co/iVcstiLyOq
5º Posición:
Partido Político: CIUDADANOS
Fecha: 24-11-2021
Score: 1.991736
Tweet: A Sánchez ya no le queda más libertad por perder ante los separatistas:❌Sacar a la @policia de Via Laietana❌Ley infame para las universidades❌Adoctrinamiento📡@InesArrimadas “Regalan este pack de la vergüenza al nacionalismo para mantenerse en Moncloa” #ActualidadCs https://t.co/ZP0VgZ6hiz
6º Posición:
Partido Político: VOX
Fecha: 17-03-2021
Score: 1.991732
Tweet: A los que hoy se rasgan las vestiduras hablando del suicido, errejonistas, podemitas y demás patulea pogre: muchos de los que se suicidan son hombres traumatizados por las denuncias falsas de violencia “de género” y por no poder ver a sus hijos.Y os da igual. Hipócritas.
7º Posición:
Partido Político: VOX
Fecha: 07-02-2022
Score: 1.991727
Tweet: Zapatero anda ya por el mundo con una escolta como la de un Chapo Guzmán. Como los grandes narcos y asesinos que dirigen allí los negocios del “progresismo”. Nada sería más justo que dejar a Zapatero sin escolta en cualquier rincón del país que más sufre su infamia infinita. https://t.co/4sh1dP7FzF
8º Posición:
Partido Político: VOX
Fecha: 06-09-2021
Score: 1.991721
Tweet: @Espectadorescep @Frexer3000 @_JJOAKINN_ @beerman_garcia @CapitanBitcoin Eso os pasa a los progreanalfabetos, que tiráis de wikipedia y no solo os creéis todas sus trampas sectarias y manipulaciones sino que no tenéis los elementos básicos para entender las oraciones enteras.
9º Posición:
Partido Político: VOX
Fecha: 13-05-2021
Score: 1.991710
Tweet: Contra la libertad que es libertinaje, Casado quiere medios para acabar con estas malditas manías de los españoles de moverse por España sin permiso suyo. https://t.co/W4moymYHU0
10º Posición:
Partido Político: PP
Fecha: 11-08-2021
Score: 1.991700
Tweet: Podemos amenaza con salir a la calle por la luz: “Daremos la batalla en todos los espacios” “En todos los espacios” excepto en el gobierno al que pertenecen. Son gobierno y oposición. Unos inútiles charlatanes. https://t.co/v5bY8kumf0
11º Posición:
Partido Político: PP
Fecha: 29-05-2021
Score: 1.991699
Tweet: Todo eran mentiras de unos dirigentes tramposos y sin escrúpulos. Ahora todo vale para mantenerse en el helicóptero riéndose de los españoles https://t.co/LfTJsb0JNV
12º Posición:
Partido Político: VOX
Fecha: 08-09-2021
Score: 1.991678
Tweet: El voto al PP de Casado es tan tóxico como el voto a Sánchez. O peor. Porque volverá a embarrar todo para confundir al español de buena voluntad. Y solo tapará con mentiras las heridas hoy abiertas para que pudran más como hicieron bajo Rajoy. España tiene que sanar sus heridas.
13º Posición:
Partido Político: VOX
Fecha: 09-01-2022
Score: 1.991667
Tweet: Vamos a ver, Carmeida, aquí el único que ha vetado a los madrileños y a la verdad has sido tú, engañándolos y traicionándolos.https://t.co/hJAipqyDsJ
14º Posición:
Partido Político: PODEMOS
Fecha: 28-02-2020
Score: 1.991660
Tweet: De los productores de “Paletos”, con Díaz Ayuso, llega a sus pantallas “Otro pijazo madrileño de derechas que insulta a toda la España Vaciada”. Protagoniza Jorge “prefiero a un corrupto que a un comunista” Bustos, jefe de opinión en El Mundo. https://t.co/GjjGMYDLCe
15º Posición:
Partido Político: PP
Fecha: 08-11-2021
Score: 1.991658
Tweet: La propaganda de Sánchez: “No vamos a dejar a nadie atrás”Los hechos: Con Sánchez toda España se queda atrás 👉 @FinancialTimes “España se queda atrás#HayAlternativa al (des)gobierno de Sánchez que arruina 🇪🇸 https://t.co/uCzrYJFDvB
16º Posición:
Partido Político: CIUDADANOS
Fecha: 08-03-2022
Score: 1.991656
Tweet: Es vergonzoso que la Generalitat siga con su aquelarre separatista, gastando el dinero de todos en chiringuitos en el extranjero. Con la crisis que estamos sufriendo deberían centrarse en atraer inversiones y no en vender odio a España.https://t.co/455d7X7yj1
17º Posición:
Partido Político: VOX
Fecha: 04-11-2021
Score: 1.991649
Tweet: Unai Sordo rechaza a los trabajadores de España de @solidaridad_esp Como buen subvencionado, prefiere sentarse con las patronales subvencionadas, los separatistas subvencionados, los chiringuitos de género subvencionados. Bravo por Solidaridad https://t.co/MIy9Ynjeb0
18º Posición:
Partido Político: VOX
Fecha: 18-06-2019
Score: 1.991648
Tweet: 😡 Socialismo en estado puro: el chiringuito cultural de Susana Díaz dedicaba 23 millones a pagar sueldos y sólo uno a la gestión cultural. ✅ Esperamos que la consejera actual tenga el valor de acabar con esta mamandurria.⬇⬇⬇https://t.co/j4vFkgcA0O
19º Posición:
Partido Político: VOX
Fecha: 17-03-2021
Score: 1.991647
Tweet: Esta televisión de Madrid se la entregaron Ciudadanos y el PP a Podemos y a los sórdidos matones sindicales. Así están. Esta es la televisión pública madrileña: “La que ha liado Isabel Díaz Ayuso, la que ha liado” – Periodista Digital https://t.co/NtvM5iDqAH
20º Posición:
Partido Político: VOX
Fecha: 28-10-2021
Score: 1.991641
Tweet: Los enemigos de España y la libertad son forofos de George Soros. El izquierdismo separatista y el corrupto consenso socialdemócrata rezan a su gurú con zozobra. Tienen miedo a la resistencia. No son Polonia y Hungria solas, es toda la Europa de las naciones. La auténtica Europa. https://t.co/jktlBWxbaF
21º Posición:
Partido Político: VOX
Fecha: 29-01-2020
Score: 1.991641
Tweet: Pues que no se preocupe el señor Cocho que ya lo hacemos los de VOX: Un gobierno que pacta con golpistas y comunistas, después de haber dicho en campaña que nunca lo haría, es un gobierno legal pero ilegítimo, y su presidente, un traidor a España y a los españoles
22º Posición:
Partido Político: VOX
Fecha: 02-02-2022
Score: 1.991639
Tweet: ¡Puigdemont, golpista cobarde! Menos preguntas lloronas a la Comisión Europea y más rendir cuentas de tus delitos ante los tribunales españoles. No te vas a salir de rositas. En @vox_es no nos olvidamos de tus delitos. Y más pronto que tarde…tic, tac, tic, tac https://t.co/qahEK24NvD
23º Posición:
Partido Político: VOX
Fecha: 06-09-2021
Score: 1.991631
Tweet: Esto es Macron, el ultracentrismo. Que pega palizas como la policía de Putin. ¿La obsesión de Casado por tener una ley de pandemia para recortar nuestros derechos fundamentales se debe a las ganas de imitar al cínico e hipócrita napoleoncito? https://t.co/pF3uI0bwLL
24º Posición:
Partido Político: PODEMOS
Fecha: 14-12-2021
Score: 1.991623
Tweet: Iván Espinosa de los Monteros es un defraudador condenado. Sabemos que la ultraderecha no tiene escrúpulos y que aprendieron del partido más corrupto de Europa, pero tenga un poco de decencia, deje de manchar las instituciones y dimita.
25º Posición:
Partido Político: VOX
Fecha: 09-04-2021
Score: 1.991614
Tweet: @MiquelGimenezG1 @ACOM_es Los ayuntamientos con podemitas y sus socios gabilondos por toda la geografía española se han convertido en covachas antisemitas que financian a terroristas, incitan al odio a Israel y banalizan por sistema el Holocausto. Hay que evitar que esa mugre inmoral gobierne Madrid.
26º Posición:
Partido Político: PP
Fecha: 17-01-2020
Score: 1.991612
Tweet: 📚 Qué barbaridad, señora Celaá. ¿Cómo que los hijos no pertenecen a los padres?Los hijos a quien no le pertenecen es a usted, ni a la señora Montero, ni al señor Sánchez. https://t.co/zlPNygbfOT
27º Posición:
Partido Político: CIUDADANOS
Fecha: 03-06-2019
Score: 1.991610
Tweet: El separatismo sigue dando tratos privilegiados a sus amigos, ya sean corruptos o golpistas. Es vergonzoso que se exija a los ciudadanos que cumplan con la ley, mientras los líderes nacionalistas quieren impunidad para seguir haciendo de las suyas. https://t.co/kkhtmQPQth
28º Posición:
Partido Político: CIUDADANOS
Fecha: 03-06-2019
Score: 1.991610
Tweet: El separatismo sigue dando tratos privilegiados a sus amigos, ya sean corruptos o golpistas. Es vergonzoso que se exija a los ciudadanos que cumplan con la ley, mientras los líderes nacionalistas quieren impunidad para seguir haciendo de las suyas. https://t.co/kkhtmQPQth
29º Posición:
Partido Político: VOX
Fecha: 12-04-2021
Score: 1.991607
Tweet: Nadia Calviño es una estafadora política tan consumada co,o Sánchez, Iglesias o Yolanda Díaz.Es una estafa más pretender que en este gobierno queda cualquier resto de probidad. https://t.co/at3Bpx2FTp
30º Posición:
Partido Político: PODEMOS
Fecha: 10-02-2021
Score: 1.991607
Tweet: Rapero de izquierdas canta que el emérito es corrupto 👉 a prisión.Neonazi que trabajó para VOX dispara con una escopeta a las fotos del Presidente, el Vicepresidente y varios Ministros 👉 no hay delito.Plena normalidad democrática. Di que sí. https://t.co/ahDuUGfVU5
“Political Toxicity in Tweets” by Ricardo Moya at jarroba.com, licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0. Registered at Safe Creative.

You may also like
“If It Works, Don’t Touch It” and Other Excuses That No Longer Hold
Four corporate excuse-phrases, why the calculus has changed with AI, and what to do if you're stuck in an inertia-bound organization.
Tricks for Using AI Well
Context before question, structured prompts, iterative refinement, adversarial agents, human linter, and knowing when to stop. Six AI tricks.










Leave a comment