EDA - Data Wrangling

Análise exploratória dos dados básicos (pré-feature engineering)

Author

Marlon

Published

May 7, 2026

1 Introdução

Este notebook apresenta a visão geral do dataset com contexto externo integrado.

O dataset foi enriquecido com informações de encontros presenciais, permitindo análises que consideram a diferença entre comunicação quando juntos vs separados.

Objetivo: Visão panorâmica descritiva do dataset completo.

Estrutura: - Estatísticas gerais - Volume diário (com contexto) - Distribuição por remetente - Tipos de mensagem


2 Setup

2.1 Bibliotecas

Code
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from pathlib import Path
import warnings

warnings.filterwarnings('ignore')

# Configurações de visualização
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)

2.2 Caminhos

Code
# Estrutura de diretórios
PROJECT_ROOT = Path.home() / 'Desktop' / 'whatsapp-interaction-analysis'
DATA_DIR = PROJECT_ROOT / 'data' / 'processed' / 'export_2024-10_2025-10'

# Arquivo de entrada (gerado pelo wrangling)
INPUT_FILE = DATA_DIR / 'messages.parquet'

print(f"📁 Diretório de dados: {DATA_DIR}")
print(f"📂 Arquivo de entrada: {INPUT_FILE.name}")
print(f"✅ Arquivo existe: {INPUT_FILE.exists()}")
📁 Diretório de dados: /Users/mosx/Desktop/whatsapp-interaction-analysis/data/processed/export_2024-10_2025-10
📂 Arquivo de entrada: messages.parquet
✅ Arquivo existe: True

3 Carregamento dos Dados

Code
# Carrega dataset do wrangling
df = pd.read_parquet(INPUT_FILE)

print(f"📊 Shape: {df.shape}")
print(f"📅 Período: {df['timestamp'].min().date()} → {df['timestamp'].max().date()}")
print(f"👥 Remetentes: {df['remetente'].unique().tolist()}")
📊 Shape: (91924, 8)
📅 Período: 2024-10-19 → 2025-10-20
👥 Remetentes: ['P1', 'P2']

3.1 Estrutura do Dataset

Code
# Exibe informações sobre as colunas
df.info()
<class 'pandas.DataFrame'>
RangeIndex: 91924 entries, 0 to 91923
Data columns (total 8 columns):
 #   Column          Non-Null Count  Dtype         
---  ------          --------------  -----         
 0   timestamp       91924 non-null  datetime64[us]
 1   remetente       91924 non-null  category      
 2   tipo_mensagem   91924 non-null  category      
 3   grupo_mensagem  91924 non-null  category      
 4   conteudo        68686 non-null  str           
 5   arquivo         1254 non-null   str           
 6   transcricao     91924 non-null  bool          
 7   date_match      91924 non-null  bool          
dtypes: bool(2), category(3), datetime64[us](1), str(2)
memory usage: 4.5 MB

3.2 Amostra dos Dados

Code
# Primeiras linhas
df.head(10)
timestamp remetente tipo_mensagem grupo_mensagem conteudo arquivo transcricao date_match
0 2024-10-19 13:52:51 P1 text_pure TEXT Le? NaN False False
1 2024-10-19 13:53:25 P2 text_with_link TEXT https://www.localiza.com/brasil/pt-br/reservas... NaN False False
2 2024-10-19 13:53:25 P1 image_omitted IMG NaN NaN False False
3 2024-10-19 13:53:41 P2 sticker_omitted STICKER NaN NaN False False
4 2024-10-19 13:53:43 P2 text_pure TEXT Oi NaN False False
5 2024-10-19 13:54:34 P1 image_omitted IMG NaN NaN False False
6 2024-10-19 13:56:05 P1 image_omitted IMG NaN NaN False False
7 2024-10-19 13:56:37 P2 text_pure TEXT Arrasou NaN False False
8 2024-10-19 13:56:49 P2 text_pure TEXT confortável NaN False False
9 2024-10-19 13:56:59 P2 text_pure TEXT esse vai de boas NaN False False

3.3 Estatísticas Descritivas

Code
# Resumo estatístico
df.describe(include='all')
timestamp remetente tipo_mensagem grupo_mensagem conteudo arquivo transcricao date_match
count 91924 91924 91924 91924 68686 1254 91924 91924
unique NaN 2 21 11 41666 1254 2 2
top NaN P1 text_pure TEXT Sim -0082879-Blip ViraTexto.vcf False False
freq NaN 46948 62885 67297 394 1 91229 91645
mean 2025-05-16 17:51:51.580436 NaN NaN NaN NaN NaN NaN NaN
min 2024-10-19 13:52:51 NaN NaN NaN NaN NaN NaN NaN
25% 2025-02-24 17:35:35.500000 NaN NaN NaN NaN NaN NaN NaN
50% 2025-06-15 12:34:06.500000 NaN NaN NaN NaN NaN NaN NaN
75% 2025-07-21 08:52:56 NaN NaN NaN NaN NaN NaN NaN
max 2025-10-20 15:25:49 NaN NaN NaN NaN NaN NaN NaN
Warning💡 Observação

68.686 mensagens têm conteúdo textual (74.7%), o resto são mídias sem transcrição. Isso indica que análises de texto serão aplicáveis à maioria dos dados.


4 Visão Geral do Dataset

4.1 Volume de Mensagens

Code
# Total de mensagens
total_msgs = len(df)
print(f"📊 Total de mensagens: {total_msgs:,}")

# Por remetente
msgs_por_remetente = df['remetente'].value_counts()
print(f"\n👥 Mensagens por remetente:")
for remetente, count in msgs_por_remetente.items():
    pct = (count / total_msgs) * 100
    print(f"   {remetente}: {count:,} ({pct:.1f}%)")
📊 Total de mensagens: 91,924

👥 Mensagens por remetente:
   P1: 46,948 (51.1%)
   P2: 44,976 (48.9%)
Code
# Visualização
fig = px.bar(
    x=msgs_por_remetente.index,
    y=msgs_por_remetente.values,
    labels={'x': 'Remetente', 'y': 'Quantidade de Mensagens'},
    title='Volume de Mensagens por Remetente',
    color=msgs_por_remetente.index,
    text=msgs_por_remetente.values
)

fig.update_traces(texttemplate='%{text:,}', textposition='outside')
fig.update_layout(showlegend=False, height=400)
fig.show()
Figure 1: Distribuição de mensagens por remetente
Warning💡 Insight

Distribuição equilibrada entre P1 (51.1%) e P2 (48.9%), indicando participação ativa de ambos na conversa. Essa proporção similar sugere que features comparativas entre remetentes terão poder discriminatório moderado.

4.2 Distribuição de Tipos de Mensagem

4.2.1 Por Tipo Específico

Code
# Contagem por tipo específico
tipo_counts = df['tipo_mensagem'].value_counts()
print(f"📋 Tipos de mensagem identificados: {len(tipo_counts)}")
print(f"\nTop 10 tipos mais frequentes:")
print(tipo_counts)
📋 Tipos de mensagem identificados: 21

Top 10 tipos mais frequentes:
tipo_mensagem
text_pure             62885
video_note_omitted     9168
audio_omitted          8040
text_with_emoji        3858
image_omitted          3287
sticker_omitted        1869
message_edited          658
text_with_link          554
audio_attached          512
image_attached          298
sticker_attached        248
video_omitted           248
video_attached          186
document_omitted         38
gif_omitted              28
message_deleted          22
missed_call              11
contact_attached          9
system_message            2
voice_call                2
file_attached             1
Name: count, dtype: int64
Code
# Visualização Tipos de mensagem
top_10_tipos = tipo_counts

fig = px.bar(
    x=top_10_tipos.values,
    y=top_10_tipos.index,
    orientation='h',
    labels={'x': 'Quantidade', 'y': 'Tipo de Mensagem'},
    title='Top 10 Tipos de Mensagem',
    text=top_10_tipos.values
)

fig.update_traces(texttemplate='%{text:,}', textposition='outside')
fig.update_layout(height=800, yaxis={'categoryorder': 'total ascending'})
fig.show()
Figure 2: Tipos de mensagem mais frequentes
Warning💡 Insight - Granularidade de Tipos

Predominância massiva de text_pure (68.4%), seguido por mídias omitidas (vídeo/áudio).

Problema: 21 tipos específicos com distribuição muito desigual: - Top 3 (text_pure, video_note_omitted, audio_omitted) = 87.1% dos dados - Bottom 10 tipos têm < 100 mensagens cada (ruído estatístico)

Solução: Trabalhar com grupo_mensagem (11 categorias) em vez de tipo_mensagem (21 tipos): - Tipos raros já estão agrupados: missed_call + voice_call → CALL - file_attached → FILE
- message_deleted + message_edited → SYSTEM

Para Feature Engineering: Usar grupo_mensagem como feature categórica principal. Considerar tipo_mensagem apenas se análise específica justificar (ex: diferenciar audio_omitted vs audio_attached).

4.2.2 Por Grupo (Categoria)

Code
# Contagem por grupo
grupo_counts = df['grupo_mensagem'].value_counts()
print(f"📦 Grupos de mensagem: {len(grupo_counts)}")
print(f"\nDistribuição por grupo:")
for grupo, count in grupo_counts.items():
    pct = (count / total_msgs) * 100
    print(f"   {grupo}: {count:,} ({pct:.1f}%)")
📦 Grupos de mensagem: 11

Distribuição por grupo:
   TEXT: 67,297 (73.2%)
   VID: 9,602 (10.4%)
   AUDIO: 8,552 (9.3%)
   IMG: 3,585 (3.9%)
   STICKER: 2,117 (2.3%)
   SYSTEM: 682 (0.7%)
   DOC: 38 (0.0%)
   GIF: 28 (0.0%)
   CALL: 13 (0.0%)
   CONTACT: 9 (0.0%)
   FILE: 1 (0.0%)
Code
# Visualização
fig = px.pie(
    names=grupo_counts.index,
    values=grupo_counts.values,
    title='Distribuição por Grupo de Mensagem',
    hole=0.4
)

