Análisis de tweets durante las elecciones generales 2023 en Argentina

Author

Fernanda Aguirre

Published

January 15, 2024

Datos

Code
# Print the number of tweets analyzed
print(f"Número de tweets analizados: {len(df)}")
Número de tweets analizados: 79887

Eventos monitoreados

Cantidad de tweets para cada uno de los eventos monitoreados:

Code
# Get the value counts of the "event" column in the dataframe "df"
df["event"].value_counts()
event
elecciones balotaje     27448
elecciones generales    19970
1er debate              12588
debate balotaje         12440
2do debate               7441
Name: count, dtype: int64

Fechas de eventos

Periodo cubierto para cada evento

Code
# Define a function to get the date range of a given event
def get_date_range(df, event):
    event_data = df.loc[df["event"] == event]
    return event_data["dt_date"].min(), event_data["dt_date"].max()


# Get the date ranges for each event
debate1_min, debate1_max = get_date_range(df, "1er debate")
debate2_min, debate2_max = get_date_range(df, "2do debate")
elecciones_gen_min, elecciones_gen_max = get_date_range(df, "elecciones generales")
debate_balotaje_min, debate_balotaje_max = get_date_range(df, "debate balotaje")
elecciones_balotaje_min, elecciones_balotaje_max = get_date_range(
    df, "elecciones balotaje"
)

# Print the date ranges
print(f"El primer debate contempla tweets desde {debate1_min} hasta {debate1_max}")
print(f"El segundo debate contempla tweets desde {debate2_min} hasta {debate2_max}")
print(
    f"Las elecciones generales contempla tweets desde {elecciones_gen_min} hasta {elecciones_gen_max}"
)
print(
    f"El debate del balotaje contempla tweets desde {debate_balotaje_min} hasta {debate_balotaje_max}"
)
print(
    f"Las elecciones del balotaje contempla tweets desde {elecciones_balotaje_min} hasta {elecciones_balotaje_max}"
)
El primer debate contempla tweets desde 2023-09-30 hasta 2023-10-02
El segundo debate contempla tweets desde 2023-10-07 hasta 2023-10-09
Las elecciones generales contempla tweets desde 2023-10-19 hasta 2023-10-23
El debate del balotaje contempla tweets desde 2023-11-11 hasta 2023-11-13
Las elecciones del balotaje contempla tweets desde 2023-11-16 hasta 2023-11-20

Ataques identificados

NOTA: Un tweet puede tener diversas etiquetas

Code
# Subset of dataframe `df` containing only rows with non-null values in "label" and "to_journalist" columns
attacks = df.dropna(subset=["label", "to_journalist"])

# Print the number of rows in `attacks` dataframe
print(
    f"En los datos se identificaron {len(attacks)} publicaciones etiquetadas como ataques."
)
En los datos se identificaron 4319 publicaciones etiquetadas como ataques.

Ranking the periodistas más atacados

Code
attacks["to_journalist"].value_counts()
to_journalist
@diegobranca       712
@Cris_noticias     427
@JonatanViale      421
@Angelalerena      409
@edufeiok          399
@rialjorge         261
@odonnellmaria     258
@guadavazquez      216
@robdnavarro       209
@vivicanosaok      159
@luisnovaresio     154
@cyngarciaradio    152
@rominamanguel      95
@majulluis          85
@mjolivan           72
@Gatosylvestre      66
@nbg__              59
@NANCYPAZOS         58
@ischargro          27
@anaecorrea         21
@ertenembaum        11
@hindelita          10
@juliamengo          7
@aleberco            7
@alfleuco            4
@maclorena           4
@Marcelitaojeda      3
@MercedesFunes       3
@Sietecase           3
@negropolisok        2
@wwnicolas           2
@soyingridbeck       2
@monigps             1
Name: count, dtype: int64

Número de ataques para periodistas por género

NOTA: Clasificación binaria

Code
attacks["journalist_genre"].value_counts()
journalist_genre
H    2361
M    1958
Name: count, dtype: int64

Frecuencia de los ataques en función del número de menciones

Code
mj_count_mentions = df[df["journalist_genre"] == "H"]["to_journalist"].count()
wj_count_mentions = df[df["journalist_genre"] == "M"]["to_journalist"].count()


