CARREGAMENTO E PREPARAÇÃO DOS DADOS

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)

Fonte

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.~

Descrição do 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)

Seleção e amostragem

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())

LIMPEZA | Transformando caos em dado utilizável

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))

CARACTERIZAÇÃO INICIAL | O tamanho importa?

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.

TOKENIZAÇÃO | Que palavras contam a história?

data("stop_words")

tokens <- news %>%
  unnest_tokens(word, text_clean) %>%
  filter(!word %in% stop_words$word,
         str_detect(word, "^[a-z]+$"))

Palavras mais frequentes

top_words <- tokens %>% count(word, sort = TRUE)
top_words %>% head(20) %>% kable()
word n
reuters 9992
hesaid 3717
washington 3302
trump 3013
twitter 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

Separando por categoria

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 | Palavras que marcam identidade

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)")

ANÁLISE DE SENTIMENTO | Notícias falsas são mais negativas?

bing <- get_sentiments("bing")
afinn <- get_sentiments("afinn")

Bing sentiment (positivo x negativo)

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)

AFINN (pontuação por palavra)

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.

CONCLUSÕES

Revisão do problema

Buscamos identificar estruturas linguísticas que separam notícias reais e falsas usando NLP.

Metodologia

Aplicamos limpeza textual, tokenização, TF-IDF e análise de sentimento (BING e AFINN) em 5000 notícias balanceadas.

Principais descobertas

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.

Implicações práticas

Os insights podem ser utilizados para:

Sistemas automáticos de detecção

Ferramentas de fact-checking

Estudos sociológicos de desinformação

Limitações e futuros trabalhos

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