fig.update_traces(
    textposition='inside',
    textinfo='label+percent',
    hovertemplate='<b>%{label}</b><br>Quantidade: %{value:,}<br>Percentual: %{percent}<extra></extra>'
)

fig.update_layout(height=1000)
fig.show()
Figure 3: Distribuição por grupo de mensagem
Warning💡 Insight 1 - Grupos de Mensagem

Distribuição extremamente concentrada: - TEXT (73.2%) domina completamente - VID (10.4%) + AUDIO (9.3%) = 19.7% (mídia) - Demais grupos (IMG, STICKER, SYSTEM, etc.) = 7.1%

Implicação: Features de análise textual (tamanho, emojis, pontuação) terão maior poder estatístico que features de mídia.

Grupos DOC, GIF, CALL, CONTACT, FILE (< 0.1% cada): Volume insuficiente para análises robustas - considerar agrupar em categoria “OUTROS” ou ignorar.

Code
# Analisa presença de transcrições
transcricao_counts = df['transcricao'].value_counts()
total_transcricoes = transcricao_counts.get(True, 0)
pct_transcricoes = (total_transcricoes / total_msgs) * 100

print(f"🎤 Mensagens com transcrição: {total_transcricoes:,} ({pct_transcricoes:.1f}%)")
print(f"📝 Mensagens sem transcrição: {transcricao_counts.get(False, 0):,}")

# Transcrições por tipo de mensagem
if total_transcricoes > 0:
    print(f"\n📊 Transcrições por tipo de mensagem:")
    transcricoes_por_tipo = df[df['transcricao'] == True]['grupo_mensagem'].value_counts()
    print(transcricoes_por_tipo)
🎤 Mensagens com transcrição: 695 (0.8%)
📝 Mensagens sem transcrição: 91,229

📊 Transcrições por tipo de mensagem:
grupo_mensagem
AUDIO      510
VID        185
CALL         0
CONTACT      0
DOC          0
FILE         0
GIF          0
IMG          0
STICKER      0
SYSTEM       0
TEXT         0
Name: count, dtype: int64
Code
# Visualização se houver transcrições
if total_transcricoes > 0:
    fig = px.bar(
        x=transcricoes_por_tipo.index,
        y=transcricoes_por_tipo.values,
        labels={'x': 'Tipo de Mensagem', 'y': 'Quantidade com Transcrição'},
        title='Mensagens Transcritas por Tipo',
        text=transcricoes_por_tipo.values
    )
    
    fig.update_traces(texttemplate='%{text:,}', textposition='outside')
    fig.update_layout(height=400)
    fig.show()
Figure 4: Mensagens com transcrição por tipo

4.3 Mensagens com Transcrição

As transcrições representam mensagens enviadas por áudio ou vídeo (em vez de digitadas). Esse comportamento indica um estilo de comunicação diferente - usar funcionalidades de mídia do WhatsApp em vez de texto puro.

Vamos analisar: 1. Proporção geral de TEXTO vs MÍDIA 2. Das mídias transcritíveis (AUDIO + VID), quantas foram processadas

Code
# Agrupa em TEXT vs MÍDIA
df['tipo_comunicacao'] = df['grupo_mensagem'].apply(
    lambda x: 'TEXTO' if x == 'TEXT' else 'MÍDIA'
)

comunicacao_counts = df['tipo_comunicacao'].value_counts()

# Visualização
fig = px.bar(
    x=comunicacao_counts.index,
    y=comunicacao_counts.values,
    title='Distribuição: Texto vs Mídia',
    labels={'x': 'Tipo de Comunicação', 'y': 'Quantidade'},
    text=comunicacao_counts.values,
    color=comunicacao_counts.index
)

fig.update_traces(texttemplate='%{text:,}<br>(%{y:.1%})', textposition='outside')
fig.update_layout(height=400, showlegend=False)
fig.show()

print(f"📝 TEXTO: {comunicacao_counts['TEXTO']:,} ({comunicacao_counts['TEXTO']/len(df)*100:.1f}%)")
print(f"📱 MÍDIA: {comunicacao_counts['MÍDIA']:,} ({comunicacao_counts['MÍDIA']/len(df)*100:.1f}%)")
📝 TEXTO: 67,297 (73.2%)
📱 MÍDIA: 24,627 (26.8%)

Gráfico 2: Mídias Transcritíveis (barras empilhadas)

Code
# Foca em AUDIO e VID
midias_transcritiveis = df[df['grupo_mensagem'].isin(['AUDIO', 'VID'])].copy()

# Cria categorias
midias_transcritiveis['status'] = midias_transcritiveis['transcricao'].apply(
    lambda x: 'Transcrita' if x else 'Não Transcrita'
)

# Crosstab
transcricao_por_tipo = pd.crosstab(
    midias_transcritiveis['grupo_mensagem'],
    midias_transcritiveis['status']
)

# Visualização (barras empilhadas 100%)
fig = px.bar(
    transcricao_por_tipo,
    barmode='stack',
    title='Status de Transcrição: Áudios e Vídeos',
    labels={'value': 'Quantidade', 'grupo_mensagem': 'Tipo'},
    color_discrete_map={'Transcrita': '#2ecc71', 'Não Transcrita': '#e74c3c'}
)

fig.update_layout(height=400, legend_title_text='Status')
fig.show()

# Estatísticas
total_transcritiveis = len(midias_transcritiveis)
total_transcritas = midias_transcritiveis['transcricao'].sum()
pct_transcritas = (total_transcritas / total_transcritiveis) * 100

print(f"\n📊 Mídias transcritíveis (AUDIO + VID): {total_transcritiveis:,}")
print(f"✅ Transcritas: {total_transcritas:,} ({pct_transcritas:.1f}%)")
print(f"❌ Não transcritas: {total_transcritiveis - total_transcritas:,} ({100-pct_transcritas:.1f}%)")

print(f"\nDetalhamento:")
for tipo in ['AUDIO', 'VID']:
    df_tipo = midias_transcritiveis[midias_transcritiveis['grupo_mensagem'] == tipo]
    transcritas = df_tipo['transcricao'].sum()
    total = len(df_tipo)
    print(f"  {tipo}: {transcritas}/{total} transcritas ({transcritas/total*100:.1f}%)")

📊 Mídias transcritíveis (AUDIO + VID): 18,154
✅ Transcritas: 695 (3.8%)
❌ Não transcritas: 17,459 (96.2%)

Detalhamento:
  AUDIO: 510/8552 transcritas (6.0%)
  VID: 185/9602 transcritas (1.9%)
Warning💡 Insight - Estilo de Comunicação & Análises Possíveis

Preferência por texto vs mídia: - 73.2% das mensagens são TEXTO (digitadas) - 26.8% são MÍDIA (áudio, vídeo, imagens, stickers, etc.)

Transcrições de áudio/vídeo: - Das 18.154 mídias transcritíveis (AUDIO+VID), 3.8% foram processadas - AUDIO: 510 transcrições (6.0% dos áudios) - VID: 185 transcrições (1.9% dos vídeos)

Implicação para FE:

✅ Feature grupo_mensagem (TEXT/AUDIO/VID) - Captura diferença de estilo de comunicação - Permite análise: “P1 usa mais áudio que P2?”

✅ Feature transcricao (booleana) - Subset de 695 mensagens com conteúdo transcrito - Permite enriquecer corpus textual total

📊 Análises de Conteúdo Viáveis:

Com amostra atual (510 áudios / 185 vídeos): - ✅ Comparação de tamanho médio por tipo (texto vs áudio vs vídeo) - ✅ Sentiment geral por tipo (áudios são mais emotivos?) - ✅ Vocabulário específico (word clouds, n-grams) - ✅ Análises qualitativas (tópicos, espontaneidade) - ⚠️ Topic modeling aprofundado (amostra marginal para vídeos)

Limitação: Comparar 67k textos vs 510 áudios vs 185 vídeos tem desbalanceamento. Análises devem ser proporcionais (%, médias) não absolutas (contagens).

Se coletar mais dados (+1.5 mês): - Estimativa: +200-300 transcrições - Total: ~900-1000 transcrições → Topic modeling mais robusto


5 Análise de Tipos de Mensagem

5.1 Padrão por Remetente

Code
# Crosstab de remetente vs grupo de mensagem
crosstab = pd.crosstab(
    df['remetente'], 
    df['grupo_mensagem'],
    margins=True,
    margins_name='Total'
)

print("📊 Distribuição de tipos de mensagem por remetente:")
print(crosstab)
📊 Distribuição de tipos de mensagem por remetente:
grupo_mensagem  AUDIO  CALL  CONTACT  DOC  FILE  GIF   IMG  STICKER  SYSTEM  \
remetente                                                                     
P1               5334     0        3   24     0   10  1965     1026     422   
P2               3218    13        6   14     1   18  1620     1091     260   
Total            8552    13        9   38     1   28  3585     2117     682   

grupo_mensagem   TEXT   VID  Total  
remetente                           
P1              33220  4944  46948  
P2              34077  4658  44976  
Total           67297  9602  91924  
Code
# Proporções (%)
crosstab_pct = pd.crosstab(
    df['remetente'], 
    df['grupo_mensagem'],
    normalize='index'
) * 100

print("\n📊 Proporções (%) por remetente:")
print(crosstab_pct.round(1))

📊 Proporções (%) por remetente:
grupo_mensagem  AUDIO  CALL  CONTACT  DOC  FILE  GIF  IMG  STICKER  SYSTEM  \
remetente                                                                    
P1               11.4   0.0      0.0  0.1   0.0  0.0  4.2      2.2     0.9   
P2                7.2   0.0      0.0  0.0   0.0  0.0  3.6      2.4     0.6   

grupo_mensagem  TEXT   VID  
remetente                   
P1              70.8  10.5  
P2              75.8  10.4  
Code
# Visualização
fig = px.bar(
    crosstab_pct.T,
    barmode='group',
    labels={'value': 'Percentual (%)', 'grupo_mensagem': 'Tipo de Mensagem'},
    title='Distribuição de Tipos de Mensagem por Remetente (%)'
)

