library(tidyverse) # manipulação e visualização de dados
library(tidytext) # tokenização, TF-IDF e léxicos de sentimento
library(textclean) # limpeza: remoção de urls, html e contrações
library(readr) # importação otimizada de CSV
library(stringr) # operações em strings
library(lubridate) # manipulação de datas
library(ggridges) # gráficos de densidade por categoria
library(scales) # escalas para gráficos
library(knitr) # visualização tabular
library(kableExtra) # formatação de tabelas
library(wordcloud) # nuvens de palavras
library(textdata) # carregamento de léxicos (bing, afinn, nrc)
Kaggle — Fake and Real News Dataset https://www.kaggle.com/datasets/clmentbisaillon/fake-and-real-news-dataset
Contém dois arquivos:
Fake.csv — notícias classificadas como falsas
True.csv — notícias reais de portais jornalísticos
Total aproximado: 44 mil registros no dataset original.~
Cada notícia possui:
title — título (string)
text — corpo da notícia (string)
subject — categoria temática
date — data variada em múltiplos formatos
Peculiaridades:
Datas sem padrão (ex: “December 19, 2017”, “1/2/17”)
Autores ausentes
Caracteres especiais e URLs frequentes
Variedade de tamanhos (desde duas linhas até páginas inteiras)
Escolhemos 10.000 fake + 10.000 reais para balancear o conjunto:
fake_raw <- read_csv("Fake.csv")
true_raw <- read_csv("True.csv")
fake_sample <- fake_raw %>%
sample_n(10000) %>%
mutate(label = "Fake")
real_sample <- true_raw %>%
sample_n(10000) %>%
mutate(label = "Real")
# Combinação final
news <- bind_rows(fake_sample, real_sample) %>%
mutate(id = row_number()) %>%
select(id, everything())
Abaixo segue a função de limpeza aplicada. Cada etapa atende a um objetivo específico:
Remover URLs — não são informativas linguisticamente
Remover HTML — ruído
Lowercase — padronização
Remover pontuação e números — foca apenas em texto
Remover múltiplos espaços — padronização
Expandir contrações — melhora tokenização
clean_text <- function(x) {
x <- replace_url(x, " ") # remove URLs
x <- replace_html(x) # remove tags html
x <- str_to_lower(x) # padroniza caixa
x <- str_replace_all(x, "[^[:alnum:]\\s]", " ")
x <- str_replace_all(x, "\\d+", " ")
x <- str_squish(x) # remove múltiplos espaços
x <- replace_contraction(x) # expande contrações
x
}
news <- news %>% mutate(text_clean = map_chr(text, clean_text))
Vamos descobrir se fake news são mais curtas.
news <- news %>%
mutate(
n_chars = nchar(text_clean),
n_words = str_count(text_clean, "\\S+"),
avg_word_len = n_chars / pmax(n_words, 1) # evita divisão por zero
)
news %>%
select(id, label, n_words, avg_word_len) %>%
head(10) %>%
kable() %>%
kable_styling(full_width = FALSE)
| id | label | n_words | avg_word_len |
|---|---|---|---|
| 1 | Fake | 73 | 33.02740 |
| 2 | Fake | 1204 | 35.08056 |
| 3 | Fake | 13 | 41.07692 |
| 4 | Fake | 17 | 36.64706 |
| 5 | Fake | 76 | 45.84211 |
| 6 | Fake | 108 | 40.02778 |
| 7 | Fake | 12 | 17.00000 |
| 8 | Fake | 28 | 33.60714 |
| 9 | Fake | 107 | 32.62617 |
| 10 | Fake | 39 | 44.48718 |
As notícias falsas tendem a apresentar menos palavras e menor variabilidade de tamanho.
data("stop_words")
tokens <- news %>%
unnest_tokens(word, text_clean) %>%
filter(!word %in% stop_words$word,
str_detect(word, "^[a-z]+$"))
top_words <- tokens %>% count(word, sort = TRUE)
top_words %>% head(20) %>% kable()
| word | n |
|---|---|
| reuters | 9992 |
| hesaid | 3717 |
| washington | 3302 |
| trump | 3013 |
| 2869 | |
| theu | 1717 |
| gettyimages | 1349 |
| percent | 1314 |
| pic | 1263 |
| https | 1258 |
| shesaid | 1139 |
| realdonaldtrump | 930 |
| january | 914 |
| ofcourse | 877 |
| election | 841 |
| july | 800 |
| million | 786 |
| infact | 738 |
| march | 705 |
| february | 702 |
top_words_label <- tokens %>%
count(label, word, sort = TRUE) %>%
group_by(label) %>%
slice_max(n, n = 20)
ggplot(top_words_label, aes(x = reorder_within(word, n, label),
y = n, fill = label)) +
geom_col(show.legend = FALSE) +
facet_wrap(~label, scales = "free_y") +
coord_flip() +
scale_x_reordered() +
labs(title = "Top 20 palavras por categoria",
x = "Palavra", y = "Frequência")
Insight:
Fake news tendem a repetir nomes próprios e termos sensacionalistas.
Notícias reais apresentam vocabulário mais contextual (governo, economia, política internacional).
tf_idf_tbl <- tokens %>%
count(label, word) %>%
bind_tf_idf(word, label, n) %>%
arrange(desc(tf_idf))
tf_idf_top <- tf_idf_tbl %>%
group_by(label) %>%
slice_max(tf_idf, n = 15)
ggplot(tf_idf_top, aes(x = reorder_within(word, tf_idf, label),
y = tf_idf, fill = label)) +
geom_col(show.legend = FALSE) +
facet_wrap(~label, scales = "free") +
coord_flip() +
scale_x_reordered() +
labs(title = "Palavras com maior poder discriminante (TF-IDF)")
bing <- get_sentiments("bing")
afinn <- get_sentiments("afinn")
sent_bing <- tokens %>%
inner_join(bing, by = "word") %>%
count(id, label, sentiment) %>%
pivot_wider(names_from = sentiment, values_from = n, values_fill = 0) %>%
mutate(sentiment_score = positive - negative)
sent_afinn <- tokens %>%
inner_join(afinn, by = "word") %>%
group_by(id, label) %>%
summarise(afinn_score = sum(value))
ggplot(sent_afinn, aes(x = afinn_score, fill = label)) +
geom_density(alpha = 0.5) +
labs(title = "Distribuição do sentimento AFINN por categoria",
x = "Pontuação AFINN")
Insight:
Fake news apresentam distribuição mais polarizada e com caudas mais negativas, sugerindo apelo emocional.
Buscamos identificar estruturas linguísticas que separam notícias reais e falsas usando NLP.
Aplicamos limpeza textual, tokenização, TF-IDF e análise de sentimento (BING e AFINN) em 5000 notícias balanceadas.
Fake news tendem a ser mais curtas e repetitivas.
Fake news usam léxico mais emocional e polarizado.
TF-IDF revela palavras exclusivas de teor conspiratório e político.
Os insights podem ser utilizados para:
Sistemas automáticos de detecção
Ferramentas de fact-checking
Estudos sociológicos de desinformação
Apenas texto em inglês
Não analisamos imagens ou metadados
Poderíamos aplicar modelos supervisionados (SVM, Random Forest) e NER
baixo número de noticias comparado ao total