---
title: "EDA Final - Análises com Contexto Integrado"
subtitle: "Exploratory Data Analysis com encontros presenciais (versão definitiva)"
author: "Marlon"
date: today
format:
html:
toc: true
toc-depth: 3
code-fold: show
code-tools: true
df-print: paged
theme: cosmo
number-sections: true
execute:
warning: false
message: false
freeze: true
---
# Introdução
Este notebook realiza a **análise exploratória DEFINITIVA** dos dados, integrando **contexto externo** (encontros presenciais) em todas as análises.
**Diferença dos notebooks anteriores:**
- **02.1-EDA-data-wrangling.qmd:** Análises temporais PURAS (sem contexto externo)
- **03-contexto-externo.qmd:** Criação e validação das features de encontros
- **02.3-EDA-conteudo-interacao.qmd (ESTE):** Análises temporais COM contexto (versão final)
**Objetivo:**
- Revisitar TODAS as análises temporais comparando **junto vs separado**
- Identificar padrões finais para Feature Engineering
- Consolidar aprendizados antes de criar features derivadas
***
# Setup
## Bibliotecas
```{python}
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
from datetime import datetime, timedelta
from scipy.stats import chi2_contingency, ttest_ind, chisquare
import warnings
warnings.filterwarnings('ignore')
# Configurações
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)
# Cores
COLORS = {
'P1': '#3498db',
'P2': '#e74c3c',
'neutral': '#95a5a6',
'juntos': '#2ecc71',
'separados': '#7f8c8d'
}
```
## Caminhos e Carregamento
```{python}
# 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 - DATASET ENRIQUECIDO
INPUT_FILE = DATA_DIR / 'messages_with_context.parquet'
print(f"📁 Diretório: {DATA_DIR}")
print(f"📂 Input: {INPUT_FILE.name}")
print(f"✅ Existe: {INPUT_FILE.exists()}")
```
```{python}
# Carrega dataset ENRIQUECIDO
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()}")
print(f"\n📋 Colunas disponíveis:")
for i, col in enumerate(df.columns, 1):
print(f" {i:2d}. {col}")
```
***
# Visão Geral do Dataset
## Estatísticas Básicas
```{python}
total_msgs = len(df)
periodo_dias = (df['timestamp'].max() - df['timestamp'].min()).days + 1
print("="*80)
print("📊 OVERVIEW - Dataset Enriquecido")
print("="*80)
print(f"\n📈 Volume:")
print(f" • Total de mensagens: {total_msgs:,}")
print(f" • Período: {df['timestamp'].min().date()} a {df['timestamp'].max().date()}")
print(f" • Dias no período: {periodo_dias}")
print(f" • Média geral: {total_msgs/periodo_dias:.1f} msgs/dia")
print(f"\n👥 Remetentes:")
for remetente in df['remetente'].unique():
count = df[df['remetente'] == remetente].shape[0]
pct = count / total_msgs * 100
print(f" • {remetente}: {count:,} mensagens ({pct:.1f}%)")
print(f"\n🏠 Contexto Externo:")
msgs_juntos = df[df['em_encontro']].shape[0]
msgs_separados = df[~df['em_encontro']].shape[0]
print(f" • Durante encontros: {msgs_juntos:,} ({msgs_juntos/total_msgs*100:.1f}%)")
print(f" • Fora de encontros: {msgs_separados:,} ({msgs_separados/total_msgs*100:.1f}%)")
print("\n" + "="*80)
```
## Volume Diário: Junto vs Separado
```{python}
# Agrupa por dia
df['data'] = df['timestamp'].dt.date
msgs_por_dia = df.groupby('data').agg({
'timestamp': 'count',
'em_encontro': 'first'
}).rename(columns={'timestamp': 'count'})
# Estatísticas por contexto
junto_stats = msgs_por_dia[msgs_por_dia['em_encontro']]['count']
separado_stats = msgs_por_dia[~msgs_por_dia['em_encontro']]['count']
print("📊 Volume por Dia - Estatísticas Descritivas:\n")
print(f"{'Métrica':<20} {'Juntos':<15} {'Separados':<15}")
print("="*50)
print(f"{'Média':<20} {junto_stats.mean():<15.1f} {separado_stats.mean():<15.1f}")
print(f"{'Mediana':<20} {junto_stats.median():<15.0f} {separado_stats.median():<15.0f}")
print(f"{'Desvio Padrão':<20} {junto_stats.std():<15.1f} {separado_stats.std():<15.1f}")
print(f"{'Mínimo':<20} {junto_stats.min():<15.0f} {separado_stats.min():<15.0f}")
print(f"{'Máximo':<20} {junto_stats.max():<15.0f} {separado_stats.max():<15.0f}")
```
::: {.callout-note}
### 📊 Síntese - Visão Geral
**Volume total:**
- 91.924 mensagens ao longo de 1 ano
- P1 (51.1%) vs P2 (48.9%) - distribuição equilibrada
**Impacto do contexto externo:**
- Quando juntos: 117.8 msgs/dia (68.3% de redução)
- Quando separados: 372.0 msgs/dia
- 14.6% das mensagens ocorrem durante encontros (33.2% do tempo)
**Confirmação:** Presença física reduz drasticamente comunicação digital, mas não elimina.
:::
***
# Distribuição de Remetentes
## Volume Geral
```{python}
# Volume por remetente
msgs_por_remetente = df.groupby('remetente').size().reset_index(name='count')
msgs_por_remetente['percentual'] = (msgs_por_remetente['count'] / total_msgs * 100).round(1)
print("📊 Distribuição por Remetente:\n")
print(msgs_por_remetente.to_string(index=False))
```
## Volume por Remetente × Contexto
```{python}
#| label: fig-remetente-contexto
#| fig-cap: "Volume de mensagens por remetente e contexto"
# Agrupa por remetente e contexto
msgs_remetente_contexto = df.groupby(['remetente', 'em_encontro']).size().reset_index(name='count')
msgs_remetente_contexto['contexto'] = msgs_remetente_contexto['em_encontro'].map({
True: 'Juntos',
False: 'Separados'
})
# Calcula percentual por remetente
for remetente in ['P1', 'P2']:
total_rem = df[df['remetente'] == remetente].shape[0]
mask = msgs_remetente_contexto['remetente'] == remetente
msgs_remetente_contexto.loc[mask, 'percentual'] = (
msgs_remetente_contexto.loc[mask, 'count'] / total_rem * 100
)
# Visualização
fig = px.bar(
msgs_remetente_contexto,
x='remetente',
y='count',
color='contexto',
barmode='group',
title='Distribuição de Mensagens: Remetente × Contexto',
labels={'count': 'Quantidade de Mensagens', 'remetente': 'Remetente'},
color_discrete_map={'Juntos': COLORS['juntos'], 'Separados': COLORS['separados']},
text='percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=400)
fig.show()
# Estatísticas
print("\n📊 Distribuição por Remetente × Contexto:\n")
pivot = msgs_remetente_contexto.pivot(index='remetente', columns='contexto', values='count')
pivot['Total'] = pivot.sum(axis=1)
pivot['% Juntos'] = (pivot['Juntos'] / pivot['Total'] * 100).round(1)
pivot['% Separados'] = (pivot['Separados'] / pivot['Total'] * 100).round(1)
print(pivot)
```
::: {.callout-warning}
### 💡 Insight - Remetentes por Contexto
**Proporcionalidade IDÊNTICA:**
- P1: 85.1% separados | 14.9% juntos
- P2: 85.7% separados | 14.3% juntos
- Diferença: apenas **0.6 pontos percentuais**
**Conclusão:**
- Ambos seguem exatamente a mesma proporção de comunicação por contexto
- Não há diferença comportamental entre P1 e P2 quanto ao impacto de estar junto/separado
- P1 não é "mais digital" ou "menos digital" quando juntos em relação a P2
**Interpretação:**
- Relacionamento equilibrado - ambos reduzem comunicação digital igualmente quando juntos
- Não há assimetria (ex: "P1 continua enviando muito quando junto, P2 para")
- Reforça que presença física afeta IGUALMENTE ambos os participantes
**Implicação para FE:**
- ❌ Feature `remetente × em_encontro` é **DESNECESSÁRIA**
- ✅ Feature `em_encontro` sozinha já captura o padrão (universal para ambos)
- 🎯 Comportamento sincronizado se mantém também no contexto externo
:::
***
# Tipos de Mensagem
## Distribuição Geral
```{python}
# Tipos de mensagem
msgs_por_tipo = df.groupby('tipo_mensagem').size().reset_index(name='count').sort_values('count', ascending=False)
msgs_por_tipo['percentual'] = (msgs_por_tipo['count'] / total_msgs * 100).round(1)
print("📊 Distribuição por Tipo de Mensagem:\n")
print(msgs_por_tipo.head(10).to_string(index=False))
# Grupos
msgs_por_grupo = df.groupby('grupo_mensagem').size().reset_index(name='count').sort_values('count', ascending=False)
msgs_por_grupo['percentual'] = (msgs_por_grupo['count'] / total_msgs * 100).round(1)
print("\n📊 Distribuição por Grupo de Mensagem:\n")
print(msgs_por_grupo.to_string(index=False))
```
## Tipos × Contexto
```{python}
#| label: fig-tipo-contexto
#| fig-cap: "Distribuição de tipos de mensagem: junto vs separado"
# Agrupa por grupo e contexto
msgs_tipo_contexto = df.groupby(['grupo_mensagem', 'em_encontro']).size().reset_index(name='count')
msgs_tipo_contexto['contexto'] = msgs_tipo_contexto['em_encontro'].map({
True: 'Juntos',
False: 'Separados'
})
# Calcula percentual por contexto
for contexto_bool in [True, False]:
total_ctx = df[df['em_encontro'] == contexto_bool].shape[0]
mask = msgs_tipo_contexto['em_encontro'] == contexto_bool
msgs_tipo_contexto.loc[mask, 'percentual'] = (
msgs_tipo_contexto.loc[mask, 'count'] / total_ctx * 100
)
# Filtra grupos principais
grupos_principais = msgs_por_grupo.head(6)['grupo_mensagem'].tolist()
msgs_tipo_contexto_plot = msgs_tipo_contexto[msgs_tipo_contexto['grupo_mensagem'].isin(grupos_principais)]
# Visualização
fig = px.bar(
msgs_tipo_contexto_plot,
x='grupo_mensagem',
y='percentual',
color='contexto',
barmode='group',
title='Distribuição de Tipos de Mensagem: Junto vs Separado (%)',
labels={'percentual': 'Percentual (%)', 'grupo_mensagem': 'Tipo de Mensagem'},
color_discrete_map={'Juntos': COLORS['juntos'], 'Separados': COLORS['separados']},
text='percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=400)
fig.show()
```
## Tipos × Remetente × Contexto
```{python}
# Calcula proporções
tipo_rem_ctx = df.groupby(['remetente', 'em_encontro', 'grupo_mensagem']).size().reset_index(name='count')
# Normaliza por remetente e contexto
tipo_rem_ctx['contexto'] = tipo_rem_ctx['em_encontro'].map({True: 'Juntos', False: 'Separados'})
for remetente in ['P1', 'P2']:
for contexto_bool in [True, False]:
total = df[(df['remetente'] == remetente) & (df['em_encontro'] == contexto_bool)].shape[0]
mask = (tipo_rem_ctx['remetente'] == remetente) & (tipo_rem_ctx['em_encontro'] == contexto_bool)
if total > 0:
tipo_rem_ctx.loc[mask, 'percentual'] = (tipo_rem_ctx.loc[mask, 'count'] / total * 100)
# Foca nos grupos principais
grupos_interesse = ['TEXT', 'AUDIO', 'VID', 'IMG']
tipo_rem_ctx_plot = tipo_rem_ctx[tipo_rem_ctx['grupo_mensagem'].isin(grupos_interesse)]
print("📊 Tipos de Mensagem × Remetente × Contexto (%):\n")
pivot_tipo = tipo_rem_ctx_plot.pivot_table(
index='grupo_mensagem',
columns=['remetente', 'contexto'],
values='percentual',
fill_value=0
).round(1)
print(pivot_tipo)
```
::: {.callout-warning}
### 💡 Insight - Tipos de Mensagem por Contexto
**Mudança CLARA nas preferências:**
**Quando SEPARADOS:**
- TEXT: 71.6%
- ÁUDIO: 9.7%
- VÍDEO: 11.9%
- IMG: 3.7%
**Quando JUNTOS:**
- TEXT: 82.4% ⬆️ (+10.8pp)
- ÁUDIO: 7.2% ⬇️ (-2.5pp)
- VÍDEO: 2.0% ⬇️ (-9.9pp - QUEDA DRÁSTICA!)
- IMG: 4.9% ⬆️ (+1.2pp)
**Padrão identificado:**
- Quando **separados**: Preferem mídias ricas (áudio/vídeo) para compensar distância
- Quando **juntos**: Preferem texto (comunicação rápida/utilitária)
- **VÍDEO cai 83%** quando juntos (11.9% → 2.0%) - mídia mais "pesada" é evitada
**Diferenças por remetente (CRÍTICO!):**
*P1 (maior variação):*
- ÁUDIO separado: **12.1%** → Junto: **7.3%** (queda de 40%)
- VÍDEO separado: **12.1%** → Junto: **1.9%** (queda de 84%)
- P1 usa MUITO mais mídia quando separado
*P2 (comportamento estável no áudio):*
- ÁUDIO separado: **7.2%** → Junto: **7.1%** (praticamente estável!)
- VÍDEO separado: **11.7%** → Junto: **2.2%** (queda de 81%)
- P2 mantém áudio independente do contexto
**Interpretação:**
- P1 depende mais de áudio/vídeo para expressão quando separado
- P2 usa áudio de forma mais consistente (menos afetado por contexto)
- Ambos reduzem VÍDEO drasticamente quando juntos (mídia cara/demorada)
- Texto domina comunicação presencial (praticidade)
**Implicação para FE:**
- ✅ Feature `grupo_mensagem × em_encontro` É ÚTIL (padrões mudam)
- ✅ Feature `grupo_mensagem × remetente × em_encontro` PODE SER ÚTIL (P1 muda mais que P2)
- 🎯 VÍDEO é forte indicador de separação (11% separado vs 2% junto)
- 🎯 Proporção TEXTO/MÍDIA é proxy de contexto
:::
> Isso muda a análise anterior! P1 e P2 são similares em VOLUME, mas diferentes em TIPO DE MÍDIA!
## Evolução Temporal dos Tipos de Mensagem
### Distribuição por Mês
```{python}
#| label: fig-tipos-mes
#| fig-cap: "Evolução temporal dos tipos de mensagem ao longo do ano"
# Prepara dados
df['mes'] = df['timestamp'].dt.to_period('M').astype(str)
df['ano_mes'] = df['timestamp'].dt.strftime('%Y-%m')
# Agrupa por mês e grupo
tipos_mes = df.groupby(['ano_mes', 'grupo_mensagem']).size().reset_index(name='count')
# Calcula percentual por mês
total_por_mes = df.groupby('ano_mes').size().reset_index(name='total')
tipos_mes = tipos_mes.merge(total_por_mes, on='ano_mes')
tipos_mes['percentual'] = (tipos_mes['count'] / tipos_mes['total'] * 100)
# Filtra grupos principais
grupos_principais = ['TEXT', 'AUDIO', 'VID', 'IMG']
tipos_mes_plot = tipos_mes[tipos_mes['grupo_mensagem'].isin(grupos_principais)]
# Visualização - Linhas
fig = px.line(
tipos_mes_plot,
x='ano_mes',
y='percentual',
color='grupo_mensagem',
title='Evolução dos Tipos de Mensagem ao Longo do Tempo (%)',
labels={'percentual': 'Percentual (%)', 'ano_mes': 'Mês', 'grupo_mensagem': 'Tipo'},
markers=True
)
fig.update_layout(height=400, hovermode='x unified')
fig.show()
# Estatísticas
print("\n📊 Evolução Temporal - Estatísticas:\n")
for grupo in grupos_principais:
dados_grupo = tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo]['percentual']
print(f"{grupo}:")
print(f" • Início (out/2024): {dados_grupo.iloc[0]:.1f}%")
print(f" • Fim (out/2025): {dados_grupo.iloc[-1]:.1f}%")
print(f" • Média: {dados_grupo.mean():.1f}%")
print(f" • Máximo: {dados_grupo.max():.1f}% ({tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo].loc[dados_grupo.idxmax(), 'ano_mes']})")
print(f" • Mínimo: {dados_grupo.min():.1f}% ({tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo].loc[dados_grupo.idxmin(), 'ano_mes']})")
print()
```
### Composição ao Longo do Tempo
```{python}
#| label: fig-tipos-area
#| fig-cap: "Composição de tipos de mensagem (área empilhada)"
# Visualização - Área empilhada
fig = px.area(
tipos_mes_plot,
x='ano_mes',
y='percentual',
color='grupo_mensagem',
title='Composição de Tipos de Mensagem ao Longo do Tempo',
labels={'percentual': 'Percentual (%)', 'ano_mes': 'Mês', 'grupo_mensagem': 'Tipo'}
)
fig.update_layout(height=400)
fig.show()
```
### Distribuição por Fase Temporal
```{python}
# Define fases baseadas na análise anterior
def classificar_fase(data):
if data < pd.Timestamp('2025-01-01'):
return 'Fase 1: Crescimento (Out-Dez/2024)'
elif data < pd.Timestamp('2025-06-01'):
return 'Fase 2: Estabilização (Jan-Mai/2025)'
elif data < pd.Timestamp('2025-08-01'):
return 'Fase 3: Pico (Jun-Jul/2025)'
else:
return 'Fase 4: Normalização (Ago-Out/2025)'
df['fase_temporal'] = df['timestamp'].apply(classificar_fase)
# Agrupa por fase e tipo
tipos_fase = df.groupby(['fase_temporal', 'grupo_mensagem']).size().reset_index(name='count')
# Calcula percentual
total_por_fase = df.groupby('fase_temporal').size().reset_index(name='total')
tipos_fase = tipos_fase.merge(total_por_fase, on='fase_temporal')
tipos_fase['percentual'] = (tipos_fase['count'] / tipos_fase['total'] * 100)
# Filtra grupos principais
tipos_fase_plot = tipos_fase[tipos_fase['grupo_mensagem'].isin(grupos_principais)]
# Ordena fases
ordem_fases = [
'Fase 1: Crescimento (Out-Dez/2024)',
'Fase 2: Estabilização (Jan-Mai/2025)',
'Fase 3: Pico (Jun-Jul/2025)',
'Fase 4: Normalização (Ago-Out/2025)'
]
tipos_fase_plot['fase_temporal'] = pd.Categorical(
tipos_fase_plot['fase_temporal'],
categories=ordem_fases,
ordered=True
)
tipos_fase_plot = tipos_fase_plot.sort_values('fase_temporal')
# Visualização
fig = px.bar(
tipos_fase_plot,
x='fase_temporal',
y='percentual',
color='grupo_mensagem',
barmode='group',
title='Distribuição de Tipos por Fase Temporal',
labels={'percentual': 'Percentual (%)', 'fase_temporal': 'Fase', 'grupo_mensagem': 'Tipo'},
text='percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=800, xaxis={'tickangle': -45})
fig.show()
# Tabela comparativa
print("\n📊 Tipos de Mensagem por Fase:\n")
pivot_fase = tipos_fase_plot.pivot(index='grupo_mensagem', columns='fase_temporal', values='percentual').round(1)
print(pivot_fase)
```
### Evolução por Remetente
```{python}
#| label: fig-tipos-remetente-tempo
#| fig-cap: "Evolução temporal por remetente"
# Cria subplots
fig = make_subplots(
rows=2, cols=2,
subplot_titles=['TEXT - Evolução', 'AUDIO - Evolução', 'VID - Evolução', 'IMG - Evolução'],
vertical_spacing=0.12,
horizontal_spacing=0.1
)
grupos_analise = ['TEXT', 'AUDIO', 'VID', 'IMG']
for idx, grupo in enumerate(grupos_analise):
row = (idx // 2) + 1
col = (idx % 2) + 1
for remetente in ['P1', 'P2']:
# Filtra dados
df_rem = df[df['remetente'] == remetente]
tipos_rem_mes = df_rem.groupby(['ano_mes', 'grupo_mensagem']).size().reset_index(name='count')
total_rem_mes = df_rem.groupby('ano_mes').size().reset_index(name='total')
tipos_rem_mes = tipos_rem_mes.merge(total_rem_mes, on='ano_mes')
tipos_rem_mes['percentual'] = (tipos_rem_mes['count'] / tipos_rem_mes['total'] * 100)
# Filtra grupo
dados_grupo = tipos_rem_mes[tipos_rem_mes['grupo_mensagem'] == grupo]
# Adiciona traço
fig.add_trace(
go.Scatter(
x=dados_grupo['ano_mes'],
y=dados_grupo['percentual'],
name=remetente,
mode='lines+markers',
line=dict(color=COLORS['P1'] if remetente == 'P1' else COLORS['P2']),
legendgroup=remetente,
showlegend=(idx == 0)
),
row=row, col=col
)
fig.update_xaxes(title_text="Mês", tickangle=-45)
fig.update_yaxes(title_text="Percentual (%)")
fig.update_layout(height=1200, title_text="Evolução dos Tipos de Mensagem: P1 vs P2")
fig.show()
```
### Tipos × Tempo × Contexto
```{python}
#| label: fig-tipos-tempo-contexto
#| fig-cap: "Evolução temporal considerando contexto (junto vs separado)"
# Agrupa por mês, tipo e contexto
tipos_mes_ctx = df.groupby(['ano_mes', 'grupo_mensagem', 'em_encontro']).size().reset_index(name='count')
# Total por mês e contexto
total_mes_ctx = df.groupby(['ano_mes', 'em_encontro']).size().reset_index(name='total')
tipos_mes_ctx = tipos_mes_ctx.merge(total_mes_ctx, on=['ano_mes', 'em_encontro'])
tipos_mes_ctx['percentual'] = (tipos_mes_ctx['count'] / tipos_mes_ctx['total'] * 100)
# Adiciona label de contexto
tipos_mes_ctx['contexto'] = tipos_mes_ctx['em_encontro'].map({True: 'Juntos', False: 'Separados'})
# Filtra grupos principais
tipos_mes_ctx_plot = tipos_mes_ctx[tipos_mes_ctx['grupo_mensagem'].isin(grupos_principais)]
# Cria subplots para cada tipo
fig = make_subplots(
rows=2, cols=2,
subplot_titles=['TEXT', 'AUDIO', 'VID', 'IMG'],
vertical_spacing=0.12,
horizontal_spacing=0.1
)
for idx, grupo in enumerate(grupos_principais):
row = (idx // 2) + 1
col = (idx % 2) + 1
dados_grupo = tipos_mes_ctx_plot[tipos_mes_ctx_plot['grupo_mensagem'] == grupo]
for contexto in ['Separados', 'Juntos']:
dados_ctx = dados_grupo[dados_grupo['contexto'] == contexto]
fig.add_trace(
go.Scatter(
x=dados_ctx['ano_mes'],
y=dados_ctx['percentual'],
name=contexto,
mode='lines+markers',
line=dict(color=COLORS['separados'] if contexto == 'Separados' else COLORS['juntos']),
legendgroup=contexto,
showlegend=(idx == 0)
),
row=row, col=col
)
fig.update_xaxes(title_text="Mês", tickangle=-45)
fig.update_yaxes(title_text="Percentual (%)")
fig.update_layout(height=600, title_text="Evolução Temporal dos Tipos: Junto vs Separado")
fig.show()
```
::: {.callout-warning}
### 💡 Insight - Evolução Temporal dos Tipos de Mensagem
**MUDANÇA DRAMÁTICA ao longo do ano - Relacionamento amadureceu:**
***
#### **1. ÁUDIO em QUEDA LIVRE (71% de redução)**
**Evolução geral:**
- Out/2024: **21.7%** (alta dependência de voz)
- Out/2025: **6.2%** (uso marginal)
- Pico de queda: Dez/2024 → Jan/2025 (21% → 6%)
**Por remetente:**
- **P1:** 23% → 7% (queda de 70%)
- **P2:** 19% → 5% (queda de 74%)
- Ambos reduziram drasticamente, mas P1 mantém uso levemente maior
**Por contexto:**
- **Separados:** 21% → 6% (queda acontece MESMO separados!)
- **Juntos:** Sempre baixo (5-12%, oscilante)
- **Conclusão:** Queda é TEMPORAL, não por contexto
**Interpretação:**
- Início: Áudio = expressividade, intimidade (voz conecta mais que texto)
- Evolução: Comunicação ficou mais eficiente (texto é mais rápido)
- Relacionamento amadureceu: Menos necessidade de "ouvir a voz"
***
#### **2. TEXTO CRESCENTE (+12pp ao longo do ano)**
**Evolução geral:**
- Out/2024: 66.7%
- Out/2025: 78.9%
- Pico: Set/2025 (84.4%)
- Crescimento constante e linear
**Por remetente:**
- **P1:** 60% → 76% (+16pp)
- **P2:** 74% → 82% (+8pp)
- P2 sempre usou mais texto, P1 está convergindo
**Por contexto:**
- **Separados:** Cresce de 66% para 74% (+8pp)
- **Juntos:** DOMINA e cresce ainda mais (78% → 88%!)
- Quando juntos, texto é ainda mais preferido
**Interpretação:**
- Texto = eficiência, praticidade, rapidez
- Relacionamento ficou mais "econômico" na comunicação
- Quando juntos, texto domina (coordenação rápida)
***
#### **3. VÍDEO teve "ERA DOURADA" (Jan-Mai/2025)**
**Ciclo identificado:**
- Out/2024: **0%** (não usavam!)
- Jan/2025: **EXPLOSÃO para 18.4%**
- Jan-Mai/2025: Patamar alto (~15%)
- Ago/2025: Queda para 5-6%
**Por remetente:**
- **P1 e P2:** Curvas PERFEITAMENTE sincronizadas
- Ambos começaram a usar VID no mesmo momento
- Ambos abandonaram juntos
**Por contexto:**
- **Separados:** Pico Jan/2025 (20%) - VID é mídia de SEPARAÇÃO
- **Juntos:** Sempre baixo (0-5%)
- VID usado quase exclusivamente quando separados
**Por fase:**
- Fase 1 (Out-Dez): 5.4% - descobrindo
- Fase 2 (Jan-Mai): **14.8%** - ERA DOURADA
- Fase 3 (Jun-Jul): 10.7% - normalização
- Fase 4 (Ago-Out): 4.4% - abandono
**Interpretação:**
- Jan-Mai/2025 foi período especial (mais encontros? Mais saudade?)
- VID = mídia rica, mas "cara" (tempo para gravar/assistir)
- Relacionamento testou VID, mas voltou para texto (mais eficiente)
***
#### **4. IMAGEM relativamente estável**
**Evolução:**
- P1: 13% → 6% (cai, mas P1 sempre usa mais)
- P2: 0% → 2% (aumenta levemente)
**Por contexto:**
- Separados: 3-7% (estável)
- Juntos: Pico Mai/2025 (7.5%) - fotos de momentos juntos?
***
#### **Padrões Identificados - Ciclo de Maturação**
**Fase 1 - Descoberta (Out-Dez/2024):**
- Alto uso de ÁUDIO (15.7%) - voz para criar intimidade
- Baixo VID (5.4%) - ainda explorando
- TEXT dominante mas não absoluto (72.1%)
**Fase 2 - Experimentação (Jan-Mai/2025):**
- ÁUDIO CAI drasticamente (5.9%)
- VID EXPLODE (14.8%) - testando mídia rica
- TEXT estável (72.6%)
- **Período mais diversificado de mídias**
**Fase 3 - Pico (Jun-Jul/2025):**
- Equilíbrio entre tipos
- VID ainda alto (10.7%)
- ÁUDIO começa a subir de novo (10.2%)
**Fase 4 - Normalização (Ago-Out/2025):**
- TEXT domina absolutamente (76.4%)
- ÁUDIO marginal (11.3%)
- VID abandono (4.4%)
- **Relacionamento "amadurecido" = comunicação eficiente**
***
#### **Sincronização P1 vs P2 PERFEITA**
**Em TODAS as mudanças temporais:**
- VID: Ambos explodem em Jan, ambos caem em Ago
- ÁUDIO: Ambos reduzem no mesmo ritmo
- TEXT: Ambos aumentam constantemente
**Única diferença persistente:**
- P1 usa mais ÁUDIO (+2-5pp) - já sabíamos
- P1 usa mais IMG (+4-6pp)
**Conclusão:** P1 e P2 não só sincronizam comportamento, mas também EVOLUEM juntos.
***
#### **Implicações para FE**
**Features de evolução temporal:**
- ✅ `mes` ou `trimestre` **ESSENCIAIS** - preferências mudam drasticamente
- ✅ `fase_temporal` (1-4) captura ciclo de maturação
- ✅ Interação `mes × grupo_mensagem` útil (VID em Jan≠VID em Out)
**Features a criar:**
- ✅ `era_video` (booleana): Jan-Mai/2025
- ✅ `fase_audio_alto` (booleana): Out-Dez/2024
- ✅ `tendencia_texto` (numérica): Captura crescimento linear
**Features a evitar:**
- ❌ Assumir estabilidade temporal - preferências MUDARAM
- ❌ Interação `mes × remetente × grupo` - sincronização se mantém
**Conclusão crítica:**
- Relacionamento NÃO é estático - evolui ao longo do tempo
- Mudança é por MATURAÇÃO, não só por contexto externo
- Fase 2 (Jan-Mai) foi especial - investigar se coincide com encontros/eventos
:::
### Validações - Temporal vs Contexto
```{python}
# ============================================================================
# VALIDAÇÃO 1: AUDIO vs Distância Temporal
# ============================================================================
print("="*80)
print("🔍 VALIDAÇÃO 1: AUDIO está relacionado com distância de encontros?")
print("="*80)
# Filtra apenas mensagens separadas com distância conhecida
df_separados_dist = df[
(~df['em_encontro']) &
(df['dias_desde_ultimo_encontro'].notna())
].copy()
# Agrupa por mês
audio_vs_distancia = df_separados_dist.groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'AUDIO').mean() * 100,
'dias_desde_ultimo_encontro': 'mean'
}).reset_index()
audio_vs_distancia.columns = ['ano_mes', 'pct_audio', 'dias_medio_desde']
# Correlação
corr_audio_desde = audio_vs_distancia[['pct_audio', 'dias_medio_desde']].corr().iloc[0, 1]
print(f"\n📊 Correlação % AUDIO vs Dias Desde Último Encontro:")
print(f" • r = {corr_audio_desde:.3f}")
if corr_audio_desde > 0.3:
print(f" ✅ Correlação POSITIVA moderada: Mais tempo separado = Mais áudio")
print(f" → Hipótese CONTEXTO reforçada (áudio compensa distância)")
elif corr_audio_desde < -0.3:
print(f" ✅ Correlação NEGATIVA moderada: Mais tempo separado = Menos áudio")
print(f" → Hipótese TEMPORAL reforçada (mudança independente de contexto)")
else:
print(f" ⚠️ Correlação FRACA: Áudio NÃO está claramente relacionado com distância")
print(f" → Outras variáveis podem estar influenciando")
# Tabela
print(f"\n📋 Detalhes por mês:")
print(audio_vs_distancia[['ano_mes', 'pct_audio', 'dias_medio_desde']].round(1).to_string(index=False))
```
```{python}
# ============================================================================
# VALIDAÇÃO 2: VID vs Tempo Separado no Mês
# ============================================================================
print("\n" + "="*80)
print("🔍 VALIDAÇÃO 2: VID está relacionado com % tempo separado?")
print("="*80)
# Calcula % tempo separado por mês
vid_vs_separacao = df.groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'VID').mean() * 100,
'em_encontro': lambda x: (~x).mean() * 100 # % tempo separado
}).reset_index()
vid_vs_separacao.columns = ['ano_mes', 'pct_vid', 'pct_tempo_separado']
# Correlação
corr_vid_separado = vid_vs_separacao[['pct_vid', 'pct_tempo_separado']].corr().iloc[0, 1]
print(f"\n📊 Correlação % VID vs % Tempo Separado:")
print(f" • r = {corr_vid_separado:.3f}")
if corr_vid_separado > 0.3:
print(f" ✅ Correlação POSITIVA: Mais tempo separado = Mais vídeo")
print(f" → VID é mídia de COMPENSAÇÃO de distância")
elif corr_vid_separado < -0.3:
print(f" ⚠️ Correlação NEGATIVA: Estranho - investigar mais")
else:
print(f" ⚠️ Correlação FRACA: VID NÃO está relacionado com separação")
print(f" → Pico Jan-Mai pode ter outra explicação")
# Tabela
print(f"\n📋 Detalhes por mês:")
print(vid_vs_separacao[['ano_mes', 'pct_vid', 'pct_tempo_separado']].round(1).to_string(index=False))
```
```{python}
# ============================================================================
# VALIDAÇÃO 3: TEXTO vs Volume Total
# ============================================================================
print("\n" + "="*80)
print("🔍 VALIDAÇÃO 3: TEXTO cresce quando volume CAI? (eficiência)")
print("="*80)
# Volume e % texto por mês
texto_vs_volume = df.groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'TEXT').mean() * 100,
'timestamp': 'count'
}).reset_index()
texto_vs_volume.columns = ['ano_mes', 'pct_texto', 'volume_total']
# Correlação
corr_texto_volume = texto_vs_volume[['pct_texto', 'volume_total']].corr().iloc[0, 1]
print(f"\n📊 Correlação % TEXTO vs Volume Total:")
print(f" • r = {corr_texto_volume:.3f}")
if corr_texto_volume < -0.3:
print(f" ✅ Correlação NEGATIVA: Mais texto = Menos volume total")
print(f" → Hipótese EFICIÊNCIA reforçada (texto é mais conciso)")
elif corr_texto_volume > 0.3:
print(f" ⚠️ Correlação POSITIVA: Mais texto = Mais volume")
print(f" → Texto NÃO é substituto 1:1 de outras mídias")
else:
print(f" ⚠️ Correlação FRACA: Texto independente de volume")
# Tabela
print(f"\n📋 Detalhes por mês:")
print(texto_vs_volume[['ano_mes', 'pct_texto', 'volume_total']].round(1).to_string(index=False))
```
```{python}
# ============================================================================
# VALIDAÇÃO 4: Uso de AUDIO vs TEXTO ao Longo do Tempo (Separados vs Juntos)
# ============================================================================
print("\n" + "="*80)
print("🔍 VALIDAÇÃO 4: Queda de AUDIO é igual quando SEPARADOS e quando JUNTOS?")
print("="*80)
# Audio separados
audio_sep = df[~df['em_encontro']].groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'AUDIO').mean() * 100
}).reset_index()
audio_sep.columns = ['ano_mes', 'audio_separados']
# Audio juntos
audio_jun = df[df['em_encontro']].groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'AUDIO').mean() * 100
}).reset_index()
audio_jun.columns = ['ano_mes', 'audio_juntos']
# Merge
audio_comparacao = audio_sep.merge(audio_jun, on='ano_mes', how='outer').fillna(0)
# Comparação
print(f"\n📊 AUDIO ao longo do tempo:")
print(audio_comparacao.round(1).to_string(index=False))
# Tendência
inicio_sep = audio_comparacao['audio_separados'].iloc[0]
fim_sep = audio_comparacao['audio_separados'].iloc[-1]
queda_sep = ((inicio_sep - fim_sep) / inicio_sep * 100)
print(f"\n📉 Queda quando SEPARADOS:")
print(f" • Início: {inicio_sep:.1f}%")
print(f" • Fim: {fim_sep:.1f}%")
print(f" • Queda: {queda_sep:.1f}%")
if queda_sep > 50:
print(f" ✅ Queda FORTE mesmo quando separados")
print(f" → Mudança é TEMPORAL, não só por contexto")
else:
print(f" ⚠️ Queda moderada quando separados")
```
```{python}
#| label: fig-validacao-audio-contexto
#| fig-cap: "AUDIO ao longo do tempo: separados vs juntos"
# Visualização
fig = go.Figure()
fig.add_trace(go.Scatter(
x=audio_comparacao['ano_mes'],
y=audio_comparacao['audio_separados'],
mode='lines+markers',
name='Separados',
line=dict(color=COLORS['separados'], width=2),
marker=dict(size=8)
))
fig.add_trace(go.Scatter(
x=audio_comparacao['ano_mes'],
y=audio_comparacao['audio_juntos'],
mode='lines+markers',
name='Juntos',
line=dict(color=COLORS['juntos'], width=2),
marker=dict(size=8)
))
fig.update_layout(
title='Evolução do AUDIO: Separados vs Juntos',
xaxis_title='Mês',
yaxis_title='% AUDIO',
height=400,
hovermode='x unified'
)
fig.show()
```
::: {.callout-note}
#### 📊 Síntese das Validações
**Conclusão definitiva: Mudança é TEMPORAL PURA, não contexto disfarçado!**
##### **Resultados das Validações**
**VALIDAÇÃO 1 - AUDIO vs Distância de Encontros:**
- Correlação: **r = -0.020** (praticamente ZERO!)
- ❌ AUDIO NÃO está relacionado com `dias_desde_ultimo_encontro`
- Exemplo: Abril (28.6 dias desde) = 3.9% áudio | Maio (5.1 dias desde) = 10.2% áudio
- **Conclusão:** Queda de áudio NÃO é explicada por distância temporal dos encontros
**VALIDAÇÃO 2 - VID vs % Tempo Separado:**
- Correlação: **r = 0.280** (fraca)
- ⚠️ VID NÃO está fortemente relacionado com estar separado
- Exemplo: Jun/2025 (100% separado) = 15.7% VID | Jan/2025 (88% separado) = 18.4% VID
- **Conclusão:** Pico de VID em Jan-Mai NÃO é explicado por estar mais tempo separado
**VALIDAÇÃO 3 - TEXTO vs Volume Total:**
- Correlação: **r = -0.189** (fraca negativa)
- ⚠️ TEXTO NÃO cresce porque volume diminui
- Set/2025: 84.4% texto com volume baixo (3.253) | Jun/2025: 68.3% texto com volume alto (14.020)
- **Conclusão:** Crescimento de texto NÃO é por "eficiência" (compensar volume baixo)
**VALIDAÇÃO 4 - AUDIO Separados vs Juntos ao Longo do Tempo:**
- Queda quando SEPARADOS: **68.1%** (21.7% → 6.9%)
- Queda quando JUNTOS: Curvas PARALELAS (gráfico mostra padrão similar)
- ✅ **Mudança acontece INDEPENDENTE do contexto**
- **Conclusão:** Mudança é TEMPORAL, não por estar junto/separado
***
###### **Interpretação dos Resultados**
**O que SABEMOS agora:**
1. ✅ Preferências de mídia mudam ao longo do tempo de forma INDEPENDENTE de contexto externo
2. ✅ Queda de AUDIO (68%) acontece mesmo quando separados
3. ✅ Pico de VID (Jan-Mai) NÃO é explicado por estar mais separado
4. ✅ Crescimento de TEXTO NÃO é por compensar volume baixo
5. ✅ Mudança é sincronizada entre P1 e P2 (evoluem juntos)
**O que NÃO SABEMOS (hipóteses restantes):**
**Hipótese A - Maturação do Relacionamento:**
- Menos necessidade de "ouvir a voz" (já conhecem bem)
- Comunicação mais confortável/eficiente
- Descoberta gradual de preferências
- ✅ Consistente com dados (queda linear de áudio, crescimento de texto)
**Hipótese B - Mudança de Hábito/Rotina:**
- Trabalho ficou mais puxado (menos tempo para áudio)
- Descoberta de que texto é mais prático
- Adaptação a rotina compartilhada
- ✅ Consistente com dados (mudança gradual mas persistente)
**Hipótese C - Teste e Abandono de VID:**
- Jan-Mai: Experimentaram VID como alternativa
- Descobriram que VID é "caro" (tempo para gravar/assistir)
- Voltaram para texto (mais eficiente)
- ✅ Consistente com dados (pico e queda de VID)
**Hipótese D - Combinação:**
- Relacionamento amadureceu (menos áudio)
- Testaram VID como substituto (Jan-Mai)
- Descobriram que texto funciona melhor (crescimento constante)
- ✅ Mais provável: COMBINAÇÃO de fatores
***
###### **Padrão Temporal Identificado**
**Out-Dez/2024 (Fase 1 - Descoberta):**
- Alto AUDIO (15.7%) - voz para criar intimidade
- Baixo VID (5.4%) - ainda não descobriram
- TEXT dominante (72.1%)
**Jan-Mai/2025 (Fase 2 - Experimentação):**
- AUDIO cai drasticamente (5.9%) - menos necessidade
- VID EXPLODE (14.8%) - testando alternativa
- TEXT estável (72.6%)
**Jun-Jul/2025 (Fase 3 - Transição):**
- AUDIO sobe levemente (10.2%) - ressurgimento?
- VID cai (10.7%) - abandonando
- TEXT cresce (72.2%)
**Ago-Out/2025 (Fase 4 - Consolidação):**
- AUDIO baixo mas oscilante (11.3%)
- VID abandonado (4.4%)
- TEXT DOMINA (76.4%)
**Interpretação:** Relacionamento passou por ciclo de **experimentação → teste → consolidação**
***
###### **Conclusão Final**
**Mudança é TEMPORAL, não contexto:**
- Preferências evoluem ao longo do ano
- Evolução é SINCRONIZADA (P1 e P2 juntos)
- Não é explicada por distância, separação ou volume
- Mais provável: **Maturação do relacionamento + Descoberta de preferências**
**Implicação para FE:**
- ✅ Features temporais (`mes`, `trimestre`) são ESSENCIAIS
- ✅ Mudança é REAL e preditiva (não ruído)
- ✅ Relacionamento tem "fases" distintas ao longo do ano
- ⚠️ Não assumir estabilidade - preferências EVOLUEM
**Limitação:**
Sem dados externos (mudança de trabalho, eventos específicos, etc), não podemos distinguir completamente entre "maturação", "hábito" ou "rotina". Mas sabemos que NÃO é contexto de proximidade física.
:::
***
# 📊 Código Completo - Evolução Temporal (REFATORADO - Só Relacionamento)
```{python}
# ============================================================================
# EVOLUÇÃO TEMPORAL DOS TIPOS - Só Relacionamento (pós 16/dez/2024)
# ============================================================================
# Filtra relacionamento
data_marco = pd.Timestamp('2024-12-16')
df_rel = df[df['timestamp'] >= data_marco].copy()
# Prepara dados
df_rel['mes'] = df_rel['timestamp'].dt.to_period('M').astype(str)
df_rel['ano_mes'] = df_rel['timestamp'].dt.strftime('%Y-%m')
print("="*80)
print(f"📊 Análise temporal: {len(df_rel):,} mensagens de relacionamento")
print(f" Período: {df_rel['timestamp'].min().date()} → {df_rel['timestamp'].max().date()}")
print("="*80)
```
## Distribuição por Mês
```{python}
#| label: fig-tipos-mes-refatorado
#| fig-cap: "Evolução temporal dos tipos de mensagem (relacionamento)"
# Agrupa por mês e grupo
tipos_mes = df_rel.groupby(['ano_mes', 'grupo_mensagem']).size().reset_index(name='count')
# Calcula percentual por mês
total_por_mes = df_rel.groupby('ano_mes').size().reset_index(name='total')
tipos_mes = tipos_mes.merge(total_por_mes, on='ano_mes')
tipos_mes['percentual'] = (tipos_mes['count'] / tipos_mes['total'] * 100)
# Filtra grupos principais
grupos_principais = ['TEXT', 'AUDIO', 'VID', 'IMG']
tipos_mes_plot = tipos_mes[tipos_mes['grupo_mensagem'].isin(grupos_principais)]
# Visualização - Linhas
fig = px.line(
tipos_mes_plot,
x='ano_mes',
y='percentual',
color='grupo_mensagem',
title='Evolução dos Tipos de Mensagem ao Longo do Tempo (%) - Relacionamento',
labels={'percentual': 'Percentual (%)', 'ano_mes': 'Mês', 'grupo_mensagem': 'Tipo'},
markers=True
)
fig.update_layout(height=600, hovermode='x unified')
fig.show()
# Estatísticas
print("\n📊 Evolução Temporal - Estatísticas (Relacionamento):\n")
for grupo in grupos_principais:
dados_grupo = tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo]['percentual']
# Encontra primeiro e último mês
primeiro_mes = tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo]['ano_mes'].iloc[0]
ultimo_mes = tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo]['ano_mes'].iloc[-1]
print(f"{grupo}:")
print(f" • Início ({primeiro_mes}): {dados_grupo.iloc[0]:.1f}%")
print(f" • Fim ({ultimo_mes}): {dados_grupo.iloc[-1]:.1f}%")
print(f" • Variação: {dados_grupo.iloc[-1] - dados_grupo.iloc[0]:.1f}pp")
print(f" • Média: {dados_grupo.mean():.1f}%")
print(f" • Máximo: {dados_grupo.max():.1f}% ({tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo].loc[dados_grupo.idxmax(), 'ano_mes']})")
print(f" • Mínimo: {dados_grupo.min():.1f}% ({tipos_mes_plot[tipos_mes_plot['grupo_mensagem'] == grupo].loc[dados_grupo.idxmin(), 'ano_mes']})")
print()
```
## Composição ao Longo do Tempo
```{python}
#| label: fig-tipos-area-refatorado
#| fig-cap: "Composição de tipos de mensagem (área empilhada) - Relacionamento"
# Visualização - Área empilhada
fig = px.area(
tipos_mes_plot,
x='ano_mes',
y='percentual',
color='grupo_mensagem',
title='Composição de Tipos de Mensagem ao Longo do Tempo (Relacionamento)',
labels={'percentual': 'Percentual (%)', 'ano_mes': 'Mês', 'grupo_mensagem': 'Tipo'}
)
fig.update_layout(height=400)
fig.show()
```
## Distribuição por Fase Temporal
```{python}
# ============================================================================
# FASES REDEFINIDAS - Só Relacionamento
# ============================================================================
def classificar_fase(data):
"""
Fases baseadas APENAS em relacionamento (pós 16/dez/2024)
"""
if data < pd.Timestamp('2025-01-01'):
return 'Fase 1: Início (Dez/2024)'
elif data < pd.Timestamp('2025-06-01'):
return 'Fase 2: Experimentação (Jan-Mai/2025)'
elif data < pd.Timestamp('2025-08-01'):
return 'Fase 3: Transição (Jun-Jul/2025)'
else:
return 'Fase 4: Consolidação (Ago-Out/2025)'
df_rel['fase_temporal'] = df_rel['timestamp'].apply(classificar_fase)
# Agrupa por fase e tipo
tipos_fase = df_rel.groupby(['fase_temporal', 'grupo_mensagem']).size().reset_index(name='count')
# Calcula percentual
total_por_fase = df_rel.groupby('fase_temporal').size().reset_index(name='total')
tipos_fase = tipos_fase.merge(total_por_fase, on='fase_temporal')
tipos_fase['percentual'] = (tipos_fase['count'] / tipos_fase['total'] * 100)
# Filtra grupos principais
tipos_fase_plot = tipos_fase[tipos_fase['grupo_mensagem'].isin(grupos_principais)]
# Ordena fases
ordem_fases = [
'Fase 1: Início (Dez/2024)',
'Fase 2: Experimentação (Jan-Mai/2025)',
'Fase 3: Transição (Jun-Jul/2025)',
'Fase 4: Consolidação (Ago-Out/2025)'
]
tipos_fase_plot['fase_temporal'] = pd.Categorical(
tipos_fase_plot['fase_temporal'],
categories=ordem_fases,
ordered=True
)
tipos_fase_plot = tipos_fase_plot.sort_values('fase_temporal')
# Visualização
fig = px.bar(
tipos_fase_plot,
x='fase_temporal',
y='percentual',
color='grupo_mensagem',
barmode='group',
title='Distribuição de Tipos por Fase Temporal (Relacionamento)',
labels={'percentual': 'Percentual (%)', 'fase_temporal': 'Fase', 'grupo_mensagem': 'Tipo'},
text='percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=800, xaxis={'tickangle': -45})
fig.show()
# Tabela comparativa
print("\n📊 Tipos de Mensagem por Fase (Relacionamento):\n")
pivot_fase = tipos_fase_plot.pivot(index='grupo_mensagem', columns='fase_temporal', values='percentual').round(1)
print(pivot_fase)
```
## Evolução por Remetente
```{python}
#| label: fig-tipos-remetente-tempo-refatorado
#| fig-cap: "Evolução temporal por remetente (relacionamento)"
# Cria subplots
fig = make_subplots(
rows=2, cols=2,
subplot_titles=['TEXT - Evolução', 'AUDIO - Evolução', 'VID - Evolução', 'IMG - Evolução'],
vertical_spacing=0.12,
horizontal_spacing=0.1
)
grupos_analise = ['TEXT', 'AUDIO', 'VID', 'IMG']
for idx, grupo in enumerate(grupos_analise):
row = (idx // 2) + 1
col = (idx % 2) + 1
for remetente in ['P1', 'P2']:
# Filtra dados (df_rel já filtrado)
df_rem = df_rel[df_rel['remetente'] == remetente]
tipos_rem_mes = df_rem.groupby(['ano_mes', 'grupo_mensagem']).size().reset_index(name='count')
total_rem_mes = df_rem.groupby('ano_mes').size().reset_index(name='total')
tipos_rem_mes = tipos_rem_mes.merge(total_rem_mes, on='ano_mes')
tipos_rem_mes['percentual'] = (tipos_rem_mes['count'] / tipos_rem_mes['total'] * 100)
# Filtra grupo
dados_grupo = tipos_rem_mes[tipos_rem_mes['grupo_mensagem'] == grupo]
# Adiciona traço
fig.add_trace(
go.Scatter(
x=dados_grupo['ano_mes'],
y=dados_grupo['percentual'],
name=remetente,
mode='lines+markers',
line=dict(color=COLORS['P1'] if remetente == 'P1' else COLORS['P2']),
legendgroup=remetente,
showlegend=(idx == 0)
),
row=row, col=col
)
fig.update_xaxes(title_text="Mês", tickangle=-45)
fig.update_yaxes(title_text="Percentual (%)")
fig.update_layout(height=1200, title_text="Evolução dos Tipos de Mensagem: P1 vs P2 (Relacionamento)")
fig.show()
```
## Tipos × Tempo × Contexto
```{python}
#| label: fig-tipos-tempo-contexto-refatorado
#| fig-cap: "Evolução temporal considerando contexto (relacionamento)"
# Agrupa por mês, tipo e contexto (df_rel já filtrado)
tipos_mes_ctx = df_rel.groupby(['ano_mes', 'grupo_mensagem', 'em_encontro']).size().reset_index(name='count')
# Total por mês e contexto
total_mes_ctx = df_rel.groupby(['ano_mes', 'em_encontro']).size().reset_index(name='total')
tipos_mes_ctx = tipos_mes_ctx.merge(total_mes_ctx, on=['ano_mes', 'em_encontro'])
tipos_mes_ctx['percentual'] = (tipos_mes_ctx['count'] / tipos_mes_ctx['total'] * 100)
# Adiciona label de contexto
tipos_mes_ctx['contexto'] = tipos_mes_ctx['em_encontro'].map({True: 'Juntos', False: 'Separados'})
# Filtra grupos principais
tipos_mes_ctx_plot = tipos_mes_ctx[tipos_mes_ctx['grupo_mensagem'].isin(grupos_principais)]
# Cria subplots para cada tipo
fig = make_subplots(
rows=2, cols=2,
subplot_titles=['TEXT', 'AUDIO', 'VID', 'IMG'],
vertical_spacing=0.12,
horizontal_spacing=0.1
)
for idx, grupo in enumerate(grupos_principais):
row = (idx // 2) + 1
col = (idx % 2) + 1
dados_grupo = tipos_mes_ctx_plot[tipos_mes_ctx_plot['grupo_mensagem'] == grupo]
for contexto in ['Separados', 'Juntos']:
dados_ctx = dados_grupo[dados_grupo['contexto'] == contexto]
fig.add_trace(
go.Scatter(
x=dados_ctx['ano_mes'],
y=dados_ctx['percentual'],
name=contexto,
mode='lines+markers',
line=dict(color=COLORS['separados'] if contexto == 'Separados' else COLORS['juntos']),
legendgroup=contexto,
showlegend=(idx == 0)
),
row=row, col=col
)
fig.update_xaxes(title_text="Mês", tickangle=-45)
fig.update_yaxes(title_text="Percentual (%)")
fig.update_layout(height=600, title_text="Evolução Temporal dos Tipos: Junto vs Separado (Relacionamento)")
fig.show()
```
::: {.callout-note}
## 💡 Insight - Evolução Temporal dos Tipos de Mensagem (Relacionamento)
**NOTA METODOLÓGICA:** Análise restrita ao período de relacionamento (pós 16/dez/2024, N=89.336). Período de amizade excluído para evitar mistura de contextos.
***
**MUDANÇA DRAMÁTICA durante relacionamento - Comunicação evoluiu em 10 meses:**
***
#### **1. ÁUDIO em QUEDA (-56% de redução)**
**Evolução:**
- **Dez/2024:** 14.1% (início - intimidade por voz)
- **Mar/2025:** 4.2% (mínimo - queda acentuada)
- **Out/2025:** 6.2% (estabilizou baixo)
- **Variação:** -7.9pp (-56%)
**Por fase:**
- Fase 1 (Início): **14.1%** - voz cria intimidade inicial
- Fase 2 (Experimentação): **5.9%** - QUEDA BRUSCA (-58%)
- Fase 3 (Transição): **10.2%** - ressurgimento temporário
- Fase 4 (Consolidação): **11.3%** - volta a subir levemente
**Por remetente:**
- **Sincronização perfeita** - ambos seguem mesma curva
- P1 mantém uso levemente maior (~2-5pp)
- Picos e vales acontecem nos mesmos meses
**Por contexto:**
- **Separados:** Queda de 15% → 7% (mesmo padrão)
- **Juntos:** Sempre baixo (5-11%, oscilante)
- **Conclusão:** Mudança é TEMPORAL, não contextual
**Interpretação:**
- **Dez/2024 (início):** Áudio = ferramenta de intimidade ("preciso ouvir sua voz")
- **Jan-Mai/2025:** Texto vence por eficiência
- **Jun-Out/2025:** Leve retorno (11%) = áudio se reposiciona como "afeto especial", não rotina
***
#### **2. TEXTO CRESCENTE (+8.3pp, +12%)**
**Evolução:**
- **Dez/2024:** 70.6%
- **Out/2025:** 78.9%
- **Pico:** Set/2025 (84.4%)
- **Crescimento CONSTANTE e LINEAR**
**Por fase:**
- Fase 1: 70.6%
- Fase 2: 72.6% (+2pp)
- Fase 3: 72.2% (estável)
- Fase 4: **76.4%** (+5.8pp desde início)
**Por remetente:**
- **P2:** Sempre usou mais texto (convergindo para 86%)
- **P1:** Crescimento maior (convergindo para P2, ~76%)
- Ambos aumentam, mas P2 lidera
**Por contexto:**
- **Separados:** Cresce de 67% → 75%
- **Juntos:** DOMINA e cresce (80% → 87%!)
- Quando juntos, texto é AINDA mais preferido
**Interpretação:**
- Texto = eficiência máxima (rápido escrever, rápido ler)
- Relacionamento "amadureceu" = comunicação econômica
- **Modo coordenação** (juntos) é quase 90% texto
***
#### **3. VÍDEO teve "ERA DOURADA" confirmada (Jan-Mai/2025) 🔥**
**Ciclo COMPLETO:**
- **Dez/2024:** 7.7% (começando a usar)
- **Jan/2025:** **18.4%** - EXPLOSÃO! 🚀
- **Jan-Mai/2025:** Patamar alto (~14-18%)
- **Jun/2025:** 15.7% (ainda alto)
- **Ago/2025:** Declínio acelerado
- **Set/2025:** 0.6% (quase ZERO!)
- **Out/2025:** 5.6% (leve retorno)
**Por fase:**
- Fase 1 (Dez): 7.7% - descobrindo
- Fase 2 (Jan-Mai): **14.8%** - ERA DOURADA 🏆
- Fase 3 (Jun-Jul): 10.7% - declínio começando
- Fase 4 (Ago-Out): **4.4%** - abandono (-70% desde pico!)
**Por remetente (sincronização PERFEITA!):**
- **Jan/2025:** P1 e P2 EXPLODEM juntos (~18%)
- **Fev-Mai:** Ambos mantêm alto (~14-15%)
- **Jun-Ago:** Ambos CAEM juntos
- **Set:** Ambos quase PARAM (P1=0.4%, P2=0.8%!)
- Curvas são IDÊNTICAS - decisão compartilhada
**Por contexto:**
- **Separados:** VID é mídia de SEPARAÇÃO
- Jan: 20% (pico quando separados!)
- Jun: 16% (ainda alto)
- Out: 7% (declínio)
- **Juntos:** Sempre baixo (0-6%)
- VID = compensar distância com riqueza visual+voz
**Interpretação - O ciclo do VID:**
**Jan/2025 - Por que EXPLODIRAM?**
- Primeiro mês completo de relacionamento
- Possivelmente mais separação/saudade nesse período
- VID = áudio + visual (riqueza máxima)
- Experimentação: "isso conecta mais!"
**Jan-Mai - Por que SUSTENTARAM?**
- "Era Dourada" = período de teste bem-sucedido
- VID funcionou para criar intimidade à distância
- Fase de exploração de canais
**Jun-Set - Por que ABANDONARAM?**
- VID é "cara": Tempo para gravar, assistir
- Texto é MUITO mais eficiente
- Relacionamento amadureceu: Não precisa riqueza visual constante
- Set/2025: Quase ZERO (0.6%) = decisão clara de abandonar
**Out - Por que VOLTOU levemente?**
- VID reposicionado: Não rotina, mas "momentos especiais"
- Uso ocasional (5.6%) vs rotineiro (18%)
***
#### **4. IMAGEM estável (3-5%)**
**Evolução:**
- Oscila entre 3.4% e 4.7%
- **P1:** Usa consistentemente mais (4-8%)
- **P2:** Uso marginal (~2-3%)
- Diferença persistente - preferência pessoal de P1
***
#### **Fases do Relacionamento - Maturação em 4 Etapas:**
| Fase | Período | Duração | TEXT | AUDIO | VID | IMG | Característica |
|------|---------|---------|------|-------|-----|-----|----------------|
| **1. Início** | Dez/2024 | 2 sem | 70.6% | **14.1%** | 7.7% | 4.7% | Voz cria intimidade |
| **2. Experimentação** | Jan-Mai/2025 | 5 meses | 72.6% | 5.9% | **14.8%** | 3.9% | Testando VID |
| **3. Transição** | Jun-Jul/2025 | 2 meses | 72.2% | 10.2% | 10.7% | 3.8% | Buscando equilíbrio |
| **4. Consolidação** | Ago-Out/2025 | 3 meses | **76.4%** | 11.3% | 4.4% | 4.0% | Texto domina |
**Narrativa:**
1. **Início:** Relacionamento novo = áudio para intimidade
2. **Experimentação:** VID explode (Jan) = testando conexão visual
3. **Transição:** Ajustando mix de mídias
4. **Consolidação:** Texto vence = eficiência
***
#### **Sincronização P1 vs P2 é ABSOLUTA:**
**Todos os movimentos são SIMULTÂNEOS:**
- ✅ VID: Explodem **juntos** em Jan (ambos ~18%)
- ✅ VID: Caem **juntos** em Set (ambos <1%!)
- ✅ AUDIO: Reduzem no mesmo ritmo (14% → 6%)
- ✅ TEXT: Aumentam constantemente (ambos)
**Única diferença persistente:**
- P1 usa mais AUDIO (+2-5pp) - gosta mais de voz
- P1 usa mais IMG (+4-6pp) - mais visual
**Conclusão:** P1 e P2 têm **TRAJETÓRIA COMPARTILHADA** - evoluem juntos, não separadamente.
***
#### **Validação: Temporal vs Contextual**
**Mudanças são TEMPORAIS PURAS (não por contexto):**
| Tipo | Separados | Juntos | Conclusão |
|------|-----------|--------|-----------|
| **AUDIO** | Cai 15%→7% | Sempre baixo (5-11%) | Queda TEMPORAL |
| **VID** | Explode e cai | Sempre baixo | Ciclo TEMPORAL |
| **TEXT** | Cresce 67%→75% | Cresce 80%→87% | Crescimento TEMPORAL |
**Validação anterior confirmada:**
- Correlação AUDIO vs distância = r=-0.02 (ZERO!)
- Mudanças acontecem INDEPENDENTE de estar junto/separado
- **Não é contexto**, é **evolução do relacionamento**
***
#### **Comparação: Antes vs Depois do Filtro**
| Aspecto | COM Amizade | SÓ Relacionamento | Diferença |
|---------|-------------|-------------------|-----------|
| **AUDIO início** | 21.7% | 14.1% | -7.6pp (outlier removido) |
| **AUDIO queda** | -71% | -56% | Mais realista |
| **VID pico** | Jan 18.4% | Jan 18.4% | Igual (correto!) |
| **Fases** | Out-Dez misturado | Dez-Out limpo | ✅ Purificado |
| **Interpretação** | "Descoberta" confusa | Evolução clara | ✅ Clarificado |
**Conclusão do filtro:**
- ✅ Removeu viés de amizade (Out-Nov)
- ✅ Dados mais limpos e interpretáveis
- ✅ Padrões evolutivos ficaram CLAROS
- ✅ Fases fazem SENTIDO narrativo
***
#### **Implicações para Feature Engineering:**
**Features CRÍTICAS:**
- ✅ `fase_temporal` (4 fases redefinidas) - captura evolução
- ✅ `mes_relacionamento` (1-11, não calendário) - linear
- ✅ `era_video` (booleana: Jan-Mai/2025) - período especial
- ✅ `tipo × fase` - interação captura mudanças dramáticas
**Insights para modelagem:**
- ⚠️ Modelo SEM `fase_temporal` terá baixo poder preditivo
- ⚠️ VID em Jan≠VID em Set (18% vs 0.6%) - MUDAR completamente
- ✅ TEXT cresce linearmente - `mes` pode capturar
- ✅ Sincronização P1×P2 - feature `remetente × tipo` pode ser redundante
**Descoberta de OURO:**
> Relacionamento não é estático - **AMADURECE**.
> Comunicação evolui de "rica/expressiva" (voz/vídeo) para "eficiente" (texto).
> Modelos precisam capturar essa **trajetória temporal**.
:::
# 📊 Código Completo - Validações Temporais (REFATORADO)
```{python}
# ============================================================================
# VALIDAÇÕES - Temporal vs Contexto (Só Relacionamento, pós 16/dez/2024)
# ============================================================================
# Usa df_rel já filtrado anteriormente
# Se não tiver, descomente:
# data_marco = pd.Timestamp('2024-12-16')
# df_rel = df[df['timestamp'] >= data_marco].copy()
print("="*80)
print("🔬 VALIDAÇÕES - Temporal vs Contexto (Relacionamento)")
print("="*80)
print(f"\nObjetivo: Confirmar que mudanças em tipos de mensagem são TEMPORAIS,")
print(f"não explicadas por variação de contexto (junto vs separado).\n")
print("="*80)
```
## VALIDAÇÃO 1: AUDIO vs Distância Temporal
```{python}
print("\n" + "="*80)
print("VALIDAÇÃO 1: AUDIO vs Distância desde Último Encontro")
print("="*80)
print("\nHipótese a TESTAR: 'Queda de AUDIO é por estar mais perto de encontros'")
print("Se VERDADEIRA: Correlação negativa (menos AUDIO quando perto de encontro)")
print("Se FALSA: Correlação próxima de zero (mudança é temporal pura)\n")
# Filtra apenas mensagens SEPARADAS com distância conhecida
df_distancia = df_rel[
(~df_rel['em_encontro']) &
(df_rel['dias_desde_ultimo_encontro'].notna())
].copy()
# Adiciona mês para agregação
df_distancia['ano_mes'] = df_distancia['timestamp'].dt.strftime('%Y-%m')
# Agrupa por mês
validacao1 = df_distancia.groupby('ano_mes').agg({
'dias_desde_ultimo_encontro': 'mean', # Média de dias desde último encontro
'grupo_mensagem': lambda x: (x == 'AUDIO').sum(), # Quantidade de AUDIO
'timestamp': 'count' # Total de mensagens
}).reset_index()
validacao1.columns = ['ano_mes', 'dias_medio_desde', 'audio_count', 'total_msgs']
validacao1['audio_pct'] = (validacao1['audio_count'] / validacao1['total_msgs'] * 100)
# Correlação
corr1 = validacao1['dias_medio_desde'].corr(validacao1['audio_pct'])
print(f"📊 Resultado:\n")
print(f" Correlação: r = {corr1:.3f}")
if abs(corr1) < 0.3:
print(f" ✅ CORRELAÇÃO FRACA - Mudança é TEMPORAL, não por distância!")
print(f" (Se fosse contexto, correlação seria forte)")
elif corr1 < -0.3:
print(f" ⚠️ CORRELAÇÃO NEGATIVA - Menos AUDIO perto de encontros")
print(f" (Mudança pode ser contextual)")
else:
print(f" ⚠️ CORRELAÇÃO POSITIVA - Mais AUDIO longe de encontros")
print(f" (Mudança pode ser contextual)")
# Tabela com exemplos
print(f"\n📊 Exemplos (meses selecionados):\n")
exemplos1 = validacao1.sort_values('dias_medio_desde')[['ano_mes', 'dias_medio_desde', 'audio_pct']].head(3)
exemplos2 = validacao1.sort_values('dias_medio_desde')[['ano_mes', 'dias_medio_desde', 'audio_pct']].tail(3)
print("Meses com MENOR distância desde encontro:")
for _, row in exemplos1.iterrows():
print(f" {row['ano_mes']}: {row['dias_medio_desde']:.1f} dias → {row['audio_pct']:.1f}% AUDIO")
print("\nMeses com MAIOR distância desde encontro:")
for _, row in exemplos2.iterrows():
print(f" {row['ano_mes']}: {row['dias_medio_desde']:.1f} dias → {row['audio_pct']:.1f}% AUDIO")
print(f"\n💡 Interpretação:")
print(f" Se correlação ~0: AUDIO cai com o TEMPO, não com proximidade de encontros")
# Visualização
fig = px.scatter(
validacao1,
x='dias_medio_desde',
y='audio_pct',
trendline='ols',
title=f'VALIDAÇÃO 1: AUDIO vs Distância Temporal (r={corr1:.3f})',
labels={
'dias_medio_desde': 'Dias Médios Desde Último Encontro',
'audio_pct': '% de AUDIO'
},
text='ano_mes'
)
fig.update_traces(textposition='top center')
fig.update_layout(height=400)
fig.show()
print("="*80)
```
## VALIDAÇÃO 2: VID vs % Tempo Separado
```{python}
print("\n" + "="*80)
print("VALIDAÇÃO 2: VID vs % Tempo Separado no Mês")
print("="*80)
print("\nHipótese a TESTAR: 'VID explode porque ficaram mais separados em Jan-Mai'")
print("Se VERDADEIRA: Correlação forte (mais VID quando mais separados)")
print("Se FALSA: Correlação fraca (mudança é temporal pura)\n")
# Calcula % tempo separado por mês
tempo_separado = df_rel.groupby('ano_mes').agg({
'em_encontro': lambda x: (~x).sum() / len(x) * 100 # % separados
}).reset_index()
tempo_separado.columns = ['ano_mes', 'pct_separado']
# % VID por mês (geral, não só separados)
vid_mes = df_rel.groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'VID').sum() / len(x) * 100
}).reset_index()
vid_mes.columns = ['ano_mes', 'vid_pct']
# Merge
validacao2 = tempo_separado.merge(vid_mes, on='ano_mes')
# Correlação
corr2 = validacao2['pct_separado'].corr(validacao2['vid_pct'])
print(f"📊 Resultado:\n")
print(f" Correlação: r = {corr2:.3f}")
if abs(corr2) < 0.3:
print(f" ✅ CORRELAÇÃO FRACA - VID não explica por % tempo separado!")
print(f" (Mudança é temporal, não por estar mais/menos separado)")
elif corr2 > 0.3:
print(f" ⚠️ CORRELAÇÃO POSITIVA - Mais VID quando mais separados")
print(f" (Mudança pode ser contextual)")
else:
print(f" ⚠️ CORRELAÇÃO NEGATIVA - Menos VID quando mais separados")
print(f" (Contra-intuitivo)")
# Tabela com exemplos
print(f"\n📊 Exemplos:\n")
for _, row in validacao2.iterrows():
print(f" {row['ano_mes']}: {row['pct_separado']:.0f}% separados → {row['vid_pct']:.1f}% VID")
print(f"\n💡 Interpretação:")
print(f" Jan tinha 88% separado com 18% VID")
print(f" Jun tinha 100% separado com 16% VID")
print(f" → VID NÃO explica apenas por 'estar separado'")
# Visualização
fig = px.scatter(
validacao2,
x='pct_separado',
y='vid_pct',
trendline='ols',
title=f'VALIDAÇÃO 2: VID vs % Tempo Separado (r={corr2:.3f})',
labels={
'pct_separado': '% do Mês Separados',
'vid_pct': '% de VID'
},
text='ano_mes'
)
fig.update_traces(textposition='top center')
fig.update_layout(height=400)
fig.show()
print("="*80)
```
## VALIDAÇÃO 3: TEXTO vs Volume Total
```{python}
print("\n" + "="*80)
print("VALIDAÇÃO 3: TEXTO vs Volume Total de Mensagens")
print("="*80)
print("\nHipótese a TESTAR: 'TEXTO cresce porque volume caiu (eficiência)'")
print("Se VERDADEIRA: Correlação negativa (mais TEXTO quando menos mensagens)")
print("Se FALSA: Correlação fraca (mudança é temporal pura)\n")
# Volume e % texto por mês
validacao3 = df_rel.groupby('ano_mes').agg({
'timestamp': 'count', # Volume total
'grupo_mensagem': lambda x: (x == 'TEXT').sum() / len(x) * 100 # % TEXT
}).reset_index()
validacao3.columns = ['ano_mes', 'volume_total', 'text_pct']
# Correlação
corr3 = validacao3['volume_total'].corr(validacao3['text_pct'])
print(f"📊 Resultado:\n")
print(f" Correlação: r = {corr3:.3f}")
if abs(corr3) < 0.3:
print(f" ✅ CORRELAÇÃO FRACA - TEXTO não cresce por redução de volume!")
print(f" (Mudança é temporal, não compensação de eficiência)")
elif corr3 < -0.3:
print(f" ⚠️ CORRELAÇÃO NEGATIVA - Mais TEXTO quando menos mensagens")
print(f" (Pode indicar eficiência)")
else:
print(f" ⚠️ CORRELAÇÃO POSITIVA - Mais TEXTO quando mais mensagens")
print(f" (Contra-intuitivo)")
# Tabela com exemplos
print(f"\n📊 Exemplos:\n")
exemplos_vol = validacao3.sort_values('volume_total', ascending=False).head(3)
print("Meses com MAIOR volume:")
for _, row in exemplos_vol.iterrows():
print(f" {row['ano_mes']}: {row['volume_total']:,} msgs → {row['text_pct']:.1f}% TEXTO")
exemplos_vol2 = validacao3.sort_values('volume_total').head(3)
print("\nMeses com MENOR volume:")
for _, row in exemplos_vol2.iterrows():
print(f" {row['ano_mes']}: {row['volume_total']:,} msgs → {row['text_pct']:.1f}% TEXTO")
print(f"\n💡 Interpretação:")
print(f" Se correlação ~0: TEXTO cresce com TEMPO, não como compensação de volume")
# Visualização
fig = px.scatter(
validacao3,
x='volume_total',
y='text_pct',
trendline='ols',
title=f'VALIDAÇÃO 3: TEXTO vs Volume Total (r={corr3:.3f})',
labels={
'volume_total': 'Volume Total de Mensagens',
'text_pct': '% de TEXTO'
},
text='ano_mes'
)
fig.update_traces(textposition='top center')
fig.update_layout(height=400)
fig.show()
print("="*80)
```
## VALIDAÇÃO 4: AUDIO Separados ao Longo do Tempo
```{python}
print("\n" + "="*80)
print("VALIDAÇÃO 4: AUDIO ao Longo do Tempo (Separados vs Juntos)")
print("="*80)
print("\nHipótese a TESTAR: 'Queda de AUDIO é porque ficaram mais juntos'")
print("Se VERDADEIRA: AUDIO só cai quando juntos, estável quando separados")
print("Se FALSA: AUDIO cai em AMBOS contextos (mudança temporal pura)\n")
# AUDIO separados ao longo do tempo
audio_sep = df_rel[~df_rel['em_encontro']].groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'AUDIO').sum() / len(x) * 100
}).reset_index()
audio_sep.columns = ['ano_mes', 'audio_pct_sep']
# AUDIO juntos ao longo do tempo
audio_jun = df_rel[df_rel['em_encontro']].groupby('ano_mes').agg({
'grupo_mensagem': lambda x: (x == 'AUDIO').sum() / len(x) * 100 if len(x) > 0 else 0
}).reset_index()
audio_jun.columns = ['ano_mes', 'audio_pct_jun']
# Merge
validacao4 = audio_sep.merge(audio_jun, on='ano_mes', how='outer').fillna(0)
# Calcula variação SEPARADOS
primeiro_sep = validacao4['audio_pct_sep'].iloc[0]
ultimo_sep = validacao4['audio_pct_sep'].iloc[-1]
variacao_sep = ((ultimo_sep - primeiro_sep) / primeiro_sep * 100)
print(f"📊 Resultado:\n")
print(f" AUDIO quando SEPARADOS:")
print(f" Início: {primeiro_sep:.1f}%")
print(f" Fim: {ultimo_sep:.1f}%")
print(f" Variação: {variacao_sep:.1f}%")
if abs(variacao_sep) > 30:
print(f" ✅ QUEDA SIGNIFICATIVA mesmo quando separados!")
print(f" (Mudança é TEMPORAL, não por estar mais juntos)")
else:
print(f" ⚠️ Estável quando separados")
print(f" (Mudança pode ser por estar mais juntos)")
# Comparação visual
print(f"\n📊 Comparação mês a mês:\n")
for _, row in validacao4.iterrows():
print(f" {row['ano_mes']}: Sep={row['audio_pct_sep']:.1f}% | Jun={row['audio_pct_jun']:.1f}%")
# Tendência
from scipy import stats
x = range(len(validacao4))
slope_sep, intercept, r_value, p_value, std_err = stats.linregress(x, validacao4['audio_pct_sep'])
print(f"\n📊 Tendência (Separados):")
print(f" Inclinação: {slope_sep:.3f}%/mês")
print(f" R²: {r_value**2:.3f}")
if slope_sep < -0.5:
print(f" ✅ TENDÊNCIA DE QUEDA clara quando separados")
print(f" (Mudança temporal confirmada)")
# Visualização
fig = go.Figure()
fig.add_trace(go.Scatter(
x=validacao4['ano_mes'],
y=validacao4['audio_pct_sep'],
mode='lines+markers',
name='Separados',
line=dict(color=COLORS['separados'], width=3)
))
fig.add_trace(go.Scatter(
x=validacao4['ano_mes'],
y=validacao4['audio_pct_jun'],
mode='lines+markers',
name='Juntos',
line=dict(color=COLORS['juntos'], width=3)
))
fig.update_layout(
title='VALIDAÇÃO 4: AUDIO ao Longo do Tempo por Contexto',
xaxis_title='Mês',
yaxis_title='% AUDIO',
height=400,
hovermode='x unified'
)
fig.show()
print(f"\n💡 Interpretação:")
print(f" Se curvas PARALELAS (ambas caem): Mudança é TEMPORAL")
print(f" Se só 'Juntos' cai: Mudança seria CONTEXTUAL")
print("="*80)
```
## Síntese das Validações
```{python}
print("\n" + "="*80)
print("📊 SÍNTESE DAS 4 VALIDAÇÕES")
print("="*80)
print(f"\n🔬 Resumo dos Resultados:\n")
print(f"VALIDAÇÃO 1 - AUDIO vs Distância Temporal:")
print(f" • Correlação: r = {corr1:.3f}")
if abs(corr1) < 0.3:
print(f" • ✅ APROVADA: Mudança é temporal, não por distância de encontros")
else:
print(f" • ⚠️ REPROVADA: Correlação significativa detectada")
print(f"\nVALIDAÇÃO 2 - VID vs % Tempo Separado:")
print(f" • Correlação: r = {corr2:.3f}")
if abs(corr2) < 0.3:
print(f" • ✅ APROVADA: VID não explica por estar mais/menos separado")
else:
print(f" • ⚠️ REPROVADA: Correlação significativa detectada")
print(f"\nVALIDAÇÃO 3 - TEXTO vs Volume Total:")
print(f" • Correlação: r = {corr3:.3f}")
if abs(corr3) < 0.3:
print(f" • ✅ APROVADA: TEXTO não cresce por compensação de volume")
else:
print(f" • ⚠️ REPROVADA: Correlação significativa detectada")
print(f"\nVALIDAÇÃO 4 - AUDIO Separados vs Juntos:")
print(f" • Variação quando separados: {variacao_sep:.1f}%")
if abs(variacao_sep) > 30:
print(f" • ✅ APROVADA: AUDIO cai mesmo quando separados")
else:
print(f" • ⚠️ REPROVADA: AUDIO estável quando separados")
# Conclusão geral
aprovadas = sum([
abs(corr1) < 0.3,
abs(corr2) < 0.3,
abs(corr3) < 0.3,
abs(variacao_sep) > 30
])
print(f"\n" + "="*80)
print(f"🎯 CONCLUSÃO GERAL: {aprovadas}/4 validações aprovadas")
print("="*80)
if aprovadas >= 3:
print(f"\n✅ HIPÓTESE CONFIRMADA:")
print(f" Mudanças em tipos de mensagem são TEMPORAIS PURAS,")
print(f" NÃO explicadas por variações de contexto (junto vs separado).")
print(f"\n Relacionamento EVOLUIU ao longo do tempo:")
print(f" • AUDIO: Voz para intimidade (início) → Texto eficiente (consolidação)")
print(f" • VID: Experimentação (Jan-Mai) → Abandono (eficiência venceu)")
print(f" • TEXT: Crescimento constante (amadurecimento)")
else:
print(f"\n⚠️ HIPÓTESE PARCIALMENTE CONFIRMADA:")
print(f" Algumas mudanças podem ter componente contextual.")
print("="*80)
```
::: {.callout-important}
## 🔬 Insights Validações - Temporal vs Contextual
**Objetivo:** Confirmar se mudanças em tipos de mensagem são puramente temporais ou têm componente contextual.
**Metodologia:** 4 validações estatísticas cruzando tipos de mensagem com variáveis contextuais.
***
#### **VALIDAÇÃO 1: AUDIO vs Distância de Encontros ✅ APROVADA**
**Hipótese testada:** "AUDIO cai porque ficam mais perto de encontros"
**Resultado:**
- **Correlação: r = -0.020** (praticamente ZERO!)
- Meses perto de encontro (4-5 dias): 6.9% a 15.1% AUDIO
- Meses longe de encontro (28-42 dias): 3.9% a 10.8% AUDIO
- **Não há relação sistemática**
**Conclusão:** ✅ **AUDIO cai com o TEMPO, não com proximidade de encontros**
**Evidência:**
- Abril (28.6 dias longe): 3.9% AUDIO (BAIXO apesar de distante!)
- Dezembro (4 dias longe): 15.1% AUDIO (ALTO apesar de perto!)
- Padrão é temporal: Dez=15% → Jan=6.5% → Abr=3.9%
***
#### **VALIDAÇÃO 2: VID vs % Tempo Separado ⚠️ PARCIALMENTE REPROVADA**
**Hipótese testada:** "VID explode porque ficaram mais separados em Jan-Mai"
**Resultado:**
- **Correlação: r = 0.646** (MODERADA/FORTE!)
- Quanto mais separados → mais VID
- Jan (88% separado): 18.4% VID
- Jun (100% separado): 15.7% VID
- Set (0% separado): 0.6% VID
**MAS:** Correlação não explica TUDO:
- Jun tinha 100% separado com 15.7% VID
- Jul tinha 89% separado com apenas 6.7% VID
- Ago tinha 94% separado com apenas 5.1% VID
**Conclusão:** ⚠️ **VID é MISTA - temporal + contextual**
**Interpretação nuançada:**
- ✅ VID **é mídia de separação** (r=0.646 confirma)
- ✅ **MAS** tem ciclo temporal (Jan-Mai alta, Jun-Out declínio)
- ✅ Combina: (1) Preferência temporal + (2) Uso contextual
- **Jan-Mai:** Alta separação + experimentação = VID explode
- **Jun-Out:** Alta separação + consolidação = VID cai (texto vence)
**Padrão identificado:**
> VID serve para **compensar distância**, mas relacionamento **amadureceu** e abandonou (preferiu eficiência do texto).
***
#### **VALIDAÇÃO 3: TEXTO vs Volume Total ⚠️ PARCIALMENTE REPROVADA**
**Hipótese testada:** "TEXTO cresce porque volume caiu (eficiência)"
**Resultado:**
- **Correlação: r = -0.410** (MODERADA NEGATIVA!)
- Mais TEXTO quando MENOS mensagens
- Meses com ALTO volume (14-17k msgs): 68-75% TEXTO
- Meses com BAIXO volume (3-4k msgs): 78-84% TEXTO
**Conclusão:** ⚠️ **TEXTO é MISTA - temporal + eficiência**
**Interpretação nuançada:**
- ✅ TEXTO cresce ao longo do ano (temporal: 70.6% → 78.9%)
- ✅ **MAS** também cresce quando volume cai (eficiência)
- **Dois efeitos combinados:**
1. **Temporal:** Relacionamento amadurece → prefere texto
2. **Eficiência:** Menos volume → mensagens mais diretas (texto)
**Padrão identificado:**
> Quando conversam MUITO (14-17k msgs/mês): Mais espaço para mídia rica (AUDIO/VID)
>
> Quando conversam POUCO (3-4k msgs/mês): Comunicação mais eficiente (TEXTO domina)
**Exemplo:**
- Jun/2025: 14.020 msgs → 68.3% TEXTO (espaço para mídia)
- Set/2025: 3.253 msgs → 84.4% TEXTO (eficiência máxima)
***
#### **VALIDAÇÃO 4: AUDIO por Contexto ✅ APROVADA**
**Hipótese testada:** "AUDIO cai porque ficaram mais juntos"
**Resultado:**
- **Variação quando SEPARADOS: -53.9%**
- Dez/2024 (separados): 15.1% AUDIO
- Out/2025 (separados): 6.9% AUDIO
- **Queda acontece MESMO quando separados!**
**Curvas são PARALELAS:**
- Ambas (separados e juntos) caem ao longo do tempo
- Dez: Sep=15.1%, Jun=11.1%
- Jan: Sep=6.5%, Jun=5.6%
- Out: Sep=6.9%, Jun=4.9%
**Conclusão:** ✅ **AUDIO cai TEMPORALMENTE, não por estar mais juntos**
**Validação cruzada com VAL 1:**
- Validação 1: r=-0.020 (sem relação com distância)
- Validação 4: -53.9% quando separados
- **Dupla confirmação:** AUDIO é temporal pura!
***
#### **📊 SÍNTESE DAS 4 VALIDAÇÕES**
| Validação | Tipo | Correlação | Status | Conclusão |
|-----------|------|------------|--------|-----------|
| **1. AUDIO vs Distância** | AUDIO | r=-0.020 | ✅ APROVADA | Temporal pura |
| **2. VID vs % Separado** | VID | r=0.646 | ⚠️ REPROVADA | **Mista** (temporal+contextual) |
| **3. TEXTO vs Volume** | TEXTO | r=-0.410 | ⚠️ REPROVADA | **Mista** (temporal+eficiência) |
| **4. AUDIO por Contexto** | AUDIO | -53.9% | ✅ APROVADA | Temporal pura |
**Score: 2/4 aprovadas** (50%)
***
#### **🎯 CONCLUSÃO FINAL - Padrão HÍBRIDO**
**AUDIO = 100% TEMPORAL**
- ✅ Validações 1 e 4 aprovadas
- r=-0.020 com distância de encontros (ZERO!)
- Cai -53.9% mesmo quando separados
- **Interpretação:** Início relacionamento = intimidade por voz → Amadurecimento = eficiência do texto
- **Não há componente contextual** - é evolução pura do relacionamento
**VID = TEMPORAL (70%) + CONTEXTUAL (30%)**
- ⚠️ r=0.646 com % tempo separado (MODERADA!)
- **Componente temporal:** Ciclo Jan-Mai (experimentação) → Jun-Out (abandono)
- **Componente contextual:** Mais VID quando mais separados (compensar distância)
- **Interpretação:** VID = ferramenta de separação que foi testada e abandonada
- Jan-Mai: Combinação de (1) experimentação temporal + (2) alta separação
- Jun-Out: Apesar de separação alta, abandonaram (texto venceu)
**TEXTO = TEMPORAL (60%) + EFICIÊNCIA (40%)**
- ⚠️ r=-0.410 com volume total (MODERADA!)
- **Componente temporal:** Cresce de 70.6% → 78.9% ao longo do ano
- **Componente eficiência:** Mais texto quando menos volume
- **Interpretação:** Duplo efeito:
1. Amadurecimento = preferência por texto
2. Volume baixo = comunicação mais direta (texto)
***
#### **💡 Descoberta CRÍTICA - Não é Binário!**
**Hipótese inicial (simplificada):**
- ❌ "Mudanças são 100% temporais" → FALSO
- ❌ "Mudanças são 100% contextuais" → FALSO
**Realidade (nuançada):**
- ✅ **AUDIO:** Temporal pura (amadurecimento)
- ✅ **VID:** Híbrida (experimentação temporal + compensação de distância)
- ✅ **TEXTO:** Híbrida (amadurecimento + eficiência quando volume baixo)
**Implicação para modelagem:**
> Modelos precisam capturar **AMBOS** os efeitos:
> - Features temporais: `mes`, `fase_temporal`, `trimestre`
> - Features contextuais: `em_encontro`, `dias_desde/ate`, `volume_mes`
> - **Interações:** `tipo × mes`, `tipo × em_encontro`, `tipo × volume`
***
#### **🔍 Por que VID e TEXTO são Mistas?**
**VID - Temporal + Contextual:**
- **Temporal:** Jan-Mai = período de experimentação (relacionamento testando)
- **Contextual:** VID compensa distância (r=0.646)
- **Combinação:** Jan-Mai tinha ALTA separação (75-100%) + momento de testar = EXPLOSÃO
- **Declínio:** Jun-Out mantém separação ALTA mas texto venceu = ABANDONO
- **Conclusão:** VID não é só "estar separado" - é "estar separado NO MOMENTO CERTO"
**TEXTO - Temporal + Eficiência:**
- **Temporal:** Relacionamento amadurece = texto é suficiente (70% → 79%)
- **Eficiência:** Pouco volume = comunicação direta, sem "luxo" de mídia rica
- **Exemplo:** Set/2025 (3k msgs, 84% texto) vs Jun/2025 (14k msgs, 68% texto)
- **Conclusão:** TEXTO domina quando: (1) Relacionamento maduro + (2) Volume baixo
***
#### **📈 Implicação para Feature Engineering**
**Features ESSENCIAIS (capturar ambos efeitos):**
**Temporais:**
- ✅ `fase_temporal` (4 fases) - captura ciclo AUDIO/VID
- ✅ `mes_relacionamento` (1-11) - captura evolução linear
- ✅ `era_video` (Jan-Mai) - marca experimentação
**Contextuais:**
- ✅ `em_encontro` - separa modos comunicação
- ✅ `dias_desde/ate_encontro` - proximidade influencia
- ✅ `volume_mes` - eficiência vs riqueza
- ✅ `pct_separado_mes` - % tempo separado no mês
**Interações CRÍTICAS:**
- 🔥 `tipo × fase_temporal` - VID muda MUITO por fase
- 🔥 `tipo × em_encontro` - padrões diferentes
- 🔥 `tipo × volume_mes` - TEXTO domina quando volume baixo
- 🔥 `tipo × pct_separado_mes` - VID correlaciona com separação
**Features EVITAR:**
- ❌ `tipo × dias_desde` - AUDIO não correlaciona (r=-0.020)
- ⚠️ Usar `tipo` sozinho sem temporal/contextual = perda de poder
***
#### **🎓 Lições Metodológicas**
**1. Nem tudo é temporal, nem tudo é contextual:**
- Realidade é NUANÇADA
- Validações revelam padrões mistos
**2. Correlações moderadas SÃO significativas:**
- r=0.646 (VID) e r=-0.410 (TEXTO) importam!
- Não é só r>0.8 que vale
**3. Múltiplas validações complementam:**
- VAL 1 + VAL 4 convergem (AUDIO temporal)
- VAL 2 + VAL 3 revelam componentes contextuais
**4. Visualizações confirmam números:**
- Validação 4: Curvas paralelas = temporal
- Validação 2: Tendência clara = contextual
**5. Sempre questionar hipóteses simplificadas:**
- "É temporal" → PARCIALMENTE
- "É contextual" → PARCIALMENTE
- Realidade = HÍBRIDA
:::
# Transcrições
## Volume de Transcrições
```{python}
# Mensagens com transcrição (flag True)
msgs_com_transcricao = df[df['transcricao'] == True].shape[0]
pct_transcricao = (msgs_com_transcricao / total_msgs) * 100
print("📊 Transcrições:\n")
print(f" • Total de mensagens com transcrição: {msgs_com_transcricao:,} ({pct_transcricao:.1f}%)")
# Distribuição por grupo (só AUDIO e VID podem ter)
print(f"\n • Distribuição por grupo:")
for grupo in ['AUDIO', 'VID']:
total_grupo = df[df['grupo_mensagem'] == grupo].shape[0]
com_trans = df[(df['grupo_mensagem'] == grupo) & (df['transcricao'] == True)].shape[0]
pct = (com_trans / total_grupo * 100) if total_grupo > 0 else 0
print(f" - {grupo}: {com_trans:,}/{total_grupo:,} ({pct:.1f}%)")
# Detalhes por tipo específico
trans_por_tipo = df[df['transcricao'] == True].groupby('tipo_mensagem').size().reset_index(name='count')
trans_por_tipo = trans_por_tipo.sort_values('count', ascending=False)
print(f"\n • Detalhes por tipo:")
for _, row in trans_por_tipo.head(5).iterrows():
tipo_total = df[df['tipo_mensagem'] == row['tipo_mensagem']].shape[0]
pct = (row['count'] / tipo_total * 100) if tipo_total > 0 else 0
print(f" - {row['tipo_mensagem']}: {row['count']:,} ({pct:.1f}% do total de {row['tipo_mensagem']})")
```
::: {.callout-note}
### 📊 Transcrições - Confirmação
**Volume geral:**
- Apenas **0.8% das mensagens** têm transcrição (695 de 91.924)
- Concentradas em mídias "attached" (arquivos baixados e transcritos)
**Distribuição:**
- **AUDIO:** 510 transcrições (6.0% dos áudios)
- **VID:** 185 transcrições (1.9% dos vídeos)
- Áudios são **3x mais transcritos** que vídeos
**Qualidade da amostra:**
- N=510 (áudio) → Suficiente para análises exploratórias de conteúdo
- N=185 (vídeo) → Marginal, mas viável para comparações qualitativas
- Tipos "omitted" (não baixados) não têm transcrição (esperado)
**Implicação:**
- Análises de conteúdo transcrito são **viáveis** mas **limitadas**
- Permite comparar conteúdo falado (áudio) vs escrito (texto)
- Não permite análises profundas de NLP (amostra pequena)
- Flag `transcricao` pode ser feature (indica áudio/vídeo baixado)
:::
***
# Análise de Conteúdo
# Padrões Temporais com Contexto
## Evolução ao Longo do Tempo
*Nota: Timeline com encontros marcados já foi gerado no notebook 03-contexto-externo.qmd*
## Distribuição por Hora do Dia
### Visão Geral: Junto vs Separado
```{python}
#| label: fig-hora-contexto-geral
#| fig-cap: "Distribuição por hora: junto vs separado"
# Calcula distribuição
df['hora'] = df['timestamp'].dt.hour
hora_junto = df[df['em_encontro']].groupby('hora').size()
hora_separado = df[~df['em_encontro']].groupby('hora').size()
# Normaliza
hora_junto_pct = (hora_junto / hora_junto.sum() * 100).reindex(range(24), fill_value=0)
hora_separado_pct = (hora_separado / hora_separado.sum() * 100).reindex(range(24), fill_value=0)
# Visualização
fig = go.Figure()
fig.add_trace(go.Scatter(
x=list(range(24)),
y=hora_separado_pct,
mode='lines+markers',
name='Separados',
line=dict(color=COLORS['separados'], width=2),
marker=dict(size=6)
))
fig.add_trace(go.Scatter(
x=list(range(24)),
y=hora_junto_pct,
mode='lines+markers',
name='Juntos',
line=dict(color=COLORS['juntos'], width=2),
marker=dict(size=6)
))
fig.update_layout(
title='Distribuição por Hora do Dia: Junto vs Separado (%)',
xaxis_title='Hora do Dia',
yaxis_title='Proporção de Mensagens (%)',
height=400,
hovermode='x unified',
xaxis=dict(tickmode='linear', tick0=0, dtick=1)
)
fig.show()
# Identifica picos
pico_separado_hora = hora_separado_pct.idxmax()
pico_separado_val = hora_separado_pct.max()
pico_junto_hora = hora_junto_pct.idxmax()
pico_junto_val = hora_junto_pct.max()
print(f"\n📊 Picos por Contexto:")
print(f" • Separados: {pico_separado_hora}h ({pico_separado_val:.1f}%)")
print(f" • Juntos: {pico_junto_hora}h ({pico_junto_val:.1f}%)")
```
### Por Remetente × Contexto
```{python}
#| label: fig-hora-remetente-contexto
#| fig-cap: "Padrão horário por remetente e contexto"
# Cria subplots
fig = make_subplots(
rows=1, cols=2,
subplot_titles=['P1: Junto vs Separado', 'P2: Junto vs Separado']
)
for i, remetente in enumerate(['P1', 'P2'], 1):
df_rem = df[df['remetente'] == remetente]
# Separados
hora_sep = df_rem[~df_rem['em_encontro']].groupby('hora').size()
hora_sep_pct = (hora_sep / hora_sep.sum() * 100).reindex(range(24), fill_value=0)
# Juntos
hora_jun = df_rem[df_rem['em_encontro']].groupby('hora').size()
hora_jun_pct = (hora_jun / hora_jun.sum() * 100).reindex(range(24), fill_value=0)
# Adiciona traços
fig.add_trace(
go.Scatter(x=list(range(24)), y=hora_sep_pct, name='Separados',
line=dict(color=COLORS['separados'], width=2),
legendgroup=remetente, showlegend=(i==1)),
row=1, col=i
)
fig.add_trace(
go.Scatter(x=list(range(24)), y=hora_jun_pct, name='Juntos',
line=dict(color=COLORS['juntos'], width=2),
legendgroup=remetente, showlegend=(i==1)),
row=1, col=i
)
fig.update_xaxes(title_text="Hora do Dia", tickmode='linear', tick0=0, dtick=2)
fig.update_yaxes(title_text="Proporção (%)")
fig.update_layout(height=400, title_text="Padrão Horário por Remetente e Contexto")
fig.show()
```
::: {.callout-warning}
#### 💡 Insight - Padrão Horário com Contexto
**Mudança DRÁSTICA no comportamento (confirmado):**
**Quando SEPARADOS (cinza):**
- Padrão **unimodal noturno** - pico único às 19h (10.5%)
- Crescimento gradual ao longo do dia (8h → 19h)
- Comunicação concentrada no período pós-trabalho (18h-23h)
- Madrugada vazia (0h-6h)
**Quando JUNTOS (verde):**
- Padrão **bimodal diurno** - dois picos distintos:
- Primeiro pico: 12h-13h (10.5%) - horário de almoço/pausa
- Segundo pico: 18h (7%) - fim de expediente
- Comunicação mais distribuída ao longo do dia
- Início mais cedo (atividade desde 8h)
**Por remetente - Sincronização PERFEITA:**
- **P1 e P2 têm curvas PRATICAMENTE IDÊNTICAS** em ambos os contextos
- Separados: Ambos pico 19h (sobreposição total)
- Juntos: Ambos bimodal 12h + 18h (sobreposição total)
- **Rotinas permanecem sincronizadas** independente de estarem juntos ou separados
**Interpretação:**
- Separados: WhatsApp = **conexão emocional** (conversa noturna pós-trabalho)
- Juntos: WhatsApp = **coordenação/trabalho** (comunicação nas pausas do dia)
- Pico 12h-13h reflete disponibilidade dela (trabalho puxado, almoço = janela livre)
- Pico noturno desaparece quando juntos (substituído por convívio presencial)
- Relacionamento mantém sincronia temporal em AMBOS os modos de operação
**Implicação para FE:**
- ✅ Feature `hora × em_encontro` é **ESSENCIAL** (padrões completamente diferentes)
- ✅ Feature `periodo_dia × em_encontro` útil
- ❌ Feature `hora × remetente × em_encontro` é **DESNECESSÁRIA** (P1 e P2 sincronizados)
- 🎯 Padrão horário sozinho tem poder limitado sem contexto
:::
***
## Distribuição por Dia da Semana
### Visão Geral: Junto vs Separado
```{python}
#| label: fig-dia-contexto-geral
#| fig-cap: "Distribuição por dia da semana: junto vs separado"
# Prepara dados
df['dia_semana_num'] = df['timestamp'].dt.dayofweek
dias_semana = ['Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado', 'Domingo']
df['dia_semana'] = df['dia_semana_num'].apply(lambda x: dias_semana[x])
# Calcula distribuição
dia_junto = df[df['em_encontro']].groupby('dia_semana').size().reindex(dias_semana, fill_value=0)
dia_separado = df[~df['em_encontro']].groupby('dia_semana').size().reindex(dias_semana, fill_value=0)
# Normaliza
dia_junto_pct = (dia_junto / dia_junto.sum() * 100)
dia_separado_pct = (dia_separado / dia_separado.sum() * 100)
# Visualização
fig = go.Figure()
fig.add_trace(go.Bar(
x=dias_semana,
y=dia_separado_pct,
name='Separados',
marker_color=COLORS['separados'],
text=dia_separado_pct.round(1),
textposition='outside'
))
fig.add_trace(go.Bar(
x=dias_semana,
y=dia_junto_pct,
name='Juntos',
marker_color=COLORS['juntos'],
text=dia_junto_pct.round(1),
textposition='outside'
))
fig.update_layout(
title='Distribuição por Dia da Semana: Junto vs Separado (%)',
yaxis_title='Proporção de Mensagens (%)',
height=400,
barmode='group'
)
fig.show()
# Estatísticas
print(f"\n📊 Dia com MAIS mensagens:")
print(f" • Separados: {dia_separado_pct.idxmax()} ({dia_separado_pct.max():.1f}%)")
print(f" • Juntos: {dia_junto_pct.idxmax()} ({dia_junto_pct.max():.1f}%)")
print(f"\n📊 Dia com MENOS mensagens:")
print(f" • Separados: {dia_separado_pct.idxmin()} ({dia_separado_pct.min():.1f}%)")
print(f" • Juntos: {dia_junto_pct.idxmin()} ({dia_junto_pct.min():.1f}%)")
```
### Por Remetente × Contexto
```{python}
#| label: fig-dia-remetente-contexto
#| fig-cap: "Padrão semanal por remetente e contexto"
# Cria subplots
fig = make_subplots(
rows=1, cols=2,
subplot_titles=['P1: Junto vs Separado', 'P2: Junto vs Separado']
)
for i, remetente in enumerate(['P1', 'P2'], 1):
df_rem = df[df['remetente'] == remetente]
# Separados
dia_sep = df_rem[~df_rem['em_encontro']].groupby('dia_semana').size().reindex(dias_semana, fill_value=0)
dia_sep_pct = (dia_sep / dia_sep.sum() * 100)
# Juntos
dia_jun = df_rem[df_rem['em_encontro']].groupby('dia_semana').size().reindex(dias_semana, fill_value=0)
dia_jun_pct = (dia_jun / dia_jun.sum() * 100)
# Adiciona traços
fig.add_trace(
go.Bar(x=dias_semana, y=dia_sep_pct, name='Separados',
marker_color=COLORS['separados'], legendgroup=remetente, showlegend=(i==1)),
row=1, col=i
)
fig.add_trace(
go.Bar(x=dias_semana, y=dia_jun_pct, name='Juntos',
marker_color=COLORS['juntos'], legendgroup=remetente, showlegend=(i==1)),
row=1, col=i
)
fig.update_xaxes(title_text="Dia da Semana")
fig.update_yaxes(title_text="Proporção (%)")
fig.update_layout(height=400, title_text="Padrão Semanal por Remetente e Contexto", barmode='group')
fig.show()
```
::: {.callout-warning}
#### 💡 Insight - Padrão Semanal com Contexto
**INVERSÃO TOTAL do padrão (confirmado):**
**Quando SEPARADOS (cinza):**
- Distribuição relativamente uniforme (12-16%)
- Levemente maior aos domingos (16.3%)
- **Segunda é o DIA MENOR** (11.8%)
- Sem padrão claro útil/FDS
**Quando JUNTOS (verde):**
- **Segunda DISPARA:** 26.8% - quase 1/3 da comunicação!
- Terça-Quinta: 15-18% (dias úteis normais)
- **Sábado/Domingo CAEM drasticamente:** 3.3% e 7.4%
- Concentração absoluta em dias úteis
**Inversão identificada:**
- Segunda: Menor quando separados (11.8%) → **MAIOR quando juntos (26.8%)**
- Domingo: Maior quando separados (16.3%) → Menor quando juntos (7.4%)
- Diferença Segunda vs Sábado quando juntos: **8x** (26.8% vs 3.3%)
**Por remetente:**
- Análise anterior mostrou P1 e P2 sincronizados (padrões idênticos)
- Mudança de comportamento é por CONTEXTO, não por pessoa
**Interpretação:**
- Separados: **Comunicação constante** (conexão emocional todos os dias)
- Juntos: **Comunicação utilitária** (trabalho, coordenação em dias úteis)
- Segunda alta = início da semana (discussões sobre trabalho dela, planejamento)
- FDS juntos = aproveitam presença física SEM celular (lazer presencial)
- Relacionamento tem dois "protocolos" completamente diferentes
**Implicação para FE:**
- ✅ Feature `dia_semana × em_encontro` é **MUITO ÚTIL** (inversão de padrão)
- ✅ Feature `eh_segunda × em_encontro` pode ser específica útil
- ✅ Feature `eh_fds × em_encontro` captura comportamento oposto
- ❌ Feature `dia_semana × remetente × em_encontro` é **DESNECESSÁRIA** (sincronizados)
- 🎯 Dia da semana sozinho tem poder ZERO sem contexto (já provamos p=0.96)
:::
***
## Heatmap: Hora × Dia da Semana
### Separados vs Juntos
```{python}
#| label: fig-heatmap-contexto
#| fig-cap: "Heatmap hora × dia: separados vs juntos"
# Cria subplots
fig = make_subplots(
rows=1, cols=2,
subplot_titles=['Quando Separados', 'Quando Juntos'],
specs=[[{'type': 'heatmap'}, {'type': 'heatmap'}]]
)
# Separados
df_sep = df[~df['em_encontro']]
heatmap_sep = df_sep.groupby(['dia_semana_num', 'hora']).size().unstack(fill_value=0)
heatmap_sep.index = [dias_semana[i] for i in heatmap_sep.index]
# Juntos
df_jun = df[df['em_encontro']]
heatmap_jun = df_jun.groupby(['dia_semana_num', 'hora']).size().unstack(fill_value=0)
heatmap_jun.index = [dias_semana[i] for i in heatmap_jun.index]
# Normaliza pela escala máxima (para comparação visual)
vmax = max(heatmap_sep.values.max(), heatmap_jun.values.max())
# Adiciona heatmaps
fig.add_trace(
go.Heatmap(
z=heatmap_sep.T.values,
x=heatmap_sep.index,
y=heatmap_sep.columns,
colorscale='Blues',
zmin=0,
zmax=vmax,
showscale=False
),
row=1, col=1
)
fig.add_trace(
go.Heatmap(
z=heatmap_jun.T.values,
x=heatmap_jun.index,
y=heatmap_jun.columns,
colorscale='Greens',
zmin=0,
zmax=vmax,
showscale=True
),
row=1, col=2
)
fig.update_xaxes(title_text="Dia da Semana")
fig.update_yaxes(title_text="Hora do Dia")
fig.update_layout(height=1000, title_text="Heatmap: Hora × Dia da Semana por Contexto")
fig.show()
```
::: {.callout-warning}
### 💡 Insight - Heatmap por Contexto
**Quando SEPARADOS (azul esquerda):**
- **Faixa noturna universal:** 18h-21h intensa em TODOS os dias (azul escuro consistente)
- Padrão horário NÃO varia por dia da semana
- Madrugada vazia uniformemente (0h-6h azul clarinho)
- Domingo levemente mais intenso, mas diferença mínima
**Padrão identificado:** Hora × Dia são **INDEPENDENTES** (sem interação)
***
**Quando JUNTOS (verde direita):**
- **Segunda DOMINA:** Blocos intensos ao longo de TODO o dia (10h-13h, 18h-21h)
- Terça-Sexta: Atividade moderada concentrada em horários específicos
- **FDS praticamente VAZIO:** Verde claríssimo/branco (quase zero atividade)
- Comunicação mais distribuída ao longo do dia (não só noite)
**Padrão identificado:** Hora × Dia **INTERAGEM** fortemente
***
**Comparação lado a lado:**
| Aspecto | Separados | Juntos |
|---------|-----------|--------|
| **Padrão horário** | Universal (19h todos os dias) | Varia por dia (segunda ≠ sábado) |
| **Padrão semanal** | Uniforme (dias similares) | Concentrado (úteis >> FDS) |
| **Interação** | Hora e Dia independentes | Hora e Dia interagem |
| **Volume** | Alto e constante | Baixo e seletivo |
**Conclusão crítica:**
- Quando **separados**: `hora` e `dia_semana` são features **independentes** (sem necessidade de interação)
- Quando **juntos**: `hora` e `dia_semana` **DEVEM** interagir (segunda às 12h ≠ sábado às 12h)
**Implicação para FE:**
- ✅ Feature `hora × dia_semana × em_encontro` pode ser útil (interação tripla)
- ✅ Ou criar features específicas: `segunda_diurna_junto`, `fds_junto`, etc.
- ✅ Simplificação: `eh_util × hora × em_encontro` captura essência
- 🎯 Relacionamento muda ESTRUTURALMENTE quando juntos (não é só volume)
:::
***
# Padrões de Interação
## Tempo entre Mensagens
```{python}
# Calcula tempo entre mensagens consecutivas
df_sorted = df.sort_values('timestamp').copy()
df_sorted['tempo_desde_ultima'] = df_sorted['timestamp'].diff().dt.total_seconds()
df_sorted['contexto'] = df_sorted['em_encontro'].map({True: 'Juntos', False: 'Separados'})
# 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())
```
```{python}
#| label: fig-tempo-entre
#| fig-cap: "Distribuição do tempo entre mensagens"
# 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()
```
::: {.callout-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)
:::
### Distribuição por Faixas Interpretáveis
#### 1. ESCALA LOG (Melhor para cauda longa)
```{python}
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()
```
#### 2. BINS CUSTOMIZADOS
```{python}
# 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}%)")
```
**Opção: Barras por faixa x Contexto (Mais claro!)**
```{python}
# Define faixas
bins_segundos = [0, 10, 60, 300, 1800, 3600, float('inf')]
labels = ['0-10s', '10s-1min', '1-5min', '5-30min', '30min-1h', '>1h']
# Calcula por contexto
resultados = []
for ctx in ['Separados', 'Juntos']:
tempo_ctx = df_sorted[df_sorted['contexto'] == ctx]['tempo_desde_ultima'].dropna()
faixas = pd.cut(tempo_ctx, bins=bins_segundos, labels=labels)
contagem = faixas.value_counts().sort_index()
for faixa, qtd in contagem.items():
resultados.append({
'Contexto': ctx,
'Faixa': faixa,
'Quantidade': qtd,
'Percentual': (qtd / len(tempo_ctx) * 100)
})
df_faixas = pd.DataFrame(resultados)
# Gráfico de barras agrupadas
fig = px.bar(
df_faixas,
x='Faixa',
y='Percentual',
color='Contexto',
barmode='group',
title='Distribuição por Faixa de Tempo: Junto vs Separado',
labels={'Percentual': 'Percentual (%)', 'Faixa': 'Tempo entre Mensagens'},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']},
text='Percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=450)
fig.show()
# Tabela
print("\n📊 Distribuição por faixa e contexto:\n")
pivot = df_faixas.pivot(index='Faixa', columns='Contexto', values='Percentual').round(1)
print(pivot)
```
#### CDF - % ACUMULADA (Ver percentis claramente)
```{python}
# Ordena tempos
tempo_sorted = np.sort(tempo_entre_msgs / 60) # Em minutos
cdf = np.arange(1, len(tempo_sorted) + 1) / len(tempo_sorted)
# Plota CDF
fig = px.line(
x=tempo_sorted,
y=cdf * 100,
title='Distribuição Acumulada do Tempo entre Mensagens',
labels={'x': 'Minutos', 'y': '% Acumulada'}
)
# Limita eixo X para ver melhor (até 60 min)
fig.update_xaxes(range=[0, 60])
# Adiciona linhas de referência
fig.add_hline(y=50, line_dash="dash", annotation_text="50%")
fig.add_hline(y=75, line_dash="dash", annotation_text="75%")
fig.add_hline(y=90, line_dash="dash", annotation_text="90%")
fig.add_hline(y=95, line_dash="dash", annotation_text="95%")
fig.show()
print(f"\n📊 Percentis:")
print(f" 50% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 50):.2f} min")
print(f" 75% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 75):.2f} min")
print(f" 90% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 90):.2f} min")
print(f" 95% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 95):.2f} min")
print(f" 99% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 99):.2f} min")
```
**Opção 2: CDF por Contexto (Melhor que histograma!)**
```{python}
# CDF separado por contexto
fig = go.Figure()
for ctx in ['Separados', 'Juntos']:
tempo_ctx = df_sorted[df_sorted['contexto'] == ctx]['tempo_desde_ultima'].dropna()
tempo_sorted = np.sort(tempo_ctx / 60) # Minutos
cdf = np.arange(1, len(tempo_sorted) + 1) / len(tempo_sorted) * 100
fig.add_trace(go.Scatter(
x=tempo_sorted,
y=cdf,
mode='lines',
name=ctx,
line=dict(width=3, color=COLORS['separados'] if ctx == 'Separados' else COLORS['juntos'])
))
fig.update_layout(
title='Distribuição Acumulada por Contexto',
xaxis_title='Minutos',
yaxis_title='% Acumulada',
xaxis=dict(range=[0, 60]), # Foca até 1h
height=700,
hovermode='x unified'
)
# Adiciona linhas de referência
fig.add_hline(y=50, line_dash="dash", annotation_text="50%", line_color="gray")
fig.add_hline(y=75, line_dash="dash", annotation_text="75%", line_color="gray")
fig.add_hline(y=90, line_dash="dash", annotation_text="90%", line_color="gray")
fig.show()
# Tabela comparativa
print("\n📊 Percentis por contexto:\n")
for ctx in ['Separados', 'Juntos']:
tempo_ctx = df_sorted[df_sorted['contexto'] == ctx]['tempo_desde_ultima'].dropna() / 60
print(f"{ctx}:")
print(f" 50%: {np.percentile(tempo_ctx, 50):.2f} min")
print(f" 75%: {np.percentile(tempo_ctx, 75):.2f} min")
print(f" 90%: {np.percentile(tempo_ctx, 90):.2f} min")
print(f" 95%: {np.percentile(tempo_ctx, 95):.2f} min\n")
```
#### Tempo por Contexto
```{python}
# Calcula tempo por contexto
df_sorted['contexto'] = df_sorted['em_encontro'].map({True: 'Juntos', False: 'Separados'})
# Estatísticas por contexto
print("📊 Tempo entre mensagens por contexto:\n")
print(df_sorted.groupby('contexto')['tempo_desde_ultima'].describe())
# Boxplot comparativo
fig = px.box(
df_sorted[df_sorted['tempo_desde_ultima'].notna()],
x='contexto',
y='tempo_desde_ultima',
title='Tempo entre Mensagens: Junto vs Separado',
labels={'tempo_desde_ultima': 'Segundos', 'contexto': 'Contexto'},
log_y=True # Escala LOG no Y
)
fig.show()
# Histograma lado a lado
fig = px.histogram(
df_sorted[df_sorted['tempo_desde_ultima'].notna()],
x='tempo_desde_ultima',
color='contexto',
nbins=50,
title='Distribuição do Tempo: Junto vs Separado',
labels={'tempo_desde_ultima': 'Minutos'},
barmode='overlay',
opacity=0.7,
log_y=True # Escala LOG
)
# Limita até 1 hora para visualização
fig.update_xaxes(range=[0, 3600])
fig.show()
```
```{python}
# Filtra até 1 hora para visualização clara
df_plot = df_sorted[
(df_sorted['tempo_desde_ultima'].notna()) &
(df_sorted['tempo_desde_ultima'] <= 3600)
].copy()
df_plot['tempo_minutos'] = df_plot['tempo_desde_ultima'] / 60
fig = px.box(
df_plot,
x='contexto',
y='tempo_minutos',
color='contexto',
title='Tempo entre Mensagens: Junto vs Separado (até 1h)',
labels={'tempo_minutos': 'Minutos', 'contexto': ''},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']}
)
fig.update_layout(showlegend=False, height=800)
fig.show()
```
::: {.callout-note}
#### 💡 Insight - Tempo entre Mensagens
**Comunicação é PREDOMINANTEMENTE instantânea:**
**Faixas de tempo:**
- **0-10 segundos:** 29.1% (rajadas imediatas)
- **10s-1 minuto:** 26.0%
- **Soma (0-1min):** **55.1%** - mais da METADE!
- **1-5 minutos:** 11.2%
- **>24 horas:** apenas 22 mensagens (0.0%)
**Percentis reveladores:**
- **50%:** 8 segundos (mediana = conversa rápida)
- **75%:** 36 segundos (ainda muito rápido)
- **90%:** 3.1 minutos
- **95%:** 9.9 minutos
- **99%:** 81.8 minutos (1.4h)
**Padrão identificado:** Relacionamento tem comunicação de **alta frequência e baixa latência**.
***
**POR CONTEXTO - Descoberta CONTRA-INTUITIVA:**
| Métrica | Separados | Juntos | Diferença |
|---------|-----------|--------|-----------|
| **Média** | 270s (4.5min) | 778s (13min) | Juntos 2.9x MAIOR |
| **Mediana** | 8s | 11s | Similar |
| **Máximo** | 2.418M s (28d) | 253k s (2.9d) | Separados maior |
**Interpretação:**
**Quando SEPARADOS:**
- Mediana baixa (8s) = Conversas **constantes**
- Média baixa (4.5min) = Menos gaps grandes
- Outliers maiores (28 dias) = Encontros presenciais
- **Padrão:** Comunicação contínua, poucos silêncios
**Quando JUNTOS:**
- Mediana similar (11s) = Quando conversam, também é rápido
- Média ALTA (13min) = Gaps maiores entre mensagens
- Desvio padrão ALTO (6.664s) = Muito mais variável
- **Padrão:** Comunicação intercalada com convívio presencial
**Conclusão:**
- **Separados:** WhatsApp = canal PRINCIPAL → comunicação constante
- **Juntos:** WhatsApp = canal SECUNDÁRIO → usado em momentos específicos, intercalado com interação física
- Média alta quando juntos NÃO significa "respondem devagar" - significa "passam períodos sem celular" (aproveitando presença)
- Gaps quando juntos são **intencionais** (convívio presencial), não ausência de comunicação
**Validação da hipótese:**
- Presença física MODERA padrão de comunicação digital
- Relacionamento ajusta protocolo: Constante (separados) ↔ Pontual (juntos)
- Mediana similar (8s vs 11s) mostra que **quando engajam**, velocidade é a mesma
**Padrão BIMODAL quando juntos (descoberta crítica!):**
**Quando JUNTOS:**
- **39.6%** das mensagens são INSTANTÂNEAS (0-10s) vs 27.3% separados
- **Mas também** 2.3% têm gaps >1h (2x mais que separados!)
- = Relacionamento opera em DOIS modos:
1. **Coordenação urgente:** Rápida, prática, imediata
2. **Convívio silencioso:** Períodos longos sem celular
**Quando SEPARADOS:**
- Distribuição mais UNIFORME (27.3% em 0-10s)
- Menos gaps >1h (1.1%)
- = Comunicação CONSTANTE e ESTÁVEL
**Conclusão:**
- Juntos NÃO significa "respondem mais devagar" - significa padrão DIFERENTE
- Quando engajam, são MAIS rápidos (39.6% vs 27.3% instantâneo)
- Gaps quando juntos são **INTENCIONAIS** (convívio), não desengajamento
- CDF mostra: até 50% são iguais, depois Juntos tem cauda mais longa (95%: 17min vs 9min)
**Implicação para FE:**
- ⚠️ Feature `tempo_desde_ultima_msg` sozinha é ENGANOSA
- ✅ Feature `tempo × em_encontro` captura melhor o padrão
- ✅ Gaps longos quando juntos NÃO indicam "baixo engajamento"
- 🎯 Criar feature `gap_tipo` (rajada/conversa/silencio/convivio)
:::
## Validação Pré-Pós Relacionamento
**Validação Rápida**
```{python}
# Quantas mensagens são PRÉ-relacionamento?
data_marco = pd.Timestamp('2024-12-16')
msgs_pre = df[df['timestamp'] < data_marco]
msgs_pos = df[df['timestamp'] >= data_marco]
print("📊 DIVISÃO PRÉ vs PÓS RELACIONAMENTO:\n")
print(f"📅 Antes de 16/dez/2024 (AMIZADE):")
print(f" • Mensagens: {len(msgs_pre):,}")
print(f" • Percentual: {len(msgs_pre)/len(df)*100:.1f}%")
print(f" • Período: {(data_marco - df['timestamp'].min()).days} dias")
print(f" • Média/dia: {len(msgs_pre)/(data_marco - df['timestamp'].min()).days:.1f}")
print(f"\n💕 Depois de 16/dez/2024 (RELACIONAMENTO):")
print(f" • Mensagens: {len(msgs_pos):,}")
print(f" • Percentual: {len(msgs_pos)/len(df)*100:.1f}%")
print(f" • Período: {(df['timestamp'].max() - data_marco).days} dias")
print(f" • Média/dia: {len(msgs_pos)/(df['timestamp'].max() - data_marco).days:.1f}")
# Hipótese: NUNCA ficaram sem falar pós-16/dez
msgs_por_dia_pos = msgs_pos.groupby(msgs_pos['timestamp'].dt.date).size()
dias_sem_msg = (msgs_por_dia_pos == 0).sum()
dias_totais = (df['timestamp'].max().date() - data_marco.date()).days + 1
dias_com_msg = len(msgs_por_dia_pos)
print(f"\n💬 Hipótese 'Nunca ficaram sem falar':")
print(f" • Dias no período: {dias_totais}")
print(f" • Dias COM mensagem: {dias_com_msg}")
print(f" • Dias SEM mensagem: {dias_totais - dias_com_msg}")
print(f" • Taxa: {dias_com_msg/dias_totais*100:.1f}% dos dias")
```
**Dias sem falar x Encontros:**
```{python}
# Identifica QUAIS dias não tiveram mensagem
data_marco = pd.Timestamp('2024-12-16')
msgs_pos = df[df['timestamp'] >= data_marco].copy()
# Todos os dias no período
inicio = data_marco.date()
fim = df['timestamp'].max().date()
todos_dias = pd.date_range(inicio, fim, freq='D')
# Dias COM mensagem
dias_com_msg = set(msgs_pos['timestamp'].dt.date.unique())
# Dias SEM mensagem
dias_sem_msg = [dia.date() for dia in todos_dias if dia.date() not in dias_com_msg]
print(f"📅 Dias SEM mensagem no relacionamento ({len(dias_sem_msg)}):\n")
# Carrega encontros (CAMINHO DIRETO)
encontros = pd.read_csv('/Users/mosx/Desktop/whatsapp-interaction-analysis/data/external/encontros.csv')
encontros['inicio'] = pd.to_datetime(encontros['inicio'])
encontros['fim'] = pd.to_datetime(encontros['fim'])
# Verifica se cada dia SEM msg estava em encontro
dias_sem_msg_info = []
for dia in dias_sem_msg:
em_encontro = False
encontro_nome = None
for _, enc in encontros.iterrows():
if enc['inicio'].date() <= dia <= enc['fim'].date():
em_encontro = True
encontro_nome = enc['nome']
break
dias_sem_msg_info.append({
'Data': dia,
'Em Encontro': em_encontro,
'Encontro': encontro_nome if em_encontro else 'Separados'
})
df_sem_msg = pd.DataFrame(dias_sem_msg_info)
print(df_sem_msg.to_string(index=False))
# Resumo
total_sem_msg = len(dias_sem_msg)
sem_msg_em_encontro = df_sem_msg['Em Encontro'].sum()
sem_msg_separados = total_sem_msg - sem_msg_em_encontro
print(f"\n📊 RESUMO:")
print(f" • Total de dias sem mensagem: {total_sem_msg}")
print(f" • Durante encontros: {sem_msg_em_encontro} ({sem_msg_em_encontro/total_sem_msg*100:.1f}%)")
print(f" • Quando separados: {sem_msg_separados} ({sem_msg_separados/total_sem_msg*100:.1f}%)")
if sem_msg_em_encontro > 0:
print(f"\n✅ HIPÓTESE VALIDADA: Dias sem mensagem coincidem com encontros!")
elif sem_msg_separados == total_sem_msg:
print(f"\n⚠️ Todos os dias sem mensagem foram quando SEPARADOS - investigar motivo!")
```
::: {.callout-tip}
### 🎯 Descoberta - Consistência Absoluta na Comunicação
**Durante RELACIONAMENTO (pós 16/dez/2024):**
- **309 dias** no período
- **301 dias COM mensagem** (97.4%)
- **8 dias SEM mensagem** (2.6%)
**Descoberta CRÍTICA:**
- **100% dos dias sem mensagem = Durante encontros presenciais**
- **0% quando separados**
**Conclusão definitiva:**
> "Nunca ficaram um único dia sem se falar quando separados."
**Interpretação:**
- Relacionamento à distância tem **ZERO gaps** de comunicação
- Único motivo para não trocar mensagens = **presença física**
- Comunicação digital é **CONSTANTE** exceto quando substituída por convívio presencial
- Dias sem msg no encontro longo (5 de 8) = momentos de total imersão no convívio
**Validação temporal:**
- Out-15/Dez (amizade): 2.8% mensagens, média 45/dia
- 16/Dez+ (relacionamento): **97.2%** mensagens, média **290/dia** (6.4x)
- Mudança drástica marca início do relacionamento
**Implicação:**
- Gaps longos (>24h) no dataset = **SEMPRE** durante encontros ou pré-relacionamento
- Relacionamento tem dois protocolos: Comunicação digital constante (separados) ↔ Convívio presencial (juntos)
- Feature `em_encontro` é ESSENCIAL - sem ela, modelo não entende ausência de mensagens
:::
::: {.callout-note}
Verificar todas as analises acima que são afetadas por esses outliers quando se trata em padrão de troca de mensagem, depois pode voltar com o conteúdo?
Todas as analises daqui para baixo consideram isso.?
:::
## Tempo entre Mensagens (Refatorado)
```{python}
# ============================================================================
# TEMPO ENTRE MENSAGENS - Só Relacionamento (pós 16/dez/2024)
# ============================================================================
# Filtra relacionamento
data_marco = pd.Timestamp('2024-12-16')
df_sorted = df.sort_values('timestamp').copy()
df_sorted = df_sorted[df_sorted['timestamp'] >= data_marco].copy()
# Calcula tempo desde última mensagem
df_sorted['tempo_desde_ultima'] = df_sorted['timestamp'].diff().dt.total_seconds()
# Adiciona contexto
df_sorted['contexto'] = df_sorted['em_encontro'].map({True: 'Juntos', False: 'Separados'})
# Remove primeira mensagem (NaN)
tempo_entre_msgs = df_sorted['tempo_desde_ultima'].dropna()
print("="*80)
print("⏱️ TEMPO ENTRE MENSAGENS - Relacionamento (pós 16/dez/2024)")
print("="*80)
# ============================================================================
# 1. ESTATÍSTICAS DESCRITIVAS
# ============================================================================
print("\n⏱️ Tempo entre mensagens (segundos):")
print(tempo_entre_msgs.describe())
print("\n⏱️ Em minutos:")
print((tempo_entre_msgs / 60).describe())
# ============================================================================
# 2. HISTOGRAMA INICIAL (até 1h)
# ============================================================================
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, showlegend=False)
fig.show()
```
::: {.callout-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.000 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:**
Contexto: Tempo é MENOR quando separados (conversam mais)? Ou quando juntos (coordenação rápida)?
Faixas: Qual % das mensagens são "instantâneas" (<10s) vs "conversas" (1-5min) vs "retomadas" (>1h)?
Outliers: Gaps de 1+ dia coincidem com encontros presenciais?
**Próximas visualizações necessárias:**
✅ Escala logarítmica (ver toda a distribuição)
✅ Bins customizados (faixas interpretáveis)
✅ CDF (% acumulada - ver percentis claramente)
✅ Separação por contexto (junto vs separado)
:::
Escala logaritima
```{python}
# ============================================================================
# 3. ESCALA LOGARÍTMICA
# ============================================================================
# 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.update_layout(height=400, showlegend=False)
fig.show()
# ============================================================================
# 4. BINS CUSTOMIZADOS (Faixas Interpretáveis)
# ============================================================================
# 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.update_layout(height=400, showlegend=False)
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}%)")
# ============================================================================
# 5. CDF - DISTRIBUIÇÃO ACUMULADA
# ============================================================================
# Ordena tempos
tempo_sorted = np.sort(tempo_entre_msgs / 60) # Em minutos
cdf = np.arange(1, len(tempo_sorted) + 1) / len(tempo_sorted)
# Plota CDF
fig = px.line(
x=tempo_sorted,
y=cdf * 100,
title='Distribuição Acumulada do Tempo entre Mensagens',
labels={'x': 'Minutos', 'y': '% Acumulada'}
)
# Limita eixo X para ver melhor (até 60 min)
fig.update_xaxes(range=[0, 60])
# Adiciona linhas de referência
fig.add_hline(y=50, line_dash="dash", annotation_text="50%", line_color="gray")
fig.add_hline(y=75, line_dash="dash", annotation_text="75%", line_color="gray")
fig.add_hline(y=90, line_dash="dash", annotation_text="90%", line_color="gray")
fig.add_hline(y=95, line_dash="dash", annotation_text="95%", line_color="gray")
fig.update_layout(height=400, showlegend=False)
fig.show()
print(f"\n📊 Percentis:")
print(f" 50% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 50):.2f} min")
print(f" 75% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 75):.2f} min")
print(f" 90% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 90):.2f} min")
print(f" 95% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 95):.2f} min")
print(f" 99% das mensagens em até: {np.percentile(tempo_entre_msgs/60, 99):.2f} min")
# ============================================================================
# 6. BOXPLOT POR CONTEXTO
# ============================================================================
# Filtra até 1 hora para visualização clara
df_plot = df_sorted[
(df_sorted['tempo_desde_ultima'].notna()) &
(df_sorted['tempo_desde_ultima'] <= 3600)
].copy()
df_plot['tempo_minutos'] = df_plot['tempo_desde_ultima'] / 60
fig = px.box(
df_plot,
x='contexto',
y='tempo_minutos',
color='contexto',
title='Tempo entre Mensagens: Junto vs Separado (até 1h)',
labels={'tempo_minutos': 'Minutos', 'contexto': ''},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']}
)
fig.update_layout(showlegend=False, height=400)
fig.show()
# ============================================================================
# 7. ESTATÍSTICAS POR CONTEXTO
# ============================================================================
print("\n📊 Tempo entre mensagens por contexto:\n")
print(df_sorted.groupby('contexto')['tempo_desde_ultima'].describe())
# ============================================================================
# 8. CDF POR CONTEXTO
# ============================================================================
fig = go.Figure()
for ctx in ['Separados', 'Juntos']:
tempo_ctx = df_sorted[df_sorted['contexto'] == ctx]['tempo_desde_ultima'].dropna()
tempo_sorted_ctx = np.sort(tempo_ctx / 60) # Minutos
cdf_ctx = np.arange(1, len(tempo_sorted_ctx) + 1) / len(tempo_sorted_ctx) * 100
fig.add_trace(go.Scatter(
x=tempo_sorted_ctx,
y=cdf_ctx,
mode='lines',
name=ctx,
line=dict(width=3, color=COLORS['separados'] if ctx == 'Separados' else COLORS['juntos'])
))
fig.update_layout(
title='Distribuição Acumulada por Contexto',
xaxis_title='Minutos',
yaxis_title='% Acumulada',
xaxis=dict(range=[0, 60]), # Foca até 1h
height=400,
hovermode='x unified'
)
# Adiciona linhas de referência
fig.add_hline(y=50, line_dash="dash", annotation_text="50%", line_color="gray")
fig.add_hline(y=75, line_dash="dash", annotation_text="75%", line_color="gray")
fig.add_hline(y=90, line_dash="dash", annotation_text="90%", line_color="gray")
fig.show()
# Tabela comparativa
print("\n📊 Percentis por contexto:\n")
for ctx in ['Separados', 'Juntos']:
tempo_ctx = df_sorted[df_sorted['contexto'] == ctx]['tempo_desde_ultima'].dropna() / 60
print(f"{ctx}:")
print(f" 50%: {np.percentile(tempo_ctx, 50):.2f} min")
print(f" 75%: {np.percentile(tempo_ctx, 75):.2f} min")
print(f" 90%: {np.percentile(tempo_ctx, 90):.2f} min")
print(f" 95%: {np.percentile(tempo_ctx, 95):.2f} min\n")
# ============================================================================
# 9. BARRAS POR FAIXA × CONTEXTO
# ============================================================================
# Define faixas
bins_segundos = [0, 10, 60, 300, 1800, 3600, float('inf')]
labels = ['0-10s', '10s-1min', '1-5min', '5-30min', '30min-1h', '>1h']
# Calcula por contexto
resultados = []
for ctx in ['Separados', 'Juntos']:
tempo_ctx = df_sorted[df_sorted['contexto'] == ctx]['tempo_desde_ultima'].dropna()
faixas = pd.cut(tempo_ctx, bins=bins_segundos, labels=labels)
contagem = faixas.value_counts().sort_index()
for faixa, qtd in contagem.items():
resultados.append({
'Contexto': ctx,
'Faixa': faixa,
'Quantidade': qtd,
'Percentual': (qtd / len(tempo_ctx) * 100)
})
df_faixas = pd.DataFrame(resultados)
# Gráfico de barras agrupadas
fig = px.bar(
df_faixas,
x='Faixa',
y='Percentual',
color='Contexto',
barmode='group',
title='Distribuição por Faixa de Tempo: Junto vs Separado',
labels={'Percentual': 'Percentual (%)', 'Faixa': 'Tempo entre Mensagens'},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']},
text='Percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=450)
fig.show()
# Tabela
print("\n📊 Distribuição por faixa e contexto:\n")
pivot = df_faixas.pivot(index='Faixa', columns='Contexto', values='Percentual').round(1)
print(pivot)
print("="*80)
```
::: {.callout-note}
### 💡 Insight - Tempo entre Mensagens
**Comunicação é PREDOMINANTEMENTE instantânea:**
**Estatísticas gerais (relacionamento):**
- **Mediana:** 8 segundos (resposta muito rápida!)
- **75%:** 36 segundos (ainda rápido)
- **90%:** 3.1 minutos
- **95%:** 9.8 minutos
- **99%:** 76.7 minutos (~1.3h)
- **Máximo:** 4.214 minutos (~70 horas = ~2.9 dias)
**Nota:** Máximo caiu de 28 dias para 2.9 dias após filtrar período de amizade - gaps enormes eram pré-relacionamento!
**Faixas de tempo:**
- **0-10 segundos:** 28.7% (quase instantâneo)
- **10s-1 minuto:** 25.8%
- **Soma (0-1min):** **54.5%** - mais da METADE!
- **1-5 minutos:** 11.2%
- **>24 horas:** apenas 16 mensagens (0.0%)!
**Conclusão:** No relacionamento, comunicação tem **alta frequência e baixa latência** - raramente ficam >1h sem trocar mensagens.
***
**POR CONTEXTO - Descoberta CONTRA-INTUITIVA:**
| Métrica | Separados | Juntos | Diferença |
|---------|-----------|--------|-----------|
| **Média** | 218s (3.6min) | 756s (12.6min) | Juntos **3.5x MAIOR** 🔥 |
| **Mediana** | 7s | 11s | Similar (ambos rápidos) |
| **95%** | 8.9min | 16.8min | Juntos quase 2x |
| **Máximo** | 149k s (41h) | 253k s (70h) | Juntos tem o recorde |
**Distribuição por faixa - Padrão BIMODAL quando juntos:**
| Faixa | Separados | Juntos | Interpretação |
|-------|-----------|--------|---------------|
| **0-10s** | 26.8% | 39.6% | Juntos **+12.8pp** (MUITO mais instantâneo!) 🔥 |
| **10s-1min** | 25.4% | 28.2% | +2.8pp |
| **1-5min** | 11.1% | 11.6% | Similar |
| **5-30min** | 5.2% | 6.9% | +1.7pp |
| **30min-1h** | 0.9% | 1.2% | +0.3pp |
| **>1h** | 1.1% | 2.3% | Juntos **2.1x MAIS** 🔥 |
**Padrão BIMODAL identificado:**
**Quando JUNTOS:**
- **39.6%** são INSTANTÂNEAS (0-10s) vs 26.8% separados
- **MAS TAMBÉM** 2.3% têm gaps >1h (2x mais que separados!)
- Média ALTA (12.6min) apesar de mediana baixa (11s)
- **= Dois modos:** Ou é MUITO rápido (coordenação) OU tem gap longo (convívio)
**Quando SEPARADOS:**
- Distribuição mais **UNIFORME** (26.8% em 0-10s)
- Menos gaps >1h (1.1%)
- Média baixa (3.6min), mediana baixa (7s)
- **= Um modo:** Comunicação CONSTANTE e ESTÁVEL
**Interpretação:**
**Separados:**
- Atenção CONSTANTE ao celular
- Fluxo estável ao longo do dia
- Poucos silêncios longos
- **Padrão:** WhatsApp = canal PRINCIPAL
**Juntos:**
- Atenção INTERCALADA
- Ou respondem MUITO rápido (39.6% em <10s - coordenação)
- Ou têm gaps longos (convívio sem celular)
- **Padrão:** WhatsApp = canal SECUNDÁRIO
**Validação:**
- Mediana similar (7s vs 11s) mostra que **quando engajam**, velocidade é a mesma
- Média ALTA quando juntos NÃO significa "respondem devagar"
- Significa: Passam períodos SEM celular (aproveitando presença física)
- Gaps quando juntos são **INTENCIONAIS** (convívio presencial), não ausência de comunicação
***
**Impacto do filtro de relacionamento:**
**ANTES (com amizade):**
- Máximo: 2.417.848s = **28 dias**
- >24h: 22 mensagens
- Gaps enormes distorciam análise
**DEPOIS (só relacionamento):**
- Máximo: 252.875s = **2.9 dias** (88% menor!)
- >24h: 16 mensagens (27% menos)
- Padrão mais limpo e consistente
**Conclusão:** Gaps de >3 dias eram do período de amizade. No relacionamento, NUNCA ficam >3 dias sem mensagem (exceto durante encontros).
***
**Integração com análises anteriores:**
**Consistente com:**
- ✅ "Nunca ficaram sem falar quando separados" (gaps >1 dia = 0.0%)
- ✅ Respostas rápidas: 81% em <60s (bate com mediana de 8s)
- ✅ Sequências: Juntos mais mensagens isoladas (39.6% instantâneas)
- ✅ Turnos: Juntos têm turnos mais curtos (comunicação pontual)
**Novo entendimento:**
> Relacionamento tem **DOIS protocolos de tempo**:
> 1. **Modo atenção constante** (separados): 7s mediana, 3.6min média, poucos gaps
> 2. **Modo atenção intercalada** (juntos): 11s mediana, 12.6min média, bimodal (rápido OU gap)
**Implicação para FE:**
- ✅ Feature `tempo_desde_ultima_msg` × `em_encontro` captura padrão bimodal
- ✅ Criar feature `eh_gap_longo` (>1h) × contexto
- ⚠️ Média sozinha é ENGANOSA (bimodalidade quando juntos)
- 🎯 Separar em faixas: `instantaneo` (<10s), `rapido` (10s-1min), `moderado` (1-5min), `gap` (>5min)
:::
## Respostas Rápidas
```{python}
# ============================================================================
# RESPOSTAS RÁPIDAS - Só Relacionamento (pós 16/dez/2024)
# ============================================================================
# Filtra só relacionamento
data_marco = pd.Timestamp('2024-12-16')
df_rel = df_sorted[df_sorted['timestamp'] >= data_marco].copy()
tempo_rel = df_rel['tempo_desde_ultima'].dropna()
print("="*80)
print("⚡ RESPOSTAS RÁPIDAS - Relacionamento (pós 16/dez/2024)")
print("="*80)
# ============================================================================
# 1. VISÃO GERAL
# ============================================================================
print("\n📊 Velocidade Geral de Resposta:\n")
thresholds = [10, 30, 60, 120, 300] # segundos
for threshold in thresholds:
count = (tempo_rel < threshold).sum()
pct = (count / len(tempo_rel)) * 100
print(f" < {threshold}s: {count:,} ({pct:.1f}%)")
# ============================================================================
# 2. POR CONTEXTO (Junto vs Separado)
# ============================================================================
print("\n⚡ Velocidade por CONTEXTO:\n")
for ctx in ['Separados', 'Juntos']:
df_ctx = df_rel[df_rel['contexto'] == ctx]
tempo_ctx = df_ctx['tempo_desde_ultima'].dropna()
print(f"{ctx}:")
print(f" < 10s: {(tempo_ctx < 10).sum():,} ({(tempo_ctx < 10).sum()/len(tempo_ctx)*100:.1f}%)")
print(f" < 30s: {(tempo_ctx < 30).sum():,} ({(tempo_ctx < 30).sum()/len(tempo_ctx)*100:.1f}%)")
print(f" < 60s: {(tempo_ctx < 60).sum():,} ({(tempo_ctx < 60).sum()/len(tempo_ctx)*100:.1f}%)")
print(f" < 120s: {(tempo_ctx < 120).sum():,} ({(tempo_ctx < 120).sum()/len(tempo_ctx)*100:.1f}%)")
print(f" < 300s: {(tempo_ctx < 300).sum():,} ({(tempo_ctx < 300).sum()/len(tempo_ctx)*100:.1f}%)")
print()
# ============================================================================
# 3. POR REMETENTE (P1 vs P2)
# ============================================================================
print("⚡ Velocidade por REMETENTE:\n")
for rem in ['P1', 'P2']:
df_rem = df_rel[df_rel['remetente'] == rem]
tempo_rem = df_rem['tempo_desde_ultima'].dropna()
print(f"{rem}:")
print(f" < 10s: {(tempo_rem < 10).sum():,} ({(tempo_rem < 10).sum()/len(tempo_rem)*100:.1f}%)")
print(f" < 30s: {(tempo_rem < 30).sum():,} ({(tempo_rem < 30).sum()/len(tempo_rem)*100:.1f}%)")
print(f" < 60s: {(tempo_rem < 60).sum():,} ({(tempo_rem < 60).sum()/len(tempo_rem)*100:.1f}%)")
print()
# ============================================================================
# 4. VISUALIZAÇÃO - Contexto
# ============================================================================
# Prepara dados
dados_comparacao = []
for ctx in ['Separados', 'Juntos']:
df_ctx = df_rel[df_rel['contexto'] == ctx]
tempo_ctx = df_ctx['tempo_desde_ultima'].dropna()
dados_comparacao.append({
'Contexto': ctx,
'Faixa': '< 10s',
'Percentual': (tempo_ctx < 10).sum()/len(tempo_ctx)*100
})
dados_comparacao.append({
'Contexto': ctx,
'Faixa': '10-30s',
'Percentual': ((tempo_ctx >= 10) & (tempo_ctx < 30)).sum()/len(tempo_ctx)*100
})
dados_comparacao.append({
'Contexto': ctx,
'Faixa': '30-60s',
'Percentual': ((tempo_ctx >= 30) & (tempo_ctx < 60)).sum()/len(tempo_ctx)*100
})
dados_comparacao.append({
'Contexto': ctx,
'Faixa': '1-2min',
'Percentual': ((tempo_ctx >= 60) & (tempo_ctx < 120)).sum()/len(tempo_ctx)*100
})
dados_comparacao.append({
'Contexto': ctx,
'Faixa': '2-5min',
'Percentual': ((tempo_ctx >= 120) & (tempo_ctx < 300)).sum()/len(tempo_ctx)*100
})
dados_comparacao.append({
'Contexto': ctx,
'Faixa': '> 5min',
'Percentual': (tempo_ctx >= 300).sum()/len(tempo_ctx)*100
})
df_comp = pd.DataFrame(dados_comparacao)
# Ordena faixas
ordem_faixas = ['< 10s', '10-30s', '30-60s', '1-2min', '2-5min', '> 5min']
df_comp['Faixa'] = pd.Categorical(df_comp['Faixa'], categories=ordem_faixas, ordered=True)
df_comp = df_comp.sort_values('Faixa')
# Gráfico
fig = px.bar(
df_comp,
x='Faixa',
y='Percentual',
color='Contexto',
barmode='group',
title='Velocidade de Resposta: Junto vs Separado',
labels={'Percentual': '%', 'Faixa': 'Tempo de Resposta'},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']},
text='Percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=450)
fig.show()
print("="*80)
```
***
## Sequências de Mensagens
```{python}
# ============================================================================
# SEQUÊNCIAS DE MENSAGENS - Só Relacionamento (pós 16/dez/2024)
# ============================================================================
# Usa df_rel já filtrado anteriormente
# Se não tiver, descomente abaixo:
# data_marco = pd.Timestamp('2024-12-16')
# df_rel = df_sorted[df_sorted['timestamp'] >= data_marco].copy()
# Recalcula sequências (pode ter mudado com filtro)
df_rel['mudou_remetente'] = df_rel['remetente'] != df_rel['remetente'].shift()
df_rel['sequencia'] = df_rel.groupby(
(df_rel['mudou_remetente']).cumsum()
).cumcount() + 1
print("="*80)
print("📊 SEQUÊNCIAS DE MENSAGENS - Relacionamento (pós 16/dez/2024)")
print("="*80)
# ============================================================================
# 1. VISÃO GERAL
# ============================================================================
print("\n📊 Sequências - Estatísticas Gerais:\n")
print(df_rel['sequencia'].describe())
print(f"\n📈 Maior sequência: {df_rel['sequencia'].max()} mensagens consecutivas")
# Distribuição
seq_dist = df_rel.groupby('sequencia').size().sort_index()
print(f"\n📊 Top 20 sequências mais comuns:")
for seq, count in seq_dist.head(20).items():
pct = (count / len(df_rel)) * 100
print(f" {seq:2d} msg: {count:,} ({pct:.1f}%)")
# ============================================================================
# 2. POR CONTEXTO (Junto vs Separado)
# ============================================================================
print("\n📊 Sequências por CONTEXTO:\n")
for ctx in ['Separados', 'Juntos']:
df_ctx = df_rel[df_rel['contexto'] == ctx]
print(f"{ctx}:")
print(f" Média: {df_ctx['sequencia'].mean():.2f} msgs")
print(f" Mediana: {df_ctx['sequencia'].median():.0f} msgs")
print(f" Máximo: {df_ctx['sequencia'].max()} msgs")
print(f" 75%: {df_ctx['sequencia'].quantile(0.75):.0f} msgs")
print(f" 90%: {df_ctx['sequencia'].quantile(0.90):.0f} msgs")
print()
# ============================================================================
# 3. POR REMETENTE
# ============================================================================
print("📊 Sequências por REMETENTE:\n")
for rem in ['P1', 'P2']:
df_rem = df_rel[df_rel['remetente'] == rem]
print(f"{rem}:")
print(f" Média: {df_rem['sequencia'].mean():.2f} msgs")
print(f" Mediana: {df_rem['sequencia'].median():.0f} msgs")
print(f" Máximo: {df_rem['sequencia'].max()} msgs")
print()
# ============================================================================
# 4. VISUALIZAÇÃO - Geral (Top 15)
# ============================================================================
fig = px.bar(
x=seq_dist.index[:15],
y=seq_dist.values[:15],
title='Top 15 Tamanhos de Sequências Mais Comuns',
labels={'x': 'Tamanho da Sequência', 'y': 'Frequência'},
text=seq_dist.values[:15]
)
fig.update_traces(texttemplate='%{text:,}', textposition='outside')
fig.update_layout(height=400)
fig.show()
# ============================================================================
# 5. VISUALIZAÇÃO - Contexto (Boxplot)
# ============================================================================
# Limita até sequência 20 para visualização
df_plot = df_rel[df_rel['sequencia'] <= 20].copy()
fig = px.box(
df_plot,
x='contexto',
y='sequencia',
color='contexto',
title='Tamanho de Sequências: Junto vs Separado (até 20 msgs)',
labels={'sequencia': 'Tamanho da Sequência', 'contexto': ''},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']}
)
fig.update_layout(showlegend=False, height=400)
fig.show()
# ============================================================================
# 6. DISTRIBUIÇÃO POR FAIXAS × CONTEXTO
# ============================================================================
# Define faixas de sequência
def classificar_sequencia(seq):
if seq == 1:
return '1 msg'
elif seq <= 3:
return '2-3 msgs'
elif seq <= 5:
return '4-5 msgs'
elif seq <= 10:
return '6-10 msgs'
else:
return '>10 msgs'
df_rel['faixa_seq'] = df_rel['sequencia'].apply(classificar_sequencia)
# Calcula por contexto
dados_faixas = []
for ctx in ['Separados', 'Juntos']:
df_ctx = df_rel[df_rel['contexto'] == ctx]
faixas = df_ctx['faixa_seq'].value_counts()
for faixa, count in faixas.items():
dados_faixas.append({
'Contexto': ctx,
'Faixa': faixa,
'Quantidade': count,
'Percentual': (count / len(df_ctx)) * 100
})
df_faixas = pd.DataFrame(dados_faixas)
# Ordena faixas
ordem_faixas = ['1 msg', '2-3 msgs', '4-5 msgs', '6-10 msgs', '>10 msgs']
df_faixas['Faixa'] = pd.Categorical(df_faixas['Faixa'], categories=ordem_faixas, ordered=True)
df_faixas = df_faixas.sort_values('Faixa')
# Gráfico
fig = px.bar(
df_faixas,
x='Faixa',
y='Percentual',
color='Contexto',
barmode='group',
title='Distribuição de Sequências por Faixa: Junto vs Separado',
labels={'Percentual': '%', 'Faixa': 'Tamanho da Sequência'},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']},
text='Percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_layout(height=450)
fig.show()
# Tabela
print("\n📊 Distribuição por faixa e contexto:\n")
pivot = df_faixas.pivot(index='Faixa', columns='Contexto', values='Percentual').round(1)
print(pivot)
print("="*80)
```
::: {.callout-note}
### 💡 Insight - Sequências de Mensagens
**Comunicação é fragmentada em sequências curtas:**
**Estatísticas gerais:**
- **Mediana:** 2 mensagens consecutivas
- **Média:** 3.3 mensagens
- **75%:** até 4 mensagens
- **Máximo:** 54 mensagens (outlier extremo!)
**Distribuição:**
- **30.6%** são mensagens ISOLADAS (1 msg)
- **22.7%** são pares (2 msgs)
- **66.9%** têm 1-3 mensagens (maioria!)
- Apenas **3.0%** têm >10 mensagens
**Conclusão:** Comunicação é **altamente fragmentada** - raramente mandam rajadas longas, preferem mensagens curtas e alternadas.
***
**POR CONTEXTO - Confirma padrão de coordenação vs conversa:**
| Métrica | Separados | Juntos | Diferença |
|---------|-----------|--------|-----------|
| **Média** | 3.45 msgs | 2.73 msgs | -0.72 (juntos MAIS curto) |
| **Mediana** | 2 msgs | 2 msgs | Idêntica |
| **90%** | 7 msgs | 5 msgs | -2 msgs |
| **Máximo** | 34 msgs | 54 msgs | +20 (outlier quando junto!) |
**Distribuição por faixa:**
| Faixa | Separados | Juntos | Interpretação |
|-------|-----------|--------|---------------|
| **1 msg** | 29.2% | 38.7% | Juntos: +9.5pp (MAIS pontual) |
| **2-3 msgs** | 35.7% | 40.0% | Juntos: +4.3pp |
| **4-5 msgs** | 17.0% | 12.7% | Separados: +4.3pp (MAIS desenvolvido) |
| **6-10 msgs** | 14.5% | 6.4% | Separados: **+8.1pp** (MUITO mais) |
| **>10 msgs** | 3.6% | 2.2% | Separados: +1.4pp |
**Padrão INVERTIDO:**
**Quando JUNTOS:**
- **78.7%** têm 1-3 msgs (comunicação PONTUAL)
- Apenas 6.4% têm 6-10 msgs (quase metade de separados!)
- Mensagens resolvem rapidamente
- **Interpretação:** "Chegou?" "Sim" = conversa fechada
**Quando SEPARADOS:**
- **64.9%** têm 1-3 msgs (ainda maioria, mas menos)
- **14.5%** têm 6-10 msgs (2.3x mais que juntos!)
- Mais sequências médias/longas
- **Interpretação:** Contar o dia, explicar situações, desenvolver assuntos
**Conclusão:**
- Juntos = Coordenação (rápida, direta, pontual)
- Separados = Conversa (mais desenvolvida, explicações, narrativas)
- Mediana igual (2) mostra que AMBOS fragmentam, mas separados estendem mais
***
**POR REMETENTE - Simetria mantida:**
| Métrica | P1 | P2 | Diferença |
|---------|----|----|-----------|
| **Média** | 3.52 msgs | 3.16 msgs | 0.36 (P1 levemente mais verboso) |
| **Mediana** | 2 msgs | 2 msgs | Idêntica |
| **Máximo** | 54 msgs | 32 msgs | P1 tem sequência mais longa |
**Conclusão:**
- Padrões MUITO similares
- P1 ligeiramente mais "explicativo" (3.52 vs 3.16)
- Mas diferença é marginal (0.36 msgs)
- **Quem mandou 54 msgs?** P1 (provavelmente contando algo importante!)
***
**Integração com análises anteriores:**
**Consistente com:**
- ✅ Velocidade de resposta: Rápida em ambos contextos (mas padrão diferente)
- ✅ Tempo entre mensagens: Juntos tem mais mensagens isoladas
- ✅ Tipos × Contexto: Juntos usa mais texto curto, separados usa mais mídia rica
**Validação cruzada:**
- Juntos: 38.7% mensagens isoladas + 47.4% respostas <10s = Coordenação rápida
- Separados: 14.5% sequências 6-10 msgs + preferência por AUDIO/VID = Conversas desenvolvidas
**Novo padrão identificado:**
> Relacionamento tem **dois protocolos de conversa**:
> 1. **Modo coordenação** (juntos): Mensagens curtas, pontuais, resolvem rápido
> 2. **Modo narrativa** (separados): Sequências mais longas, explicações, contar o dia
**Implicação para FE:**
- ✅ Feature `sequencia_tamanho` útil
- ✅ Feature `eh_sequencia_longa` (>5 msgs) × contexto captura diferença
- ⚠️ Máximo (54 vs 34) é outlier - não representa padrão
- 🎯 Faixas: `isolada` (1) vs `curta` (2-3) vs `media` (4-10) vs `longa` (>10)
:::
## Análise de turnos
```{python}
# ============================================================================
# ANÁLISE DE TURNOS - Só Relacionamento (pós 16/dez/2024)
# ============================================================================
# Usa df_rel já filtrado anteriormente
# Se não tiver, descomente:
# data_marco = pd.Timestamp('2024-12-16')
# df_rel = df_sorted[df_sorted['timestamp'] >= data_marco].copy()
print("="*80)
print("💬 ANÁLISE DE TURNOS - Relacionamento (pós 16/dez/2024)")
print("="*80)
# ============================================================================
# 1. DEFINE TURNOS (gap > 1 hora)
# ============================================================================
THRESHOLD_INICIO = 3600 # 1 hora em segundos
df_rel['inicio_conversa'] = (
(df_rel['tempo_desde_ultima'] > THRESHOLD_INICIO) |
(df_rel['tempo_desde_ultima'].isna())
)
df_rel['turno_id'] = df_rel['inicio_conversa'].cumsum()
# ============================================================================
# 2. VISÃO GERAL
# ============================================================================
total_turnos = df_rel['inicio_conversa'].sum()
msgs_por_turno = len(df_rel) / total_turnos
print(f"\n💬 Turnos de conversa (gap > 1h):")
print(f" Total de turnos: {total_turnos:,}")
print(f" Média msgs/turno: {msgs_por_turno:.1f}")
# Tamanho dos turnos
turnos_tamanho = df_rel.groupby('turno_id').size()
print(f"\n📊 Tamanho dos turnos - Estatísticas:")
print(turnos_tamanho.describe())
print(f"\n📈 Percentis:")
print(f" 50%: {turnos_tamanho.quantile(0.50):.0f} msgs")
print(f" 75%: {turnos_tamanho.quantile(0.75):.0f} msgs")
print(f" 90%: {turnos_tamanho.quantile(0.90):.0f} msgs")
print(f" 95%: {turnos_tamanho.quantile(0.95):.0f} msgs")
print(f" 99%: {turnos_tamanho.quantile(0.99):.0f} msgs")
# ============================================================================
# 3. POR CONTEXTO
# ============================================================================
print("\n💬 Turnos por CONTEXTO:\n")
for ctx in ['Separados', 'Juntos']:
df_ctx = df_rel[df_rel['contexto'] == ctx]
# Recalcula turnos por contexto
df_ctx = df_ctx.copy()
df_ctx['inicio_conv_ctx'] = (
(df_ctx['tempo_desde_ultima'] > THRESHOLD_INICIO) |
(df_ctx['tempo_desde_ultima'].isna())
)
df_ctx['turno_id_ctx'] = df_ctx['inicio_conv_ctx'].cumsum()
total_turnos_ctx = df_ctx['inicio_conv_ctx'].sum()
msgs_por_turno_ctx = len(df_ctx) / total_turnos_ctx if total_turnos_ctx > 0 else 0
turnos_tamanho_ctx = df_ctx.groupby('turno_id_ctx').size()
print(f"{ctx}:")
print(f" Total turnos: {total_turnos_ctx:,}")
print(f" Média msgs/turno: {msgs_por_turno_ctx:.1f}")
print(f" Mediana msgs/turno: {turnos_tamanho_ctx.median():.0f}")
print(f" Máximo msgs/turno: {turnos_tamanho_ctx.max()}")
print(f" 90% até: {turnos_tamanho_ctx.quantile(0.90):.0f} msgs")
print()
# ============================================================================
# 4. DURAÇÃO DOS TURNOS
# ============================================================================
print("⏱️ Duração dos turnos (tempo entre início e última msg):\n")
# Calcula duração de cada turno
duracao_turnos = df_rel.groupby('turno_id').agg({
'timestamp': ['min', 'max'],
'turno_id': 'size'
}).reset_index(drop=True)
duracao_turnos.columns = ['inicio', 'fim', 'num_msgs']
duracao_turnos['duracao_segundos'] = (
duracao_turnos['fim'] - duracao_turnos['inicio']
).dt.total_seconds()
duracao_turnos['duracao_minutos'] = duracao_turnos['duracao_segundos'] / 60
print(duracao_turnos['duracao_minutos'].describe())
print(f"\n⏱️ Percentis de duração:")
print(f" 50%: {duracao_turnos['duracao_minutos'].quantile(0.50):.1f} min")
print(f" 75%: {duracao_turnos['duracao_minutos'].quantile(0.75):.1f} min")
print(f" 90%: {duracao_turnos['duracao_minutos'].quantile(0.90):.1f} min")
print(f" 95%: {duracao_turnos['duracao_minutos'].quantile(0.95):.1f} min")
# ============================================================================
# 5. VISUALIZAÇÃO - Distribuição de tamanho
# ============================================================================
# Limita para visualização (até 100 msgs)
turnos_plot = turnos_tamanho[turnos_tamanho <= 100]
fig = px.histogram(
x=turnos_plot,
nbins=50,
title='Distribuição do Tamanho dos Turnos (até 100 msgs)',
labels={'x': 'Mensagens por Turno', 'y': 'Frequência'}
)
fig.update_layout(height=400, showlegend=False)
fig.show()
# ============================================================================
# 6. VISUALIZAÇÃO - Boxplot por Contexto
# ============================================================================
# Prepara dados
dados_ctx = []
for ctx in ['Separados', 'Juntos']:
df_ctx = df_rel[df_rel['contexto'] == ctx].copy()
df_ctx['inicio_conv_ctx'] = (
(df_ctx['tempo_desde_ultima'] > THRESHOLD_INICIO) |
(df_ctx['tempo_desde_ultima'].isna())
)
df_ctx['turno_id_ctx'] = df_ctx['inicio_conv_ctx'].cumsum()
turnos_ctx = df_ctx.groupby('turno_id_ctx').size().reset_index(name='tamanho')
turnos_ctx['Contexto'] = ctx
dados_ctx.append(turnos_ctx)
df_turnos_ctx = pd.concat(dados_ctx, ignore_index=True)
# Filtra até 100 para visualização
df_turnos_plot = df_turnos_ctx[df_turnos_ctx['tamanho'] <= 100]
fig = px.box(
df_turnos_plot,
x='Contexto',
y='tamanho',
color='Contexto',
title='Tamanho dos Turnos: Junto vs Separado (até 100 msgs)',
labels={'tamanho': 'Mensagens por Turno', 'Contexto': ''},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']}
)
fig.update_layout(showlegend=False, height=400)
fig.show()
# ============================================================================
# 7. FAIXAS DE TAMANHO × CONTEXTO
# ============================================================================
def classificar_turno(tamanho):
if tamanho <= 5:
return 'Muito curto (1-5)'
elif tamanho <= 20:
return 'Curto (6-20)'
elif tamanho <= 50:
return 'Médio (21-50)'
elif tamanho <= 100:
return 'Longo (51-100)'
else:
return 'Muito longo (>100)'
df_turnos_ctx['faixa'] = df_turnos_ctx['tamanho'].apply(classificar_turno)
# Calcula proporções
dados_faixas = []
for ctx in ['Separados', 'Juntos']:
df_ctx = df_turnos_ctx[df_turnos_ctx['Contexto'] == ctx]
faixas = df_ctx['faixa'].value_counts()
for faixa, count in faixas.items():
dados_faixas.append({
'Contexto': ctx,
'Faixa': faixa,
'Percentual': (count / len(df_ctx)) * 100
})
df_faixas = pd.DataFrame(dados_faixas)
# Ordena
ordem_faixas = ['Muito curto (1-5)', 'Curto (6-20)', 'Médio (21-50)',
'Longo (51-100)', 'Muito longo (>100)']
df_faixas['Faixa'] = pd.Categorical(df_faixas['Faixa'], categories=ordem_faixas, ordered=True)
df_faixas = df_faixas.sort_values('Faixa')
# Gráfico
fig = px.bar(
df_faixas,
x='Faixa',
y='Percentual',
color='Contexto',
barmode='group',
title='Distribuição de Turnos por Tamanho: Junto vs Separado',
labels={'Percentual': '%', 'Faixa': 'Tamanho do Turno'},
color_discrete_map={'Separados': COLORS['separados'], 'Juntos': COLORS['juntos']},
text='Percentual'
)
fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
fig.update_xaxes(tickangle=-45)
fig.update_layout(height=450)
fig.show()
# Tabela
print("\n📊 Distribuição por faixa e contexto:\n")
pivot = df_faixas.pivot(index='Faixa', columns='Contexto', values='Percentual').round(1)
print(pivot)
print("="*80)
```
::: {.callout-note}
### 💡 Insight - Turnos de Conversa
**Definição:** Turno = sessão de conversa delimitada por gaps >1h
**Distribuição ASSIMÉTRICA:**
- **Média:** 79.3 msgs/turno (alta!)
- **Mediana:** 15 msgs/turno (baixa!)
- **Diferença brutal** = poucos turnos MUITO longos puxam média para cima
**Estatísticas gerais:**
- **Total:** 1.126 turnos em 309 dias = **3.6 turnos/dia**
- **50%:** até 15 msgs
- **75%:** até 94 msgs
- **90%:** até 242 msgs
- **Máximo:** 1.290 msgs em um único turno! 🤯
**Duração:**
- **Mediana:** 42.5 minutos (~45min de conversa)
- **Média:** 113 minutos (~2h)
- **90%:** até 348 minutos (~6h)
- **Máximo:** 1.078 minutos (~18h!) - conversaram o dia TODO
**Conclusão:** Turnos são majoritariamente MÉDIOS (15 msgs, 45min), mas alguns são MARATONAS (>100 msgs, >6h).
***
**POR CONTEXTO - Diferença DRAMÁTICA:**
| Métrica | Separados | Juntos | Diferença |
|---------|-----------|--------|-----------|
| **Total turnos** | 822 | 304 | 2.7x mais separados |
| **Média msgs/turno** | 92.5 | 43.7 | **2.1x maior!** 🔥 |
| **Mediana** | 16 | 11 | 1.5x maior |
| **90%** | 296 msgs | 113 msgs | **2.6x maior!** |
| **Máximo** | 1.290 msgs | 844 msgs | Separados tem o recorde |
**Distribuição por faixa - Padrão INVERTIDO:**
| Faixa | Separados | Juntos | Interpretação |
|-------|-----------|--------|---------------|
| **Muito curto (1-5)** | 31.8% | 42.1% | Juntos: +10.3pp (MUITO mais pontual) |
| **Curto (6-20)** | 19.8% | 15.1% | Similar |
| **Médio (21-50)** | 11.7% | 15.8% | Juntos um pouco mais |
| **Longo (51-100)** | 9.0% | 14.1% | Juntos +5.1pp |
| **Muito longo (>100)** | 27.7% | 12.8% | Separados: **+14.9pp** (MUITO mais!) 🔥 |
**Padrão claro:**
**Quando SEPARADOS:**
- **27.7%** dos turnos são MARATONAS (>100 msgs)
- Média alta (92.5 msgs)
- 90% até 296 msgs
- **Interpretação:** Conversas LONGAS e DESENVOLVIDAS
- Padrão: Começam conversa → contam o dia → desenvolvem assuntos → conversam por HORAS
**Quando JUNTOS:**
- **42.1%** dos turnos são MUITO CURTOS (1-5 msgs)
- Apenas **12.8%** são maratonas (>100)
- Média baixa (43.7 msgs)
- 90% até 113 msgs
- **Interpretação:** Comunicação PONTUAL e OBJETIVA
- Padrão: Mensagem rápida → resolve → fecha → volta ao convívio
**Diferença de frequência:**
- Separados: 822 turnos em ~245 dias = **3.4 turnos/dia**
- Juntos: 304 turnos em ~122 dias = **2.5 turnos/dia**
- Separados iniciam conversas 36% MAIS frequentemente
***
**Integração com análises anteriores - TUDO se encaixa:**
**1. Sequências de mensagens:**
- ✅ Separados: 14.5% sequências 6-10 msgs (conversas desenvolvidas)
- ✅ Juntos: 38.7% mensagens isoladas (coordenação pontual)
- ✅ Turnos refletem: Separados sustentam conversas mais longas
**2. Velocidade de resposta:**
- ✅ Ambos rápidos (~80% em <60s)
- ✅ Mas separados SUSTENTAM por HORAS (turnos de 6h+)
- ✅ Juntos respondem rápido mas FECHAM rápido
**3. Tempo entre mensagens:**
- ✅ Juntos: Padrão bimodal (rápido OU gap)
- ✅ Separados: Mais constante
- ✅ Turnos explicam: Juntos têm gaps ENTRE turnos (convívio), separados DENTRO de turnos (conversando)
**4. Tipos de mensagem:**
- ✅ Separados: Mais AUDIO/VID (conversas ricas, desenvolvidas)
- ✅ Juntos: Mais TEXT (coordenação rápida)
- ✅ Turnos: Separados têm espaço para mídia rica em conversas longas
**5. Volume:**
- ✅ Separados: 290 msgs/dia em 3.4 turnos = 85 msgs/turno (bate!)
- ✅ Juntos: 118 msgs/dia em 2.5 turnos = 47 msgs/turno (bate!)
***
**SÍNTESE - Dois protocolos COMPLETAMENTE diferentes:**
**Protocolo SEPARADOS (Modo Conexão Emocional):**
- 📞 **3.4 turnos/dia** (iniciam conversas frequentemente)
- 💬 **92.5 msgs/turno** (conversas LONGAS)
- ⏱️ **Duração:** Maioria >1h, alguns >6h
- 🎯 **Objetivo:** Conexão emocional, contar o dia, desenvolver assuntos
- 📊 **Padrão:** Conversas sustentadas, ricas em mídia, desenvolvidas
- 🔥 **27.7% são MARATONAS** (>100 msgs) - literalmente conversam por HORAS
**Protocolo JUNTOS (Modo Coordenação):**
- 📱 **2.5 turnos/dia** (iniciam menos conversas)
- 💬 **43.7 msgs/turno** (conversas CURTAS)
- ⏱️ **Duração:** Maioria <1h
- 🎯 **Objetivo:** Coordenação prática, resolver rapidamente
- 📊 **Padrão:** Pontual, objetivo, fecha rápido
- 🔥 **42.1% são MUITO CURTOS** (1-5 msgs) - resolvem e voltam ao convívio
**Validação da hipótese central:**
> Relacionamento opera em **DOIS MODOS DISTINTOS**:
> - **Separados:** WhatsApp = PRINCIPAL canal → conversas longas, frequentes, ricas
> - **Juntos:** WhatsApp = SECUNDÁRIO → coordenação rápida, intercalada com presença física
**Implicação para FE:**
- 🥇 Feature `turno_tamanho` × `em_encontro` é **CRÍTICA**
- ✅ Criar faixas: `micro` (1-5), `curto` (6-20), `medio` (21-50), `longo` (51-100), `maratona` (>100)
- ✅ Feature `duracao_turno` também útil
- ✅ Feature `turnos_por_dia` pode capturar intensidade
- ⚠️ Turno sem contexto tem distribuição bimodal (dificulta modelagem)
- 🎯 Interação `tamanho × contexto` explica maior variância que tamanho sozinho
:::
***
# Efeito da Distância Temporal
## Dias Desde Último Encontro × Volume
```{python}
#| label: fig-dias-desde-volume
#| fig-cap: "Volume de mensagens vs dias desde último encontro"
# Filtra mensagens FORA de encontros com distância conhecida
df_distancia = df[
(~df['em_encontro']) &
(df['dias_desde_ultimo_encontro'].notna())
].copy()
if len(df_distancia) > 0:
# Agrupa por dias desde
volume_por_distancia = df_distancia.groupby('dias_desde_ultimo_encontro').size().reset_index(name='count')
# Visualização
fig = px.scatter(
volume_por_distancia,
x='dias_desde_ultimo_encontro',
y='count',
trendline='lowess',
title='Volume de Mensagens vs Dias Desde Último Encontro',
labels={
'dias_desde_ultimo_encontro': 'Dias Desde Último Encontro',
'count': 'Quantidade de Mensagens'
}
)
fig.update_traces(marker=dict(size=8, opacity=0.6))
fig.update_layout(height=400)
fig.show()
# Estatísticas
print(f"\n📊 Análise de Saudade (Dias Desde Último Encontro):\n")
print(f" • Mensagens analisadas: {len(df_distancia):,}")
print(f" • Distância mínima: {df_distancia['dias_desde_ultimo_encontro'].min():.0f} dias")
print(f" • Distância máxima: {df_distancia['dias_desde_ultimo_encontro'].max():.0f} dias")
print(f" • Distância média: {df_distancia['dias_desde_ultimo_encontro'].mean():.1f} dias")
# Correlação
corr = df_distancia.groupby('dias_desde_ultimo_encontro')['timestamp'].count().corr(
df_distancia.groupby('dias_desde_ultimo_encontro')['dias_desde_ultimo_encontro'].first()
)
print(f"\n • Correlação dias vs volume: {corr:.3f}")
if corr > 0.3:
print(f" ✅ Correlação POSITIVA moderada - volume aumenta com distância")
elif corr < -0.3:
print(f" ✅ Correlação NEGATIVA moderada - volume diminui com distância")
else:
print(f" ⚠️ Correlação FRACA - volume não varia sistematicamente com distância")
```
## Dias Até Próximo Encontro × Volume
```{python}
#| label: fig-dias-ate-volume
#| fig-cap: "Volume de mensagens vs dias até próximo encontro"
# Filtra mensagens FORA de encontros com distância conhecida
df_antecipacao = df[
(~df['em_encontro']) &
(df['dias_ate_proximo_encontro'].notna())
].copy()
if len(df_antecipacao) > 0:
# Agrupa por dias até
volume_por_antecipacao = df_antecipacao.groupby('dias_ate_proximo_encontro').size().reset_index(name='count')
# Visualização
fig = px.scatter(
volume_por_antecipacao,
x='dias_ate_proximo_encontro',
y='count',
trendline='lowess',
title='Volume de Mensagens vs Dias Até Próximo Encontro',
labels={
'dias_ate_proximo_encontro': 'Dias Até Próximo Encontro',
'count': 'Quantidade de Mensagens'
}
)
fig.update_traces(marker=dict(size=8, opacity=0.6))
fig.update_layout(height=400)
fig.show()
# Estatísticas
print(f"\n📊 Análise de Antecipação (Dias Até Próximo Encontro):\n")
print(f" • Mensagens analisadas: {len(df_antecipacao):,}")
print(f" • Distância mínima: {df_antecipacao['dias_ate_proximo_encontro'].min():.0f} dias")
print(f" • Distância máxima: {df_antecipacao['dias_ate_proximo_encontro'].max():.0f} dias")
print(f" • Distância média: {df_antecipacao['dias_ate_proximo_encontro'].mean():.1f} dias")
# Correlação
corr = df_antecipacao.groupby('dias_ate_proximo_encontro')['timestamp'].count().corr(
df_antecipacao.groupby('dias_ate_proximo_encontro')['dias_ate_proximo_encontro'].first()
)
print(f"\n • Correlação dias vs volume: {corr:.3f}")
if corr < -0.3:
print(f" ✅ Correlação NEGATIVA moderada - volume aumenta próximo ao encontro")
elif corr > 0.3:
print(f" ⚠️ Correlação POSITIVA - volume diminui próximo ao encontro")
else:
print(f" ⚠️ Correlação FRACA - volume não varia sistematicamente")
```
::: {.callout-warning}
## 💡 Insight - Distância Temporal e Volume
**Achado contra-intuitivo:**
**1. Dias DESDE último encontro (correlação: -0.84):**
- **Logo após separação (0-10 dias):** Volume MUITO ALTO (~2.500-3.000 msgs)
- **Meio período (10-30 dias):** Volume CAI gradualmente (~1.500-2.000 msgs)
- **Distante do encontro (30+ dias):** Volume ESTABILIZA BAIXO (~500-1.000 msgs)
**Interpretação:**
- **Saudade é mais intensa logo após separação** (efeito recente forte)
- Com o tempo, **adaptam-se à distância** (normalização)
- Não é "quanto mais tempo separados, mais saudade" - é o oposto!
***
**2. Dias ATÉ próximo encontro (correlação: -0.91):**
- **Longe do encontro (40-60 dias):** Volume ALTO (~2.000-3.000 msgs)
- **Meio período (20-40 dias):** Volume CAI gradualmente (~1.000-2.000 msgs)
- **Próximo ao encontro (0-10 dias):** Volume BAIXO (~200-500 msgs)
**Interpretação:**
- **Antecipação NÃO aumenta volume** - ao contrário!
- Próximo ao encontro: **Ocupados se preparando** (viagem, logística, trabalho)
- Volume cai porque sabem que logo estarão juntos (menos urgência)
- Última semana: Comunicação mínima (expectativa de presença física iminente)
***
**Padrão identificado - "Ciclo de Separação":**
```
Separação → PICO de saudade (0-7 dias) → Normalização (8-30 dias) →
Estabilização (30+ dias) → Preparação/Antecipação (últimos 7 dias) → Encontro
```
**Fases do volume:**
1. **Pós-separação:** Muito alto (compensa ausência recente)
2. **Normalização:** Médio-alto (adaptados à distância)
3. **Pré-encontro:** Baixo (preparação, menos urgência)
***
**Correlações fortíssimas:**
- `dias_desde_ultimo_encontro`: **-0.84** (muito forte)
- `dias_ate_proximo_encontro`: **-0.91** (fortíssima!)
- Ambas NEGATIVAS = volume diminui com tempo em AMBAS as direções
**Conclusão:**
- Volume é MAIOR quando estão **no meio** do período de separação
- Volume é MENOR nos **extremos** (logo antes e logo depois de encontros)
- Hipótese "saudade cresce com tempo" é **FALSA**
- Hipótese "ansiedade cresce próximo ao encontro" é **FALSA**
**Modelo mental correto:**
- **Separação recente:** Saudade aguda → volume alto
- **Adaptação:** Normalizam comunicação → volume estável
- **Próximo ao encontro:** Preparação/expectativa → volume baixo
- **Durante encontro:** Presença física → volume muito baixo (já sabíamos)
***
**Implicação para FE:**
- ✅ Feature `dias_desde_ultimo_encontro` é **ALTAMENTE PREDITIVA** (r=-0.84)
- ✅ Feature `dias_ate_proximo_encontro` é **ALTAMENTE PREDITIVA** (r=-0.91)
- ✅ Criar features derivadas:
- `fase_separacao` (recente / meio / pré-encontro)
- `dias_min_encontro` = min(desde, até) - captura proximidade a qualquer encontro
- 🎯 Distância temporal é um dos **preditores mais fortes** identificados
- 🎯 Relacionamento tem "curva de adaptação" clara e previsível
:::
***
# Síntese Final para Feature Engineering
::: {.callout-note}
## 📊 Resumo Executivo - EDA com Contexto Integrado
**Contexto externo domina TODOS os padrões:**
- 68.3% de redução no volume quando juntos (p < 0.001)
- 92.9% dos gaps coincidem com encontros
- Padrões temporais mudam RADICALMENTE por contexto
- P1 e P2 mantêm sincronização perfeita em TODOS os contextos
***
### **Padrões Identificados**
**Padrões horários:**
- Separados: Unimodal noturno (pico 19h = 10.5%)
- Juntos: Bimodal diurno (picos 12h = 11% + 18h = 7.5%)
- P1 e P2 com curvas idênticas em ambos os contextos
- **Implicação:** Feature `hora × em_encontro` **ESSENCIAL**
**Padrões semanais:**
- Separados: Uniforme (12-16%, sem padrão útil/FDS)
- Juntos: Concentrado em dias úteis (segunda 26.8%, sábado 3.3%, domingo 7.4%)
- Inversão completa: segunda é MENOR quando separados (11.8%) e MAIOR quando juntos
- **Implicação:** Feature `dia_semana × em_encontro` **MUITO ÚTIL**
**Heatmap Hora × Dia:**
- Separados: Hora e Dia são INDEPENDENTES (faixa 18h-21h universal)
- Juntos: Hora e Dia INTERAGEM (segunda intensa, FDS vazio)
- **Implicação:** Interação tripla `hora × dia_semana × em_encontro` pode ser útil
**Tipos de mensagem:**
- Separados: TEXT 71.6% | VÍDEO 11.9% | ÁUDIO 9.7% | IMG 3.7%
- Juntos: TEXT 82.4% (+10.8pp) | VÍDEO 2.0% (-9.9pp) | ÁUDIO 7.2% (-2.5pp) | IMG 4.9% (+1.2pp)
- VÍDEO cai 83% quando juntos (11.9% → 2.0%) - mídia "cara" evitada
- P1 muda mais que P2: P1 usa 58% mais áudio quando separado, P2 mantém áudio estável
- **Implicação:** Feature `grupo_mensagem × em_encontro` **ÚTIL**
**Distância temporal (ACHADO CRÍTICO):**
- `dias_desde_ultimo_encontro`: Correlação **-0.84** (fortíssima!)
- Volume ALTO logo após separação (0-10 dias: ~2.500-3.000 msgs)
- Volume CAI com tempo (30+ dias: ~500-1.000 msgs)
- Padrão: Saudade aguda → Adaptação → Normalização
- `dias_ate_proximo_encontro`: Correlação **-0.91** (fortíssima!)
- Volume ALTO longe do encontro (40-60 dias: ~2.000-3.000 msgs)
- Volume BAIXO próximo ao encontro (0-10 dias: ~200-500 msgs)
- Padrão: Preparação/expectativa → Menos urgência
- **Implicação:** Features `dias_desde_ultimo_encontro` e `dias_ate_proximo_encontro` **ALTAMENTE PREDITIVAS**
**Marcos especiais:**
- Apenas 3 datas (N muito pequeno, padrões heterogêneos)
- Função: Contexto narrativo/storytelling, não preditivo
- **Implicação:** Features `eh_marco_especial` e `marco_nome` **NÃO ÚTEIS** para FE
**Remetentes:**
- P1 (51.1%) vs P2 (48.9%) - distribuição equilibrada
- Proporção juntos/separados idêntica (P1: 14.9%/85.1% | P2: 14.3%/85.7%)
- Padrões temporais sincronizados em TODOS os contextos
- **Implicação:** Features de interação com `remetente` são **DESNECESSÁRIAS**
**Evolução temporal (ACHADO ADICIONAL):**
- Preferências de mídia mudam DRASTICAMENTE ao longo do ano:
- ÁUDIO: 21.7% → 6.2% (queda de 68%)
- TEXTO: 66.7% → 78.9% (crescimento de 12pp)
- VID: 0% → pico 18.4% (Jan) → 5.6% (ciclo de teste/abandono)
- Mudança é TEMPORAL PURA (validado: r=-0.02 com distância de encontros)
- Acontece MESMO quando separados (queda de 68% em áudio separado)
- Sincronizada entre P1 e P2 (evoluem juntos)
- **Implicação:** Features `mes` ou `trimestre` são **ESSENCIAIS** - preferências NÃO são estáticas
**4 Fases Temporais Identificadas:**
1. **Fase 1 - Descoberta** (Out-Dez/2024): Alto áudio (15.7%), baixo VID (5.4%)
2. **Fase 2 - Experimentação** (Jan-Mai/2025): Áudio cai (5.9%), VID explode (14.8%)
3. **Fase 3 - Transição** (Jun-Jul/2025): Equilíbrio, áudio ressurge (10.2%)
4. **Fase 4 - Consolidação** (Ago-Out/2025): Texto domina (76.4%), VID abandonado (4.4%)
***
### **Features ESSENCIAIS para FE**
**Contexto externo (CRÍTICAS):**
- ✅ `em_encontro` (booleana) - **MODERADOR PRIMÁRIO** de todos os padrões
- ✅ `encontro_id` (categórica) - identifica qual encontro
- ✅ `dias_desde_ultimo_encontro` (numérica) - **r=-0.84**
- ✅ `dias_ate_proximo_encontro` (numérica) - **r=-0.91**
- ✅ `fase_separacao` (categórica): recente/meio/pré-encontro
**Interações com contexto (ESSENCIAIS):**
- ✅ `hora × em_encontro` - padrões completamente diferentes (unimodal vs bimodal)
- ✅ `periodo_dia × em_encontro` - essencial (noite vs dia)
- ✅ `dia_semana × em_encontro` - inversão de padrão (segunda/FDS)
- ✅ `grupo_mensagem × em_encontro` - preferências mudam (vídeo cai 83%)
- ✅ `eh_segunda × em_encontro` - específica útil (26.8% quando juntos)
- ✅ `eh_fds × em_encontro` - comportamento oposto
**Features standalone (poder moderado SEM contexto):**
- ✅ `hora` (0-23) - forte quando separados
- ✅ `periodo_dia` (Madrugada/Manhã/Tarde/Noite) - captura ciclo diário
- ✅ `mes` / `trimestre` - captura evolução temporal (4 fases)
- ✅ `grupo_mensagem` - preferências individuais (P1 usa mais áudio)
- ⚠️ `dia_semana` - poder limitado sem contexto (p=0.96 para FDS)
**Features temporais (EVOLUÇÃO):**
- ✅ `mes` ou `trimestre` - **ESSENCIAIS** (preferências mudam 68%)
- ✅ `fase_temporal` (1-4) - captura ciclo de maturação
- ✅ `era_video` (booleana): Jan-Mai/2025 (pico de experimentação)
- ⚠️ Interação `mes × grupo_mensagem` - VID em Jan ≠ VID em Out
**Features a EVITAR:**
- ❌ `fim_de_semana` - zero poder discriminatório (p=0.96)
- ❌ `hora × remetente` - padrões sincronizados (curvas idênticas)
- ❌ `dia_semana × remetente` - padrões idênticos
- ❌ `grupo_mensagem × remetente × em_encontro` - sincronização se mantém
- ❌ `eh_marco_especial` / `marco_nome` - amostra muito pequena (N=3)
***
### **Conclusão Crítica**
**O relacionamento opera em DOIS MODOS radicalmente diferentes:**
**1. Modo DISTÂNCIA (66.8% do tempo | 245 dias):**
- Volume: **372 msgs/dia** (alto)
- Função: **Conexão emocional constante**
- Padrão horário: Unimodal noturno (pico 19h = conversa pós-trabalho)
- Padrão semanal: Uniforme (comunicação todos os dias)
- Tipos: Mídias ricas (11.9% vídeo, 9.7% áudio) - compensam distância
- Curva de separação: Pico pós-separação → Normalização → Estabilização
**2. Modo PRESENÇA (33.2% do tempo | 122 dias):**
- Volume: **118 msgs/dia** (baixo - redução de 68.3%)
- Função: **Coordenação/trabalho utilitário**
- Padrão horário: Bimodal diurno (picos 12h + 18h = pausas do trabalho)
- Padrão semanal: Concentrado dias úteis (segunda 26.8%, FDS 3-7%)
- Tipos: Texto domina (82.4%) - comunicação rápida/prática
- Preparação: Volume cai próximo ao encontro (expectativa presença física)
**Mudança estrutural:**
- Não é só VOLUME que muda - é o **PROPÓSITO** da comunicação
- Separados: WhatsApp = Canal principal (emocional)
- Juntos: WhatsApp = Canal secundário (logístico)
- Relacionamento à distância com encontros regulares tem dinâmica PRÓPRIA
***
### **Implicação para Modelagem**
**Poder preditivo hierárquico:**
1. 🥇 **Contexto externo:** `em_encontro` + `dias_desde/ate` (correlações -0.84/-0.91)
2. 🥈 **Evolução temporal:** `mes`/`trimestre` (mudança de 68% em preferências)
3. 🥉 **Interações temporais:** `hora × contexto`, `dia × contexto`
4. 🏅 **Tipos de conteúdo:** `grupo_mensagem × contexto`
5. ⚠️ **Features puras:** Poder limitado sem contexto/tempo
**Qualquer modelo preditivo SEM `em_encontro` terá:**
- ❌ Poder SEVERAMENTE limitado
- ❌ Confundirá dois modos de operação completamente distintos
- ❌ Não capturará 68% da variância do volume
**Features de distância temporal são CRÍTICAS:**
- Capturam "ciclo de separação" (saudade → adaptação → preparação)
- Correlações fortíssimas (-0.84 e -0.91)
- Podem ser os preditores mais poderosos do dataset
***
**Próximo passo:** Feature Engineering (04-feature-engineering.qmd) integrando features de contexto externo
:::
---------------------------
# Refatoração:
# Código Completo - Evolução Temporal (REFATORADO - Só Relacionamento)