fig.update_layout(height=500, xaxis_tickangle=-45)
fig.show()
Figure 5: Distribuição de tipos de mensagem por remetente
Warning💡 Insight - Preferências por Remetente

Diferenças significativas identificadas:

P1: - Usa 58% mais áudio que P2 (11.4% vs 7.2%) - Menos texto proporcionalmente (70.8% vs 75.8%) - Ligeiramente mais imagens (4.2% vs 3.6%)

P2: - Preferência maior por texto digitado (75.8% vs 70.8%) - Menos dependente de áudio para comunicação

Semelhanças: - Uso de vídeo praticamente idêntico (~10.5%) - Stickers e demais tipos: diferenças marginais (<0.5%)

Implicação: A feature grupo_mensagem combinada com remetente tem poder discriminatório - P1 e P2 têm estilos de comunicação distintos (P1 = mais áudio, P2 = mais texto).


Proporções gerais: - P1: 70.8% texto | 26.1% mídia (áudio/vídeo/imagem) - P2: 75.8% texto | 21.1% mídia (áudio/vídeo/imagem)

Diferenças-chave identificadas:

Áudio: - P1 usa 58% mais áudio que P2 (11.4% vs 7.2%) - Maior dependência de comunicação por voz

Texto: - P2 prefere mais texto digitado (+5 pontos percentuais) - Comunicação mais escrita

Vídeo e outras mídias: - Uso praticamente idêntico: VID ~10.5%, IMG ~4%, STICKER ~2%

Implicação para FE: Feature grupo_mensagem + remetente tem poder discriminatório - estilos de comunicação distintos. Vale investigar se essa diferença: - É constante ao longo do tempo? - Muda em contextos específicos (encontros, períodos do dia)?

Próxima análise: Investigar se essa diferença é temporal (mudou ao longo do tempo?) ou contextual (muda durante encontros?).

5.1.1 Insights sobre Tipos

Code
# Análise de preferências
print("💡 INSIGHTS - Tipos de Mensagem:\n")

for remetente in df['remetente'].unique():
    df_rem = df[df['remetente'] == remetente]
    top_tipo = df_rem['grupo_mensagem'].value_counts().head(1)
    tipo_nome = top_tipo.index[0]
    tipo_pct = (top_tipo.values[0] / len(df_rem)) * 100
    
    print(f"• {remetente}: {tipo_pct:.1f}% das mensagens são {tipo_nome}")

# Comparação TEXT vs não-TEXT
for remetente in df['remetente'].unique():
    df_rem = df[df['remetente'] == remetente]
    pct_text = (df_rem['grupo_mensagem'] == 'TEXT').sum() / len(df_rem) * 100
    pct_media = (df_rem['grupo_mensagem'].isin(['AUDIO', 'VID', 'IMG'])).sum() / len(df_rem) * 100
    print(f"• {remetente}: {pct_text:.1f}% texto | {pct_media:.1f}% mídia (áudio/vídeo/imagem)")
💡 INSIGHTS - Tipos de Mensagem:

• P1: 70.8% das mensagens são TEXT
• P2: 75.8% das mensagens são TEXT
• P1: 70.8% texto | 26.1% mídia (áudio/vídeo/imagem)
• P2: 75.8% texto | 21.1% mídia (áudio/vídeo/imagem)

6 Padrões Temporais Básicos

6.1 Distribuição ao Longo do Tempo

Code
# Timeline de mensagens
df['data'] = df['timestamp'].dt.date
msgs_por_dia = df.groupby('data').size().reset_index(name='count')

print(f"📅 Período analisado: {msgs_por_dia['data'].min()} a {msgs_por_dia['data'].max()}")
print(f"📊 Dias com mensagens: {len(msgs_por_dia)}")
print(f"📈 Média de mensagens/dia: {msgs_por_dia['count'].mean():.1f}")
print(f"📊 Mediana: {msgs_por_dia['count'].median():.0f}")
print(f"📈 Máximo em um dia: {msgs_por_dia['count'].max()}")
📅 Período analisado: 2024-10-19 a 2025-10-20
📊 Dias com mensagens: 325
📈 Média de mensagens/dia: 282.8
📊 Mediana: 216
📈 Máximo em um dia: 1288
Code
# Visualização
fig = px.line(
    msgs_por_dia,
    x='data',
    y='count',
    title='Volume de Mensagens por Dia',
    labels={'data': 'Data', 'count': 'Quantidade de Mensagens'}
)

fig.update_traces(mode='lines+markers')
fig.update_layout(height=900, hovermode='x unified')
fig.show()
Figure 6: Volume de mensagens ao longo do tempo
Warning💡 Insight - Evolução Temporal

Volume e variabilidade: - Média: 283 msgs/dia | Mediana: 216 msgs/dia
- Máximo: 1.288 mensagens (jun/2025) - pico de 4.5x a média - Oscilação: 0 a 1.288 msgs/dia (variabilidade extrema)

Evolução em 4 fases:

  1. Out-Dez/2024: Crescimento inicial (~100-300 msgs/dia)
  2. Jan-Mai/2025: Estabilização (~200-500 msgs/dia)
  3. Jun-Jul/2025: PICO (600-1.200 msgs/dia) - evento desconhecido
  4. Ago-Out/2025: Normalização (~200-400 msgs/dia)

Gaps e contexto externo (CRÍTICO): - Diversos dias com volume ~0 ao longo do período - Hipótese principal: Gaps coincidem com encontros presenciais (“temporadas juntos”) - Relacionamento à distância com encontros regulares (SP ↔︎ BSB) - Implicação: Volume de mensagens é proxy de “distância física” (conversam menos quando juntos)

Pico Jun-Jul/2025: - Período mais intenso pode indicar separação após encontro prolongado - Ou preparação/saudade pré-encontro - Validação necessária: Cruzar timeline com datas de viagens/encontros

Implicações para FE: - ✅ Features temporais (mês, trimestre) capturam mudanças de fase - ✅ Feature “volume diário” pode medir intensidade da relação
- 🔍 Contexto externo (encontros) provavelmente influencia comunicação fortemente

6.2 Distribuição por Hora do Dia

Code
# Extrai hora
df['hora'] = df['timestamp'].dt.hour

# Contagem por hora
msgs_por_hora = df.groupby('hora').size().reset_index(name='count')

print(f"⏰ Horários de pico:")
top_horas = msgs_por_hora.nlargest(5, 'count')
for _, row in top_horas.iterrows():
    print(f"   {row['hora']:02d}h: {row['count']:,} mensagens")
⏰ Horários de pico:
   19h: 9,377 mensagens
   18h: 8,124 mensagens
   20h: 7,373 mensagens
   17h: 7,177 mensagens
   16h: 6,954 mensagens
Code
# Visualização
fig = px.bar(
    msgs_por_hora,
    x='hora',
    y='count',
    title='Distribuição de Mensagens por Hora do Dia',
    labels={'hora': 'Hora do Dia', 'count': 'Quantidade de Mensagens'}
)

fig.update_layout(height=400, xaxis=dict(tickmode='linear', tick0=0, dtick=1))
fig.show()
Figure 7: Distribuição de mensagens por hora do dia
Warning💡 Insight - Padrões Intradiários

Concentração noturna extrema: - Pico às 19h: 9.377 mensagens (10.2% do total) - Janela 18h-21h: 38% de toda comunicação - Crescimento: 8h → 19h (gradual ao longo do dia) - Queda: Abrupta após 21h

Períodos de baixa atividade: - Madrugada (0h-6h): ~5% do total - Manhã (7h-11h): atividade moderada

Interpretação: Padrão típico de relacionamento à distância - comunicação concentrada no pós-trabalho quando ambos estão livres.

Para FE: - periodo_dia (Manhã/Tarde/Noite) essencial - horario_comercial captura diferença trabalho vs lazer - hora para nuances específicas

6.2.1 Comparação por Remetente (Hora do Dia)

Code
# Agrupa por hora e remetente
msgs_hora_remetente = df.groupby(['hora', 'remetente']).size().reset_index(name='count')

# Calcula proporção por remetente
total_p1 = df[df['remetente'] == 'P1'].shape[0]
total_p2 = df[df['remetente'] == 'P2'].shape[0]

msgs_hora_remetente['proporcao'] = msgs_hora_remetente.apply(
    lambda row: (row['count'] / total_p1 * 100) if row['remetente'] == 'P1' else (row['count'] / total_p2 * 100),
    axis=1
)

# Visualização
fig = px.line(
    msgs_hora_remetente,
    x='hora',
    y='proporcao',
    color='remetente',
    title='Padrão Horário por Remetente (%)',
    labels={'hora': 'Hora do Dia', 'proporcao': 'Proporção de Mensagens (%)', 'remetente': 'Remetente'},
    markers=True
)

fig.update_layout(height=400, xaxis=dict(tickmode='linear', tick0=0, dtick=1))
fig.show()

# Estatísticas
print("\n📊 Comparação de horários de pico por remetente:\n")
for remetente in ['P1', 'P2']:
    df_rem = msgs_hora_remetente[msgs_hora_remetente['remetente'] == remetente]
    hora_pico = df_rem.loc[df_rem['count'].idxmax(), 'hora']
    count_pico = df_rem.loc[df_rem['count'].idxmax(), 'count']
    prop_pico = df_rem.loc[df_rem['count'].idxmax(), 'proporcao']
    print(f"{remetente}: Pico às {hora_pico:02d}h ({count_pico:,} msgs | {prop_pico:.1f}% do total de {remetente})")

# Teste estatístico - há diferença entre os padrões horários?
from scipy.stats import chi2_contingency

contingency_table = pd.crosstab(df['hora'], df['remetente'])
chi2, p_value, dof, expected = chi2_contingency(contingency_table)

print(f"\n📊 Teste Chi-square (padrão horário P1 vs P2):")
print(f"   p-value: {p_value:.4f}")
if p_value < 0.05:
    print(f"   ✅ Padrões horários SÃO diferentes entre P1 e P2")
else:
    print(f"   ❌ Padrões horários SÃO similares entre P1 e P2")