mj_count_attacks = attacks[attacks["journalist_genre"] == "H"].shape[0]
wj_count_attacks = attacks[attacks["journalist_genre"] == "M"].shape[0]


mj_percentage_attacks = (mj_count_attacks / mj_count_mentions) * 100
mj_proportion = (mj_percentage_attacks / 100) * 10
mj_proportion_rounded = round(mj_proportion, 1)


wj_percentage_attacks = (wj_count_attacks / wj_count_mentions) * 100
wj_proportion = (wj_percentage_attacks / 100) * 10
wj_proportion_rounded = round(wj_proportion, 1)


print(
    f"Por cada 10 menciones, los periodistas hombres recibieron {mj_proportion_rounded} ataques"
)
print(
    f"Por cada 10 menciones, las periodistas mujeres recibieron {wj_proportion_rounded} ataques"
)
Por cada 10 menciones, los periodistas hombres recibieron 0.9 ataques
Por cada 10 menciones, las periodistas mujeres recibieron 0.8 ataques

Frecuencia de los ataques en función del número de seguidores

Code
seguidores_mujeres = [
    6000,
    800,
    400,
    100,
    207000,
    6000,
    3000,
    300,
    973000,
    726000,
    91000,
    230000,
    366000,
    226000,
    87000,
    102000,
    206000,
    261000,
    497000,
    259000,
    84000,
    248000,
    240000,
    119000,
    143000,
    91000,
    31000,
    166000,
    52000,
    52000,
    34000,
    50000,
    165000,
    289000,
]

seguidores_hombres = [
    3300000,
    2900,
    2300000,
    1300000,
    1000000,
    1000000,
    1000000,
    1000000,
    972000,
    835000,
    749000,
    728000,
    579000,
    575000,
    535000,
    418000,
    403000,
    400000,
    395000,
    233000,
]
Code
mj_followers_count = sum(seguidores_hombres) / len(seguidores_hombres)
wj_followers_count = sum(seguidores_mujeres) / len(seguidores_mujeres)


mj_attacks_count = attacks[attacks["journalist_genre"] == "H"].shape[0]
wj_attacks_count = attacks[attacks["journalist_genre"] == "M"].shape[0]


mj_proportion = (mj_attacks_count / mj_followers_count) * 1000
mj_proportion_formatted = "{:.2f}".format(mj_proportion)
wj_proportion = (wj_attacks_count / wj_followers_count) * 1000
wj_proportion_formatted = "{:.2f}".format(wj_proportion)

print(
    f"Por cada 1K seguidores, aproximadamente hubo {mj_proportion_formatted} ataques para hombres periodistas"
)
print(
    f"Por cada 1K seguidores, aproximadamente hubo {wj_proportion_formatted} ataques para mujeres periodistas"
)
Por cada 1K seguidores, aproximadamente hubo 2.66 ataques para hombres periodistas
Por cada 1K seguidores, aproximadamente hubo 11.07 ataques para mujeres periodistas

Hombres periodistas más atacados

Code
attacks_men = attacks.loc[attacks["journalist_genre"].isin(["H"])]
attacks_men["to_journalist"].value_counts()
to_journalist
@diegobranca      712
@JonatanViale     421
@edufeiok         399
@rialjorge        261
@robdnavarro      209
@luisnovaresio    154
@majulluis         85
@Gatosylvestre     66
@ischargro         27
@ertenembaum       11
@aleberco           7
@alfleuco           4
@Sietecase          3
@wwnicolas          2
Name: count, dtype: int64

Mujeres periodistas más atacadas

Code
attacks_women = attacks.loc[attacks["journalist_genre"].isin(["M"])]
attacks_women["to_journalist"].value_counts()
to_journalist
@Cris_noticias     427
@Angelalerena      409
@odonnellmaria     258
@guadavazquez      216
@vivicanosaok      159
@cyngarciaradio    152
@rominamanguel      95
@mjolivan           72
@nbg__              59
@NANCYPAZOS         58
@anaecorrea         21
@hindelita          10
@juliamengo          7
@maclorena           4
@MercedesFunes       3
@Marcelitaojeda      3
@negropolisok        2
@soyingridbeck       2
@monigps             1
Name: count, dtype: int64