Figure 8: Comparação de padrões horários entre P1 e P2

📊 Comparação de horários de pico por remetente:

P1: Pico às 19h (4,768 msgs | 10.2% do total de P1)
P2: Pico às 19h (4,609 msgs | 10.2% do total de P2)

📊 Teste Chi-square (padrão horário P1 vs P2):
   p-value: 0.0000
   ✅ Padrões horários SÃO diferentes entre P1 e P2
Warning💡 Insight - Padrões Horários por Remetente

Sobreposição quase perfeita: - As curvas de P1 e P2 são praticamente idênticas - Ambos têm pico às 19h (~10% das mensagens) - Crescimento gradual similar: 8h → 19h - Queda similar: após 21h - Madrugada igualmente vazia (0h-6h)

Validação estatística: - Chi-square: p = [valor do teste] - [Se p > 0.05]: Padrões horários SÃO estatisticamente similares - [Se p < 0.05]: Apesar de diferença estatística, efeito prático é mínimo (curvas sobrepostas)

Interpretação: - Ambos seguem a mesma rotina diária (provavelmente sincronizada) - Horários de sono similares (madrugada vazia) - Horários de trabalho similares (baixa atividade 8h-17h) - Horários de conversa similares (pico noturno pós-trabalho)

Implicação para FE: - ❌ Feature hora × remetente NÃO é necessária - comportamento idêntico - ✅ Feature hora sozinha já captura o padrão (universal para ambos) - 🎯 Relacionamento sincronizado - conversam nos mesmos horários


Paradoxo estatístico: - Chi-square: p < 0.001 → Estatisticamente diferente - MAS efeito prático é mínimo (curvas visualmente sobrepostas) - Com N=91.924, até diferenças de 0.5% tornam-se significativas

Interpretação: - Ambos seguem a mesma rotina diária (sincronizada) - Qualquer diferença detectada é ruído, não padrão sistemático - Relacionamento à distância com horários de conversa alinhados

Implicação para FE: - ❌ Feature hora × remetente desnecessária - efeito prático irrelevante - ✅ Feature hora sozinha já captura padrão (universal para ambos) - 📚 Lição: Com datasets grandes, sempre validar significância estatística COM análise visual

6.3 Distribuição por Dia da Semana

Code
# Extrai dia da semana
dias_semana = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado', 'Domingo']
df['dia_semana_num'] = df['timestamp'].dt.dayofweek
df['dia_semana'] = df['dia_semana_num'].apply(lambda x: dias_semana[x])

# Contagem por dia da semana
msgs_por_dia_semana = df.groupby('dia_semana').size().reindex(dias_semana).reset_index(name='count')

print(f"📅 Mensagens por dia da semana:")
for _, row in msgs_por_dia_semana.iterrows():
    print(f"   {row['dia_semana']}: {row['count']:,}")
📅 Mensagens por dia da semana:
   Segunda: 12,848
   Terça: 14,558
   Quarta: 14,181
   Quinta: 12,883
   Sexta: 12,477
   Sábado: 11,185
   Domingo: 13,792
Code
# Visualização
fig = px.bar(
    msgs_por_dia_semana,
    x='dia_semana',
    y='count',
    title='Distribuição de Mensagens por Dia da Semana',
    labels={'dia_semana': 'Dia da Semana', 'count': 'Quantidade de Mensagens'}
)

fig.update_layout(height=400)
fig.show()
Figure 9: Distribuição de mensagens por dia da semana
Warning💡 Insight - Padrões Semanais (ou a falta deles)

Distribuição: Terça (14.558) > Domingo (13.792) > Quarta (14.181) > Quinta (12.883) > Segunda (12.848) > Sexta (12.477) > Sábado (11.185)

Validação estatística: - ✅ Chi-square (p<0.001): Dias TÊM diferenças - ❌ T-test weekday vs weekend (p=0.96): SEM diferença útil/FDS - Weekday: 282.5 msgs/dia - Weekend: 283.8 msgs/dia

Achado surpreendente: Relacionamentos à distância geralmente têm pico em FDS. Aqui a distribuição é uniforme.

Hipóteses: 1. Comunicação durante expediente (não só tempo livre) 2. Encontros presenciais em dias variados (não concentrados em FDS) ← REFORÇAR ESSA - Temporadas juntos (semanas inteiras de encontro) diluem padrão semanal 3. Rotina muito estável independente do dia

Para FE: - ⚠️ dia_semana (7 níveis): manter por completude - ❌ fim_de_semana (booleana): baixíssimo poder (desconsiderar)


Uniformidade visual total: - P1 e P2: ~12-15% em todos os dias (barras alinhadas) - Não há “dia preferencial” para nenhum dos dois - Distribuição equilibrada ao longo da semana

Paradoxo estatístico: - Chi-square: p = 0.027 → Estatisticamente diferente - MAS efeito prático é mínimo (barras praticamente iguais) - Diferenças detectadas são marginais (~1-2 pontos percentuais)

Interpretação: - Ambos conversam de forma igualmente distribuída na semana - Não há padrão tipo “P1 prefere FDS, P2 prefere dias úteis” - Reforça hipótese de encontros presenciais em dias variados (afeta ambos)

Implicação para FE: - ❌ Feature dia_semana × remetente desnecessária - efeito irrelevante - ❌ Feature fim_de_semana × remetente também desnecessária - 🎯 Dupla confirmação: dia da semana é feature fraca E igual para ambos

6.3.1 Comparação por Remetente (Dia da Semana)

Code
# Agrupa por dia e remetente
msgs_dia_remetente = df.groupby(['dia_semana', 'remetente']).size().reset_index(name='count')

# Reordena dias da semana
msgs_dia_remetente['dia_semana'] = pd.Categorical(
    msgs_dia_remetente['dia_semana'],
    categories=dias_semana,
    ordered=True
)
msgs_dia_remetente = msgs_dia_remetente.sort_values('dia_semana')

# Calcula proporção
msgs_dia_remetente['proporcao'] = msgs_dia_remetente.apply(
    lambda row: (row['count'] / total_p1 * 100) if row['remetente'] == 'P1' else (row['count'] / total_p2 * 100),
    axis=1
)

# Visualização
fig = px.bar(
    msgs_dia_remetente,
    x='dia_semana',
    y='proporcao',
    color='remetente',
    barmode='group',
    title='Padrão Semanal por Remetente (%)',
    labels={'dia_semana': 'Dia da Semana', 'proporcao': 'Proporção de Mensagens (%)', 'remetente': 'Remetente'}
)

fig.update_layout(height=400)
fig.show()

# Teste estatístico
contingency_table_dia = pd.crosstab(df['dia_semana'], df['remetente'])
chi2_dia, p_value_dia, dof_dia, expected_dia = chi2_contingency(contingency_table_dia)

print(f"\n📊 Teste Chi-square (padrão semanal P1 vs P2):")
print(f"   p-value: {p_value_dia:.4f}")
if p_value_dia < 0.05:
    print(f"   ✅ Padrões semanais SÃO diferentes entre P1 e P2")
else:
    print(f"   ❌ Padrões semanais SÃO similares entre P1 e P2")
Figure 10: Comparação de padrões semanais entre P1 e P2

📊 Teste Chi-square (padrão semanal P1 vs P2):
   p-value: 0.0265
   ✅ Padrões semanais SÃO diferentes entre P1 e P2
Warning💡 Insight - Padrões Semanais por Remetente

Uniformidade total: - P1 e P2 têm distribuição praticamente idêntica em todos os dias - Variação: ~12-15% em todos os dias (para ambos) - Não há “dia preferencial” para nenhum dos dois - Barras azuis e vermelhas alinhadas em todos os dias

Validação estatística: - Chi-square: p = [valor do teste] - [Interpretar conforme resultado]

Interpretação: - Comunicação igualmente distribuída ao longo da semana para ambos - Não há padrão tipo “P1 conversa mais em FDS, P2 mais em dias úteis” - Reforça hipótese de encontros presenciais em dias variados (afeta ambos igualmente)

Implicação para FE: - ❌ Feature dia_semana × remetente NÃO é necessária - comportamento idêntico - ❌ Feature fim_de_semana × remetente também desnecessária - 🎯 Padrões semanais são fracos E iguais para ambos (dupla confirmação de baixa utilidade)

6.3.2 Heatmap: Hora × Dia da Semana

Code
# Cria matriz hora x dia da semana
heatmap_data = df.groupby(['dia_semana_num', 'hora']).size().unstack(fill_value=0)
heatmap_data.index = [dias_semana[i] for i in heatmap_data.index]

# Visualização
fig = px.imshow(
    heatmap_data.T,
    labels=dict(x="Dia da Semana", y="Hora do Dia", color="Mensagens"),
    title='Heatmap: Distribuição de Mensagens por Hora e Dia da Semana',
    aspect="auto",
    color_continuous_scale='Blues'
)

fig.update_layout(height=500)
fig.show()
Figure 11: Heatmap de mensagens por hora e dia da semana

6.3.3 Validação Estatística

Os gráficos anteriores sugerem padrões visuais, mas precisamos confirmar estatisticamente se as diferenças observadas são reais ou apenas variação aleatória.

Testes aplicados: 1. Chi-square: Verifica se a distribuição entre os 7 dias da semana é uniforme 2. T-test (Weekday vs Weekend): Testa diferença entre dias úteis e finais de semana 3. Padrão alternado: Valida se existe padrão visual de dias alternados

Critério de decisão: p-value < 0.05 indica diferença estatisticamente significativa.

Code
# Testes estatísticos
from scipy.stats import chisquare, ttest_ind

# 1. Chi-square: Distribuição uniforme entre dias da semana?
obs = msgs_por_dia_semana['count'].values
stat_chi, p_chi = chisquare(obs)

print(f"📊 Teste Chi-square (distribuição uniforme):")
print(f"   p-value: {p_chi:.4f}")
if p_chi > 0.05:
    print(f"   ✅ Distribuição uniforme (p > 0.05) - dias da semana SEM diferença significativa")
else:
    print(f"   ❌ Distribuição não-uniforme (p < 0.05) - dias da semana COM diferença significativa")

# 2. T-test: Dias úteis vs Fim de semana
df['eh_fds'] = df['dia_semana_num'] >= 5

weekday_days = df[~df['eh_fds']].groupby('data').size()
weekend_days = df[df['eh_fds']].groupby('data').size()

weekday_avg = weekday_days.mean()
weekend_avg = weekend_days.mean()

stat_t, p_t = ttest_ind(weekday_days, weekend_days)

print(f"\n📊 T-test (Dias úteis vs Fim de semana):")
print(f"   Média weekday: {weekday_avg:.1f} msgs/dia")
print(f"   Média weekend: {weekend_avg:.1f} msgs/dia")
print(f"   Diferença: {((weekday_avg - weekend_avg) / weekend_avg * 100):.1f}%")
print(f"   p-value: {p_t:.4f}")
if p_t > 0.05:
    print(f"   ✅ SEM diferença significativa (p > 0.05)")
else:
    print(f"   ❌ COM diferença significativa (p < 0.05)")

# 3. Validação: Dias alternados (Seg/Qua/Sex/Dom vs Ter/Qui/Sáb)
print("\n📊 Testando padrão de dias alternados:\n")

grupo_a = ['Segunda', 'Quarta', 'Sexta', 'Domingo']
grupo_b = ['Terça', 'Quinta', 'Sábado']

msgs_grupo_a = msgs_por_dia_semana[msgs_por_dia_semana['dia_semana'].isin(grupo_a)]['count'].sum()
msgs_grupo_b = msgs_por_dia_semana[msgs_por_dia_semana['dia_semana'].isin(grupo_b)]['count'].sum()

media_a = msgs_grupo_a / 4
media_b = msgs_grupo_b / 3

print(f"Grupo A (Seg/Qua/Sex/Dom): {msgs_grupo_a:,} msgs | Média: {media_a:.1f} msgs/dia")
print(f"Grupo B (Ter/Qui/Sáb): {msgs_grupo_b:,} msgs | Média: {media_b:.1f} msgs/dia")
print(f"Diferença: {((media_a - media_b) / media_b * 100):.1f}%")

df['grupo_alternado'] = df['dia_semana'].isin(grupo_a)
grupo_a_days = df[df['grupo_alternado']].groupby('data').size()
grupo_b_days = df[~df['grupo_alternado']].groupby('data').size()

stat_t_alt, p_t_alt = ttest_ind(grupo_a_days, grupo_b_days)
print(f"T-test p-value: {p_t_alt:.4f}")
if p_t_alt < 0.05:
    print(f"✅ Padrão alternado É estatisticamente significativo!")
else:
    print(f"❌ Padrão alternado NÃO é estatisticamente significativo")

# Teste comunicação matinal
manha_grupo_a = df[(df['grupo_alternado']) & (df['hora'].between(6, 11))].shape[0]
manha_grupo_b = df[(~df['grupo_alternado']) & (df['hora'].between(6, 11))].shape[0]

prop_manha_a = manha_grupo_a / msgs_grupo_a * 100
prop_manha_b = manha_grupo_b / msgs_grupo_b * 100

print(f"\n📊 Comunicação por período:")
print(f"Grupo A - Manhã (6h-11h): {prop_manha_a:.1f}%")
print(f"Grupo B - Manhã (6h-11h): {prop_manha_b:.1f}%")
print(f"Diferença: {(prop_manha_a - prop_manha_b):.1f}pp")

noite_grupo_a = df[(df['grupo_alternado']) & (df['hora'].between(18, 22))].shape[0]
noite_grupo_b = df[(~df['grupo_alternado']) & (df['hora'].between(18, 22))].shape[0]

prop_noite_a = noite_grupo_a / msgs_grupo_a * 100
prop_noite_b = noite_grupo_b / msgs_grupo_b * 100

print(f"Grupo A - Noite (18h-22h): {prop_noite_a:.1f}%")
print(f"Grupo B - Noite (18h-22h): {prop_noite_b:.1f}%")
print(f"Diferença: {(prop_noite_a - prop_noite_b):.1f}pp")
📊 Teste Chi-square (distribuição uniforme):
   p-value: 0.0000
   ❌ Distribuição não-uniforme (p < 0.05) - dias da semana COM diferença significativa

📊 T-test (Dias úteis vs Fim de semana):
   Média weekday: 282.5 msgs/dia
   Média weekend: 283.8 msgs/dia
   Diferença: -0.5%
   p-value: 0.9647
   ✅ SEM diferença significativa (p > 0.05)

📊 Testando padrão de dias alternados:

Grupo A (Seg/Qua/Sex/Dom): 53,298 msgs | Média: 13324.5 msgs/dia
Grupo B (Ter/Qui/Sáb): 38,626 msgs | Média: 12875.3 msgs/dia
Diferença: 3.5%
T-test p-value: 0.7521
❌ Padrão alternado NÃO é estatisticamente significativo

📊 Comunicação por período:
Grupo A - Manhã (6h-11h): 17.9%
Grupo B - Manhã (6h-11h): 15.6%
Diferença: 2.3pp
Grupo A - Noite (18h-22h): 39.5%
Grupo B - Noite (18h-22h): 36.7%
Diferença: 2.8pp
Warning💡 Insight - Cruzamento Hora × Dia

Padrão visual vs realidade estatística:

O heatmap sugere visualmente um “padrão alternado” (Seg/Qua/Sex/Dom mais intensos), mas os testes mostram: - Diferença de volume: apenas 3.5% (p = 0.75) → NÃO significativo - Comunicação matinal: diferença de 2.3pp → marginal - Comunicação noturna: diferença de 2.8pp → marginal

Conclusão: A variação entre dias é ruído aleatório, não padrão sistemático.

Padrões REAIS identificados:

✅ Concentração noturna universal: - 18h-22h concentra ~38% de toda comunicação (todos os dias) - 19h-20h é pico absoluto (faixa azul escuro consistente)

✅ Madrugada vazia: - 0h-6h praticamente sem atividade (azul clarinho uniforme)

✅ Crescimento gradual: - Atividade aumenta de 8h até 19h - Queda abrupta após 21h

Implicações para FE: - ✅ Feature periodo_dia (Manhã/Tarde/Noite) essencial - diferença clara - ✅ Feature hora captura nuances (pico 19h) - ❌ Feature dia_semana específico: poder limitado (variação não-sistemática) - ❌ Feature fim_de_semana: desconsiderar (já provamos p = 0.96)

Lição importante: Nem tudo que “parece padrão” no heatmap é estatisticamente real. Sempre validar com testes! 📊

6.4 Insights Temporais

Code
print("💡 INSIGHTS - Padrões Temporais:\n")

# Horário de pico
hora_pico = msgs_por_hora.loc[msgs_por_hora['count'].idxmax(), 'hora']
print(f"• Horário de pico: {hora_pico:02d}h")

# Dia da semana mais ativo
dia_pico = msgs_por_dia_semana.loc[msgs_por_dia_semana['count'].idxmax(), 'dia_semana']
print(f"• Dia da semana mais ativo: {dia_pico}")

# Comparação weekday vs weekend
df['fim_de_semana'] = df['dia_semana_num'] >= 5
msgs_weekday = df[~df['fim_de_semana']].shape[0]
msgs_weekend = df[df['fim_de_semana']].shape[0]
print(f"• Mensagens em dias úteis: {msgs_weekday:,} ({msgs_weekday/total_msgs*100:.1f}%)")
print(f"• Mensagens em finais de semana: {msgs_weekend:,} ({msgs_weekend/total_msgs*100:.1f}%)")

# Período do dia
df['periodo_dia'] = df['hora'].apply(lambda h: 
    'Madrugada' if 0 <= h <= 5 else
    'Manhã' if 6 <= h <= 11 else
    'Tarde' if 12 <= h <= 17 else
    'Noite'
)
msgs_por_periodo = df['periodo_dia'].value_counts()
periodo_pico = msgs_por_periodo.idxmax()
print(f"• Período do dia mais ativo: {periodo_pico} ({msgs_por_periodo[periodo_pico]:,} mensagens)")
💡 INSIGHTS - Padrões Temporais:

• Horário de pico: 19h
• Dia da semana mais ativo: Terça
• Mensagens em dias úteis: 66,947 (72.8%)
• Mensagens em finais de semana: 24,977 (27.2%)
• Período do dia mais ativo: Noite (37,902 mensagens)
Note📊 Síntese - Padrões Temporais

Volume e distribuição temporal: - Período: 1 ano (out/2024 - out/2025), 325 dias ativos - Média: 283 msgs/dia | Pico: 1.288 msgs (jun/2025) - Evolução em 4 fases distintas identificadas

Padrões intradiários (FORTE poder discriminatório): - ✅ Hora: Pico às 19h (9.377 msgs) vs madrugada (~5%) - ✅ Período: Noite (18h-23h) = 38% de toda comunicação - 📈 Crescimento gradual 8h→19h | Queda abrupta pós-21h

Padrões semanais (FRACO poder discriminatório): - ⚠️ Dia da semana: Variação existe (Ter=14.558 vs Sáb=11.185), mas não sistemática - ❌ Fim de semana: SEM diferença estatística (p=0.96) - Weekday: 282.5 msgs/dia | Weekend: 283.8 msgs/dia

Interação Hora × Dia: - 🎯 Não há interação relevante - padrão horário (pico 18h-21h) é universal para todos os dias - Features hora e dia_semana são independentes - Não há necessidade de criar features de interação

Validação estatística: - Chi-square dias da semana: p<0.001 (há diferenças) - T-test weekday vs weekend: p=0.96 (sem diferença) - Conclusão: Variação entre dias existe, mas não segue lógica útil/FDS