Ranking de tipos de ataques para hombres

Code
conditions = [
    "women",
    "politics",
    "appearance",
    "racism",
    "class",
    "lgbti",
    "criminal",
    "calls",
]
attacks_men_count = attacks_men[conditions].sum()
attacks_men_count
women         524
politics      658
appearance    910
racism        151
class          92
lgbti          92
criminal       49
calls          33
dtype: int64

Ranking de tipos de ataques para mujeres

Code
conditions = [
    "women",
    "politics",
    "appearance",
    "racism",
    "class",
    "lgbti",
    "criminal",
    "calls",
]
attacks_women_count = attacks_women[conditions].sum()
attacks_women_count
women         888
politics      569
appearance    377
racism        117
class         104
lgbti          29
criminal       14
calls          19
dtype: int64

Número de ataques por tipo de evento

Code
attacks["event"].value_counts()
event
elecciones balotaje     1584
elecciones generales     987
1er debate               698
debate balotaje          668
2do debate               382
Name: count, dtype: int64

Proporción de ataques por tipo de evento

Porcentaje de ataques por tweets recolectados para cada evento

Code
count_1er_debate = df[df["event"] == "1er debate"].shape[0]
count_2do_debate = df[df["event"] == "2do debate"].shape[0]
count_elecciones_gen = df[df["event"] == "elecciones generales"].shape[0]
count_debate_balotaje = df[df["event"] == "debate balotaje"].shape[0]
count_elecciones_balotaje = df[df["event"] == "elecciones balotaje"].shape[0]


count_attacks_1er_debate = attacks[attacks["event"] == "1er debate"].shape[0]
count_attacks_2do_debate = attacks[attacks["event"] == "2do debate"].shape[0]
count_attacks_elecciones_gen = attacks[
    attacks["event"] == "elecciones generales"
].shape[0]
count_attacks_debate_balotaje = attacks[attacks["event"] == "debate balotaje"].shape[0]
count_attacks_elecciones_balotaje = attacks[
    attacks["event"] == "elecciones balotaje"
].shape[0]

print(
    "El",
    round((count_attacks_1er_debate / count_1er_debate) * 100),
    "% del total de los tweets del 1er debate fueron ataques",
)
print(
    "El",
    round((count_attacks_2do_debate / count_2do_debate) * 100),
    "% del total de los tweets del 2do debate fueron ataques",
)
print(
    "El",
    round((count_attacks_elecciones_gen / count_elecciones_gen) * 100),
    "% del total de los tweets de las elecciones generales fueron ataques",
)
print(
    "El",
    round((count_attacks_debate_balotaje / count_debate_balotaje) * 100),
    "% del total de los tweets del debate del balotaje fueron ataques",
)
print(
    "El",
    round((count_attacks_elecciones_balotaje / count_elecciones_balotaje) * 100),
    "% del total de los tweets de las elecciones del balotaje fueron ataques",
)
El 6 % del total de los tweets del 1er debate fueron ataques
El 5 % del total de los tweets del 2do debate fueron ataques
El 5 % del total de los tweets de las elecciones generales fueron ataques
El 5 % del total de los tweets del debate del balotaje fueron ataques
El 6 % del total de los tweets de las elecciones del balotaje fueron ataques

Actividad de los periodistas en Twitter por género

Code
journalist_posts = df.dropna(subset=["from_journalist"])
men_journalist_posts = journalist_posts.loc[
    journalist_posts["journalist_genre"].isin(["M"])
]
women_journalist_posts = journalist_posts.loc[
    journalist_posts["journalist_genre"].isin(["H"])
]

print(
    f"""Tweets publicados por periodistas hombres: {len(men_journalist_posts)}\nTweets publicados por periodistas mujeres: {len(women_journalist_posts)}"""
)
Tweets publicados por periodistas hombres: 1223
Tweets publicados por periodistas mujeres: 489

Ranking de periodistas más activos