Diferenças por remetente: - ⚠️ Padrões horários: Estatisticamente diferentes (p<0.001) mas praticamente idênticos (pico 19h para ambos, curvas sobrepostas) - ⚠️ Padrões semanais: Estatisticamente diferentes (p=0.027) mas praticamente idênticos (12-15% em todos os dias) - 🎯 Conclusão: P1 e P2 têm rotinas temporais sincronizadas - diferenças estatísticas são artefato de N grande - Implicação: Features de interação hora × remetente ou dia × remetente são desnecessárias (efeito prático irrelevante) - 📚 Lição: Significância estatística ≠ Relevância prática (sempre validar com visualização)

Implicações para Feature Engineering:

Criar: - ✅ hora (0-23) - padrão muito claro - ✅ periodo_dia (Madrugada/Manhã/Tarde/Noite) - essencial - ✅ mes / trimestre - captura evolução temporal

Evitar: - ❌ fim_de_semana - poder discriminatório zero (p=0.96) - ❌ hora × remetente - efeito prático irrelevante - ❌ dia_semana × remetente - efeito prático irrelevante - ❌ hora × dia_semana - não há interação - ⚠️ dia_semana - manter por completude, mas expectativa baixa

Contexto externo é CRÍTICO:

Evidências: - Gaps no timeline sugerem “temporadas juntos” (SP ↔︎ BSB) - Uniformidade semanal atípica (encontros em dias variados, não só FDS) - Pico Jun-Jul/2025 pode relacionar-se a período específico de separação/encontro - Variabilidade extrema: 0 a 1.288 msgs/dia

Variável crítica: Presença física vs distância - Quando juntos: Volume ≈ 0 msgs/dia - Quando separados: Volume ≈ 200-500 msgs/dia
- Picos: Saudade pré-encontro ou ansiedade pós-separação

Próximos passos: - ✅ Integrar dados de encontros presenciais como features externas - ✅ Criar em_encontro (booleana) - ✅ Criar dias_desde_ultimo_encontro / dias_ate_proximo_encontro - 🔍 Validar se gaps coincidem com datas de encontros


Important🔍 Nota Importante - Limitação da Análise Atual

Esta análise exploratória identificou padrões temporais fortes (hora do dia) e padrões fracos (dia da semana), mas há uma variável crítica faltando: dados de encontros presenciais.

Evidências de que encontros são fundamentais: - Gaps inexplicados no timeline - Uniformidade semanal atípica (não segue padrão FDS) - Variabilidade extrema de volume (0 a 1.288 msgs/dia) - Picos e vales não explicados apenas por padrões temporais

Impacto na interpretação: - Muitas das “anomalias” temporais provavelmente se explicam por contexto externo - Features temporais puras têm poder limitado sem considerar presença física - Análises de conteúdo e interação também serão afetadas por esse contexto

Solução: A seção de “Features de Contexto Externo” no Feature Engineering integrará dados de encontros, permitindo análises mais robustas.

7 A

7.1 Adicionar dados de encontros.



8 Resumo Executivo - EDA Data Wrangling

8.1 Visão Geral

Este notebook realizou análise exploratória dos dados pós-wrangling, antes da criação de features derivadas. O objetivo foi identificar padrões nos dados básicos para justificar quais features criar no Feature Engineering.

Dataset analisado: - 91.924 mensagens ao longo de 1 ano (out/2024 - out/2025) - 8 colunas básicas: timestamp, remetente, tipo_mensagem, grupo_mensagem, conteudo, arquivo, transcricao, date_match - 2 remetentes: P1 (51.1%) e P2 (48.9%) - distribuição equilibrada


8.2 Tabela Resumo - Análises Realizadas

Análise Resultado Principal Validação Estatística Acionável para FE
Volume de Mensagens P1: 46.948 (51%) | P2: 44.976 (49%) - ✅ Distribuição equilibrada → features comparativas terão poder moderado
Tipos de Mensagem TEXT domina (73.2%) | VID (10.4%) | AUDIO (9.3%) - ✅ Criar: grupo_mensagem (11 grupos)
❌ Evitar: tipo_mensagem (21 tipos - granularidade excessiva)
Preferências por Remetente P1: 11.4% áudio | P2: 7.2% áudio
P1 usa 58% mais áudio que P2
- ✅ Feature grupo_mensagem + remetente tem poder discriminatório
Transcrições 695 msgs (0.8%) com transcrição
AUDIO: 510 | VID: 185
- ✅ Manter flag transcricao
❌ Não criar features complexas sobre transcrições (amostra pequena)
✅ Análises de conteúdo por tipo de mídia são viáveis (N=510/185 suficiente para comparações)
Evolução Temporal 4 fases: Out-Dez (crescimento) → Jan-Mai (estável) → Jun-Jul (PICO 4.5x) → Ago-Out (normalização) - ✅ Criar: mes, trimestre
🔍 Investigar: Pico Jun-Jul e gaps (hipótese: encontros presenciais)
Hora do Dia Pico 19h (10.2%) | 18h-22h = 38% do total
Madrugada ~5%
- ✅ Criar: hora, periodo_dia (essencial)
✅ Criar: horario_comercial
Dia da Semana Variação: Ter (14.558) vs Sáb (11.185) = 23% Chi²: p<0.001 (há diferenças)
T-test: p=0.96 (weekday=weekend)
⚠️ Criar: dia_semana (por completude, mas poder limitado)
❌ Evitar: fim_de_semana (p=0.96 - zero poder discriminatório)
Heatmap Hora × Dia Pico 18h-21h universal em todos os dias
Padrão alternado: p=0.75 (não significativo)
- ❌ Evitar: hora × dia_semana (não há interação)
🎯 Features são independentes
P1 vs P2 - Hora Pico idêntico (19h para ambos)
Curvas sobrepostas
Chi²: p<0.001 mas efeito prático mínimo ❌ Evitar: hora × remetente (rotinas sincronizadas)
📚 Lição: significância ≠ relevância prática
P1 vs P2 - Dia Distribuição uniforme para ambos (12-15% todos os dias) Chi²: p=0.027 mas efeito prático mínimo ❌ Evitar: dia_semana × remetente (padrões idênticos)

8.3 Achados Críticos

8.3.1 🔴 VARIÁVEL CRÍTICA FALTANDO: Encontros Presenciais

Evidências identificadas: - Gaps inexplicados no timeline (dias com volume ~0) - Uniformidade semanal atípica (não segue padrão FDS típico de relacionamento à distância) - Variabilidade extrema: 0 a 1.288 msgs/dia - Picos e vales não explicados por padrões temporais

Hipótese: - Quando juntos (SP ↔︎ BSB): Volume ≈ 0 msgs/dia - Quando separados: Volume ≈ 200-500 msgs/dia - Picos: Saudade pré-encontro ou ansiedade pós-separação

Impacto na análise: - Features temporais puras têm poder limitado sem considerar presença física - Análises de conteúdo e interação também serão afetadas

Ação obrigatória: Integrar dados de encontros ANTES de continuar EDA


8.4 Features Recomendadas para FE

8.4.1 ✅ CRIAR (alto poder discriminatório):

Feature Tipo Justificativa
hora int (0-23) Padrão muito claro (pico 19h vs madrugada)
periodo_dia category Noite (38%) >> Manhã/Madrugada - essencial
mes / trimestre int / category Captura evolução temporal (4 fases)
grupo_mensagem category TEXT (73%) vs AUDIO (9%) vs VID (10%) - preferências claras
horario_comercial bool Diferencia trabalho vs lazer

8.4.2 ❌ EVITAR (baixo/zero poder discriminatório):

Feature Justificativa
fim_de_semana p=0.96 - zero poder discriminatório
dia_semana × remetente Padrões idênticos entre P1 e P2
hora × remetente Rotinas sincronizadas (curvas sobrepostas)
hora × dia_semana Não há interação relevante
tipo_mensagem (21 tipos) Granularidade excessiva, usar grupo_mensagem

8.4.3 ⚠️ MANTER POR COMPLETUDE (expectativa baixa):

Feature Observação
dia_semana Variação existe (p<0.001) mas não sistemática

8.5 Contexto Externo - Próximos Passos

8.5.1 🎯 ANTES de continuar EDA:

  1. Criar 03-contexto-externo.qmd
  2. Integrar dados de encontros presenciais:
    • em_encontro (bool)
    • encontro_id (int)
    • dias_desde_ultimo_encontro (int)
    • dias_ate_proximo_encontro (int)
  3. Validar hipótese: gaps coincidem com encontros?
  4. Exportar messages_with_context.parquet

8.5.2 📊 DEPOIS:

  1. Continuar EDA com contexto integrado:
    • Análise de conteúdo (com/sem encontros)
    • Padrões de interação (com/sem encontros)
  2. Feature Engineering completo

8.6 Lições Aprendidas

  1. Significância estatística ≠ Relevância prática
    • Com N=91.924, até diferenças de 0.5% são “significativas”
    • Sempre validar testes com visualização
  2. Contexto externo é fundamental
    • Relacionamentos à distância têm dinâmica própria
    • Presença física vs distância modera todos os padrões
  3. Granularidade importa
    • 21 tipos vs 11 grupos: agregação reduz ruído
    • Menos features, mais interpretáveis
  4. Nem tudo que parece padrão é real
    • Heatmap sugeria padrão alternado (visual)
    • Testes mostraram p=0.75 (ruído aleatório)

Dataset exportado: messages.parquet (91.924 × 8 colunas)
Próximo arquivo: 03-contexto-externo.qmd


9 Análise de Conteúdo

9.1 Mensagens com Texto

Code
# Filtra mensagens de texto (não-nulas)
df_texto = df[df['conteudo'].notna()].copy()

print(f"📝 Mensagens com conteúdo textual: {len(df_texto):,} ({len(df_texto)/total_msgs*100:.1f}%)")
📝 Mensagens com conteúdo textual: 68,686 (74.7%)

9.2 Tamanho das Mensagens

Code
# Calcula tamanho em caracteres
df_texto['tamanho_chars'] = df_texto['conteudo'].str.len()

print(f"📊 Estatísticas de tamanho (caracteres):")
print(df_texto['tamanho_chars'].describe())
📊 Estatísticas de tamanho (caracteres):
count    68686.000000
mean        27.204772
std         85.696511
min          1.000000
25%          9.000000
50%         17.000000
75%         30.000000
max       8561.000000
Name: tamanho_chars, dtype: float64
Code
# Visualização (limitando outliers para melhor visualização)
df_plot = df_texto[df_texto['tamanho_chars'] <= df_texto['tamanho_chars'].quantile(0.95)]

fig = px.histogram(
    df_plot,
    x='tamanho_chars',
    nbins=50,
    title='Distribuição do Tamanho das Mensagens (até percentil 95)',
    labels={'tamanho_chars': 'Caracteres', 'count': 'Frequência'}
)

fig.update_layout(height=400)
fig.show()
Figure 12: Distribuição do tamanho das mensagens (caracteres)
Code
# Calcula tamanho em palavras
df_texto['tamanho_palavras'] = df_texto['conteudo'].str.split().str.len()

print(f"\n📊 Estatísticas de tamanho (palavras):")
print(df_texto['tamanho_palavras'].describe())

📊 Estatísticas de tamanho (palavras):
count    68686.000000
mean         5.262149
std         13.636641
min          1.000000
25%          2.000000
50%          3.000000
75%          6.000000
max       1164.000000
Name: tamanho_palavras, dtype: float64
Code
# Visualização (limitando outliers)
df_plot = df_texto[df_texto['tamanho_palavras'] <= df_texto['tamanho_palavras'].quantile(0.95)]

fig = px.histogram(
    df_plot,
    x='tamanho_palavras',
    nbins=50,
    title='Distribuição do Tamanho das Mensagens em Palavras (até percentil 95)',
    labels={'tamanho_palavras': 'Palavras', 'count': 'Frequência'}
)

fig.update_layout(height=400)
fig.show()
Figure 13: Distribuição do tamanho das mensagens (palavras)

9.3 Comparação por Remetente

Code
# Visualização comparativa
fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=('Caracteres', 'Palavras')
)

for remetente in df_texto['remetente'].unique():
    df_rem = df_texto[df_texto['remetente'] == remetente]
    
    # Caracteres
    fig.add_trace(
        go.Box(y=df_rem['tamanho_chars'], name=remetente, showlegend=False),
        row=1, col=1
    )
    
    # Palavras
    fig.add_trace(
        go.Box(y=df_rem['tamanho_palavras'], name=remetente),
        row=1, col=2
    )

fig.update_yaxes(title_text="Caracteres", row=1, col=1)
fig.update_yaxes(title_text="Palavras", row=1, col=2)
fig.update_layout(height=400, title_text="Tamanho de Mensagens por Remetente")
fig.show()
Figure 14: Comparação de tamanho de mensagens por remetente
Code
print("\n💡 INSIGHTS - Tamanho de Mensagens:\n")

for remetente in df_texto['remetente'].unique():
    df_rem = df_texto[df_texto['remetente'] == remetente]
    print(f"• {remetente}:")
    print(f"  - Média: {df_rem['tamanho_chars'].mean():.1f} caracteres | {df_rem['tamanho_palavras'].mean():.1f} palavras")
    print(f"  - Mediana: {df_rem['tamanho_chars'].median():.0f} caracteres | {df_rem['tamanho_palavras'].median():.0f} palavras")

💡 INSIGHTS - Tamanho de Mensagens:

• P1:
  - Média: 28.2 caracteres | 5.4 palavras
  - Mediana: 17 caracteres | 3 palavras
• P2:
  - Média: 26.2 caracteres | 5.1 palavras
  - Mediana: 18 caracteres | 4 palavras

10 Padrões de Interação

10.1 Tempo entre Mensagens

Code
# Calcula tempo entre mensagens consecutivas
df_sorted = df.sort_values('timestamp').copy()
df_sorted['tempo_desde_ultima'] = df_sorted['timestamp'].diff().dt.total_seconds()

# Remove primeira mensagem (NaN)
tempo_entre_msgs = df_sorted['tempo_desde_ultima'].dropna()

print(f"⏱️ Tempo entre mensagens (segundos):")
print(tempo_entre_msgs.describe())

# Converte para minutos para interpretação
print(f"\n⏱️ Em minutos:")
print((tempo_entre_msgs / 60).describe())
⏱️ Tempo entre mensagens (segundos):
count    9.192300e+04
mean     3.440703e+02
std      8.710992e+03
min      0.000000e+00
25%      0.000000e+00
50%      8.000000e+00
75%      3.600000e+01
max      2.417848e+06
Name: tempo_desde_ultima, dtype: float64

⏱️ Em minutos:
count    91923.000000
mean         5.734506
std        145.183200
min          0.000000
25%          0.000000
50%          0.133333
75%          0.600000
max      40297.466667
Name: tempo_desde_ultima, dtype: float64
Code
# Visualização (limitando a 1 hora para melhor visualização)
tempo_plot = tempo_entre_msgs[tempo_entre_msgs <= 3600]  # até 1 hora

fig = px.histogram(
    x=tempo_plot / 60,  # converte para minutos
    nbins=50,
    title='Distribuição do Tempo entre Mensagens (até 1 hora)',
    labels={'x': 'Minutos', 'count': 'Frequência'}
)

fig.update_layout(height=400)
fig.show()
Figure 15: Distribuição do tempo entre mensagens
Warning⚠️ Insight - Problema de Escala Identificado

Amplitude EXTREMA no tempo entre mensagens:

Estatísticas descritivas: - Mediana: 8 segundos (resposta muito rápida!) - 75%: 36 segundos (ainda rápido) - Máximo: 40.297 minutos = ~28 DIAS!

Problema: - Histograma linear ESCONDE 99% dos dados em uma única barra - Diferença de escala: 8 segundos vs 28 dias = 300.000x! - Distribuição tem cauda longa extrema

O que isso significa: - Maioria das mensagens são rajadas rápidas (segundos) - Mas há gaps enormes (dias/semanas) - provavelmente coincidindo com encontros ou períodos especiais - Visualização linear é inadequada - precisamos de múltiplas abordagens

Hipóteses a investigar: 1. Contexto: Tempo é MENOR quando separados (conversam mais)? Ou quando juntos (coordenação rápida)? 2. Faixas: Qual % das mensagens são “instantâneas” (<10s) vs “conversas” (1-5min) vs “retomadas” (>1h)? 3. Outliers: Gaps de 1+ dia coincidem com encontros presenciais?

Próximas visualizações necessárias: - ✅ Bins customizados (faixas interpretáveis) - ✅ CDF (% acumulada - ver percentis claramente) - ✅ Separação por contexto (junto vs separado) - ✅ Escala logarítmica (ver toda a distribuição)

10.1.1 Distribuição por Faixas Interpretáveis

10.1.1.1 1. ESCALA LOG (Melhor para cauda longa)

Code
import plotly.express as px
import numpy as np

# Remove zeros (log não aceita)
tempo_positivo = tempo_entre_msgs[tempo_entre_msgs > 0]

# Histograma em escala LOG
fig = px.histogram(
    x=np.log10(tempo_positivo / 60),  # Log10 de minutos
    nbins=50,
    title='Distribuição do Tempo entre Mensagens (Escala LOG)',
    labels={'x': 'Log10(Minutos)', 'y': 'Quantidade'}
)

# Adiciona labels customizados no eixo X
fig.update_xaxes(
    tickvals=[-2, -1, 0, 1, 2, 3, 4],
    ticktext=['0.01 min', '0.1 min', '1 min', '10 min', '100 min', '1000 min', '10000 min']
)

fig.show()

10.1.1.2 2. BINS CUSTOMIZADOS

Code
# Define bins por faixas interpretáveis
bins_segundos = [0, 10, 60, 300, 1800, 3600, 86400, float('inf')]
labels = ['0-10s', '10s-1min', '1-5min', '5-30min', '30min-1h', '1h-24h', '>24h']

tempo_faixas = pd.cut(tempo_entre_msgs, bins=bins_segundos, labels=labels)

# Conta por faixa
contagem = tempo_faixas.value_counts().sort_index()

# Gráfico de barras
fig = px.bar(
    x=contagem.index,
    y=contagem.values,
    title='Tempo entre Mensagens por Faixa',
    labels={'x': 'Faixa de Tempo', 'y': 'Quantidade'},
    text=contagem.values
)

fig.update_traces(texttemplate='%{text:,}', textposition='outside')
fig.show()

print("\n📊 Distribuição por faixa:")
for faixa, qtd in contagem.items():
    pct = (qtd / len(tempo_entre_msgs) * 100)
    print(f"   {faixa}: {qtd:,} ({pct:.1f}%)")

📊 Distribuição por faixa:
   0-10s: 26,780 (29.1%)
   10s-1min: 23,917 (26.0%)
   1-5min: 10,321 (11.2%)
   5-30min: 4,977 (5.4%)
   30min-1h: 847 (0.9%)
   1h-24h: 1,183 (1.3%)
   >24h: 22 (0.0%)

10.2 Respostas Rápidas

Code
# Define threshold de resposta rápida (60 segundos)
respostas_rapidas = (tempo_entre_msgs < 60).sum()
pct_rapidas = (respostas_rapidas / len(tempo_entre_msgs)) * 100

print(f"⚡ Respostas em < 60 segundos: {respostas_rapidas:,} ({pct_rapidas:.1f}%)")

# Diferentes thresholds
thresholds = [10, 30, 60, 120, 300]  # segundos
print(f"\n⚡ Respostas por velocidade:")
for threshold in thresholds:
    count = (tempo_entre_msgs < threshold).sum()
    pct = (count / len(tempo_entre_msgs)) * 100
    print(f"   < {threshold}s: {count:,} ({pct:.1f}%)")
⚡ Respostas em < 60 segundos: 74,390 (80.9%)

⚡ Respostas por velocidade:
   < 10s: 48,894 (53.2%)
   < 30s: 66,558 (72.4%)
   < 60s: 74,390 (80.9%)
   < 120s: 80,141 (87.2%)
   < 300s: 84,879 (92.3%)

10.3 Sequências de Mensagens

Code
# Identifica trocas de remetente
df_sorted['mudou_remetente'] = df_sorted['remetente'] != df_sorted['remetente'].shift()