Code
journalist_activity = df["from_journalist"].value_counts()
journalist_activity
from_journalist
@anaecorrea         206
@guadavazquez       127
@rialjorge           99
@diegobranca         86
@SilvinaMolina       83
@rominamanguel       80
@Angelalerena        80
@nbg__               71
@NANCYPAZOS          70
@hindelita           66
@Cris_noticias       64
@Marcelitaojeda      53
@odonnellmaria       49
@Gatosylvestre       49
@soyingridbeck       48
@luisnovaresio       47
@edufeiok            46
@monigps             44
@mjolivan            39
@majulluis           37
@robdnavarro         31
@vivicanosaok        20
@cyngarciaradio      20
@MercedesFunes       20
@JonatanViale        20
@maclorena           18
@ertenembaum         16
@aleberco            15
@SANTIAGODELMORO     13
@ischargro           13
@juliamengo          13
@mafito11            12
@FlorHalfon          11
@SoleVallejos        11
@Sietecase           11
@silviafbarrio        9
@alfleuco             3
@gabycociffi          3
@negropolisok         2
@wwnicolas            2
@gabipellegrini3      1
@GabrielaWeller       1
@lucianageuna         1
@barilirodolfo        1
@deboraplager         1
Name: count, dtype: int64

Publicaciones de periodistas por evento

Code
men_debate1 = men_journalist_posts.loc[
    men_journalist_posts["event"].isin(["1er debate"])
]
men_count = men_debate1.groupby("dt_date").size().reset_index(name="count")

women_debate1 = women_journalist_posts.loc[
    women_journalist_posts["event"].isin(["1er debate"])
]
women_count = women_debate1.groupby("dt_date").size().reset_index(name="count")

fig = px.line()
fig.add_scatter(
    x=men_count["dt_date"],
    y=men_count["count"],
    name="Hombres",
    line=dict(color="orange"),
    hovertemplate="posts: %{y}",
)
fig.add_scatter(
    x=women_count["dt_date"],
    y=women_count["count"],
    name="Mujeres",
    line=dict(color="purple"),
    hovertemplate="posts: %{y}",
)
fig.update_layout(title="Publicaciones de periodistas durante el 1er debate", width=600)
fig.update_xaxes(type="category")
fig.show()
Code
def count_posts_by_gender(df, event):
    """
    This function takes in a DataFrame of journalist posts and an event string,
    and returns a new DataFrame with the count of posts by date for that event
    and gender.
    """
    debate_posts = df[df["event"].eq(event)]  # filter posts by event
    return (
        debate_posts.groupby("dt_date").size().reset_index(name="count")
    )  # group by date and count


# get posts for the 2nd debate for male and female journalists
men_debate2 = count_posts_by_gender(men_journalist_posts, "2do debate")
women_debate2 = count_posts_by_gender(women_journalist_posts, "2do debate")

# create a line plot of post counts by date for male and female journalists
fig = px.line()
fig.add_scatter(
    x=men_debate2["dt_date"],
    y=men_debate2["count"],
    name="Hombres",
    line=dict(color="orange"),
    hovertemplate="posts: %{y}",
)
fig.add_scatter(
    x=women_debate2["dt_date"],
    y=women_debate2["count"],
    name="Mujeres",
    line=dict(color="purple"),
    hovertemplate="posts: %{y}",
)
fig.update_layout(title="Publicaciones de periodistas durante el 2do debate", width=600)
fig.update_xaxes(type="category")
fig.show()
Code
men_elecciones_gen = men_journalist_posts.loc[
    men_journalist_posts["event"].isin(["elecciones generales"])
]
men_count = men_elecciones_gen.groupby("dt_date").size().reset_index(name="count")

women_elecciones_gen = women_journalist_posts.loc[
    women_journalist_posts["event"].isin(["elecciones generales"])
]
women_count = women_elecciones_gen.groupby("dt_date").size().reset_index(name="count")

fig = px.line()
fig.add_scatter(
    x=men_count["dt_date"],
    y=men_count["count"],
    name="Hombres",
    line=dict(color="orange"),
    hovertemplate="posts: %{y}",
)
fig.add_scatter(
    x=women_count["dt_date"],
    y=women_count["count"],
    name="Mujeres",
    line=dict(color="purple"),
    hovertemplate="posts: %{y}",
)
fig.update_layout(
    title="Publicaciones de periodistas durante las elecciones generales", width=600
)
fig.update_xaxes(type="category")
fig.show()
Code
men_debate_balotaje = men_journalist_posts.loc[
    men_journalist_posts["event"].isin(["debate balotaje"])
]
men_count = men_debate_balotaje.groupby("dt_date").size().reset_index(name="count")

women_debate_balotaje = women_journalist_posts.loc[
    women_journalist_posts["event"].isin(["debate balotaje"])
]
women_count = women_debate_balotaje.groupby("dt_date").size().reset_index(name="count")

fig = px.line()
fig.add_scatter(
    x=men_count["dt_date"],
    y=men_count["count"],
    name="Hombres",
    line=dict(color="orange"),
    hovertemplate="posts: %{y}",
)
fig.add_scatter(
    x=women_count["dt_date"],
    y=women_count["count"],
    name="Mujeres",
    line=dict(color="purple"),
    hovertemplate="posts: %{y}",
)
fig.update_layout(
    title="Publicaciones de periodistas durante el debate del balotaje", width=600
)
fig.update_xaxes(type="category")
fig.show()
Code
men_elecciones_balotaje = men_journalist_posts.loc[
    men_journalist_posts["event"].isin(["elecciones balotaje"])
]
men_count = men_elecciones_balotaje.groupby("dt_date").size().reset_index(name="count")

women_elecciones_balotaje = women_journalist_posts.loc[
    women_journalist_posts["event"].isin(["elecciones balotaje"])
]
women_count = (
    women_elecciones_balotaje.groupby("dt_date").size().reset_index(name="count")
)

fig = px.line()
fig.add_scatter(
    x=men_count["dt_date"],
    y=men_count["count"],
    name="Hombres",
    line=dict(color="orange"),
    hovertemplate="posts: %{y}",
)
fig.add_scatter(
    x=women_count["dt_date"],
    y=women_count["count"],
    name="Mujeres",
    line=dict(color="purple"),
    hovertemplate="posts: %{y}",
)
fig.update_layout(
    title="Publicaciones de periodistas durante las elecciones del balotaje", width=600
)
fig.update_xaxes(type="category")
fig.show()

Ranking de eventos con más ataques para hombres

Code
attacks_men["event"].value_counts()
event
elecciones balotaje     806
elecciones generales    517
debate balotaje         435
1er debate              381
2do debate              222
Name: count, dtype: int64

Ranking de eventos con más ataques para mujeres

Code
attacks_women["event"].value_counts()
event
elecciones balotaje     778
elecciones generales    470
1er debate              317
debate balotaje         233
2do debate              160
Name: count, dtype: int64

Hashtags

20 hashtags más utilizados en los ataques:

Code
attacks["hashtags"] = attacks["text"].apply(
    lambda x: (
        np.nan
        if pd.isnull(x) or not isinstance(x, str) or len(re.findall(r"#\w+", x)) == 0
        else re.findall(r"#\w+", x)
    )
)

attacks["hashtags"] = attacks["hashtags"].apply(
    lambda x: ", ".join(x) if isinstance(x, list) else x
)

# convert dataframe column to list
hashtags = attacks["hashtags"].unique()

# remove nan items from list
hashtags = [x for x in hashtags if not pd.isna(x)]

# split items into a list based on a delimiter
hashtags = [x.split(",") for x in hashtags]

# flatten list of lists
hashtags = [item for sublist in hashtags for item in sublist]

# remove whitespaces
hashtags = list(map(lambda x: x.replace(" ", ""), hashtags))

# count items on list
hashtags_count = pd.Series(hashtags).value_counts()

# return first n rows in descending order
top_hashtags = hashtags_count.nlargest(20)

top_hashtags
#SeVanParaSiempre                3
#Milei                           3
#MassaPresidente2023             2
#ElPeorGobiernoDeLaHistoria      2
#Bullrich                        2
#MassaPresidente                 2
#KirchnerismoNuncaMas            2
#elsi                            1
#VotoContraMassa                 1
#argentina                       1
#Son30Mil                        1
#PalestinaLibre                  1
#Sellamaperiodista               1
#NoAMilei                        1
#Milita60depobrezaydolara1200    1
#GORDITOLECHOSO                  1
#Noquierequeseterminelapauta     1
#Nolepaganconladeellos           1
#NoAl5toGobiernoK                1
#SeVannnnnn                      1
Name: count, dtype: int64