# Conta mensagens consecutivas do mesmo remetente
df_sorted['sequencia'] = df_sorted.groupby(
    (df_sorted['mudou_remetente']).cumsum()
).cumcount() + 1

print(f"📊 Sequências de mensagens do mesmo remetente:")
print(df_sorted['sequencia'].describe())
print(f"\n📈 Maior sequência: {df_sorted['sequencia'].max()} mensagens consecutivas")

# Distribuição de sequências
seq_dist = df_sorted.groupby('sequencia').size().head(20)
print(f"\n📊 Distribuição das 20 sequências mais comuns:")
print(seq_dist)
📊 Sequências de mensagens do mesmo remetente:
count    91924.000000
mean         3.319438
std          3.054698
min          1.000000
25%          1.000000
50%          2.000000
75%          4.000000
max         54.000000
Name: sequencia, dtype: float64

📈 Maior sequência: 54 mensagens consecutivas

📊 Distribuição das 20 sequências mais comuns:
sequencia
1     28439
2     20946
3     12535
4      9422
5      5525
6      4520
7      2629
8      2350
9      1287
10     1214
11      702
12      668
13      360
14      349
15      200
16      193
17      116
18      115
19       65
20       62
dtype: int64
Code
# Visualização
fig = px.bar(
    x=seq_dist.index[:15],  # Top 15
    y=seq_dist.values[:15],
    title='Top 15 Tamanhos de Sequências Mais Comuns',
    labels={'x': 'Tamanho da Sequência', 'y': 'Frequência'}
)

fig.update_layout(height=400)
fig.show()
Figure 16: Distribuição de sequências de mensagens

10.4 Análise de Turnos

Code
# Identifica inícios de conversa (gap > 1 hora)
THRESHOLD_INICIO = 3600  # 1 hora em segundos
df_sorted['inicio_conversa'] = (df_sorted['tempo_desde_ultima'] > THRESHOLD_INICIO) | (df_sorted['tempo_desde_ultima'].isna())

# Conta turnos
total_turnos = df_sorted['inicio_conversa'].sum()
msgs_por_turno = len(df) / total_turnos

print(f"💬 Total de turnos de conversa: {total_turnos}")
print(f"📊 Média de mensagens por turno: {msgs_por_turno:.1f}")

# Distribução de tamanho dos turnos
df_sorted['turno_id'] = df_sorted['inicio_conversa'].cumsum()
turnos_tamanho = df_sorted.groupby('turno_id').size()

print(f"\n📊 Estatísticas de tamanho dos turnos:")
print(turnos_tamanho.describe())
💬 Total de turnos de conversa: 1206
📊 Média de mensagens por turno: 76.2

📊 Estatísticas de tamanho dos turnos:
count    1206.000000
mean       76.222222
std       138.861346
min         1.000000
25%         3.000000
50%        14.000000
75%        89.000000
max      1290.000000
dtype: float64

11 Conclusões para Feature Engineering

Com base nesta análise exploratória, identificamos os seguintes padrões que justificam a criação de features:

11.1 🎯 Features Temporais Recomendadas

Code
print("""
✅ CRIAR features temporais:

• Dia da semana (categórica ordenada)
  → Padrão claro de variação entre dias
  
• Período do dia (Madrugada/Manhã/Tarde/Noite)
  → Heatmap mostra concentração em períodos específicos
  
• Hora (numérica 0-23)
  → Distribuição não-uniforme, horários de pico identificados
  
• Fim de semana (booleana)
  → Diferença de comportamento identificada
  
❌ NÃO CRIAR features redundantes:
• Ano (se dataset tem apenas 1 ano)
• Data exata (muito granular, sem padrão)
""")

✅ CRIAR features temporais:

• Dia da semana (categórica ordenada)
  → Padrão claro de variação entre dias

• Período do dia (Madrugada/Manhã/Tarde/Noite)
  → Heatmap mostra concentração em períodos específicos

• Hora (numérica 0-23)
  → Distribuição não-uniforme, horários de pico identificados

• Fim de semana (booleana)
  → Diferença de comportamento identificada

❌ NÃO CRIAR features redundantes:
• Ano (se dataset tem apenas 1 ano)
• Data exata (muito granular, sem padrão)

11.2 💬 Features de Texto Recomendadas

Code
print("""
✅ CRIAR features de texto:

• Tamanho em caracteres e palavras
  → Distribuições assimétricas, diferenças entre remetentes
  
• Features de pontuação (?, !, ...)
  → Pode indicar emoção/urgência
  
• Detecção de links/menções
  → Compartilhamento de conteúdo externo
  
• Análise de emojis (presença, quantidade)
  → Expressividade emocional
""")

✅ CRIAR features de texto:

• Tamanho em caracteres e palavras
  → Distribuições assimétricas, diferenças entre remetentes

• Features de pontuação (?, !, ...)
  → Pode indicar emoção/urgência

• Detecção de links/menções
  → Compartilhamento de conteúdo externo

• Análise de emojis (presença, quantidade)
  → Expressividade emocional

11.3 🔄 Features de Conversação Recomendadas

Code
print("""
✅ CRIAR features de conversação:

• Tempo desde última mensagem
  → Alta variabilidade, padrões de engajamento
  
• Resposta rápida (< 60s)
  → {:.1f}% das mensagens são rápidas
  
• Sequência de mensagens do mesmo remetente
  → Padrão de "rajadas" identificado
  
• Turno de conversa
  → Delimita sessões de interação
  
• Tempo até resposta do outro remetente
  → Reciprocidade de engajamento
""".format(pct_rapidas))

✅ CRIAR features de conversação:

• Tempo desde última mensagem
  → Alta variabilidade, padrões de engajamento

• Resposta rápida (< 60s)
  → 80.9% das mensagens são rápidas

• Sequência de mensagens do mesmo remetente
  → Padrão de "rajadas" identificado

• Turno de conversa
  → Delimita sessões de interação

• Tempo até resposta do outro remetente
  → Reciprocidade de engajamento

11.4 📊 Features de Mídia Recomendadas

Code
# Análise de mídia
pct_texto = (df['grupo_mensagem'] == 'TEXT').sum() / total_msgs * 100
pct_audio = (df['grupo_mensagem'] == 'AUDIO').sum() / total_msgs * 100
pct_video = (df['grupo_mensagem'] == 'VID').sum() / total_msgs * 100

print(f"""
✅ CRIAR features de mídia:

• Tipo de mensagem (categórica)
  → {len(df['grupo_mensagem'].unique())} tipos diferentes
  → TEXT: {pct_texto:.1f}%
  → AUDIO: {pct_audio:.1f}%
  → VIDEO: {pct_video:.1f}%
  
• Presença de transcrição (booleana)
  → {total_transcricoes:,} mensagens transcritas
  → Enriquece conteúdo de áudio/vídeo
""")

✅ CRIAR features de mídia:

• Tipo de mensagem (categórica)
  → 11 tipos diferentes
  → TEXT: 73.2%
  → AUDIO: 9.3%
  → VIDEO: 10.4%

• Presença de transcrição (booleana)
  → 695 mensagens transcritas
  → Enriquece conteúdo de áudio/vídeo

11.5 📝 Resumo Final

Code
print("""
╔══════════════════════════════════════════════════════════════╗
║           RESUMO - FEATURES A CRIAR                          ║
╚══════════════════════════════════════════════════════════════╝

🎯 TEMPORAIS (6-8 features):
   • dia_semana, periodo_dia, hora, fim_de_semana
   • mes, trimestre (se período > 3 meses)
   
💬 TEXTO (8-10 features):
   • tamanho_caracteres, tamanho_palavras
   • tem_emoji, qtd_emojis, tem_link, tem_mencao
   • tem_interrogacao, tem_exclamacao, eh_caixa_alta
   
🔄 CONVERSAÇÃO (6-7 features):
   • tempo_desde_ultima_msg
   • tempo_desde_ultima_msg_mesmo_remetente
   • eh_resposta_rapida, eh_inicio_conversa
   • sequencia_mesmo_remetente, turno_conversa
   • tempo_resposta_outro_remetente
   
📊 MÍDIA & CONTEXTO (3-5 features):
   • Já temos: tipo_mensagem, grupo_mensagem, transcricao
   • Adicionar se relevante: contexto externo (encontros, etc)

Total estimado: 23-30 features
""")

╔══════════════════════════════════════════════════════════════╗
║           RESUMO - FEATURES A CRIAR                          ║
╚══════════════════════════════════════════════════════════════╝

🎯 TEMPORAIS (6-8 features):
   • dia_semana, periodo_dia, hora, fim_de_semana
   • mes, trimestre (se período > 3 meses)

💬 TEXTO (8-10 features):
   • tamanho_caracteres, tamanho_palavras
   • tem_emoji, qtd_emojis, tem_link, tem_mencao
   • tem_interrogacao, tem_exclamacao, eh_caixa_alta

🔄 CONVERSAÇÃO (6-7 features):
   • tempo_desde_ultima_msg
   • tempo_desde_ultima_msg_mesmo_remetente
   • eh_resposta_rapida, eh_inicio_conversa
   • sequencia_mesmo_remetente, turno_conversa
   • tempo_resposta_outro_remetente

📊 MÍDIA & CONTEXTO (3-5 features):
   • Já temos: tipo_mensagem, grupo_mensagem, transcricao
   • Adicionar se relevante: contexto externo (encontros, etc)

Total estimado: 23-30 features

12 Próximos Passos

  1. Feature Engineering (04-feature-engineering.qmd)
    • Criar features identificadas nesta análise
    • Documentar motivação de cada feature
    • Validar criação com estatísticas descritivas
  2. Feature Selection (04-eda-features.qmd)
    • Analisar correlações entre features criadas
    • Identificar redundâncias
    • Selecionar conjunto final (10-15 features core)
  3. Model Features (opcional)
    • Sentiment analysis (BERT)
    • Embeddings semânticos
    • Topic modeling
  4. Advanced Analysis
    • Clustering com features selecionadas
    • Análises temporais avançadas
    • Visualizações finais