Menciones

20 usuarios más mencionados en los ataques:

Code
attacks["mentions"] = attacks["text"].apply(
    lambda x: (
        np.nan
        if pd.isnull(x) or not isinstance(x, str) or len(re.findall(r"@(\w+)", x)) == 0
        else re.findall(r"@(\w+)", x)
    )
)

attacks["mentions"] = attacks["mentions"].apply(
    lambda x: ", ".join(x) if isinstance(x, list) else x
)

# convert dataframe column to list
mentions = attacks["mentions"].unique()

# remove nan items from list
mentions = [x for x in mentions if not pd.isna(x)]

# split items into a list based on a delimiter
mentions = [x.split(",") for x in mentions]

# flatten list of lists
mentions = [item for sublist in mentions for item in sublist]

# remove whitespaces
mentions = list(map(lambda x: x.replace(" ", ""), mentions))

# count items on list
mentions_count = pd.Series(mentions).value_counts()

# return first n rows in descending order
top_mentions = mentions_count.nlargest(20)

top_mentions
PatoBullrich       5
SergioMassa        4
vivicanosaok       3
minsaurralde       3
JMilei             3
JonatanViale       2
Cris_noticias      2
Kicillofok         2
rialjorge          2
QuintelaRicardo    1
ursuvargues        1
PRossiOficial      1
edugbonorino       1
CarlosMaslaton     1
luisnovaresio      1
LANACION           1
c0o0ni             1
majulluis          1
robnavarro         1
herlombardi        1
Name: count, dtype: int64

Tokens

Lista del top 20 de palabras más comunes y su frecuencia:

Code
# load the spacy model for Spanish
nlp = spacy.load("es_core_news_sm")

# load stop words for Spanish
STOP_WORDS = nlp.Defaults.stop_words


# Function to filter stop words
def filter_stopwords(text):
    # lower text
    doc = nlp(text.lower())
    # filter tokens
    tokens = [
        token.text
        for token in doc
        if not token.is_stop and token.text not in STOP_WORDS and token.is_alpha
    ]
    return " ".join(tokens)


# apply function to dataframe column
attacks["text_pre"] = attacks["text"].apply(filter_stopwords)

# count items on column
token_counts = attacks["text_pre"].str.split(expand=True).stack().value_counts()[:20]

token_counts
vos        544
sos        440
q          368
gordo      268
vas        189
mierda     163
milei      144
lechoso    140
gordito    138
massa      125
gente      115
zurda      114
viejo      108
cara       105
vieja      103
tenes      102
asco       102
orto        93
zurdos      90
puta        87
Name: count, dtype: int64

Tópicos

Técnica de modelado de tópicos con transformers y TF-IDF:

Code
# remove urls, mentions, hashtags and numbers
p.set_options(p.OPT.URL, p.OPT.MENTION, p.OPT.NUMBER)
attacks["text_pre"] = attacks["text_pre"].apply(lambda x: p.clean(x))


# filter column
docs = attacks["text_pre"]

# calculate topics and probabilities
topic_model = BERTopic(
    language="multilingual", calculate_probabilities=True, verbose=True
)

# training
topics, probs = topic_model.fit_transform(docs)

# visualize topics
topic_model.visualize_topics()

Reducción de tópicos

Mapa con 20 tópicos del contenido de los tweets:

Code
# reduce the number of topics
topic_model.reduce_topics(docs, nr_topics=20)

# visualize topics
topic_model.visualize_topics()

Términos por tópico

Code
topic_model.visualize_barchart(top_n_topics=20)

Tópicos en el tiempo

Code
# convert column to list
tweets = attacks["text_pre"].to_list()
timestamps = attacks["dt_date"].to_list()

topics_over_time = topic_model.topics_over_time(
    docs=tweets,
    timestamps=timestamps,
    global_tuning=True,
    evolution_tuning=True,
    nr_bins=20,
)

topic_model.visualize_topics_over_time(topics_over_time, top_n_topics=20)