Em uma frase: o modelo errou a margem por 1,2 ponto, contra 4,6 das pesquisas que ele agregava, e o resultado caiu dentro do intervalo central; a parte que mais ajudou foi o desconto do erro histórico, e a que mais atrapalhou foi a correção de casa ancorada nos institutos tradicionais.
previsoes_1turno_congeladas.json e não foi mais mexida. Abaixo, cada número dela contra a totalização do TSE: a barra é a faixa em que o modelo punha 80% de chance, o traço é a mediana prevista e o ponto é o resultado (escuro se caiu dentro da faixa, terracota se caiu fora).| Pergunta da véspera | O que o modelo dizia | O que aconteceu | Prob. do que aconteceu | Brier |
|---|---|---|---|---|
| Quem termina em 1º? | Flávio, com 59% | Flávio, por 1,87 pontos | 59% | 0,171 |
| Haverá 2º turno? | sim, com 98,7% | sim | 98,7% | 0,000 |
| Flávio vai ao 2º turno? | sim, com 100% | sim | 100% | 0,000 |
| Quem fica em 3º? | Cury, com 49% (o mais provável) | Augusto Cury, com 2,89% | 49% | 0,263 |
| Candidato | Apurado | Modelo (mediana) | Erro do modelo | Pesquisas (média sem desconto) | Erro das pesquisas | Quem chegou mais perto |
|---|---|---|---|---|---|---|
| Lula (PT) | 45,16 | 45,0 | −0,2 | 45,5 | +0,3 | modelo |
| Flávio Bolsonaro (PL) | 47,03 | 45,7 | −1,3 | 42,8 | −4,2 | modelo |
| Augusto Cury (Avante) | 2,89 | 2,8 | −0,1 | 3,8 | +0,9 | modelo |
| Renan Santos (Missão) | 2,24 | 2,4 | +0,2 | 3,2 | +1,0 | modelo |
| Ronaldo Caiado (PSD) | 2,18 | 2,3 | +0,1 | 3,0 | +0,8 | modelo |
| Romeu Zema (Novo) | 0,27 | 0,4 | +0,1 | 0,7 | +0,4 | modelo |
| Outros (6 candidatos) | 0,22 | 0,7 | +0,5 | 1,1 | +0,9 | modelo |
| Erro médio absoluto | 0,35 | 1,22 | modelo |
Erro = previsto menos apurado, em pontos dos votos válidos. Brier = (1 − probabilidade dada ao que aconteceu)²; 0 é acerto com certeza total e 0,25 é o que se ganha chutando 50% numa pergunta de sim ou não; na pergunta do 3º lugar, com quatro nomes, o chute uniforme (25% para cada) daria 0,563. Uma eleição só não basta para dizer se a incerteza do modelo está calibrada: os números acima dizem se ele foi útil desta vez, não se acertará da próxima.
| Previsão | Margem prevista | Erro | P(Lula em 1º) | Brier | Log-perda |
|---|---|---|---|---|---|
| Pesquisas sem desconto (agregado do modelo) | +2,7 | +4,6 | 91% | 0,834 | 2,44 |
| Média simples das últimas pesquisas | +1,6 | +3,5 | — | — | — |
| Modelo publicado (desconto +3,4, 60% do histórico) | −0,7 | +1,2 | 41% | 0,171 | 0,53 |
| Desconto histórico cheio (+5,7) | −3,0 | −1,1 | 22% | 0,048 | 0,25 |
Brier = (probabilidade dada a Lula liderar − 0)²; log-perda = −ln(probabilidade dada ao desfecho real). Menor é melhor nos dois. Margem real: −1,87.
| Componente | Avaliação | Evidência |
|---|---|---|
| Desconto do erro histórico do 1º turno | funcionou | Moveu a margem de +2,7 para −0,7, na direção certa, e eliminou 74% do erro das pesquisas. |
| Tamanho do desconto (60% da média histórica) | conservador | O erro real da âncora (+4,7) ficou entre o desconto usado (+3,4) e o histórico cheio (+5,7). Com o histórico cheio, a mediana seria −3,0 e a pontuação de Brier, menor. |
| Largura da incerteza | adequada | O resultado caiu no percentil 37, dentro do intervalo de 50%; 6 dos 7 candidatos ficaram dentro da faixa de 80%. |
| Efeitos de casa ancorados nos institutos tradicionais | falhou | A correção afastou 11 das 14 últimas pesquisas do resultado: ela penalizava quem acertou (Palver, Gerp, Futura) por divergir da âncora, e a âncora errou em bloco. O desconto de viés compensou o nível, mas a informação desses institutos foi descartada. |
| Nível de Lula | acertou | Mediana 45,0 contra 45,16 apurado. |
| Nível de Flávio e colapso da terceira via | subestimou | Mediana 45,7 contra 47,03: o colapso sorteado só redistribuía votos sem deslocar a margem, e o colapso real foi maior e todo para Flávio. |
| 3º lugar e decisão no 1º turno | acertou | Cury em 3º era o desfecho mais provável (49%); a chance de decisão no 1º turno era de 1,3%. |
| Leitura estadual | falhou no Sudeste | As estaduais previam ganho de Lula no Sudeste e houve perda de 4,3 pontos; a camada pesava 6% no nível e saiu do modelo no regime pós-1º turno. |
| Validação de origem móvel | não detectava o problema | Cobertura de 85% no intervalo de 80%, mas medida contra a própria média das pesquisas: um erro comum a todos os institutos é invisível a ela por construção. |
| Regime pós-1º turno (2º turno) | falha corrigida hoje | Com o resultado, sem nenhuma pesquisa nova, o desconto das pesquisas de 2º turno caía de +3,4 para +2,0 e a chance de Lula subia de 28% para 37%. As ondas de antes de 4/out passaram a receber o erro observado (+4,7); com isso a chance de Lula é de 17%, e pesquisas e voto contado convergem. |
| Rodada | Versão | Lula em 1º | Flávio em 1º |
|---|---|---|---|
| 30/set | v33 | 58% | 42% |
| 1º/out, tarde | v36 | 50% | 50% |
| 1º/out, noite | v38 | 52% | 48% |
| 2/out | v39 | 46% | 54% |
| 3/out, manhã | v40 | 44% | 56% |
| 3/out, tarde (auditoria) | v41 | 56% | 44% |
| 3/out, tarde (MDA, Palver) | v42 | 38% | 62% |
| 3/out, noite (2ª auditoria) | v43 | 45% | 55% |
| 3/out, noite · congelada | v44 | 41% | 59% |
A chance oscilou entre 38% e 58% para Lula com as pesquisas e as correções de método da última semana, sem nunca dar a Flávio menos de 42%. O ruído entre rodadas foi do mesmo tamanho que o efeito das auditorias: um sinal de que a previsão estava, corretamente, perto de 50%.
(1) As pesquisas de 2º turno feitas antes de 4/out recebem o erro observado da âncora no 1º turno (+4,7), e as feitas depois, o desconto das rodadas finais de 2º turno mais 25% desse erro; a mistura segue o peso que as pesquisas novas ganham no filtro. (2) O voto contado entra no placar com o erro total das pesquisas, não só o amostral, e hoje pesa 21%. (3) O erro de cada instituto no 1º turno, medido contra a âncora, vira prior do efeito de casa no 2º turno. (4) O mercado volta a pesar 25%, com preço posterior ao resultado. Pendente: rever a âncora e o encolhimento do desconto com a série de 1º turno atualizada (2010–2026: +5,2; +10,5; +2,4; +5,8; +4,7).
Em uma frase: o modelo simula a eleição 400 mil vezes a partir da tendência de hoje, somando a incerteza do que ainda pode mudar até 25/out (deriva) e o erro histórico das pesquisas.
| Quantidade | Principal (Monte Carlo) | MCMC (posterior completo) |
|---|---|---|
| Chance de Lula · só pesquisas | 17% | 23% |
| Chance de Lula · ensemble | 18% | 23% |
| Margem mediana em 25/out | -2,0 | -1,4 |
| Faixa de 80% da margem final | -4,8 a +0,7 | -3,7 a +1,1 |
| Placar verdadeiro hoje (já sem o erro das pesquisas) | -2,0 ± 4,5 | -1,3 ± 3,8 |
| Deriva até 25/out (desvio) | 0,0 | 0,0 |
| σrw: mudança diária até o 1º turno (p.p.) | 0,31 (fixo) | 0,31 (80%: 0,22 a 0,43) |
| σentre: mudança diária entre os turnos (p.p.) | 0,95 (fixo) | 0,63 (80%: 0,39 a 0,95) |
| Erro comum das pesquisas β, além do desconto histórico | sorteado: t5(0; 2,5) | -0,6 (80%: -2,7 a 1,5) |
| Convergência | — | R̂ máx. 1,014 · ESS mín. 589 |
Diferença na chance de Lula só pelas pesquisas: +6,7 p.p. (MCMC menos principal). O placar verdadeiro de hoje difere em +0,7 p.p. nos dois modelos. Com pesquisas novas, as duas leituras tendem a se aproximar.
| Passo | Lula · só pesquisas | Flávio · só pesquisas | Efeito do passo |
|---|---|---|---|
| Modelo anterior (v2): âncora em todos os institutos, viés +2,1, deriva 4,3 | 21,6% | 78,4% | |
| + âncora nos institutos calibrados (identificação dos efeitos de casa) | 30,4% | 69,6% | +8,8 p.p. para Lula |
| + viés recalibrado (+0,9 ± 2,0) | 38,9% | 61,1% | +8,5 p.p. para Lula |
| + deriva em duas fases calibrada (0,0) | 24,8% | 75,2% | -14,1 p.p. para Lula |
| + P(Flávio no 2º turno) do modelo do 1º turno (1,00) | 15,7% | 84,3% | -9,1 p.p. para Lula |
| + viés das pesquisas de 2º turno anteriores ao 1º turno (+0,8 ± 1,98) | 16,5% | 83,5% | +0,8 p.p. para Lula |
| Placar ajustado das pesquisas na última semana (Lula − Flávio, p.p. válidos) | -2 | -1 | 0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Chance de Lula | 10% | 20% | 36% | 54% | 72% | 85% | 94% | 98% | 99% | 100% | 100% |
| Chance de Flávio | 90% | 80% | 64% | 46% | 28% | 15% | 6% | 2% | 1% | 0% | 0% |
Ponto de equilíbrio (50% para cada um): placar de +0,8 para Lula nas pesquisas finais, que é o erro histórico descontado. Lula chega a 70% com +1,9 e a 90% com +3,5; Flávio chega aos mesmos 70% e 90% com placares de -0,3 e -1,9 (simetria em torno do equilíbrio). Hoje o placar ajustado é -1,3.
Em uma frase: a última pesquisa de cada instituto, o peso que ela recebe no cálculo e o desvio típico (efeito de casa) que o painel corrige.
| Instituto | Contratante | Campo | n | Método | Margem de erro (95%) | Lula × Flávio | Margem válida | Efeito de casa | Margem ajustada | Variância extra e peso | MAE 2006–22 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Futura (escolaridade pela PNAD) · 21 ondas | Apex/100% Cidades | 24/10 | 2.000 | telefônico | ±2,2 p.p. | 47,9 × 48,2 | -0,3 | -0,4 | +0,1 | σp = 0,8 · peso 0,55 | 2,4 |
| PoderData · 17 ondas | Aya | 24/10 | 4.000 | telefônico | ±1,5 p.p. | 47,0 × 49,0 | -2,1 | -1,2 | -0,9 | σp = 0,8 · peso 1,00 · âncora | 4,5 |
| BTG/Nexus · 19 ondas | BTG Pactual | 24/10 | 2.000 | telefônico | ±2,2 p.p. | 46,0 × 47,0 | -1,1 | -0,6 | -0,5 | σp = 0,8 · peso 0,54 | — |
| Quaest · 19 ondas | Globo/O Globo | 24/10 | 3.702 | presencial | ±1,6 p.p. | 45,0 × 46,0 | -1,1 | -0,5 | -0,6 | σp = 0,8 · peso 0,90 · âncora | 4,0 |
| Datafolha · 18 ondas | Folha/Globo | 24/10 | 4.050 | presencial | ±1,5 p.p. | 47,0 × 48,0 | -1,1 | 0,0 | -1,1 | σp = 0,8 · peso 1,00 · âncora | 3,4 |
| Palver (calibração v3) · 9 ondas | próprio | 24/10 | 5.000 | online | ±2,3 p.p. | 49,0 × 50,0 | -1,0 | -0,3 | -0,7 | σp = 0,8 · peso 0,51 | — |
| Gerp · 20 ondas | próprio | 24/10 | 2.400 | telefônico | ±2,0 p.p. | 46,0 × 49,0 | -3,2 | -3,8 | +0,7 | σp = 0,8 · peso 0,64 | — |
| Instituto França · 3 ondas | próprio | 23/10 | 1.537 | não informado | ±2,5 p.p. | 43,1 × 45,6 | -2,8 | -0,2 | -2,6 | σp = 0,8 · peso 0,40 | — |
| Colectta · 1 onda | Brazil Economy | 23/10 | 3.000 | presencial | ±1,8 p.p. | 46,5 × 46,0 | +0,5 | +0,2 | +0,3 | σp = 0,8 · peso 0,76 | — |
| CNT/MDA (ponderada pelo 1º turno) · 7 ondas | CNT | 23/10 | 2.032 | presencial | ±2,2 p.p. | 45,0 × 46,3 | -1,4 | -0,3 | -1,1 | σp = 0,8 · peso 0,54 | 3,9 |
| Veritá · 9 ondas | próprio | 23/10 | 15.554 | telefônico | ±1,4 p.p. | 46,5 × 49,4 | -3,1 | -2,7 | -0,4 | σp = 1,3 · peso 0,83 | 3,8 |
| AtlasIntel · 17 ondas | Bloomberg | 23/10 | 4.990 | online | ±2,0 p.p. | 48,4 × 49,9 | -1,5 | -0,2 | -1,3 | σp = 0,8 · peso 0,69 · âncora | 4,5 |
| Ipespe · 3 ondas | Abrapel | 22/10 | 1.500 | telefônico | ±2,5 p.p. | 47,0 × 48,0 | -1,1 | -0,9 | -0,2 | σp = 0,8 · peso 0,42 · âncora | 4,9 |
| Alfa Inteligência · 6 ondas | próprio | 22/10 | 2.700 | presencial | ±1,9 p.p. | 46,0 × 49,9 | -4,1 | +0,1 | -4,2 | σp = 1,0 · peso 0,65 | — |
| Real Time Big Data · 9 ondas | próprio | 22/10 | 2.000 | telefônico | ±2,2 p.p. | 45,0 × 49,0 | -4,3 | -0,8 | -3,5 | σp = 0,8 · peso 0,54 | — |
| Vox Brasil · 11 ondas | próprio | 22/10 | 2.100 | presencial | ±2,1 p.p. | 45,4 × 44,3 | +1,2 | -0,7 | +1,9 | σp = 1,4 · peso 0,45 | — |
| Indexa · 8 ondas | Broadcast/Estadão | 21/10 | 2.000 | telefônico | ±2,2 p.p. | 44,1 × 45,2 | -1,2 | +0,5 | -1,8 | σp = 0,8 · peso 0,52 | — |
| DataTrends · 5 ondas | próprio | 21/10 | 2.000 | telefônico | ±2,2 p.p. | 46,0 × 50,0 | -4,2 | -0,9 | -3,2 | σp = 0,8 · peso 0,55 | — |
| Meio/Ideia · 11 ondas | Canal Meio | 19/10 | 1.500 | telefônico | ±2,5 p.p. | 46,0 × 48,0 | -2,1 | -0,4 | -1,7 | σp = 0,8 · peso 0,42 | — |
| American Analytics · 2 ondas | Times Brasil/CNBC | 20/09 | 2.000 | não informado | ±2,2 p.p. | 43,0 × 43,0 | 0,0 | 0,0 | 0,0 | σp = 0,8 · peso 0,50 | — |
| Vetor/Arrow · 2 ondas | próprio | 18/05 | 9.000 | não informado | ±1,4 p.p. | 50,7 × 38,3 | +13,9 | -0,4 | +14,3 | σp = 6,0 · peso 0,08 | — |
| Paraná Pesquisas · 4 ondas | próprio | 28/03 | 2.080 | presencial | ±2,1 p.p. | 44,1 × 45,2 | -1,2 | -2,0 | +0,8 | σp = 0,8 · peso 0,54 · âncora | 3,8 |
Em uma frase: um único modelo estatístico estima ao mesmo tempo a tendência, o desvio de cada instituto e quanto cada um erra além do declarado; a tendência é ancorada nos institutos com histórico verificado, e o erro histórico desse mesmo conjunto é descontado na simulação.
É o núcleo dos agregadores de referência (Jackman 2005; Linzer 2013; Heidemanns, Gelman e Morris para a Economist): o placar verdadeiro xt anda um pouco a cada dia e cada pesquisa é uma medição ruidosa dele, deslocada pelo efeito de casa hp do instituto. Os três blocos são estimados em conjunto, por iteração: suavizador de Kalman para xt, média posterior encolhida para hp e, para σp, a variância que sobra nos resíduos de cada instituto além do erro amostral, encolhida para a mediana. Institutos erráticos (Veritá, Vetor, Gerp, CNT/MDA) recebem σp alto e pesam menos, sem nenhum peso arbitrário. σrw = 0,31 p.p./dia na janela de campanha (desde 1/jun, 165 pesquisas), estimado com um prior centrado em 0,25 (sem o prior: 0,32).
Efeito de casa é um desvio em relação a alguma referência, e a escolha da referência define o nível de toda a curva. A versão anterior usava a média de todos os institutos, ponderada pelo número de ondas, o que deixava o "consenso" nas mãos de quem publica mais (Gerp, Futura, Palver e Veritá, os que mais destoam para Flávio). Agora a referência é a âncora: AtlasIntel, Datafolha, Ipespe, CNT/MDA, Paraná Pesquisas, PoderData, Quaest, os institutos com histórico presidencial verificado em 2014–2022, ponderados pela qualidade histórica. Só a média deles é fixada em zero; cada um pode desviar. O motivo é consistência: o erro histórico descontado no passo 3 foi medido nesses mesmos institutos, então nível e desconto falam da mesma coisa. Trocar a âncora desloca o nível de hoje em +1,7 p.p. (ver "o que mudou").
Cada pesquisa estadual recente vira um swing em relação ao 2º turno de 2022 no estado, corrigida pelo efeito de casa nacional do instituto e pelo movimento do placar desde o fim do campo. Encolhimento em dois níveis, estado → região → país (DerSimonian-Laird, τ = 7,2); a soma ponderada pelo eleitorado dá uma segunda leitura do placar nacional (+3,6), combinada com a nacional assumindo correlação de 0,3 entre os erros (peso atual: 0%), desde que as UFs com pesquisa somem ao menos 30% do eleitorado.
Para cada um dos últimos 60 dias, o modelo foi reajustado só com as pesquisas até aquele dia e previu as pesquisas dos 7 dias seguintes. Em 233 previsões: intervalo de 80% cobriu 85% dos casos e o de 95% cobriu 94% (calibração adequada); erro absoluto médio de 1,9 p.p.; viés médio de -0,3 p.p. (as pesquisas vieram, em média, mais favoráveis a Flávio do que o previsto uma semana antes, reflexo da queda de agosto–setembro, que um passeio aleatório não extrapola). Separando: -0,3 p.p. nos institutos que o ajuste já conhecia (217 previsões) e -0,8 p.p. nos que entraram na base depois (16), que estreiam com efeito de casa zero. Esse viés é informação, não é corrigido: extrapolar tendência foi o defeito da versão anterior.
De onde vem o movimento. Quanto o placar de hoje mudaria se cada pesquisa dos últimos 7 dias fosse retirada (positivo = a pesquisa puxa a favor de Lula). Em série que termina hoje, a pesquisa mais recente pesa mais.
| Pesquisa | Campo até | Efeito no placar do 2º turno | Efeito na margem do 1º turno |
|---|---|---|---|
| Alfa Inteligência | 22/10 | -0,09 | — |
| Gerp | 24/10 | +0,09 | — |
| PoderData | 21/10 | +0,06 | — |
| Vox Brasil | 22/10 | +0,06 | — |
| Real Time Big Data | 22/10 | -0,06 | — |
| Colectta | 23/10 | +0,06 | — |
| Veritá | 22/10 | +0,05 | — |
| DataTrends | 21/10 | -0,05 | — |
Nos cinco 2ºs turnos de 2006 a 2022, as pesquisas finais dos institutos nacionais erraram a margem PT − direita em +0,3, -0,1, +1,1, -1,2 e +2,9 pontos (tabela abaixo; positivo = superestimaram o PT). A média com recência suave é +0,9; a média simples, +0,6. A versão anterior usava +2,1, com peso 2,5× no ciclo de 2022. O desvio entre ciclos (σ = 2,0) entra como incerteza. Depois do 1º turno. As pesquisas da âncora erraram a margem Lula − Flávio do 1º turno em +4,7 p.p. (positivo = superestimaram Lula). As ondas de 2º turno feitas antes de 4/out são as mesmas que carregavam esse erro e recebem o desconto observado (+4,7); as feitas depois recebem só o desconto das rodadas finais de 2º turno (+0,9). Até 22/out elas recebiam também 25% do erro do 1º turno; a revisão de 22/out (v56) tirou essa herança porque o erro do 1º turno teve a assinatura de voto útil: a média final das pesquisas errou Lula em -0,3 p.p. e Flávio em +4,2, enquanto os demais candidatos perderam 4,0 pontos, que foram quase todos para Flávio, e esse mecanismo não existe num 2º turno com dois nomes. O erro próprio de 2º turno continua no desconto: nas rodadas finais de 2006 a 2022 ele foi +0,3 (2006), -0,1 (2010), +1,1 (2014), -1,2 (2018), +2,9 (2022), com mais peso nos ciclos recentes. A herança tirava cerca de 8 pontos da chance de Lula no modelo principal (12,8% contra 20,6% com os dados de 22/out). Hoje 57 ondas pós-1º turno respondem por 100% do placar das pesquisas. O resultado oficial, com a transferência de votos dos demais candidatos, dá um placar de partida de -2,7 ± 2,5 em 4/out, que vira ± 5,0 hoje somada a deriva do eleitorado desde então; as pesquisas dão -0,9 ± 0,7 antes do desconto. O painel combina os dois pelo erro total de cada um (nas pesquisas, amostral e sistemático): hoje o resultado oficial pesa 21% e perde peso a cada dia, porque a foto de 4/out envelhece enquanto chegam pesquisas novas. (Revisão de 4/out: a regra anterior aplicava o desconto de pesquisas novas às ondas antigas e comparava o voto contado só com o erro amostral das pesquisas; com o resultado na mão, isso levava a chance de Lula de 28% para 37%.) A sensibilidade a essa hipótese está na tabela ao lado: é a escolha que mais muda o resultado, por isso fica à vista. Após o 1º turno, as pesquisas de 2º turno já incorporam o resultado e volta a valer o desconto das pesquisas finais; o erro de cada instituto no 1º turno vira prior, com metade do peso, do seu efeito de casa no 2º.
400 mil simulações. Deriva em duas fases: até 4/out, σrw = 0,31 p.p./dia; entre os turnos, 0,95 p.p./dia, calibrado no movimento das pesquisas de 2º turno de Datafolha e Ibope/Ipec entre a primeira e a última rodada em 2014, 2018 e 2022 (tabela abaixo; descontado o erro amostral e encolhido 50% para σrw, porque são só três ciclos). Total: 0,0 p.p. até 25/out. Deriva e erro das pesquisas têm caudas t de Student (5 graus de liberdade, mesmo desvio): com três a cinco ciclos de calibração, a normal subestima surpresas. Os choques regionais saíram da simulação na auditoria de 3/out: o erro histórico já foi medido no resultado nacional, que inclui os erros regionais. P(adversário = Flávio) vem da simulação do 1º turno, com 0% de risco de impugnação.
Mistura de distribuições, não só de probabilidades, por isso os cenários são coerentes com o ensemble. Os fundamentos (uma reta ajustada em três eleições, com erro de previsão acima de 8 pontos) ficam com peso zero desde a auditoria de 3/out. O mercado só entra com preço de até 5 dias e, depois do 1º turno, posterior a 4/out; sem isso, o ensemble é o próprio modelo. Depois do 1º turno, o resultado oficial passa a informar o modelo diretamente. Mercados reagem rápido a notícias, mas não são independentes das pesquisas.
Os botões no alto da página trocam a manchete e a barra de chance por rodadas extras do mesmo modelo em que a base de 2º turno fica só com as 57 pesquisas com campo depois de 4/out (19 institutos; 19 ondas de 5 institutos com histórico calibrado: AtlasIntel, Datafolha, Ipespe, PoderData, Quaest), de modo que o desvio de cada instituto é estimado apenas nelas, sem a persistência de metade do desvio medido antes do 1º turno (λ = 0,5). Tudo o mais é igual: âncora, prior do efeito de casa vindo do erro de cada instituto no 1º turno, deriva até 25/out e ensemble com os mercados. Modelo completo hoje: placar -1,3 ± 1,17, chance de Lula 17% só pelas pesquisas e 18% no ensemble, com 21% do nível vindo do voto contado (-2,7). Na leitura pesquisas pós-1º turno + voto contado o nível é combinado ao placar de partida como no modelo completo (peso 21%): placar -1,9 ± 1,19, chance de Lula 11% só pelas pesquisas e 13% no ensemble; a diferença para o modelo completo isola o efeito de estimar os desvios de instituto só nas ondas novas. Na leitura só pesquisas pós-1º turno, sem o voto contado o nível não é combinado ao placar de partida e o desconto do erro histórico incide inteiro sobre as pesquisas (+0,9 ± 2,5, contra +0,8 efetivos no modelo completo): placar -1,7 ± 0,69, chance de Lula 13% só pelas pesquisas e 15% no ensemble.
O que essas leituras medem bem: não dependem da hipótese de que metade do desvio medido em cada instituto antes do 1º turno persiste depois dele (mal identificada na auditoria de 16/out) e, na versão sem voto contado, tampouco da transferência declarada de votos. O que medem mal: com 18 dias de dados a velocidade diária do placar fica pouco identificada (σrw 0,29 contra 0,31 na série longa); institutos com uma só onda ficam perto do prior; a incerteza da média da âncora sobe (0,17 contra 0,10); e não há validação de origem móvel possível (herdam a calibração do modelo principal). Por isso a página inteira, o compartilhamento e a versão sem JavaScript seguem o modelo completo; as variantes são leituras de sensibilidade, gravadas em resultados_pos1t_estrut.json e resultados_pos1t.json.
O quarto botão mantém o modelo completo inteiro (nível, efeitos de casa, voto contado, desconto) e muda uma coisa: a deriva até 25/out, que no modelo principal é sorteada com média zero, ganha uma média. Ela vem do painel de mudança: o agregado das diferenças entre ondas consecutivas do mesmo instituto, em p.p. por dia, ponderado pelo erro de cada diferença e pela recência (meia-vida de 3 dias), vale hoje +0,40 ± 0,93 p.p. por semana para Lula. O modelo usa metade dele (encolhimento de 50%, porque a diferença entre duas ondas soma o erro amostral das duas e não há validação histórica desta leitura) vezes os 0 dias que faltam (zero no dia da votação, quando a variante coincide com o modelo principal), com desvio igual ao erro-padrão do agregado (piso de 0,1 p.p. por dia): média da deriva 0,00 ± 0,00 p.p. O MCMC paralelo recebe a mesma prior (sorteada a cada iteração, sem verossimilhança própria). Resultado: chance de Lula 17% só pelas pesquisas e 18% no ensemble (MCMC 23% e 23%), contra 17% e 18% no modelo completo. Limites: o agregado é dominado pelas 3 ou 4 diferenças mais novas, muda a cada pesquisa publicada e não distingue movimento real de ruído quando os institutos discordam; por isso é uma leitura de sensibilidade, gravada em resultados_momentum.json, e não a manchete.
Mesmo modelo do passo 1, aplicado à margem Lula − Flávio, à soma dos dois e a cada candidato menor nas 75 pesquisas de 1º turno desde agosto, com a mesma âncora. Até 3/out Lula e Flávio eram duas séries independentes, o que ignorava que disputam o mesmo voto e deixava o placar sensível demais à última pesquisa. Na simulação (200 mil), os candidatos são sorteados e renormalizados a 100%. O desconto histórico do 1º turno vem dos quatro ciclos verificáveis (+5,2, +10,5, +2,4, +5,8: as finais superestimaram a margem do PT sobre o principal adversário), encolhido a 60% da média com recência, com o desvio preditivo entre ciclos (± 3,8) e caudas t de Student. Esse erro já contém a migração tardia da 3ª via para o candidato de direita (2014, 2022), por isso o colapso sorteado só redistribui votos e acrescenta incerteza, sem deslocar de novo a margem média (deslocaria -0,5 p.p.). Os candidatos menores usam um passeio aleatório comum (0,17 p.p./dia) e só as ondas das últimas 4 semanas: com o passeio próprio de cada um, a queda de Cury no início de setembro fazia o nível copiar a última pesquisa e a chance de 3º lugar oscilar a cada onda. A previsão é congelada na última rodada antes de 4/out e comparada com o resultado oficial.
O mesmo modelo dos passos 1 a 4, escrito como um único modelo bayesiano e estimado por MCMC (mcmc_2turno.py, sem bibliotecas além de numpy e scipy). Cada pesquisa mede o placar verdadeiro mais o desconto histórico do seu regime (μ: ondas até 4/out +4,7; ondas depois +0,9) e um erro comum β; o resultado oficial do 1º turno com a transferência entra como uma medida do placar sem erro de pesquisa. A cada passo da cadeia, σrw e σentre são sorteados com a trajetória integrada (dado o resto, o modelo é linear e gaussiano), e a trajetória, β e os efeitos de casa saem juntos de uma normal conjunta exata; σp e o centro m vêm por Metropolis. Diagnósticos: R̂ dividido e tamanho efetivo de amostra multicadeia. A conferência no próprio script mostra que, com os parâmetros fixos, a normal conjunta coincide com o suavizador de Kalman do passo 1. A previsão dos dois modelos é congelada na véspera de 25/out em previsoes_2turno_congeladas.json.
Análise paralela (migracao_markov.py), condicional e sem efeito na manchete: a partir do resultado oficial por UF, uma cadeia de Markov leva cada eleitor do 1º turno (Lula, Flávio, cada candidato da 3ª via, branco/nulo, ausente) a um dos quatro destinos do 2º turno por uma matriz sorteada linha a linha (Dirichlet em torno da 2ª opção declarada da Quaest, dos fluxos brutos de comparecimento e de uma inclinação de perfil). Uma segunda cadeia, diária e não homogênea, segue a campanha de conversa pessoal de 5 a 24/out (alcance por voluntários e recrutamento em cadeia; conversão por contato tomada dos experimentos de Gerber & Green, Nickerson & Rogers e Kalla & Broockman; reversão; confirmação) e aplica o estoque convertido como fluxo entre destinos, com teto na massa disponível. 20 mil sorteios por célula, em 4 cenários de alcance × 2 respostas de Flávio, mais a referência "metas do guia cumpridas". Sem campanha dirigida a margem mediana é −2,7; com a forte, −2,7. A chance "com deriva" soma a mesma deriva t5 do passo 4, só para comparar com o modelo principal.
| Ciclo | 2º turno | Pesquisas finais dos institutos de referência (válidos) | Resultado | Erro médio da margem PT − direita |
|---|---|---|---|---|
| 2006 | Lula × Alckmin | Datafolha 61×39 | 60,8 × 39,2 | +0,3 (peso 0,25) |
| 2010 | Dilma × Serra | Datafolha 56×44 | 56,0 × 44,0 | -0,1 (peso 0,35) |
| 2014 | Dilma × Aécio | Datafolha 52×48 · Ibope 53×47 · CNT/MDA 49,7×50,3 · Vox Populi 54×46 | 51,6 × 48,4 | +1,1 (peso 0,50) |
| 2018 | Haddad × Bolsonaro | Datafolha 45×55 · Ibope 46×54 · CNT/MDA 43,2×56,8 · Paraná Pesquisas 39,4×60,6 · Vox Populi 50×50 · Ipespe 42×58 | 44,9 × 55,1 | -1,2 (peso 0,71) |
| 2022 | Lula × Bolsonaro | Datafolha 52×48 · Ipec 54×46 · Quaest 52×48 · AtlasIntel 53,4×46,6 · PoderData 53×47 · Ipespe 53×47 · CNT/MDA 51,1×48,9 · Paraná Pesquisas 50,4×49,6 | 50,9 × 49,1 | +2,9 (peso 1,00) |
| Prior 2026 | média por ciclo dos institutos de referência; ciclos ponderados por recência (meia-vida de dois ciclos); Veritá, Futura e Brasmarket fora da referência | +0,9 ± 2,0 | ||
| Desconto depois do 1º turno | ondas pré-1º turno: erro observado +4,7; ondas pós: +0,9; peso das pós: 100%; na parcela de pesquisas (79%) | +0,8 ± 2,0 |
| Ciclo | 1º turno | Margem PT − direita nas finais (válidos) | Resultado | Erro médio |
|---|---|---|---|---|
| 2010 | Dilma × Serra | Datafolha +19,0 · Ibope +20,0 | +14,3 | +5,2 |
| 2014 | Dilma × Aécio | Datafolha +18,0 · Ibope +19,0 · CNT/MDA +18,6 | +8,0 | +10,5 |
| 2018 | Haddad × Bolsonaro | Datafolha -15,0 · Ibope -16,0 · CNT/MDA -14,8 · Paraná Pesquisas -15,3 · Vox Populi -9,0 · Ipespe -16,0 | -16,8 | +2,4 |
| 2022 | Lula × Bolsonaro | Datafolha +14,0 · Ipec +14,0 · Quaest +11,0 · AtlasIntel +9,2 · PoderData +10,0 · Ipespe +14,0 · CNT/MDA +8,6 · Paraná Pesquisas +7,1 | +5,2 | +5,8 |
| Prior 2026 (1º turno) | 60% da média com recência (+5,7) | +3,4 ± 3,8 |
| Ciclo | Instituto | Primeira pesquisa do 2º turno (esq. × dir.) | Última antes da votação | Movimento da margem |
|---|---|---|---|---|
| 2014 | Datafolha | 49 × 51 (09/10) | 52 × 48 (25/10) | +6 em 16 dias |
| 2014 | Ibope | 49 × 51 (08/10) | 53 × 47 (25/10) | +8 em 17 dias |
| 2018 | Datafolha | 42 × 58 (10/10) | 45 × 55 (27/10) | +6 em 17 dias |
| 2018 | Ibope | 41 × 59 (14/10) | 46 × 54 (27/10) | +10 em 13 dias |
| 2022 | Datafolha | 53 × 47 (07/10) | 52 × 48 (29/10) | -2 em 22 dias |
| 2022 | Ipec | 55 × 45 (05/10) | 54 × 46 (29/10) | -2 em 24 dias |
Movimento entre turnos: raiz da média dos quadrados = 6,4 p.p.; descontados dois erros amostrais por par de pesquisas e dividido pelos dias, 1,31 p.p./dia, encolhido para 0,95 p.p./dia.
Hoje, o erro sistemático das pesquisas responde por 74% da incerteza sobre a margem final e a imprecisão do agregado de hoje (amostra e efeitos de casa) por 26%; o que ainda pode mudar até 25/out pesa 0%. No dia da votação a deriva é zero: resta o que as pesquisas erram em bloco, a única parcela que não se dissipa com mais pesquisas.
| Nível | Erro histórico | Lula | Flávio |
|---|---|---|---|
| nowcast combinado | viés recalibrado | 17% | 83% |
| nowcast combinado | sem correção de viés | 27% | 73% |
| média ponderada últimas | viés recalibrado | 20% | 80% |
| média ponderada últimas | sem correção de viés | 31% | 69% |
| Desconto aplicado | Lula | Flávio |
|---|---|---|
| +0,0 p.p. | 27% | 73% |
| +0,8 p.p. | 17% | 83% |
| +0,9 p.p. | 15% | 85% |
| +2,1 p.p. | 6% | 94% |
A leitura mais favorável a Lula (sem desconto) chega a 27%; a mais favorável a Flávio (desconto de +2,1) dá Lula 6%. Os mercados (Lula 20%) ficam dentro dessa faixa.
Em uma frase: todas as 60 versões do painel, de 8/set a hoje, com o que mudou no método, nos dados e na apresentação e, quando medido, o efeito de cada mudança na chance de cada candidato.
historico_versoes.json, no repositório, e o painel é gerado a partir dele.| Versão | Data | Mudança | Efeito medido |
|---|---|---|---|
| v56 | 22/out/2026 | O desconto das pesquisas de 2º turno deixa de herdar o erro do 1º turno, que foi voto útil; fica só o erro histórico das rodadas finais de 2º turno | manchete de Lula 15% para 21% (modelo principal 12,8% → 20,6%; MCMC 17,7% → 27,1%), com os mesmos dados de 22/out; desconto aplicado de +1,65 para +0,74; variantes do herói: só pesquisas pós-1º turno 11,7% → 16,9%, com voto contado 12,1% → 16,4%, com a direção das últimas ondas 16,7% → 23,4% (MCMC 21,2% → 30,5%) |
| v55 | 21/out/2026 | Painel de mudança (diferenças dentro de cada instituto), ponteiro com efeitos fixos, leitura do herói com a direção das últimas ondas, e correções de consistência | nenhum na manchete (12,3% / 14,4%, placar −1,73); a quarta leitura do herói dá Lula 14,7% (ensemble 16,2%; MCMC 17,1% / 17,9%) com deriva de média +0,33 ± 0,31; ponteiro de +1,1 para +1,9 p.p./semana ao trocar a reta pooled por efeitos fixos; Futura 19–20/out como série nova (v2) desde o commit 5d0551f |
| v53.3 | 18/out/2026 | Peso do voto contado decai com a deriva; λ só nas séries com ondas pré-4/out; validação roda o modelo do painel | peso do nível estrutural de ~53% para 27% (cai sozinho); cobertura 80% da validação de 82% para 85% (commit ec672a3, registrado em 21/out) |
| v53.2 | 16/out/2026 | λ = 0,5 no efeito de casa pós-1º turno; n efetivo declarado; Palver v3 como série nova; teto de 2.500 na AtlasIntel | chance de Lula 14,6% (modelo), 16,5% (ensemble); com λ = 1 seria 18,5%, com λ = 0, 15,0% (commit 5f3d59c, registrado em 21/out) |
| v53 | 6/out/2026 | Migração de ausentes, terceira via e brancos/nulos: cadeia de Markov + Monte Carlo, ao lado do modelo principal | nenhum na manchete; análise condicional: campanha forte +130 mil votos de saldo, as metas do guia exigiriam conversão 7 a 29× a da literatura |
| v51 | 5/out/2026 | Modelo paralelo do 2º turno estimado por MCMC (posterior completo), ao lado do modelo principal | nenhum na manchete; MCMC: Lula 21% × 20% no principal |
| v45 | 4/out/2026 | Resultado oficial; correção do desconto pós-1º turno; análise da virada, das pesquisas e da acurácia do modelo | Lula (só pesquisas) 28,4% na véspera → 36,9% com a regra antiga → 18,1% corrigido; ensemble 17,4% |
| v44 | 3/out/2026 | Previsão do 1º turno congelada antes da votação | — |
| v43 | 3/out/2026 | Segunda auditoria: regime pós-1º turno, caudas t, fundamentos com peso zero, teste de manada | ensemble 34,2% → 31,9%; Lula em 1º no 1º turno 38% → 45% |
| v41 | 3/out/2026 | Auditoria: 3ª via sem dupla contagem, desconto pré-1º turno no 2º turno, fundamentos homogêneos | Lula (só pesquisas) 50% → 31%; ensemble 48% → 35% |
| v39 | 2/out/2026 | Leitura estadual: mapa por UF e pesquisas estaduais no placar nacional | — |
| v37 | 1º/out/2026 | Abstenção entra no modelo (cenários de comparecimento por UF e perfil) | — |
| v33 | 30/set/2026 | Auditoria e modelo v3: espaço de estados ancorado, viés recalibrado, previsão do 1º turno | Lula (ensemble) 37,7% → 47% |
| v28 | 28/set/2026 | Termômetro de tendência das duas últimas semanas | — |
| v25 | 24/set/2026 | Modelo v2: pesquisas estaduais, filtro de Kalman e novos fatores | ver seção "O que mudou no modelo" |
| v16 | 17/set/2026 | Seção de viés: vícios de amostra, margem de erro e calibração histórica | — |
| v2 | 8/set/2026 | Ponderação por recência, tamanho da amostra e qualidade; base de 41 ondas | — |
| v1 | 8/set/2026 | Primeira versão: média simples das últimas pesquisas de cinco institutos | — |
RHO_1T_2T = 0: as ondas pós-1º turno recebem só o desconto das rodadas finais de 2º turno, cujo sinal varia (2018 −1,2; 2022 +2,9), com σ = 2,5 mantido. As ondas de 2º turno feitas antes de 4/out continuam com o erro observado, mas pesam menos de 1% do placar. Não mudou: o embalo continua só como variante (quarto botão do herói, deriva com metade do agregado das diferenças intra-instituto), porque o ritmo caiu de +3,0 para +1,8 p.p./semana e, em 2014 e 2018, estender a tendência além da última pesquisa teria errado mais. Na mesma crítica, o embalo integral daria 26,3% (MCMC 35,0%). Ressalva: é uma mudança de método três dias antes da eleição, motivada por uma crítica feita depois de ver os dados. A justificativa é a decomposição do erro do 1º turno, que é anterior a esta semana e independe da direção das pesquisas de 2º turno.analise-modelo-agregacao-2026-10-21.md) a partir da crítica de que o modelo carrega inércia do passado e deveria ler o "drive do presente" em diferenças intra-instituto. (1) Nova seção #mudanca (mudanca_2turno.py): para cada série com duas ondas consecutivas depois de 4/out, a diferença da margem em válidos, ponderada pelo erro da diferença (soma dos dois erros amostrais) e pela recência (meia-vida 3 dias); a versão literal (só a última diferença de cada casa) ao lado; séries v2/v3 não pareiam com a antiga; tracking da Gerp só a 5 dias ou mais; rejeição e espaço disponível (100 − voto − rejeição) na mesma máquina quando houver ondas pós-1º turno em rejeicao_2026.csv. Hoje: +1,17 ± 1,08 p.p./semana para Lula, desacelerando (janela longa +1,55). (2) Ponteiro: inclinação com intercepto por série (efeitos fixos), que cancela o efeito de casa sem estimá-lo e deixa de confundir composição com tendência; pesos com σinst em vez do RMS dos resíduos (que já continha o erro amostral); ritmo para virar parte do nível das pesquisas do modelo; "Virada em curso" só quando o ritmo alcança. (3) Variante ELEICAO_MOMENTUM=1 (resultados_momentum.json, quarto botão do herói, passo 5c): a deriva até 25/out ganha média = metade do agregado × dias restantes, com desvio encolhido na mesma proporção (piso 0,1/dia); o MCMC recebe a mesma prior. (4) Consistência: erro da série do termômetro e de serie_pos1t só das pesquisas (antes era o do nível mesclado); bias_prior_sd volta a ser o prior; o congelado grava nivel_pesquisas e nivel_ajustado; fórmula do MCMC com o SD estrutural de 4/out (2,5); notas para Palver v3, MDA v2 e Futura v2; prior do erro do 1º turno declaradamente não aplicado às séries novas. Registradas também, nesta data, as versões v53.1 a v53.4 (13–19/out), que só existiam nos commits.modelo_eleicao_2026.py (variável ELEICAO_SO_POS1T: estrut grava resultados_pos1t_estrut.json, com o voto contado combinado como no modelo completo; 1 grava resultados_pos1t.json, sem ele): a base de 2º turno fica só com as ondas de campo depois de 4/out, o efeito de casa de cada instituto é estimado apenas nelas (sem a persistência de 50% do desvio medido antes do 1º turno) e, na versão sem voto contado, o nível não é combinado ao placar de partida e o desconto do erro histórico incide inteiro sobre as pesquisas. Âncora, priors do erro do 1º turno, deriva e ensemble são os mesmos. O herói ganha três botões; o JS troca manchete, subtítulo, barra e legenda, e uma nota explica de onde vem a diferença. Passo 5b da metodologia registra o que a leitura mede bem (independe de λ e da transferência declarada) e mal (12 dias não identificam σrw; institutos de onda única ficam no prior; sem validação de origem móvel). A rodada principal não muda: zero diferença em resultados.json além dos carimbos de tempo.#evolucao (evolucao_pesquisas.py): pontos de Lula e Flávio por pesquisa, leitura por ponto e por dia, tabela com fontes; linha-síntese = placar ajustado do modelo dia a dia (não muda com o seletor); com seleção parcial, média simples tracejada das escolhidas (meia-vida 4 dias, janela 14).n_efetivo); Ipespe é CATI. No modelo, o efeito de casa medido antes do 1º turno vale metade (λ = 0,5) nas ondas novas, porque a inclinação observada era +0,58 ± 0,68 e a dispersão entre institutos subia com o efeito inteiro; AtlasIntel com n efetivo máximo de 2.500.#ponteiro (ponteiro_tendencia.py): inclinação da margem Lula − Flávio nas pesquisas com campo depois de 4/out, corrigida pelo efeito de casa, com prior de ±3 p.p./semana enquanto há poucas pesquisas; medidor com ponteiro de hoje, de 3 dias atrás e marca do ritmo necessário para virar.migracao_markov.py e seção Migração na camada 2, entre Abstenção e Escândalos. A partir do resultado oficial por UF, uma cadeia de Markov leva cada eleitor do 1º turno (Lula, Flávio, cada candidato da 3ª via, branco/nulo, ausente) a um dos quatro destinos do 2º turno por uma matriz 8 × 4 sorteada linha a linha (Dirichlet em torno da 2ª opção declarada da Quaest, dos fluxos brutos de comparecimento e de uma inclinação de perfil); uma segunda cadeia, diária e não homogênea, segue a campanha de conversa pessoal de 5 a 24/out (voluntários e recrutamento em cadeia, conversão por contato tomada de Gerber & Green, Nickerson & Rogers e Kalla & Broockman, reversão, confirmação), com resposta simétrica de Flávio e chamado aberto que sai pela culatra nas UFs dele. 20 mil sorteios por célula, 4 cenários de alcance × 2 respostas mais a referência "metas do guia cumpridas". Resultado: sem campanha dirigida a margem vai a −3,85 (80% entre −4,7 e −3,0; reproduz o modelo de transferência); a campanha forte (500 mil voluntários iniciais, 1,3 mi ao fim, 3,5 mi de nomes) rende +130 mil votos de saldo e move a margem 0,12 ponto; a máxima, +250 mil; com resposta simétrica de Flávio, +50 mil. As metas do guia (+5,1 mi) levariam a margem a +0,4, mas exigiriam, com esse alcance, conversão por contato de 7× (ausentes) a 29× (eleitores de Flávio) a da literatura. Chance de Lula "com deriva" (mesma deriva t5 do modelo principal, só para comparar): 14,6% sem campanha, 15,2% com a forte. Autoverificações no script: massa conservada, linhas somam 1, cadeia em [0, 1], reprodução do estrutural (−3,85 = −3,85), escada monotônica, semente fixa. Passo 8 da metodologia e dois termos novos no glossário (cadeia de Markov, Dirichlet). A manchete continua sendo o modelo principal.mcmc_2turno.py: o mesmo modelo do 2º turno (pesquisas da janela, efeitos de casa ancorados com o prior do erro de cada instituto no 1º turno, quebra de 4/out, nível estrutural do voto contado, desconto de viés por regime com caudas t, deriva em duas fases), escrito como um único modelo bayesiano e estimado por MCMC, com 4 cadeias de 4.000 sorteios. σrw e σentre são sorteados com a trajetória integrada; trajetória, erro comum e efeitos de casa saem de uma normal conjunta exata; σp vem por Metropolis. Diagnósticos: R̂ e tamanho efetivo de amostra. Na estreia, o placar verdadeiro de hoje é o mesmo nos dois (−3,9); a chance de Lula fica em 21% no MCMC contra 20% no principal, porque a velocidade de mudança entre os turnos deixa de ser fixa e alarga a deriva (desvio 5,4 × 4,2). A previsão dos dois passa a ser congelada até a véspera de 25/out em previsoes_2turno_congeladas.json. A manchete continua sendo o modelo principal; nenhum número dele mudou.coletar_municipios.py; Dados Abertos de 2022 lidos por HTTP Range, coletar_2022_2t.py) e malha mínima do IBGE (gerar_geo_mun.py). 11 municípios ainda aparecem com totalização parcial no arquivo municipal do TSE. Acurácia: novo bloco "Previsão congelada na véspera × resultado oficial", com faixas de 80% contra o resultado (7 de 8 dentro; só "outros" fora), as quatro perguntas da véspera (4 de 4 no desfecho mais provável; Brier médio 0,108) e o erro por candidato do modelo contra as pesquisas sem desconto (0,35 × 1,22 p.p.; o modelo chegou mais perto nos 7).resultado_1turno.csv (aciona o regime pós-1º turno) e resultado_1turno_uf.csv (27 UFs e exterior). Método: (1) com o resultado e sem nenhuma pesquisa nova, o desconto das pesquisas de 2º turno caía de +3,4 para +2,0 e a chance de Lula subia de 28% para 37%, no sentido contrário ao erro que o resultado mostrou. As ondas de 2º turno feitas antes de 4/out passam a receber o erro observado da âncora no 1º turno (+4,8); as feitas depois, a regra pós-1º turno (finais de 2º turno + 25% do erro, +2,1), misturadas pela fração do placar que vem de ondas novas no filtro (F = w_pós ÷ (w_pós + 1/(P + 6²))); (2) o voto contado com transferência (−3,9) passa a ser combinado com o erro total das pesquisas (amostral e sistemático), não só o amostral: peso de 8% para 52%; pesquisas corrigidas e voto contado agora convergem (−3,9 e −3,9); (3) mercado lido na noite da apuração vale no ensemble se marcado como posterior ao resultado (Polymarket: Lula 15%, US$ 170 mi, dois espelhos). Testado em cópia com 0, 1 e 3 ondas fictícias pós-1º turno (transição suave). Painel: topo do 1º turno com o resultado ao lado da previsão congelada; novas seções "Por que Flávio virou", "Pesquisas × urnas" (erro por instituto, método, região e UF; correção de casa contra a urna; teste de manada) e "Acurácia" (erro, percentil, Brier e log-perda, componente por componente); condições de vitória do 2º turno refeitas a partir do resultado; histórico de versões passa a viver em historico_versoes.json e vira a seção metodológica "Histórico de mudanças e ajustes", no painel e no site. Avaliação: margem prevista −0,7 contra −1,90 (erro 1,2; pesquisas sem desconto: 4,6); resultado no percentil 37; o desconto histórico funcionou e foi conservador; a correção de casa ancorada afastou 11 de 14 institutos do resultado; leitura estadual errou o Sudeste.previsoes_1turno_congeladas.json para comparar com o resultado de 4/out.transferencia_2turno.csv. 1º turno: (7) Lula e Flávio deixam de ser duas séries independentes: o modelo estima a margem e a soma dos dois; (8) desconto histórico com 2010 na base e desvio preditivo entre ciclos (± 3,8 em vez de ± 3,0). Especificação: (9) caudas t de Student no erro das pesquisas e na deriva; (10) qualidade do instituto medida pelo erro em relação à média do ciclo, não pelo erro bruto; (11) incerteza da âncora somada ao erro do nível; (12) fundamentos com peso zero (reta de três eleições); (13) choques regionais retirados; (14) teste de manada refeito nos resíduos; (15) erro amostral da margem com a fórmula exata; (16) validação separada entre institutos conhecidos e novos, e tabela de influência de cada pesquisa. Efeito, com as mesmas pesquisas: Lula só pelas pesquisas 29,8% → 29,9%; ensemble 34,2% → 31,9% (sai o componente de fundamentos, que dizia 53%); Lula em 1º no 1º turno 38% → 45%. Painel: hora da atualização no cabeçalho; rótulos do desconto corrigidos (o painel chamava de "2º turnos de 2006–22" o desconto do 1º turno); links vazios das estaduais; textos com números fixos passam a vir dos dados.abstencao_2022_uf.csv (abstenção por UF nos dois turnos de 2022, CNN Brasil a partir do TSE, fonte única; Amapá e Roraima a conferir). Checagem: a margem de 2022 reconstruída por região coincide com o resultado oficial. Sem pesquisa nacional nova até 18h40; a Datafolha de 28/set–1º/out (n=4.006) foi anunciada para hoje, mas ainda não havia sido publicada. Modelo: nível +0,3 p.p.; Lula 45% · Flávio 55% só pelas pesquisas; ensemble Lula 45% · Flávio 55%.vies), entre "Metodologia" e "Indecisos". Ela mede, na própria base, que o erro típico de uma pesquisa é 4,4 p.p. contra 2,2 p.p. previstos pela amostragem (1,97×, convergindo com Shirani-Mehr et al./JASA 2018); mostra que a margem declarada se refere a cada candidato e que a diferença entre os dois já carrega ±4,4 p.p.; aplica um teste de manada (dispersão observada 1,43× a esperada — o oposto do padrão americano de 2024); traz um forest plot dos efeitos de casa com intervalo de 95%; uma radiografia dos 19 institutos (método, contratante, viés, erro típico vs. teórico, histórico); a série de erros das finais de 2010 a 2022 com casos exemplares (Vox Populi e Paraná em 2018, o erro de ~8,8 p.p. contra Bolsonaro no 1º turno de 2022, SP 2020 e 2024); o quadro regulatório (PesqEle, multas, PL 2.558/2022) e os episódios de 2026 (Áltica, as duas ações contra a AtlasIntel, a suspensão do Veritá); e uma conclusão classificando os institutos por fidedignidade. Correção de calibração decorrente da pesquisa: o histórico do painel registrava só a atuação de 2022 da Paraná Pesquisas, que por isso recebia o melhor peso de qualidade — mas o instituto errou o 2º turno de 2018 por ~11 p.p. Foram acrescentadas as finais verificadas de Paraná (2018), Veritá e Futura (2022), e o peso de qualidade passou a ser encolhido em direção ao valor neutro conforme o número de ciclos que o sustenta. Efeito: nowcast de −1,5 para −1,7 p.p., P(Lula) no modelo de 28,6% para 27,7% e ensemble de 40,1% para 39,6%.Em uma frase: a margem de erro divulgada cobre só parte do erro real — neste ciclo, o erro típico de uma pesquisa isolada é 1,72 vezes o que a margem promete, e é por isso que o painel nunca confia numa pesquisa só.
Ela supõe amostragem aleatória simples, lista completa da população e resposta de todo mundo que é sorteado — três condições que nenhuma pesquisa eleitoral brasileira cumpre. E há uma armadilha de leitura: a margem vale para cada percentual, não para a diferença. Quando Lula tem 46% e Flávio 45% com "margem de 2 pontos", a diferença de 1 ponto carrega, só de erro amostral, um intervalo de ±4,4 p.p. — porque o erro dos dois percentuais se soma. Empates técnicos são, portanto, muito mais frequentes do que a leitura usual sugere.
Efeito de desenho: cotas e conglomerados inflam a variância; nos EUA, o erro-padrão real é ≈1,3× o da fórmula simples (mediana; Shirani-Mehr et al., JASA 2018), e amostras por cotas em ponto de fluxo tendem a fator maior. Não resposta: taxas acima de 90% em pesquisas eleitorais americanas — e a não resposta é diferencial: quem apoia o candidato que está perdendo responde menos, viés que nenhuma ponderação demográfica corrige. Cobertura: telefone exclui quem não tem linha (~4% dos adultos), painel online exclui o eleitor offline e de menor escolaridade, presencial tropeça em condomínios fechados. Ponderação: calibrar por voto declarado na eleição anterior importa o erro de memória do entrevistado. Eleitor provável e indecisos: critérios de comparecimento e a redistribuição de indecisos são hipóteses, não medições — e institutos diferentes usam hipóteses parecidas, o que faz os erros andarem juntos.
Para cada uma das 221 pesquisas da base, dá para comparar o desvio observado em relação à tendência comum estimada pelo agregador com o desvio que a teoria amostral prevê para aquela amostra. O erro típico observado é de 3,9 p.p. na margem em votos válidos, contra 2,3 p.p. previstos — uma razão de 1,72×. Traduzindo para intervalo de 95%: o intervalo honesto de uma pesquisa isolada, neste ciclo, é de cerca de ±7,6 p.p. na margem, e não os ±2,0 que aparecem na ficha técnica. A coincidência com a literatura internacional é notável: analisando 4.221 pesquisas de 608 eleições americanas, Shirani-Mehr, Rothschild, Goel e Gelman encontraram erro médio de 3,5 p.p. — também cerca do dobro do implícito nos intervalos publicados.
A parcela sistemática — o viés de cada instituto — é a maior fatia e é corrigível: é exatamente o que o agregador faz ao estimar efeitos de casa. O que sobra depois disso ainda é 1,1× o teórico: é o excesso de ruído que nenhuma ficha técnica declara.
Manada é a convergência artificial de institutos perto da eleição — publicar o que o consenso já diz, para não errar sozinho. Nos EUA de 2024, Nate Silver mostrou que 78% das pesquisas dos estados-pêndulo caíam dentro de ±2,5 p.p. quando a teoria previa 55%: probabilidade de acontecer por acaso, 1 em 9,5 trilhões. Aqui o teste é feito nos resíduos: tirados a tendência e o efeito de casa de cada instituto, as 57 ondas dos últimos 21 dias se afastam em média 1,7 p.p., contra 2,2 p.p. esperados só por erro amostral — razão de 0,78×. Perto de 1: a dispersão é a que o acaso explica, sem sinal de manada (parte da distância para 1 vem de o próprio ajuste absorver ruído). Já as margens brutas variam 2,0 p.p. entre institutos: a divergência brasileira vem de efeitos de casa metodológicos reais e persistentes, não de ruído. (Até 3/out este teste comparava as margens brutas com o erro amostral, o que dava sempre razão acima de 1 e não media manada.) A consequência prática é que a média simples de pesquisas (usada pela maioria dos agregadores da imprensa) é um estimador ruim: sem corrigir viés de casa, ela reflete quem publicou mais naquela semana.
Atenção à interpretação: efeito de casa não é sinônimo de viés político. Ele mede distância da âncora, não distância do resultado — e costuma refletir escolhas de método (quem é presencial, quem é online, quem redistribui indeciso). O consenso também erra: em 2022, os dois institutos mais próximos do resultado final do 2º turno foram justamente dois que destoavam da média.
Depois do 1º turno, o efeito de casa vale metade. Nas pesquisas feitas depois de 4/out, a margem observada acompanha só em parte o efeito de casa estimado antes (inclinação de +0,58, com intervalo de ±0,68, onde 1 seria persistência total e 0, nenhuma; sem a Gerp, perto de zero), e a dispersão entre institutos aumenta quando o efeito antigo é aplicado por inteiro. Vários institutos passaram a pesar a amostra pelo voto declarado no 1º turno, o que apaga parte das diferenças. O modelo aplica 50% do efeito de casa histórico às pesquisas posteriores a 4/out (a coluna "Efeito de casa" acima mostra o valor aplicado). A Palver trocou o método de calibração na onda de 10 a 13/out (v3, com o voto no 1º turno por região) e o próprio instituto diz que a onda não é comparável às anteriores: entra como série nova, sem o efeito de casa das ondas anteriores, e com o n efetivo de Kish informado por ela (1.783 em vez de 5.000). A AtlasIntel tem o n efetivo limitado a 2.500, porque a margem declarada (±1,0) é menor que a de uma amostra aleatória simples do mesmo tamanho (±1,4) e o efeito de desenho não é informado.
| Instituto | Método | n médio | Efeito de casa | Erro típico | Erro teórico | Razão | MAE 2006–22 | Leitura |
|---|---|---|---|---|---|---|---|---|
| Gerp · 20 ondas · última 24/10 | telefônico | 2.220 | -7,7 * | 7,4 | 2,2 | 3,31× | — | Exigem cautela |
| Futura Inteligência · 19 ondas · última 17/10 | telefônico | 2.000 | -3,8 * | 6,0 | 2,3 | 2,58× | 2,4 | Exigem cautela |
| Quaest · 19 ondas · última 24/10 | presencial | 2.198 | -1,0 | 1,8 | 2,4 | 0,75× | 4,0 | Confiáveis com ressalva |
| BTG/Nexus · 19 ondas · última 24/10 | telefônico | 2.007 | -1,1 | 1,5 | 2,3 | 0,65× | — | Mais fidedignas |
| Datafolha · 18 ondas · última 24/10 | presencial | 2.345 | 0,0 | 2,1 | 2,2 | 0,93× | 3,4 | Mais fidedignas |
| AtlasIntel · 17 ondas · última 23/10 | online | 5.215 | -0,5 | 1,8 | 2,1 | 0,85× | 4,5 | Confiáveis com ressalva |
| PoderData · 17 ondas · última 24/10 | telefônico | 2.964 | -2,4 * | 2,0 | 1,9 | 1,04× | 4,5 | Confiáveis com ressalva |
| Meio/Ideia · 11 ondas · última 19/10 | telefônico | 1.636 | -0,9 | 1,7 | 2,6 | 0,64× | — | Confiáveis com ressalva |
| Vox Brasil · 11 ondas · última 22/10 | presencial | 2.100 | -1,4 | 3,8 | 2,3 | 1,62× | — | Exigem cautela |
| Veritá · 9 ondas · última 23/10 | telefônico | 12.912 | -5,3 * | 6,5 | 1,6 | 3,99× | 3,8 | Exigem cautela |
| Real Time Big Data · 9 ondas · última 22/10 | telefônico | 2.000 | -1,6 | 1,9 | 2,4 | 0,78× | — | Mais fidedignas |
| Indexa · 8 ondas · última 21/10 | telefônico | 2.000 | +1,1 | 1,6 | 2,4 | 0,68× | — | Confiáveis com ressalva |
| Alfa Inteligência · 6 ondas · última 22/10 | presencial | 2.200 | +0,3 | 2,1 | 2,4 | 0,86× | — | Exigem cautela |
| Palver · 6 ondas · última 03/10 | online | 5.000 | -4,4 * | 4,2 | 2,8 | 1,54× | — | Exigem cautela |
| CNT/MDA · 5 ondas · última 02/10 | presencial | 2.003 | +3,9 * | 6,4 | 2,4 | 2,69× | 3,9 | Exigem cautela |
| DataTrends · 5 ondas · última 21/10 | telefônico | 2.000 | -1,8 | 2,2 | 2,3 | 0,94× | — | Série insuficiente |
| Paraná Pesquisas · 4 ondas · última 28/03 | presencial | 2.069 | -2,0 * | 3,2 | 2,4 | 1,37× | 3,8 | Confiáveis com ressalva |
| Instituto França · 3 ondas · última 23/10 | não informado | 2.179 | -0,5 | 0,9 | 2,4 | 0,40× | — | Série insuficiente |
| Ipespe · 3 ondas · última 22/10 | telefônico | 1.500 | -0,9 | 1,3 | 2,7 | 0,48× | 4,9 | Exigem cautela |
| Palver (calibração v3) · 3 ondas · última 24/10 | online | 5.000 | -0,3 | 0,8 | 2,4 | 0,32× | — | Exigem cautela |
| Vetor/Arrow · 2 ondas · última 18/05 | não informado | 9.000 | -0,4 | 10,4 | 1,5 | 7,02× | — | Série insuficiente |
| American Analytics · 2 ondas · última 20/09 | não informado | 2.000 | 0,0 | 0,4 | 2,4 | 0,16× | — | Série insuficiente |
| CNT/MDA (ponderada pelo 1º turno) · 2 ondas · última 23/10 | presencial | 2.023 | -0,3 | 0,5 | 2,3 | 0,23× | — | Exigem cautela |
| Futura (escolaridade pela PNAD) · 2 ondas · última 24/10 | telefônico | 2.000 | -0,4 | 0,8 | 2,3 | 0,33× | — | Exigem cautela |
| Colectta · 1 onda · última 23/10 | presencial | 3.000 | +0,2 | 1,8 | 1,9 | 0,94× | — | Série insuficiente |
"Efeito de casa" positivo = mede Lula melhor que a âncora; negativo = mede Flávio melhor; * marca os estatisticamente significativos (95%). "Erro típico" é o desvio observado em relação à tendência comum; "erro teórico" é o previsto pela amostragem para aquele n. "Razão" acima de 2 significa que o instituto erra o dobro do que sua ficha técnica sugere — e amostra grande não resolve: Veritá e Vetor/Arrow declaram as menores margens da base (amostras de 3,8 mil a 40 mil) e têm as maiores razões, porque amostra grande reduz o erro amostral sem tocar no viés.
Não é opinião: a PollingData mediu, sobre 4.400 pesquisas registradas no TSE, que em 2022 a vantagem média de Lula era de 14,1 p.p. em pesquisas presenciais contra 9,3 p.p. em telefônicas no 1º turno (4,8 p.p. de diferença), e 17,6 contra 13,8 no 2º turno. A direção se repete em 2026 nas variáveis de percepção: Datafolha (presencial, ponto de fluxo) mede rejeição de 46% para Flávio e 45% para Lula; a Quaest (presencial domiciliar), 54% e 52% no mesmo mês — 7 a 8 p.p. de diferença na variável que define o teto de cada candidato. Na aprovação do governo, PoderData (URA) e Quaest chegaram a divergir 10 p.p. em dezembro de 2025. Métodos autoadministrados (online, URA) tendem a medir mais voto à direita que os métodos com entrevistador — compatível com menor desejabilidade social.
Nos EUA, pesquisas patrocinadas por campanhas e partidos exibem viés médio de 4 a 5 p.p. a favor do patrocinador (FiveThirtyEight). No Brasil, o JOTA estima efeitos de casa por instituto e já registrou desvios da ordem de 4 p.p. em pesquisas de institutos ligados a contratantes corporativos. Na base deste painel, porém, o padrão não se organiza por contratante. Os maiores desvios em relação à âncora são: Gerp (-7,7, pró-Flávio); Veritá (-5,3, pró-Flávio); Palver (-4,4, pró-Flávio); CNT/MDA (+3,9, pró-Lula). Há entre eles institutos autofinanciados e institutos com contratante. Ou seja: no ciclo 2026 o viés está mais associado a método e ausência de escrutínio do que a quem paga a conta.
| Ciclo | O que as finais projetaram | Resultado | Erro na margem | Lição |
|---|---|---|---|---|
| 2010 · 2º turno | Datafolha e Ibope: Dilma 56 × 44 | 56,05 × 43,95 | ≈0,2 | O ciclo mais preciso da série — quando a disputa não é polarizada por identidade, a pesquisa funciona bem |
| 2014 · 2º turno | Vox Populi 48×41 · Datafolha 47×43 · Ibope 49×43 · CNT/MDA deu Aécio à frente | Dilma 51,6 × 48,4 | 0,6 a 3,9 | Primeiro caso recente de vencedor errado por um instituto tradicional; 1º turno subestimou Aécio em ~9 p.p. |
| 2018 · 2º turno | Datafolha 55×45 · Ibope 54×46 · Vox Populi 50×50 · Paraná Pesquisas 60,6×39,4 | Bolsonaro 55,1 × 44,9 | 0,3 (Datafolha) a 10,9 (Paraná) | O mesmo ciclo produziu o melhor e o pior desempenho da série — média de institutos esconde dispersão brutal |
| 2022 · 1º turno | Datafolha e Ipec: Lula +14 em válidos | Lula +5,2 | ≈8,8 | Erro sistemático contra Bolsonaro, com desvios de até 20 p.p. em estados — a origem do prior de viés deste painel |
| 2022 · 2º turno | CNT/MDA 51,1×48,9 · Paraná 50,4×49,6 · Datafolha e Quaest 52×48 · Ipec 54×46 · Veritá, Brasmarket e Futura deram Bolsonaro | Lula 50,9 × 49,1 | 0,4 a 9,0 | Os dois mais precisos estavam fora do consenso; três institutos erraram o vencedor — dois deles seguem na base de 2026 |
| Municipais 2020/2024 | SP 2020: Covas 38 (Ibope final) · SP 2024: Boulos 27 × Nunes 24 × Marçal 24 (Datafolha) | Covas 32,9 · Nunes 29,5 × Boulos 29,1 × Marçal 28,1 | 4 a 5,5 | Em 2024 a ordem dos três primeiros foi invertida — erro de ordenação, não só de magnitude |
Convenção: erro na margem = (margem projetada esquerda−direita) − (margem oficial), em pontos percentuais de votos válidos. É essa série que alimenta o desconto de erro sistemático usado na simulação (+0,8 ± 2,0 p.p. para o 2º turno — erro observado das pesquisas no 1º turno, nas ondas de antes de 4/out, e rodadas finais de 2º turno de 2006–2022 mais 25% desse erro, nas ondas novas; +3,4 ± 3,8 para o 1º). Note a assimetria entre turnos: o viés grande e sistemático aparece sobretudo nos 1ºs turnos (2014 e 2022), enquanto os 2ºs turnos têm viés menor e mais disperso.
Pela Lei 9.504/1997 (art. 33) toda pesquisa deve ser registrada no PesqEle até 5 dias antes da divulgação, com contratante, valor pago, plano amostral, período de campo e margem de erro; depois, é obrigatório complementar com a composição final da amostra. Divulgar sem registro custa multa de R$ 53 mil a R$ 106 mil, inclusive para quem apenas compartilha. O registro, porém, é uma formalidade de transparência, não um selo de qualidade: não há auditoria prévia de método nem sanção por errar. O projeto que puniria com 8 anos de inabilitação o instituto que errasse além da margem (PL 2.558/2022) não avançou, e a CPI das pesquisas pedida em 2022 nunca foi instalada.
Três episódios de 2026 mostram o tamanho da zona cinzenta. Fevereiro: o TSE suspendeu pesquisa da Áltica Research divulgada sem registro prévio. Junho e julho: a campanha de Flávio Bolsonaro acionou o TSE duas vezes contra a AtlasIntel — na primeira, alegando indução ao reproduzir áudio do caso Vorcaro antes das perguntas de imagem, obteve liminar do presidente do TSE suspendendo a divulgação; na segunda, contestou a documentação complementar de uma onda que dava Lula +7. Agosto: pesquisas do Veritá foram suspensas pela Justiça Eleitoral após questionamentos sobre metodologia, composição da amostra e cronograma de coleta — o mesmo instituto que registrou 93 pesquisas autofinanciadas no ano. Judicialização de pesquisa é, ela própria, um instrumento de campanha: convém separar o mérito metodológico do uso político da contestação.
Não existe instituto infalível, e a pergunta certa não é "quem está certo hoje", e sim quem erra de forma menor e mais previsível. Cruzei quatro critérios verificáveis: acurácia histórica em eleições presidenciais reais (2018 e 2022), tamanho e significância estatística do efeito de casa em 2026, excesso de erro sobre o teórico, e transparência de método, contratante e registro. O resultado abaixo é um julgamento explicitado — não um índice automático — e cada nome vem com sua ressalva.
Em 2026: 19 ondas, efeito de casa de -1,1 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. A série mais densa do ciclo, com ondas quinzenais desde março, e um dos menores erros típicos da base. Método telefônico e contratante declarados em toda onda. Ressalvas: não tem histórico presidencial próprio para aferir acurácia, e uma dispersão tão baixa também é compatível com suavização entre ondas.
Em 2026: 18 ondas, efeito de casa de 0,0 p.p., indistinguível de zero, erro típico próximo do teórico. A série mais longa do país, método presencial em ponto de fluxo e a melhor acurácia histórica entre os grandes (MAE 3,4 p.p. na margem, 2006–22, com o acerto quase exato do 2º turno de 2018).
Em 2026: 9 ondas, efeito de casa de -1,6 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Método telefônico e registro declarados, erro típico abaixo do teórico. Ressalvas: sem histórico presidencial verificável e série curta.
Em 2026: 19 ondas, efeito de casa de -1,0 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Série longa e presencial domiciliar, o método mais caro e, em tese, o de melhor cobertura, com registro e contratante declarados. Contra: o pior MAE histórico entre os grandes (4,0 p.p.). Mede rejeição acima do Datafolha, diferença que é de pergunta e método, não de momento.
Em 2026: 17 ondas, efeito de casa de -0,5 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Amostras de 5 mil, alta cadência e o melhor desempenho no 1º turno de 2022. Contra: é online não probabilística (recrutamento digital) e foi alvo de duas ações do PL em 2026, uma delas com liminar de suspensão. Amostra grande encolhe a margem declarada sem encolher o erro real.
Em 2026: 17 ondas, efeito de casa de -2,4 p.p. (mede Flávio melhor que a âncora, desvio estatisticamente significativo), erro típico próximo do teórico. Cadência fixa, método (URA) e registro TSE publicados em toda onda. Histórico verificável, ainda que mediano (MAE 4,5 p.p.).
Em 2026: 11 ondas, efeito de casa de -0,9 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Ondas telefônicas com registro TSE e contratante declarado, boa cadência. Contra: amostra menor que a média da base e nenhum histórico presidencial próprio.
Em 2026: 8 ondas, efeito de casa de +1,1 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Telefônica, contratante jornalístico (Broadcast/Estadão) e registro divulgado. Contra: série curta e sem histórico presidencial verificável.
Em 2026: 4 ondas, efeito de casa de -2,0 p.p. (mede Flávio melhor que a âncora, desvio estatisticamente significativo), erro típico 1,4× o teórico. Caso instrutivo: acertou o 2º turno de 2022 quase na mosca quando o consenso errou, mas havia errado o de 2018 por ~11 p.p. (MAE combinado de 3,8 p.p.). A última onda é de março: está fora da leitura atual.
Em 2026: 20 ondas, efeito de casa de -7,7 p.p. (mede Flávio melhor que a âncora, desvio estatisticamente significativo), erro típico 3,3× o teórico. Autofinanciado e sem histórico presidencial verificável. Não implica má-fé: pode ser uma escolha metodológica que capta melhor o eleitor de direita, mas não há como auditar.
Em 2026: 19 ondas, efeito de casa de -3,8 p.p. (mede Flávio melhor que a âncora, desvio estatisticamente significativo), erro típico 2,6× o teórico. Projetou o vencedor errado no 2º turno de 2022, na mesma direção do desvio atual: o viés não é novo.
Em 2026: 11 ondas, efeito de casa de -1,4 p.p., indistinguível de zero, erro típico 1,6× o teórico. Presencial domiciliar e autofinanciada nas ondas recentes (registro no TSE divulgado); as ondas até maio não informavam o método. Sem histórico presidencial verificável.
Em 2026: 9 ondas, efeito de casa de -5,3 p.p. (mede Flávio melhor que a âncora, desvio estatisticamente significativo), erro típico 4,0× o teórico. Amostras enormes (até 40 mil) e margem declarada entre as menores da base, com erro observado várias vezes o teórico: a ilustração mais clara de falsa precisão. Teve pesquisas suspensas pela Justiça Eleitoral em agosto de 2026 após questionamentos sobre metodologia e composição de amostra, e projetou o vencedor errado em 2022.
Em 2026: 6 ondas, efeito de casa de +0,3 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Amostras menores que o padrão da base, método não informado e sem histórico verificável.
Em 2026: 6 ondas, efeito de casa de -4,4 p.p. (mede Flávio melhor que a âncora, desvio estatisticamente significativo), erro típico 1,5× o teórico. Coleta por WhatsApp com recrutamento por anúncios e calibração por raking. Em compensação, tem a ficha técnica mais completa do ciclo e prometeu abrir os microdados após o 2º turno.
Em 2026: 5 ondas, efeito de casa de +3,9 p.p. (mede Lula melhor que a âncora, desvio estatisticamente significativo), erro típico 2,7× o teórico. O caso mais difícil da base. Tem boa acurácia histórica (MAE 3,9 p.p.; foi o mais próximo do resultado no 2º turno de 2022) e, em 2026, é o instituto da âncora que mais mede Lula à frente. Ou capta algo que os demais perdem, como em 2022, ou destoou neste ciclo. Poucas ondas, todas presenciais, contratante declarado (CNT).
Em 2026: 5 ondas, efeito de casa de -1,8 p.p., indistinguível de zero, erro típico próximo do teórico. Estreia na base por URA. Sem histórico e com série curta para medir efeito de casa (a nota não muda o peso no modelo: cada onda pesa pelo seu erro amostral e pelo ruído extra da casa).
Em 2026: 3 ondas, efeito de casa de -0,5 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Telefônica (CATI), autofinanciada, sem histórico verificável. O relatório de 16–20/out diz que "consolida levantamentos" e o total geral não fecha com os recortes: nenhum recorte tem mais de 4,1% de "não sabe" por idade (o total diz 8,84%), e o voto por região, ponderado pelos pesos do próprio relatório, dá Lula ~41,6 × Flávio ~39,3, o inverso do total.
Em 2026: 3 ondas, efeito de casa de -0,9 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. Volta à base no 2º turno de 2026 com histórico presidencial verificável (as séries XP/Ipespe de 2018 e 2022), mas mediano: errou a margem do 2º turno por 5,7 p.p. em 2018 e 4,2 em 2022, em direções opostas. Amostra pequena (1.500) e divulgações que divergem sobre o método (recrutamento digital × telefone CATI).
Em 2026: 3 ondas, efeito de casa de -0,3 p.p., indistinguível de zero, erro típico abaixo do esperado pela amostra. A série da Palver depois da mudança de ponderação (v3, 10–13/out em diante): mesma casa e mesmo método, mas efeito de casa estimado só nas ondas novas, por isso aparece à parte. O n efetivo declarado (Kish) é o que entra no erro.
Com 2 ondas, não há série para medir efeito de casa. Duas ondas com amostras de 9 mil que se moveram mais de 10 p.p. entre si em dois meses, o maior descolamento da base em relação ao que a amostra permitiria. Sem histórico e sem método declarado.
Com 2 ondas, não há série para medir efeito de casa. Série curta, sem histórico verificável no Brasil e sem método declarado.
Com 2 ondas, não há série para medir efeito de casa. A série da CNT/MDA depois da mudança de ponderação (v2, 13–15/out em diante): mesma casa, efeito de casa estimado só nas ondas novas, fora da âncora enquanto não tiver série.
Com 2 ondas, não há série para medir efeito de casa. A série da Futura depois da troca da ponderação por escolaridade (cadastro do TSE → PNAD Contínua, onda de 19–20/out em diante): mesma casa, efeito de casa estimado só nas ondas novas.
Com 1 onda, não há série para medir efeito de casa. Estreia na base na véspera do 2º turno: presencial, um dia de campo (23/out), contratante declarado (Brazil Economy) e registro TSE BR-05601/2026. Sem histórico verificável e com uma onda só, não há série para medir efeito de casa.
Correção feita nesta atualização. Ao levantar o histórico para esta seção, encontrei uma falha na calibração do próprio painel: a Paraná Pesquisas entrava com o melhor peso de qualidade da base (menor erro histórico) porque só sua atuação em 2022 estava registrada — mas o instituto errou o 2º turno de 2018 por cerca de 11 p.p., o maior desvio individual da série. O histórico foi completado (Paraná em 2018; Veritá e Futura em 2022), e o peso de qualidade passou a ser encolhido conforme o número de ciclos que o sustenta: com uma única eleição verificada, o instituto fica perto do peso neutro. O nowcast e as probabilidades desta versão já refletem a correção.
Fontes desta seção. Erro total vs. margem declarada: Shirani-Mehr, Rothschild, Goel & Gelman, "Disentangling Bias and Variance in Election Polls", JASA 113(522), 2018. Manada: Nate Silver, "There's more herding in swing state polls", 2024. Método presencial × telefônico: PollingData (base de 4.400 pesquisas registradas no TSE). Efeito de casa e viés de contratante: FiveThirtyEight e JOTA (agregador de house effects). Séries históricas: Congresso em Foco, O Povo, Poder360, Metrópoles, CartaCapital, O Janelão e Wikipédia (pesquisas presidenciais 2014/2018/2022). Regulação e episódios de 2026: TSE (PesqEle, Res. 23.747/2026 e decisões de fev., jun. e jul./2026), Senado (PL 2.558/2022), JOTA, Brasil de Fato e Correio da Manhã. Transparência: AAPOR Transparency Initiative, ABEP e ABRAPEL.
Em uma frase: cada termo técnico usado no painel, explicado sem jargão. Os mesmos textos aparecem ao tocar nas palavras sublinhadas.
| Datafolha (campo) | Ótimo/bom | Regular | Ruim/péssimo | Aprova | Desaprova |
|---|---|---|---|---|---|
| dez/2024 | 35 | 29 | 34 | — | — |
| 10–11 fev/2025 | 24 | 32 | 41 | — | — |
| set/2025 | 33 | — | 38 | 48 | — |
| 2–4 dez/2025 | 32 | — | 37 | 49 | 48 |
| mai–jun/2026 | 32 | 28 | 38 | 49 | 48 |
| 22–23 jul/2026 | 32 | 28 | 38 | 49 | 48 |
| 18–20 ago/2026 | 33 | 25 | 41 | 47 | 50 |
| 1–2 set/2026 | 31 | 25 | 42 | 45 | 51 |
| Genial/Quaest (mês) | jan/25 | mar/25 | mai/25 | jul/25 | set/25 | out/25 | jan/26 | abr/26 | mai/26 | jun/26 | jul/26 | ago/26 | 1 set/26 | 6 set/26 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Aprova | 47 | 41 | 40 | 43 | 46 | 48 | 47 | 43 | 46 | 47 | 48 | 46 | 45 | — |
| Desaprova | — | — | 57 | 53 | 51 | 49 | 49 | 52 | 49 | 48 | 47 | 48 | 48 | 50 |
| Outros institutos | Campo | Aprova | Desaprova | Ót-bom / reg / ruim-pés | Observação |
|---|---|---|---|---|---|
| AtlasIntel/Bloomberg | 10–15 dez/25 → jun/26 → 22–27 jul → 25–30 ago | 48,8 → 46 → 47,6 → 45,4 | 50,7 → 52 → 51,2 → 52,9 | 37,0 / 11,9 / 51,1 (ago) | Desaprovação: evangélicos 76%, jovens 16–24 77%, Sul 68%. Aprovação: NE 55–67%, 60+ 63%, católicos 55% |
| PoderData/Aya | mai → jun → jul → set/26 | 44 → 43 → 42 → 42 | 50 → 52 → 51 → 51 | 34 / 15 / 48 (set) | Série mais baixa para Lula, coerente com o efeito de casa de -2,4 no 2º turno |
| Ipsos-Ipec | set/25 → dez/25 → mar/26 → jun/26 | 44 → 42 → 43 → 44 | 51 → 52 → 51 → 50 | 32 / 28 / 38 (jun) | Confiança em Lula 40% / desconfiança 56% (mar) |
| BTG/Nexus · RTBD · CNT/MDA | ago/26 · ago/26 · nov/25 | 48 · 45 · 48,1 | 49 · 51 · 48,8 | 35/—/43 · 27/33/39 · 34/29/36 | — |
| Indicador econômico | Último dado | Leitura eleitoral |
|---|---|---|
| IPCA | jul/26: +0,07% m/m; 4,44% em 12 m (abaixo do teto após 2 meses); alimentação no domicílio −1,14%. IPCA-15 ago: −0,40% (4,24% em 12 m); alimentos −0,57%; energia −6,25% (bônus Itaipu) | Deflação de alimentos jun–ago ainda não convertida em percepção (49% dizem que a economia piorou). Alimentos acumulavam 7,4% em ago/25 |
| Desemprego (PNAD) | mai–jul/26: 5,3% (menor da série); ocupados 103,3 mi; CLT privado 39,4 mi (recordes); informalidade 37,5% | Ativo do incumbente — mas Nunes: 64% dizem que a renda subiu e só 10% que superou o custo de vida |
| Rendimento real | R$ 3.762; +3,3% a/a; −0,7% no trimestre; massa de renda R$ 383,5 bi (+4,1%) | Ganho real modesto e desacelerando |
| Selic | 15% → cortes de 0,25 desde mar/26 → 14,00% (5/ago); Copom 15–16/set; Focus 13,75% fim-2026 | Ciclo de corte lento; efeito sobre crédito das famílias ainda pequeno |
| PIB | 2025: +2,3%. 2T26: +0,5% t/t, +2,0% a/a; consumo das famílias −0,4% t/t; Focus 2026: 1,92% (em queda) | Desaceleração do consumo no 2º semestre é o principal risco econômico para o incumbente |
| Confiança do consumidor (FGV) | ago/26: 84,7 (−3,6), menor desde nov/2022; 4ª queda seguida; expectativas 86,1 | Famílias endividadas e inadimplentes (FGV) — o "sentido" descola do "medido" |
| Mercado / câmbio | USD/BRL R$ 5,09 (8/set); Ibovespa ≈188 mil; saída de R$ 15,6 bi de estrangeiros da B3 em ago; JPMorgan rebaixou Brasil a neutro (11/ago) | "Trade eleitoral": bolsa sobe e real se valoriza quando Flávio avança — Eurasia e Morgan Stanley leem como subestimação de risco |
| Fiscal | Dívida bruta 81,9% do PIB (jun/26); déficit primário 1,19%; nominal 9,99% (Morgan Stanley) | Morgan Stanley: o decisivo é a credibilidade fiscal pós-2026, não o vencedor |
| Sentimento | Quaest ago: piorou 49 / igual 30 / melhorou 19; próximos 12 m: melhora 42. Datafolha jun: piorou 45 / melhorou 22; pessimistas 26% (eram 35 em mar). Problema nº 1: violência 33%, economia 15%, saúde 14%, corrupção 14% | Segurança supera economia como preocupação — terreno de Flávio (Garman: Lula tem "baixa credibilidade em segurança e corrupção") |
| Fonte | Data | P(Lula) | P(Flávio) | Observação |
|---|---|---|---|---|
| Polymarket | 17/set/26 | 45% | 55% | Estável desde a sessão do STF de 15/set; volume US$ 150,7 mi; P(Lula) normalizada ≈ 45% |
| Polymarket | 16/set/26 | 45% | 54% | Depois da sessão do STF terminar sem decisão de mérito (pedido de vista de Dino); volume US$ 150,6 mi; P(Lula) normalizada ≈ 45% |
| Polymarket | 15/set/26 | 45% | 52% | Dia da sessão extraordinária do STF sobre o relatório Moraes–Vorcaro; volume acumulado US$ 150,1 mi; P(Lula) normalizada ≈ 46% |
| Polymarket | 13/set/26 | 49% | 50,2% | Virada em relação a 8/set (Lula 59%); volume acumulado US$ 149,4 mi desde a abertura do mercado (set/2025); reprecificação atribuída à crise do STF/comando da PF e à quebra parcial de sigilo do Master (9–10/set) |
| Kalshi | 10/set/26 | ≈49% | ≈51% | Manchete do mercado: "Flávio Bolsonaro Hits 51% as Supreme Court Clash Rattles Lula" — atribui a virada ao "judiciary standoff" e a cortes emergenciais de combustível. Antes de 8/set: Lula 56%; vencedor do 1º turno (leitura de 8/set): Lula 89%; 2º lugar: Flávio 88%; margem de Lula 0–5 pts 44%, 5–10 pts 38% |
| Polymarket/Kalshi (8/set, pré-crise) | 8/set/26 | 59% · 56% | 39,9% · 44% | Leitura anterior à crise do STF de 8–10/set, mantida aqui para comparação histórica; Predict.fun/Prediction Hunt média 57–58% |
| Eurasia Group (Garman) | set/25 → abr/26 → jul/26 → ago/26 | 50-50 (viés Lula) → 55% → 60% → ~60% | ~40% | Mai/26: 30% de chance de outro nome da direita no 2º turno. Ago/26: "mercado subestima Flávio"; clientes que veem Lula favorito passaram de 47% para 80% |
| XP Research | jul/26 | "leve favoritismo" Lula | — | Eleição decidida por 3–4% de indecisos, 69% mulheres; abstenção crescente penaliza a coalizão de Lula |
| Morgan Stanley (126 p.) | 25/ago/26 | sem P | — | Cenários 2027: base PIB 0,8% / Selic 12,75 / USD 5,25; pessimista 0,2% / 15,5% / 6,00 (Ibov −25%); otimista 1,5% / 9,75% / 4,50; riscos inclinados ao pessimista |
| BTG Pactual · JPMorgan | mar–ago/26 | sem P | — | BTG: carteiras por cenário (Lula: SUZB3, ITUB4, WEGE3; Flávio: BBAS3, XP, PETR4). JPM rebaixou ações brasileiras a neutro |
| "Brasil em Mapas" (nowcast Monte Carlo, 15 pesquisas) | 7/set/26 | 74% | 26% | Central 51,5 × 48,5 válidos; sem correção de viés histórico — compare com os 27% do nosso modelo sem correção |
| Painel de 6 modelos de IA (OddsShopper) | 6/ago/26 | 68% | 23,8% | "1 em 3 de chance de idade, virada de campanha ou consolidação mudar o vencedor" |
| Este painel | 25/10/2026 | modelo 17% · ensemble 18% | 83% · 82% | Único com correção explícita do viés 2006–22 e com tendência estimada (suavizador de Kalman); Itaú, Bradesco BBI, Genial, Warren, Goldman, Barclays, Economist e FT não publicaram base case com probabilidade |
| Segmento | Lula | Flávio | Fonte |
|---|---|---|---|
| Evangélicos (~30% do eleitorado) | 29% | 62% | Datafolha 18–20/ago (2º turno); Atlas: 76% desaprovam Lula; Quaest 1º turno 43 × 24 |
| Católicos | 54% | 36% | Datafolha ago; Quaest 1º turno 47 × 27 |
| Mulheres · Homens | 51% · 43% | 38% · 48% | Datafolha ago; PoderData set: mulheres 48 × 41; desaprovação masculina 50 → 57 (Quaest jul→set) |
| Nordeste · Sudeste · Sul · CO/N | 61 · 43 · 38 · 42 | 30 · 46 · 50 · 49 | Datafolha ago. Quaest estadual (abr): Flávio +26 RS, +20 PR, +13 GO/RJ, +12 SP; Lula +34 PE, +33 BA, +28 CE, +7 PA, +3 MG (empate) |
| Renda até 2 SM · 2–5 SM · >5 SM | maioria · 37 · 39 | — · 51 · 54 | Datafolha ago; Atlas: aprovação 55,6% até R$ 2 mil |
| Independentes / sem partido | 38,2% | 26% | AtlasIntel ago (Cury 19,2%); Roman: "só 10% são de fato indecisos — desinteressados, classe C, mais mulheres, buscam o novo" |
| Base sólida ("voto definitivo") | 29–33% | 24% | Nunes/Quaest 7/set; indecisos querem outsiders (20%) ou moderados (17%) |
| Ciclo | Simulação de 2º turno em setembro | Resultado | Deriva da margem | Pesquisas da véspera × resultado |
|---|---|---|---|---|
| 2006 | Lula ≈ +20 (Datafolha set apontava vitória no 1º turno com 56% válidos) | Lula 60,8 × 39,2 (+21,7) | ≈ +2 | Datafolha 61 × 39: erro +0,2 |
| 2010 | Dilma 55 × Serra 38 (Datafolha 21–22/set; +17) | Dilma 56,1 × 43,9 (+12,1) | −5 | Datafolha 56 × 44 (0) · Ibope 55 × 45 (−1) |
| 2014 | Marina 50 × Dilma 40 (ago); Dilma 49 × Aécio 41 (30/set; +8) | Dilma 51,6 × Aécio 48,4 (+3,3) | −5 (e a líder de ago nem chegou ao 2º turno) | Datafolha 52 × 48 (+0,7) · Ibope 53 × 47 (+2,2) |
| 2018 | Haddad 45 × Bolsonaro 39 (Datafolha 26–28/set; +6 Haddad) | Bolsonaro 55,1 × 44,9 (+10,3) | −16 | Datafolha 45 × 55 (+0,3) · Ibope 46 × 54 (+2,3) · MDA (−3,3) · Vox 50 × 50 (+10,3) |
| 2022 | Lula 54 × Bolsonaro 38 (Datafolha 20–22/set; +16); 26/set–1/out: +5,3 a +18,3 | Lula 50,9 × 49,1 (+1,8) | −14 | Paraná +0,8 · MDA +2,2 · Datafolha +4 · Quaest/Atlas/PoderData/Ipespe +6 · Ipec +8 → erro médio +3,1 |
| 2026 | Lula -1,3 (nowcast 25/10/2026) · -2,0 há duas semanas | ? | modelo: deriva N(0, 0,0) | Break-even de Lula na véspera: +0,8 |
| Instituto | MAE (p.p.) | Viés médio | Ativo em 2026 | Peso de qualidade |
|---|---|---|---|---|
| Futura Inteligencia | 2,4 | -2,4 | sim | 0,85 |
| Datafolha | 3,4 | +3,4 | sim | 1,32 |
| Verita | 3,8 | -3,8 | sim | 0,76 |
| Paraná Pesquisas | 3,8 | -2,1 | sim | 0,78 |
| MDA | 3,9 | +1,5 | sim | 1,32 |
| Quaest | 4,0 | +4,0 | sim | 1,15 |
| Ibope | 4,5 | +4,5 | não | — |
| PoderData | 4,5 | +4,5 | sim | 1,12 |
| AtlasIntel | 4,5 | +4,5 | sim | 0,93 |
| Ipespe | 4,9 | +2,0 | sim | 0,93 |
| Ipec | 7,5 | +7,5 | não | — |
| Vox Populi | 7,6 | +7,6 | não | — |
| Brasmarket | 7,8 | -7,8 | não | — |
O erro dos 2ºs turnos não tem direção fixa: pró-PT em 2014, 2022 e, por pouco, 2006; contra o PT em 2010 e 2018 (média por ciclo: 2006: +0,3, 2010: -0,1, 2014: +1,1, 2018: -1,2, 2022: +2,9). Nos 1ºs turnos de 2010 a 2022, ao contrário, o erro foi sempre contra o principal adversário do PT (2010: +5,2, 2014: +10,5, 2018: +2,4, 2022: +5,8). Por isso o desconto das rodadas finais de 2º turno é pequeno (+0,9 ± 2,0) e o do 1º turno é maior (+3,4 ± 3,8); erro observado das pesquisas no 1º turno, nas ondas de antes de 4/out, e rodadas finais de 2º turno de 2006–2022 mais 25% desse erro, nas ondas novas. Em 2026, com a base completa (221 ondas) e a âncora nos institutos calibrados, os efeitos de casa estatisticamente distintos de zero são: medem Lula melhor que a âncora CNT/MDA (+3,9); medem Flávio melhor Gerp (-7,7), Veritá (-5,3), Palver (-4,4), Futura (-3,8), PoderData (-2,4), Paraná Pesquisas (-2,0).
| Instituto | Eleição | Última pesquisa (esq × dir, válidos) | Resultado | Erro da margem | Fonte · verificação |
|---|---|---|---|---|---|
| AtlasIntel | 2022 · 2º turno | 53,4 × 46,6 | 50,9 × 49,1 | +5,0 | fonte · sim |
| Brasmarket (fora da referência) | 2022 · 2º turno | 47 × 53 | 50,9 × 49,1 | -7,8 | fonte · sim |
| Datafolha | 2022 · 2º turno | 52 × 48 | 50,9 × 49,1 | +2,2 | fonte · sim |
| Futura Inteligencia (fora da referência) | 2022 · 2º turno | 49,7 × 50,3 | 50,9 × 49,1 | -2,4 | fonte · sim |
| Ipec | 2022 · 2º turno | 54 × 46 | 50,9 × 49,1 | +6,2 | fonte · sim |
| Ipespe | 2022 · 2º turno | 53 × 47 | 50,9 × 49,1 | +4,2 | fonte · sim |
| CNT/MDA | 2022 · 2º turno | 51,1 × 48,9 | 50,9 × 49,1 | +0,4 | fonte · sim |
| Paraná Pesquisas | 2022 · 2º turno | 50,4 × 49,6 | 50,9 × 49,1 | -1,0 | fonte · sim |
| PoderData | 2022 · 2º turno | 53 × 47 | 50,9 × 49,1 | +4,2 | fonte · sim |
| Quaest | 2022 · 2º turno | 52 × 48 | 50,9 × 49,1 | +2,2 | fonte · sim |
| Verita (fora da referência) | 2022 · 2º turno | 49 × 51 | 50,9 × 49,1 | -3,8 | fonte · sim |
| AtlasIntel | 2022 · 1º turno | 50,3 × 41,1 | 48,4 × 43,2 | +4,0 | fonte · sim |
| Datafolha | 2022 · 1º turno | 50 × 36 | 48,4 × 43,2 | +8,8 | fonte · sim |
| Ipec | 2022 · 1º turno | 51 × 37 | 48,4 × 43,2 | +8,8 | fonte · sim |
| Ipespe | 2022 · 1º turno | 49 × 35 | 48,4 × 43,2 | +8,8 | fonte · sim |
| CNT/MDA | 2022 · 1º turno | 48,3 × 39,7 | 48,4 × 43,2 | +3,4 | fonte · sim |
| Paraná Pesquisas | 2022 · 1º turno | 47,1 × 40 | 48,4 × 43,2 | +1,9 | fonte · sim |
| PoderData | 2022 · 1º turno | 48 × 38 | 48,4 × 43,2 | +4,8 | fonte · sim |
| Quaest | 2022 · 1º turno | 49 × 38 | 48,4 × 43,2 | +5,8 | fonte · sim |
| Datafolha | 2018 · 2º turno | 45 × 55 | 44,9 × 55,1 | +0,3 | fonte · sim |
| Ibope | 2018 · 2º turno | 46 × 54 | 44,9 × 55,1 | +2,3 | fonte · sim |
| Ipespe | 2018 · 2º turno | 42 × 58 | 44,9 × 55,1 | -5,7 | fonte · sim |
| CNT/MDA | 2018 · 2º turno | 43,2 × 56,8 | 44,9 × 55,1 | -3,3 | fonte · resumo de busca |
| Paraná Pesquisas | 2018 · 2º turno | 39,4 × 60,6 | 44,9 × 55,1 | -10,9 | fonte · sim |
| Vox Populi | 2018 · 2º turno | 50 × 50 | 44,9 × 55,1 | +10,3 | fonte · sim |
| Datafolha | 2018 · 1º turno | 25 × 40 | 29,3 × 46,0 | +1,8 | fonte · sim |
| Ibope | 2018 · 1º turno | 25 × 41 | 29,3 × 46,0 | +0,8 | fonte · sim |
| Ipespe | 2018 · 1º turno | 25 × 41 | 29,3 × 46,0 | +0,8 | fonte · sim |
| CNT/MDA | 2018 · 1º turno | 27,8 × 42,6 | 29,3 × 46,0 | +1,9 | fonte · resumo de busca |
| Paraná Pesquisas | 2018 · 1º turno | 25,6 × 40,9 | 29,3 × 46,0 | +1,4 | fonte · resumo de busca |
| Vox Populi | 2018 · 1º turno | 31 × 40 | 29,3 × 46,0 | +7,8 | fonte · sim |
| Datafolha | 2014 · 2º turno | 52 × 48 | 51,6 × 48,4 | +0,7 | fonte · sim |
| Ibope | 2014 · 2º turno | 53 × 47 | 51,6 × 48,4 | +2,7 | fonte · sim |
| CNT/MDA | 2014 · 2º turno | 49,7 × 50,3 | 51,6 × 48,4 | -3,9 | fonte · sim |
| Vox Populi | 2014 · 2º turno | 54 × 46 | 51,6 × 48,4 | +4,7 | fonte · sim |
| Datafolha | 2014 · 1º turno | 44 × 26 | 41,6 × 33,5 | +10,0 | fonte · sim |
| Ibope | 2014 · 1º turno | 46 × 27 | 41,6 × 33,5 | +11,0 | fonte · sim |
| CNT/MDA | 2014 · 1º turno | 45,6 × 27 | 41,6 × 33,5 | +10,6 | fonte · sim; válidos calculados dos totais 40.6/24.0 com 11% de brancos+indecisos |
| Datafolha | 2010 · 2º turno | 56 × 44 | 56,0 × 44,0 | -0,1 | fonte · wikipedia |
| Datafolha | 2010 · 1º turno | 50 × 31 | 46,9 × 32,6 | +4,7 | fonte · sim; 2ª fonte: en.wikipedia Opinion polling for the 2010 Brazilian presidential election |
| Ibope | 2010 · 1º turno | 51 × 31 | 46,9 × 32,6 | +5,7 | fonte · sim; 2ª fonte: en.wikipedia Opinion polling for the 2010 Brazilian presidential election |
| Datafolha | 2006 · 2º turno | 61 × 39 | 60,8 × 39,2 | +0,3 | fonte · wikipedia |
| Datafolha (fora da referência) | 2006 · 1º turno | 50 × 38 | 48,6 × 41,6 | +5,0 | fonte · fonte única (a Wikipédia traz 48 x 37 sem dizer se são válidos); fora do cálculo |
| Instituto | Contratante | Campo (fim) | n | Método | Margem de erro (95%) | Lula | Flávio | Br/nulo | Indec. | Margem válida | Peso | Fonte |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Futura Inteligencia v2 | Apex/100% Cidades | 24/10/26 | 2.000 | telefônico | ±2,2 p.p. | 47,9 | 48,2 | 2,3 | 1,6 | -0,3 | 0,96 | fonte |
| PoderData | Aya | 24/10/26 | 4.000 | telefônico | ±1,5 p.p. | 47,0 | 49,0 | 3,0 | 1,0 | -2,1 | 1,73 | fonte |
| BTG/Nexus | BTG Pactual | 24/10/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 47,0 | 5,2 | 1,8 | -1,1 | 0,93 | fonte |
| Quaest | Globo/O Globo | 24/10/26 | 3.702 | presencial | ±1,6 p.p. | 45,0 | 46,0 | 8,0 | 1,0 | -1,1 | 1,56 | fonte |
| Datafolha | Folha/Globo | 24/10/26 | 4.050 | presencial | ±1,5 p.p. | 47,0 | 48,0 | 3,0 | 2,0 | -1,1 | 1,73 | fonte |
| Palver v3 | próprio | 24/10/26 | 5.000 | online | ±1,4 p.p. | 49,0 | 50,0 | 1,0 | 0,0 | -1,0 | 0,89 | fonte |
| Gerp | próprio | 24/10/26 | 2.400 | telefônico | ±2,0 p.p. | 46,0 | 49,0 | 1,0 | 3,0 | -3,2 | 1,12 | fonte |
| Colectta | Brazil Economy | 23/10/26 | 3.000 | presencial | ±1,8 p.p. | 46,5 | 46,0 | 4,4 | 3,1 | +0,5 | 1,28 | fonte |
| Verita | próprio | 23/10/26 | 15.554 | telefônico | ±0,8 p.p. | 46,5 | 49,4 | 1,4 | 2,7 | -3,1 | 1,40 | fonte |
| AtlasIntel | Bloomberg | 23/10/26 | 4.990 | online | ±1,4 p.p. | 48,4 | 49,9 | 1,7 | — | -1,5 | 1,15 | fonte |
| MDA v2 | CNT | 23/10/26 | 2.032 | presencial | ±2,2 p.p. | 45,0 | 46,3 | 5,8 | 2,9 | -1,4 | 0,90 | fonte |
| Instituto Franca | próprio | 23/10/26 | 1.537 | não informado | ±2,5 p.p. | 43,1 | 45,6 | 7,4 | 3,9 | -2,8 | 0,68 | fonte |
| Alfa Inteligencia | próprio | 22/10/26 | 2.700 | presencial | ±1,9 p.p. | 46,0 | 49,9 | 2,0 | 2,1 | -4,1 | 1,06 | fonte |
| Datafolha | Folha/Globo | 22/10/26 | 2.520 | presencial | ±2,0 p.p. | 46,0 | 47,0 | 5,0 | 2,0 | -1,1 | 1,07 | fonte |
| Vox Brasil | próprio | 22/10/26 | 2.100 | presencial | ±2,1 p.p. | 45,4 | 44,3 | 6,0 | 4,3 | +1,2 | 0,73 | fonte |
| Real Time Big Data | próprio | 22/10/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 49,0 | 5,0 | 1,0 | -4,3 | 0,88 | fonte |
| Verita | próprio | 22/10/26 | 15.554 | telefônico | ±0,8 p.p. | 47,5 | 50,2 | 1,4 | 0,9 | -2,8 | 1,37 | fonte |
| Ipespe | Abrapel | 22/10/26 | 1.500 | telefônico | ±2,5 p.p. | 47,0 | 48,0 | 3,0 | 2,0 | -1,1 | 0,69 | fonte |
| DataTrends | próprio | 21/10/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 50,0 | 4,0 | 1,0 | -4,2 | 0,87 | fonte |
| Indexa | Broadcast/Estadão | 21/10/26 | 2.000 | telefônico | ±2,2 p.p. | 44,1 | 45,2 | 9,1 | 1,6 | -1,2 | 0,82 | fonte |
| PoderData | Aya | 21/10/26 | 4.000 | telefônico | ±1,5 p.p. | 48,0 | 49,0 | 3,0 | 1,0 | -1,0 | 1,58 | fonte |
| Gerp | próprio | 21/10/26 | 2.400 | telefônico | ±2,0 p.p. | 45,0 | 50,0 | 2,0 | 2,0 | -5,3 | 1,01 | fonte |
| Futura Inteligencia v2 | Apex/100% Cidades | 20/10/26 | 2.000 | telefônico | ±2,2 p.p. | 46,8 | 48,9 | 2,0 | 2,3 | -2,2 | 0,84 | fonte |
| Quaest | Globo/O Globo | 20/10/26 | 2.154 | presencial | ±2,1 p.p. | 44,0 | 45,0 | 9,0 | 2,0 | -1,1 | 0,84 | fonte |
| Palver v3 | próprio | 20/10/26 | 5.000 | online | ±1,4 p.p. | 48,0 | 50,0 | 1,0 | 0,0 | -2,0 | 0,77 | fonte |
| Instituto Franca | próprio | 20/10/26 | 3.000 | telefônico | ±1,8 p.p. | 39,6 | 41,0 | 10,6 | 8,8 | -1,6 | 1,03 | fonte |
| Alfa Inteligencia | próprio | 19/10/26 | 2.700 | presencial | ±1,9 p.p. | 45,9 | 48,8 | 3,5 | 1,8 | -3,1 | 0,95 | fonte |
| Real Time Big Data | próprio | 19/10/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 48,0 | 5,0 | 2,0 | -3,2 | 0,79 | fonte |
| Gerp | próprio | 19/10/26 | 2.400 | telefônico | ±2,0 p.p. | 45,0 | 50,0 | 2,0 | 3,0 | -5,3 | 0,95 | fonte |
| Meio/Ideia | Canal Meio | 19/10/26 | 1.500 | telefônico | ±2,5 p.p. | 46,0 | 48,0 | 3,4 | 2,6 | -2,1 | 0,62 | fonte |
| Verita | próprio | 19/10/26 | 15.554 | telefônico | ±0,8 p.p. | 48,2 | 49,7 | 1,4 | 0,7 | -1,5 | 1,24 | fonte |
| AtlasIntel | Bloomberg | 18/10/26 | 4.998 | online | ±1,4 p.p. | 47,6 | 49,8 | 2,6 | — | -2,3 | 0,97 | fonte |
| PoderData | Aya | 18/10/26 | 3.000 | telefônico | ±1,8 p.p. | 46,0 | 49,0 | 4,0 | 2,0 | -3,2 | 1,11 | fonte |
| BTG/Nexus | BTG Pactual | 18/10/26 | 2.002 | telefônico | ±2,2 p.p. | 45,9 | 46,5 | 4,7 | 2,9 | -0,6 | 0,76 | fonte |
| DataTrends | próprio | 17/10/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 49,0 | 4,0 | 1,0 | -3,2 | 0,76 | fonte |
| Futura Inteligencia | Apex/100% Cidades | 17/10/26 | 2.000 | telefônico | ±2,2 p.p. | 47,5 | 48,3 | 2,4 | 1,9 | -0,8 | 0,76 | fonte |
| Vox Brasil | próprio | 17/10/26 | 2.100 | presencial | ±2,1 p.p. | 46,4 | 44,6 | 5,0 | 3,9 | +2,0 | 0,63 | fonte |
| Verita | próprio | 16/10/26 | 15.554 | telefônico | ±0,8 p.p. | 47,7 | 49,7 | 1,5 | 1,1 | -2,1 | 1,12 | fonte |
| Gerp | próprio | 16/10/26 | 2.400 | telefônico | ±2,0 p.p. | 45,0 | 50,0 | 2,0 | 3,0 | -5,3 | 0,86 | fonte |
| MDA v2 | CNT | 15/10/26 | 2.014 | presencial | ±2,2 p.p. | 45,9 | 46,8 | 4,9 | 2,4 | -1,0 | 0,70 | fonte |
| PoderData | Aya | 15/10/26 | 3.000 | telefônico | ±1,8 p.p. | 47,0 | 47,0 | 4,0 | 2,0 | 0,0 | 1,00 | fonte |
| Futura Inteligencia | Apex/100% Cidades | 15/10/26 | 2.000 | telefônico | ±2,2 p.p. | 47,5 | 49,0 | 1,5 | 2,1 | -1,6 | 0,72 | fonte |
| Ipespe | Abrapel | 15/10/26 | 1.500 | telefônico | ±2,5 p.p. | 46,0 | 48,0 | 3,0 | 3,0 | -2,1 | 0,54 | fonte |
| Indexa | Broadcast/Estadão | 14/10/26 | 2.000 | telefônico | ±2,2 p.p. | 42,0 | 44,0 | 12,0 | 2,0 | -2,3 | 0,63 | fonte |
| Datafolha | Folha/Globo | 14/10/26 | 2.520 | presencial | ±2,0 p.p. | 45,0 | 47,0 | 6,0 | 2,0 | -2,2 | 0,82 | fonte |
| Palver v3 | próprio | 13/10/26 | 5.000 | online | ±1,4 p.p. | 49,0 | 50,0 | 1,0 | 0,0 | -1,0 | 0,62 | fonte |
| Quaest | Globo/O Globo | 13/10/26 | 2.154 | presencial | ±2,1 p.p. | 44,0 | 46,0 | 9,0 | 1,0 | -2,2 | 0,67 | fonte |
| Gerp | próprio | 12/10/26 | 2.400 | telefônico | ±2,0 p.p. | 44,0 | 53,0 | 1,0 | 3,0 | -9,3 | 0,77 | fonte |
| PoderData | Aya | 12/10/26 | 3.000 | telefônico | ±1,8 p.p. | 46,0 | 48,0 | 5,0 | 2,0 | -2,1 | 0,90 | fonte |
| BTG/Nexus | BTG Pactual | 12/10/26 | 2.002 | telefônico | ±2,2 p.p. | 45,0 | 48,0 | 5,0 | 2,0 | -3,2 | 0,63 | fonte |
| Futura Inteligencia | Apex/100% Cidades | 10/10/26 | 2.000 | telefônico | ±2,2 p.p. | 46,6 | 50,0 | 1,7 | 1,7 | -3,5 | 0,61 | fonte |
| DataTrends | próprio | 09/10/26 | 2.000 | telefônico | ±2,2 p.p. | 44,0 | 50,0 | 5,0 | 1,0 | -6,4 | 0,58 | fonte |
| Ipespe | próprio | 08/10/26 | 1.500 | telefônico | ±2,5 p.p. | 44,0 | 49,0 | 5,0 | 3,0 | -5,4 | 0,42 | fonte |
| AtlasIntel | Bloomberg | 08/10/26 | 5.026 | online | ±1,4 p.p. | 45,7 | 51,1 | 3,2 | — | -5,6 | 0,70 | fonte |
| PoderData | Aya | 07/10/26 | 3.000 | telefônico | ±1,8 p.p. | 44,0 | 49,0 | 5,0 | 2,0 | -5,4 | 0,76 | fonte |
| Datafolha | Folha/Globo | 07/10/26 | 2.520 | presencial | ±2,0 p.p. | 45,0 | 49,0 | 5,0 | 1,0 | -4,3 | 0,66 | fonte |
| Vox Brasil | próprio | 07/10/26 | 2.100 | presencial | ±2,1 p.p. | 44,2 | 42,7 | 3,4 | 9,7 | +1,7 | 0,43 | fonte |
| Futura Inteligencia | Apex/100% Cidades | 03/10/26 | 2.000 | telefônico | ±2,2 p.p. | 45,1 | 48,0 | 5,7 | 1,2 | -3,1 | 0,47 | fonte |
| Quaest | Globo/O Globo | 03/10/26 | 3.702 | presencial | ±1,6 p.p. | 42,0 | 44,0 | 13,0 | 1,0 | -2,3 | 0,74 | fonte |
| Datafolha | Folha/Globo | 03/10/26 | 4.006 | presencial | ±1,5 p.p. | 47,0 | 46,0 | 6,0 | 2,0 | +1,1 | 0,84 | fonte |
| Palver | próprio | 03/10/26 | 5.000 | online | ±1,4 p.p. | 44,0 | 49,0 | 6,0 | 1,0 | -5,4 | 0,40 | fonte |
| AtlasIntel | próprio | 02/10/26 | 4.945 | online | ±1,4 p.p. | 47,6 | 47,4 | 5,0 | — | +0,2 | 0,56 | fonte |
| Instituto Franca | próprio | 02/10/26 | 2.000 | não informado | ±2,2 p.p. | 45,9 | 45,0 | 3,5 | 5,6 | +1,0 | 0,44 | fonte |
| MDA | CNT | 02/10/26 | 2.007 | presencial | ±2,2 p.p. | 47,3 | 43,1 | 7,6 | 1,9 | +4,6 | 0,44 | fonte |
| PoderData | Aya | 02/10/26 | 4.000 | telefônico | ±1,5 p.p. | 46,0 | 46,0 | 7,0 | 1,0 | 0,0 | 0,81 | fonte |
| Gerp | próprio | 02/10/26 | 2.400 | telefônico | ±2,0 p.p. | 44,0 | 49,0 | 6,0 | 1,0 | -5,4 | 0,53 | fonte |
| Datafolha | Folha/Globo | 01/10/26 | 2.506 | presencial | ±2,0 p.p. | 48,0 | 45,0 | 6,0 | 1,0 | +3,2 | 0,53 | fonte |
| Vox Brasil | próprio | 01/10/26 | 2.100 | não informado | ±2,1 p.p. | 45,2 | 48,2 | 2,5 | 4,1 | -3,2 | 0,38 | fonte |
| Real Time Big Data | CNN | 30/09/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 46,0 | 5,0 | 4,0 | -1,1 | 0,41 | fonte |
| DataTrends | próprio | 30/09/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 45,0 | 7,0 | 3,0 | 0,0 | 0,41 | fonte |
| Futura Inteligencia | Apex/100% Cidades | 29/09/26 | 2.000 | telefônico | ±2,2 p.p. | 43,5 | 49,0 | 6,2 | 1,3 | -5,9 | 0,41 | fonte |
| Indexa | Broadcast/Estadão | 29/09/26 | 2.000 | telefônico | ±2,2 p.p. | 43,0 | 42,0 | 7,0 | 4,0 | +1,2 | 0,38 | fonte |
| Vox Brasil | próprio | 28/09/26 | 2.100 | presencial | ±2,1 p.p. | 44,7 | 45,2 | 6,8 | 3,3 | -0,6 | 0,33 | fonte |
| Gerp | próprio | 28/09/26 | 2.400 | telefônico | ±2,0 p.p. | 43,0 | 50,0 | 5,0 | 1,0 | -7,5 | 0,47 | fonte |
| Meio/Ideia | Canal Meio | 28/09/26 | 2.000 | telefônico | ±2,2 p.p. | 48,5 | 48,0 | 2,1 | 1,5 | +0,5 | 0,41 | fonte |
| AtlasIntel | Bloomberg | 28/09/26 | 5.005 | online | ±1,4 p.p. | 47,6 | 47,7 | 4,8 | — | -0,1 | 0,49 | fonte |
| Palver | próprio | 27/09/26 | 5.000 | online | ±1,4 p.p. | 45,0 | 47,0 | 7,0 | 1,0 | -2,2 | 0,31 | fonte |
| BTG/Nexus | BTG | 27/09/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 44,0 | 8,0 | 1,0 | +2,2 | 0,37 | fonte |
| Quaest | Globo/O Globo | 27/09/26 | 2.004 | presencial | ±2,2 p.p. | 42,0 | 42,0 | 13,0 | 3,0 | 0,0 | 0,35 | fonte |
| PoderData | Aya | 23/09/26 | 3.000 | telefônico | ±1,8 p.p. | 45,0 | 46,0 | — | — | -1,1 | 0,47 | fonte |
| Real Time Big Data | — | 23/09/26 | 2.000 | telefônico | ±2,2 p.p. | 44,0 | 45,0 | 8,0 | 3,0 | -1,1 | 0,32 | fonte |
| Datafolha | Folha/Globo | 23/09/26 | 2.002 | presencial | ±2,2 p.p. | 47,0 | 45,0 | 7,0 | 1,0 | +2,2 | 0,33 | fonte |
| Futura Inteligencia | Apex | 23/09/26 | 2.000 | telefônico | ±2,2 p.p. | 43,7 | 49,4 | 5,6 | 1,3 | -6,1 | 0,34 | fonte |
| Palver | próprio | 23/09/26 | 5.000 | online | ±1,4 p.p. | 45,0 | 48,0 | 6,0 | 1,0 | -3,2 | 0,27 | fonte |
| Alfa Inteligencia | TMC | 23/09/26 | 2.700 | presencial | ±1,9 p.p. | 43,0 | 43,0 | 7,0 | 7,0 | 0,0 | 0,37 | fonte |
| AtlasIntel | Bloomberg | 22/09/26 | 5.015 | online | ±1,4 p.p. | 47,7 | 47,4 | 4,9 | — | +0,3 | 0,40 | fonte |
| American Analytics | Times Brasil/CNBC | 20/09/26 | 2.000 | não informado | ±2,2 p.p. | 43,0 | 43,0 | 6,0 | 8,0 | 0,0 | 0,28 | fonte |
| Quaest | Globo/O Globo | 20/09/26 | 2.004 | presencial | ±2,2 p.p. | 41,0 | 42,0 | 14,0 | 3,0 | -1,2 | 0,28 | fonte |
| BTG/Nexus | BTG | 20/09/26 | 2.006 | telefônico | ±2,2 p.p. | 46,0 | 45,0 | 7,0 | 1,0 | +1,1 | 0,30 | fonte |
| Palver | próprio | 18/09/26 | 5.000 | online | ±1,4 p.p. | 43,0 | 47,0 | 10,0 | — | -4,4 | 0,18 | fonte |
| PoderData | Aya | 16/09/26 | 3.000 | telefônico | ±1,8 p.p. | 44,0 | 46,0 | 8,0 | 2,0 | -2,2 | 0,37 | fonte |
| Gerp | próprio | 16/09/26 | 2.400 | telefônico | ±2,0 p.p. | 43,0 | 50,0 | 6,0 | 1,0 | -7,5 | 0,32 | fonte |
| Datafolha | Folha/Globo | 16/09/26 | 2.002 | presencial | ±2,2 p.p. | 46,0 | 44,0 | 8,0 | 1,0 | +2,2 | 0,26 | fonte |
| AtlasIntel | Bloomberg | 16/09/26 | 5.018 | online | ±1,4 p.p. | 46,8 | 47,2 | 6,0 | — | -0,4 | 0,33 | fonte |
| Futura Inteligencia | Apex | 15/09/26 | 2.000 | telefônico | ±2,2 p.p. | 43,7 | 48,1 | 6,5 | 1,7 | -4,8 | 0,26 | fonte |
| DataTrends | próprio | 14/09/26 | 2.000 | telefônico | ±2,2 p.p. | 44,0 | 45,0 | 8,0 | 3,0 | -1,1 | 0,24 | fonte |
| Quaest | Globo/O Globo | 13/09/26 | 2.004 | presencial | ±2,2 p.p. | 40,0 | 42,0 | 13,0 | 5,0 | -2,4 | 0,22 | fonte |
| MDA | CNT | 13/09/26 | 2.002 | presencial | ±2,2 p.p. | 47,3 | 40,0 | 10,2 | 2,5 | +8,4 | 0,23 | fonte |
| Indexa | Broadcast/Estadão | 13/09/26 | 2.000 | telefônico | ±2,2 p.p. | 43,0 | 42,0 | — | — | +1,2 | 0,22 | fonte |
| BTG/Nexus | BTG | 13/09/26 | 2.003 | telefônico | ±2,2 p.p. | 47,0 | 46,0 | 7,0 | — | +1,1 | 0,24 | fonte |
| Datafolha | Folha/Globo | 10/09/26 | 2.002 | presencial | ±2,2 p.p. | 46,0 | 44,0 | — | — | +2,2 | 0,21 | fonte |
| Futura Inteligencia | Apex | 10/09/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 45,4 | 7,1 | 2,5 | -0,4 | 0,21 | fonte |
| PoderData | Aya | 09/09/26 | 3.000 | telefônico | ±1,8 p.p. | 45,0 | 47,0 | — | — | -2,2 | 0,30 | fonte |
| AtlasIntel | Bloomberg | 09/09/26 | 5.000 | online | ±1,4 p.p. | 46,2 | 46,4 | 7,4 | — | -0,2 | 0,26 | fonte |
| Gerp | próprio | 08/09/26 | 2.400 | telefônico | ±2,0 p.p. | 40,0 | 47,0 | 11,0 | 3,0 | -8,0 | 0,23 | fonte |
| Palver | próprio | 07/09/26 | 5.000 | online | ±1,4 p.p. | 44,0 | 46,0 | — | — | -2,2 | 0,15 | fonte |
| BTG/Nexus | BTG | 07/09/26 | 2.002 | telefônico | ±2,2 p.p. | 45,0 | 46,0 | — | — | -1,1 | 0,19 | fonte |
| Meio/Ideia | Canal Meio | 07/09/26 | 1.500 | telefônico | ±2,5 p.p. | 46,0 | 46,0 | 3,5 | 4,5 | 0,0 | 0,15 | fonte |
| Quaest | Genial | 06/09/26 | 2.004 | presencial | ±2,2 p.p. | 41,0 | 41,0 | — | — | 0,0 | 0,17 | fonte |
| Verita | próprio | 04/09/26 | 3.804 | não informado | ±1,6 p.p. | 43,1 | 48,5 | — | — | -5,9 | 0,23 | fonte |
| PoderData | Aya | 02/09/26 | 3.000 | telefônico | ±1,8 p.p. | 44,0 | 45,0 | — | — | -1,1 | 0,23 | fonte |
| Datafolha | Folha/Globo | 02/09/26 | 2.002 | presencial | ±2,2 p.p. | 46,0 | 44,0 | 9,0 | 2,0 | +2,2 | 0,16 | fonte |
| Quaest | Genial | 01/09/26 | 2.004 | presencial | ±2,2 p.p. | 42,0 | 41,0 | 13,0 | 4,0 | +1,2 | 0,15 | fonte |
| Futura Inteligencia | Apex | 01/09/26 | 2.000 | telefônico | ±2,2 p.p. | 45,6 | 45,2 | 7,2 | 2,0 | +0,4 | 0,16 | fonte |
| Real Time Big Data | CNN | 31/08/26 | 2.000 | telefônico | ±2,2 p.p. | 44,0 | 44,0 | 9,0 | 3,0 | 0,0 | 0,15 | fonte |
| BTG/Nexus | BTG | 30/08/26 | 2.005 | telefônico | ±2,2 p.p. | 46,0 | 45,0 | — | — | +1,1 | 0,15 | fonte |
| AtlasIntel | Bloomberg | 30/08/26 | 5.014 | online | ±1,4 p.p. | 47,1 | 42,6 | 10,3 | — | +5,0 | 0,18 | fonte |
| Vox Brasil | próprio | 27/08/26 | 2.100 | presencial | ±2,1 p.p. | 44,5 | 45,1 | — | — | -0,7 | 0,12 | fonte |
| PoderData | Aya | 26/08/26 | 2.400 | telefônico | ±2,0 p.p. | 45,0 | 44,0 | — | — | +1,1 | 0,15 | fonte |
| Gerp | próprio | 25/08/26 | 2.400 | telefônico | ±2,0 p.p. | 42,0 | 47,0 | — | — | -5,6 | 0,15 | fonte |
| BTG/Nexus | BTG | 23/08/26 | 2.006 | telefônico | ±2,2 p.p. | 46,0 | 45,0 | — | — | +1,1 | 0,12 | fonte |
| Indexa | Broadcast/Estadão | 23/08/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 41,0 | — | — | +5,7 | 0,11 | fonte |
| Datafolha | Folha/Globo | 20/08/26 | 2.058 | presencial | ±2,2 p.p. | 47,0 | 43,0 | 9,0 | 2,0 | +4,4 | 0,11 | fonte |
| Verita | próprio | 20/08/26 | 3.840 | telefônico | ±1,6 p.p. | 42,0 | 47,3 | 6,1 | 4,6 | -5,9 | 0,14 | fonte |
| BTG/Nexus | BTG | 16/08/26 | 2.003 | telefônico | ±2,2 p.p. | 47,0 | 44,0 | — | — | +3,3 | 0,09 | fonte |
| Quaest | Genial | 13/08/26 | 2.004 | presencial | ±2,2 p.p. | 43,0 | 40,0 | — | — | +3,6 | 0,08 | fonte |
| PoderData | Aya | 12/08/26 | 2.400 | telefônico | ±2,0 p.p. | 46,0 | 45,0 | — | — | +1,1 | 0,10 | fonte |
| Gerp | próprio | 10/08/26 | 2.400 | telefônico | ±2,0 p.p. | 43,0 | 45,0 | — | — | -2,3 | 0,09 | fonte |
| MDA | CNT | 09/08/26 | 2.002 | presencial | ±2,2 p.p. | 48,0 | 39,1 | 10,6 | 2,3 | +10,2 | 0,07 | fonte |
| BTG/Nexus | BTG | 09/08/26 | 2.001 | telefônico | ±2,2 p.p. | 47,0 | 44,0 | — | — | +3,3 | 0,07 | fonte |
| Palver | próprio | 07/08/26 | 5.000 | online | ±1,4 p.p. | 46,0 | 46,0 | — | — | 0,0 | 0,04 | fonte |
| Futura Inteligencia | Apex | 07/08/26 | 2.000 | telefônico | ±2,2 p.p. | 46,5 | 44,0 | — | — | +2,8 | 0,07 | fonte |
| Meio/Ideia | Canal Meio | 03/08/26 | 1.500 | telefônico | ±2,5 p.p. | 48,5 | 43,0 | — | — | +6,0 | 0,05 | fonte |
| Quaest | Genial | 03/08/26 | 2.004 | presencial | ±2,2 p.p. | 44,0 | 39,0 | 13,0 | 4,0 | +6,0 | 0,06 | fonte |
| BTG/Nexus | BTG | 02/08/26 | 2.002 | telefônico | ±2,2 p.p. | 46,0 | 45,0 | — | — | +1,1 | 0,06 | fonte |
| Alfa Inteligencia | TMC | 28/07/26 | 2.700 | não informado | ±1,9 p.p. | 48,0 | 41,0 | — | — | +7,9 | 0,06 | fonte |
| PoderData | Aya | 28/07/26 | 2.400 | telefônico | ±2,0 p.p. | 46,0 | 43,0 | — | — | +3,4 | 0,06 | fonte |
| Vox Brasil | próprio | 28/07/26 | 2.100 | não informado | ±2,1 p.p. | 47,5 | 41,1 | — | — | +7,2 | 0,04 | fonte |
| AtlasIntel | Bloomberg | 27/07/26 | 5.021 | online | ±1,4 p.p. | 49,2 | 42,9 | 7,9 | — | +6,8 | 0,06 | fonte |
| BTG/Nexus | BTG | 26/07/26 | 2.004 | telefônico | ±2,2 p.p. | 47,0 | 43,0 | — | — | +4,4 | 0,05 | fonte |
| Datafolha | Folha/Globo | 24/07/26 | 2.004 | presencial | ±2,2 p.p. | 48,0 | 43,0 | — | — | +5,5 | 0,04 | fonte |
| Real Time Big Data | CNN | 20/07/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 42,0 | — | — | +3,4 | 0,04 | fonte |
| Indexa | próprio | 19/07/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 39,0 | — | — | +8,2 | 0,04 | fonte |
| Gerp | próprio | 17/07/26 | 2.000 | não informado | ±2,2 p.p. | 45,0 | 46,0 | — | — | -1,1 | 0,04 | fonte |
| PoderData | Aya | 15/07/26 | 2.400 | telefônico | ±2,0 p.p. | 45,0 | 43,0 | — | — | +2,3 | 0,04 | fonte |
| Quaest | Genial | 13/07/26 | 2.004 | presencial | ±2,2 p.p. | 45,0 | 37,0 | — | — | +9,8 | 0,03 | fonte |
| BTG/Nexus | BTG | 12/07/26 | 2.003 | telefônico | ±2,2 p.p. | 47,0 | 44,0 | — | — | +3,3 | 0,03 | fonte |
| Futura Inteligencia | Apex | 11/07/26 | 2.000 | não informado | ±2,2 p.p. | 46,3 | 46,1 | — | — | +0,2 | 0,03 | fonte |
| Gerp | próprio | 07/07/26 | 2.000 | não informado | ±2,2 p.p. | 42,0 | 45,0 | — | — | -3,4 | 0,02 | fonte |
| Meio/Ideia | Canal Meio | 06/07/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 40,0 | — | — | +5,9 | 0,02 | fonte |
| AtlasIntel | Bloomberg | 30/06/26 | 4.999 | online | ±1,4 p.p. | 48,8 | 42,3 | — | — | +7,1 | 0,02 | fonte |
| BTG/Nexus | BTG | 28/06/26 | 2.009 | telefônico | ±2,2 p.p. | 47,0 | 44,0 | — | — | +3,3 | 0,02 | fonte |
| Vox Brasil | próprio | 25/06/26 | 2.100 | não informado | ±2,1 p.p. | 45,3 | 42,8 | — | — | +2,8 | 0,01 | fonte |
| PoderData | Aya | 24/06/26 | 2.400 | telefônico | ±2,0 p.p. | 46,0 | 43,0 | — | — | +3,4 | 0,02 | fonte |
| Indexa | próprio | 20/06/26 | 2.000 | telefônico | ±2,2 p.p. | 47,0 | 40,0 | 9,0 | 4,0 | +8,0 | 0,01 | fonte |
| Gerp | próprio | 20/06/26 | 2.000 | não informado | ±2,2 p.p. | 40,0 | 42,0 | — | — | -2,4 | 0,01 | fonte |
| Datafolha | Folha/Globo | 19/06/26 | 2.004 | presencial | ±2,2 p.p. | 47,0 | 43,0 | — | — | +4,4 | 0,01 | fonte |
| American Analytics | próprio | 15/06/26 | 2.000 | não informado | ±2,2 p.p. | 45,0 | 40,0 | — | — | +5,9 | 0,01 | fonte |
| MDA | CNT | 14/06/26 | 2.002 | presencial | ±2,2 p.p. | 49,3 | 36,8 | — | — | +14,5 | 0,01 | fonte |
| BTG/Nexus | BTG | 14/06/26 | 2.017 | telefônico | ±2,2 p.p. | 49,0 | 43,0 | — | — | +6,5 | 0,01 | fonte |
| Futura Inteligencia | Apex | 12/06/26 | 2.000 | não informado | ±2,2 p.p. | 48,1 | 42,9 | — | — | +5,7 | 0,01 | fonte |
| Alfa Inteligencia | próprio | 10/06/26 | 1.400 | não informado | ±2,6 p.p. | 44,0 | 41,0 | — | — | +3,5 | 0,01 | fonte |
| Quaest | Genial | 08/06/26 | 2.004 | presencial | ±2,2 p.p. | 44,0 | 38,0 | — | — | +7,3 | 0,01 | fonte |
| Vox Brasil | próprio | 06/06/26 | 2.100 | não informado | ±2,1 p.p. | 47,8 | 41,3 | — | — | +7,3 | 0,01 | fonte |
| Gerp | próprio | 05/06/26 | 2.000 | não informado | ±2,2 p.p. | 39,1 | 44,7 | — | — | -6,7 | 0,01 | fonte |
| Real Time Big Data | CNN | 30/05/26 | 2.000 | telefônico | ±2,2 p.p. | 45,0 | 40,0 | — | — | +5,9 | 0,01 | fonte |
| PoderData | Aya | 28/05/26 | 2.400 | telefônico | ±2,0 p.p. | 46,0 | 42,0 | — | — | +4,5 | 0,01 | fonte |
| Meio/Ideia | Canal Meio | 27/05/26 | 1.500 | telefônico | ±2,5 p.p. | 46,5 | 41,4 | — | — | +5,8 | 0,01 | fonte |
| Verita | próprio | 26/05/26 | 3.330 | não informado | ±1,7 p.p. | 46,8 | 53,2 | — | — | -6,4 | 0,01 | fonte |
| Indexa | próprio | 24/05/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 41,0 | — | — | +5,7 | 0,01 | fonte |
| BTG/Nexus | BTG | 24/05/26 | 2.045 | telefônico | ±2,2 p.p. | 47,0 | 43,0 | — | — | +4,4 | 0,01 | fonte |
| Gerp | próprio | 21/05/26 | 2.000 | não informado | ±2,2 p.p. | 44,0 | 47,0 | — | — | -3,3 | 0,01 | fonte |
| Datafolha | Folha/Globo | 21/05/26 | 2.004 | presencial | ±2,2 p.p. | 47,0 | 43,0 | — | — | +4,4 | 0,01 | fonte |
| Futura Inteligencia | Apex | 20/05/26 | 2.000 | não informado | ±2,2 p.p. | 47,7 | 42,2 | — | — | +6,1 | 0,01 | fonte |
| Vox Brasil | próprio | 19/05/26 | 2.100 | não informado | ±2,1 p.p. | 46,8 | 38,1 | — | — | +10,2 | 0,00 | fonte |
| AtlasIntel | Bloomberg | 18/05/26 | 5.032 | online | ±1,4 p.p. | 48,9 | 41,8 | — | — | +7,8 | 0,01 | fonte |
| Vetor | próprio | 18/05/26 | 9.000 | não informado | ±1,0 p.p. | 50,7 | 38,3 | — | — | +13,9 | 0,00 | fonte |
| Datafolha | Folha/Globo | 14/05/26 | 2.004 | presencial | ±2,2 p.p. | 45,0 | 45,0 | — | — | 0,0 | 0,00 | fonte |
| Vox Brasil | próprio | 12/05/26 | 2.100 | não informado | ±2,1 p.p. | 40,2 | 43,8 | — | — | -4,3 | 0,00 | fonte |
| Gerp | próprio | 12/05/26 | 2.000 | não informado | ±2,2 p.p. | 43,0 | 50,0 | — | — | -7,5 | 0,00 | fonte |
| Quaest | Genial | 11/05/26 | 2.004 | presencial | ±2,2 p.p. | 42,0 | 41,0 | — | — | +1,2 | 0,00 | fonte |
| Futura Inteligencia | Apex | 08/05/26 | 2.000 | não informado | ±2,2 p.p. | 44,4 | 46,9 | — | — | -2,7 | 0,00 | fonte |
| Meio/Ideia | Canal Meio | 05/05/26 | 1.500 | telefônico | ±2,5 p.p. | 44,7 | 45,3 | — | — | -0,7 | 0,00 | fonte |
| Real Time Big Data | CNN | 04/05/26 | 2.000 | telefônico | ±2,2 p.p. | 43,0 | 44,0 | — | — | -1,1 | 0,00 | fonte |
| AtlasIntel | Bloomberg | 27/04/26 | 5.008 | online | ±1,4 p.p. | 47,5 | 47,8 | — | — | -0,3 | 0,00 | fonte |
| BTG/Nexus | BTG | 26/04/26 | 2.028 | telefônico | ±2,2 p.p. | 46,0 | 45,0 | — | — | +1,1 | 0,00 | fonte |
| Quaest | Genial | 13/04/26 | 2.004 | presencial | ±2,2 p.p. | 40,0 | 42,0 | — | — | -2,4 | 0,00 | fonte |
| MDA | CNT | 12/04/26 | 2.002 | presencial | ±2,2 p.p. | 44,9 | 40,2 | — | — | +5,5 | 0,00 | fonte |
| Futura Inteligencia | Apex | 11/04/26 | 2.000 | não informado | ±2,2 p.p. | 42,6 | 48,0 | — | — | -6,0 | 0,00 | fonte |
| Datafolha | Folha/Globo | 09/04/26 | 2.004 | presencial | ±2,2 p.p. | 45,0 | 46,0 | — | — | -1,1 | 0,00 | fonte |
| Meio/Ideia | Canal Meio | 07/04/26 | 1.500 | telefônico | ±2,5 p.p. | 45,5 | 45,8 | — | — | -0,3 | 0,00 | fonte |
| Verita | próprio | 04/04/26 | 40.500 | não informado | ±0,5 p.p. | 38,4 | 42,8 | — | — | -5,4 | 0,00 | fonte |
| BTG/Nexus | BTG | 29/03/26 | 2.000 | telefônico | ±2,2 p.p. | 46,0 | 46,0 | — | — | 0,0 | 0,00 | fonte |
| Paraná Pesquisas | próprio | 28/03/26 | 2.080 | presencial | ±2,1 p.p. | 44,1 | 45,2 | 6,2 | 4,5 | -1,2 | 0,00 | fonte |
| Gerp | próprio | 25/03/26 | 2.000 | não informado | ±2,2 p.p. | 45,0 | 48,0 | — | — | -3,2 | 0,00 | fonte |
| AtlasIntel | Bloomberg | 23/03/26 | 5.028 | online | ±1,4 p.p. | 46,6 | 47,6 | — | — | -1,1 | 0,00 | fonte |
| Alfa Inteligencia | próprio | 11/03/26 | 1.000 | não informado | ±3,1 p.p. | 41,0 | 38,0 | — | — | +3,8 | 0,00 | fonte |
| Meio/Ideia | Canal Meio | 10/03/26 | 1.500 | telefônico | ±2,5 p.p. | 47,0 | 45,0 | — | — | +2,2 | 0,00 | fonte |
| Quaest | Genial | 09/03/26 | 2.004 | presencial | ±2,2 p.p. | 41,0 | 41,0 | — | — | 0,0 | 0,00 | fonte |
| Futura Inteligencia | Apex | 06/03/26 | 2.000 | não informado | ±2,2 p.p. | 40,5 | 48,8 | — | — | -9,3 | 0,00 | fonte |
| Datafolha | Folha/Globo | 05/03/26 | 2.004 | presencial | ±2,2 p.p. | 46,0 | 43,0 | — | — | +3,4 | 0,00 | fonte |
| Real Time Big Data | CNN | 02/03/26 | 2.000 | telefônico | ±2,2 p.p. | 42,0 | 41,0 | — | — | +1,2 | 0,00 | fonte |
| Vetor | próprio | 02/03/26 | 9.000 | não informado | ±1,0 p.p. | 40,4 | 49,0 | — | — | -9,6 | 0,00 | fonte |
| Paraná Pesquisas | próprio | 25/02/26 | 2.080 | presencial | ±2,1 p.p. | 43,8 | 44,4 | — | — | -0,7 | 0,00 | fonte |
| AtlasIntel | Bloomberg | 24/02/26 | 4.986 | online | ±1,4 p.p. | 46,2 | 46,3 | — | — | -0,1 | 0,00 | fonte |
| Quaest | Genial | 09/02/26 | 2.004 | presencial | ±2,2 p.p. | 43,0 | 38,0 | — | — | +6,2 | 0,00 | fonte |
| Futura Inteligencia | Apex | 07/02/26 | 2.000 | não informado | ±2,2 p.p. | 42,4 | 48,2 | — | — | -6,4 | 0,00 | fonte |
| Meio/Ideia | Canal Meio | 02/02/26 | 1.500 | telefônico | ±2,5 p.p. | 45,8 | 41,1 | — | — | +5,4 | 0,00 | fonte |
| Gerp | próprio | 02/02/26 | 2.000 | não informado | ±2,2 p.p. | 45,0 | 45,0 | — | — | 0,0 | 0,00 | fonte |
| Paraná Pesquisas | próprio | 28/01/26 | 2.080 | presencial | ±2,1 p.p. | 44,8 | 42,2 | — | — | +3,0 | 0,00 | fonte |
| AtlasIntel | Bloomberg | 20/01/26 | 5.418 | online | ±1,3 p.p. | 49,0 | 45,0 | — | — | +4,3 | 0,00 | fonte |
| Futura Inteligencia | Apex | 19/01/26 | 2.000 | não informado | ±2,2 p.p. | 41,9 | 48,1 | — | — | -6,9 | 0,00 | fonte |
| Meio/Ideia | Canal Meio | 12/01/26 | 2.000 | telefônico | ±2,2 p.p. | 46,2 | 36,0 | — | — | +12,4 | 0,00 | fonte |
| Quaest | Genial | 11/01/26 | 2.004 | presencial | ±2,2 p.p. | 45,0 | 38,0 | — | — | +8,4 | 0,00 | fonte |
| Paraná Pesquisas | próprio | 22/12/25 | 2.038 | presencial | ±2,2 p.p. | 44,1 | 41,0 | — | — | +3,6 | 0,00 | fonte |
| AtlasIntel | Bloomberg | 15/12/25 | 8.154 | online | ±1,1 p.p. | 53,0 | 41,0 | — | — | +12,8 | 0,00 | fonte |
| Quaest | Genial | 14/12/25 | 2.004 | presencial | ±2,2 p.p. | 46,0 | 36,0 | — | — | +12,2 | 0,00 | fonte |
| Gerp | próprio | 10/12/25 | 2.000 | não informado | ±2,2 p.p. | 41,0 | 42,0 | — | — | -1,2 | 0,00 | fonte |
| Futura Inteligencia | Apex | 09/12/25 | 2.001 | não informado | ±2,2 p.p. | 42,1 | 41,6 | — | — | +0,6 | 0,00 | fonte |
| Verita | próprio | 07/12/25 | 2.519 | não informado | ±2,0 p.p. | 39,8 | 39,2 | — | — | +0,8 | 0,00 | fonte |
| Datafolha | Folha/Globo | 04/12/25 | 2.002 | presencial | ±2,2 p.p. | 51,0 | 36,0 | — | — | +17,2 | 0,00 | fonte |
| UF | Instituto | Campo (fim) | n | Método | Lula | Flávio | Registro | Fontes |
|---|---|---|---|---|---|---|---|---|
| AC | Verita | 15/09/26 | 1.030 | telefônico | 32,7 | 61,3 | BR-02386/2026 | fonte · 2ª fonte |
| AC | Quaest | 12/10/26 | 1.098 | presencial | 28,0 | 60,0 | BR-05379/2026 | fonte · 2ª fonte |
| AL | AtlasIntel | 09/09/26 | 1.208 | online | 50,6 | 40,0 | AL-00447/2026 | fonte (fonte única) |
| AL | Verita | 15/09/26 | 1.220 | telefônico | 52,8 | 40,8 | BR-04383/2026 | fonte · 2ª fonte |
| AL | Quaest | 24/09/26 | 804 | presencial | 48,0 | 34,0 | BR-04938/2026 | fonte · 2ª fonte |
| AM | AtlasIntel | 03/09/26 | 1.185 | online | 41,3 | 48,0 | AM-04939/2026 | fonte (fonte única) |
| AM | Verita | 15/09/26 | 1.220 | telefônico | 42,2 | 43,1 | BR-07214/2026 | fonte · 2ª fonte |
| AM | Quaest | 23/09/26 | 804 | presencial | 45,0 | 40,0 | BR-08101/2026 | fonte · 2ª fonte |
| AM | PoderData | 11/10/26 | 1.200 | telefônico | 49,0 | 49,0 | AM-08245/2026 | fonte · 2ª fonte |
| AM | Quaest | 12/10/26 | 1.098 | presencial | 47,0 | 43,0 | AM-09760/2026 | fonte · 2ª fonte |
| AP | Verita | 15/09/26 | 1.030 | telefônico | 43,9 | 46,2 | BR-07861/2026 | fonte · 2ª fonte |
| AP | AtlasIntel | 02/10/26 | 800 | online | 50,7 | 43,5 | AP-01238/2026 | fonte (fonte única) |
| BA | AtlasIntel | 02/09/26 | 1.804 | online | 58,1 | 29,0 | BA-08891/2026 | fonte · 2ª fonte |
| BA | Real Time Big Data | 08/09/26 | 1.600 | telefônico | 61,0 | 30,0 | BR-06566/2026 | fonte · 2ª fonte |
| BA | Verita | 15/09/26 | 2.020 | telefônico | 55,1 | 40,5 | BR-07197/2026 | fonte · 2ª fonte |
| BA | Quaest | 23/09/26 | 900 | presencial | 62,0 | 27,0 | BA-01850/2026 | fonte · 2ª fonte |
| BA | Real Time Big Data | 26/09/26 | 1.600 | telefônico | 64,0 | 28,0 | BR-02319/2026 | fonte · 2ª fonte |
| BA | AtlasIntel | 28/09/26 | 2.000 | online | 61,3 | 31,4 | BA-02425/2026 | fonte · 2ª fonte |
| CE | AtlasIntel | 02/09/26 | 1.834 | online | 54,6 | 30,1 | BR-07411/2026 | fonte · 2ª fonte |
| CE | Verita | 15/09/26 | 1.525 | telefônico | 54,1 | 36,0 | BR-07551/2026 | fonte · 2ª fonte |
| CE | Real Time Big Data | 17/09/26 | 1.600 | telefônico | 64,0 | 27,0 | BR-03822/2026 | fonte · 2ª fonte |
| CE | Quaest | 22/09/26 | 900 | presencial | 59,0 | 27,0 | CE-08268/2026 | fonte · 2ª fonte |
| CE | Real Time Big Data | 24/09/26 | 1.600 | telefônico | 63,0 | 28,0 | BR-05316/2026 | fonte (fonte única) |
| DF | Datafolha | 21/08/26 | 910 | presencial | 39,0 | 51,0 | — | fonte · 2ª fonte |
| DF | AtlasIntel | 01/09/26 | 1.193 | online | 42,5 | 47,4 | DF-02018/2026 | fonte (fonte única) |
| DF | Datafolha | 11/09/26 | 910 | presencial | 39,0 | 51,0 | — | fonte (fonte única) |
| DF | Verita | 15/09/26 | 1.220 | telefônico | 49,7 | 45,4 | BR-01988/2026 | fonte · 2ª fonte |
| DF | Quaest | 22/09/26 | 1.104 | presencial | 38,0 | 49,0 | DF-02596/2026 | fonte · 2ª fonte |
| DF | Datafolha | 24/09/26 | 910 | presencial | 41,0 | 51,0 | DF-01888/2026 | fonte · 2ª fonte |
| DF | Quaest | 28/09/26 | 1.104 | presencial | 33,0 | 48,0 | DF-02515/2026 | fonte · 2ª fonte |
| DF | Datafolha | 30/09/26 | 910 | presencial | 39,0 | 51,0 | DF-00905/2026 | fonte · 2ª fonte |
| DF | AtlasIntel | 02/10/26 | 1.200 | online | 39,6 | 55,3 | DF-04297/2026 | fonte (fonte única) |
| DF | Datafolha | 08/10/26 | 910 | presencial | 43,0 | 49,0 | DF-03365/2026 | fonte · 2ª fonte |
| DF | Quaest | 11/10/26 | 1.104 | não informado | 40,0 | 50,0 | DF-08737/2026 | fonte (fonte única) |
| DF | Quaest | 20/10/26 | 1.104 | presencial | 39,0 | 50,0 | BR-03163/2026 | fonte (fonte única) |
| ES | Verita | 17/09/26 | 1.220 | telefônico | 43,1 | 50,9 | BR-00729/2026 | fonte · 2ª fonte |
| ES | Quaest | 13/10/26 | 1.098 | não informado | 38,0 | 51,0 | BR-01973/2026 | fonte · 2ª fonte |
| GO | AtlasIntel | 01/09/26 | 1.214 | online | 32,9 | 54,6 | GO-05293/2026 | fonte (fonte única) |
| GO | Verita | 17/09/26 | 1.525 | telefônico | 33,4 | 53,4 | BR-06701/2026 | fonte · 2ª fonte |
| GO | Real Time Big Data | 21/09/26 | 1.600 | telefônico | 33,0 | 57,0 | — | fonte · 2ª fonte |
| MA | Real Time Big Data | 09/09/26 | 1.600 | telefônico | 65,0 | 30,0 | — | fonte · 2ª fonte |
| MA | Verita | 17/09/26 | 1.525 | telefônico | 53,7 | 38,6 | BR-07504/2026 | fonte · 2ª fonte |
| MA | Quaest | 24/09/26 | 900 | presencial | 63,0 | 26,0 | — | fonte · 2ª fonte |
| MG | Datafolha | 20/08/26 | 1.204 | presencial | 46,0 | 42,0 | MG-00446/2026 | fonte · 2ª fonte |
| MG | AtlasIntel | 04/09/26 | 1.804 | online | 48,7 | 42,1 | — | fonte (fonte única) |
| MG | Datafolha | 10/09/26 | 1.204 | presencial | 46,0 | 45,0 | — | fonte · 2ª fonte |
| MG | Verita | 17/09/26 | 2.030 | telefônico | 38,4 | 45,6 | BR-03240/2026 | fonte · 2ª fonte |
| MG | Quaest | 22/09/26 | 1.506 | presencial | 40,0 | 40,0 | BR-07664/2026 | fonte · 2ª fonte |
| MG | Datafolha | 24/09/26 | 1.204 | presencial | 44,0 | 46,0 | BR-06824/2026 | fonte · 2ª fonte |
| MG | Quaest | 28/09/26 | 1.506 | presencial | 41,0 | 40,0 | BR-05155/2026 | fonte · 2ª fonte |
| MG | Datafolha | 01/10/26 | 1.204 | presencial | 48,0 | 44,0 | — | fonte · 2ª fonte |
| MG | Real Time Big Data | 02/10/26 | 2.000 | telefônico | 46,0 | 46,0 | BR-01068/2026 | fonte · 2ª fonte |
| MG | AtlasIntel | 02/10/26 | 2.189 | online | 47,4 | 50,2 | BR-05974/2026 | fonte (fonte única) |
| MS | AtlasIntel | 01/09/26 | 1.204 | online | 36,1 | 57,1 | MS-02047/2026 | fonte (fonte única) |
| MS | Real Time Big Data | 09/09/26 | 1.600 | telefônico | 38,0 | 52,0 | BR-07174/2026 | fonte (fonte única) |
| MS | Verita | 17/09/26 | 1.220 | telefônico | 34,4 | 53,5 | BR-07855/2026 | fonte · 2ª fonte |
| MT | AtlasIntel | 09/09/26 | 1.209 | online | 35,2 | 59,1 | MT-02766/2026 | fonte (fonte única) |
| MT | Verita | 17/09/26 | 1.220 | telefônico | 28,1 | 54,9 | BR-02939/2026 | fonte · 2ª fonte |
| MT | Quaest | 24/09/26 | 804 | presencial | 30,0 | 56,0 | MT-08098/2026 | fonte · 2ª fonte |
| PA | Verita | 17/09/26 | 1.525 | telefônico | 39,6 | 48,8 | BR-01223/2026 | fonte · 2ª fonte |
| PA | Quaest | 25/09/26 | 804 | presencial | 44,0 | 40,0 | — | fonte · 2ª fonte |
| PB | AtlasIntel | 02/09/26 | 1.207 | online | 56,3 | 33,4 | PB-01118/2026 | fonte · 2ª fonte |
| PB | Verita | 18/09/26 | 1.220 | telefônico | 51,6 | 40,9 | BR-08734/2026 | fonte · 2ª fonte |
| PB | Quaest | 21/09/26 | 804 | presencial | 60,0 | 26,0 | — | fonte · 2ª fonte |
| PB | AtlasIntel | 02/10/26 | 1.268 | online | 61,4 | 33,9 | BR-07092/2026 | fonte (fonte única) |
| PE | Datafolha | 20/08/26 | 1.204 | presencial | 62,0 | 29,0 | — | fonte · 2ª fonte |
| PE | Real Time Big Data | 02/09/26 | 1.600 | telefônico | 59,0 | 35,0 | BR-05862/2026 | fonte · 2ª fonte |
| PE | AtlasIntel | 16/09/26 | 1.793 | online | 57,5 | 36,2 | BR-00357/2026 | fonte · 2ª fonte |
| PE | Verita | 18/09/26 | 2.010 | telefônico | 52,9 | 40,1 | BR-07802/2026 | fonte · 2ª fonte |
| PE | AtlasIntel | 20/09/26 | 1.836 | online | 60,5 | 32,7 | PE-02023/2026 | fonte · 2ª fonte |
| PE | Quaest | 22/09/26 | 1.302 | presencial | 58,0 | 25,0 | PE-05671/2026 | fonte · 2ª fonte |
| PE | Datafolha | 24/09/26 | 1.204 | presencial | 64,0 | 29,0 | PE-08125/2026 | fonte · 2ª fonte |
| PE | Real Time Big Data | 26/09/26 | 1.600 | telefônico | 61,0 | 35,0 | BR-02938/2026 | fonte · 2ª fonte |
| PE | Quaest | 28/09/26 | 1.302 | presencial | 61,0 | 24,0 | PE-00324/2026 | fonte · 2ª fonte |
| PE | Datafolha | 01/10/26 | 1.204 | presencial | 65,0 | 29,0 | PE-06822/2026 | fonte · 2ª fonte |
| PI | Verita | 18/09/26 | 1.220 | telefônico | 57,6 | 28,7 | BR-07059/2026 | fonte · 2ª fonte |
| PI | AtlasIntel | 28/09/26 | 1.218 | online | 70,0 | 26,0 | PI-07497/2026 | fonte · 2ª fonte |
| PR | AtlasIntel | 09/09/26 | 1.799 | online | 32,1 | 60,7 | PR-09655/2026 | fonte (fonte única) |
| PR | Real Time Big Data | 15/09/26 | 1.600 | telefônico | 30,0 | 61,0 | BR-08843/2026 | fonte · 2ª fonte |
| PR | AtlasIntel | 16/09/26 | 1.794 | online | 38,8 | 57,5 | PR-02671/2026 | fonte · 2ª fonte |
| PR | Verita | 18/09/26 | 2.010 | telefônico | 30,6 | 58,9 | BR-01839/2026 | fonte · 2ª fonte |
| PR | Real Time Big Data | 28/09/26 | 1.600 | telefônico | 30,0 | 61,0 | BR-03875/2026 | fonte · 2ª fonte |
| RJ | Real Time Big Data | 01/09/26 | 2.000 | telefônico | 41,0 | 45,0 | — | fonte (fonte única) |
| RJ | Datafolha | 10/09/26 | 1.204 | presencial | 41,0 | 49,0 | — | fonte · 2ª fonte |
| RJ | Real Time Big Data | 12/09/26 | 2.000 | telefônico | 41,0 | 47,0 | BR-09111/2026 | fonte · 2ª fonte |
| RJ | Verita | 18/09/26 | 2.030 | telefônico | 39,3 | 53,4 | BR-02698/2026 | fonte · 2ª fonte |
| RJ | AtlasIntel | 20/09/26 | 1.829 | online | 44,7 | 44,9 | RJ-01150/2026 | fonte (fonte única) |
| RJ | Quaest | 22/09/26 | 1.302 | presencial | 36,0 | 44,0 | RJ-04982/2026 | fonte · 2ª fonte |
| RJ | Datafolha | 24/09/26 | 1.204 | presencial | 42,0 | 51,0 | RJ-02205/2026 | fonte · 2ª fonte |
| RJ | Quaest | 28/09/26 | 1.302 | presencial | 34,0 | 44,0 | RJ-04419/2026 | fonte · 2ª fonte |
| RJ | Real Time Big Data | 29/09/26 | 2.000 | telefônico | 41,0 | 52,0 | BR-05193/2026 | fonte · 2ª fonte |
| RJ | Datafolha | 01/10/26 | 1.204 | presencial | 42,0 | 51,0 | RJ-02070/2026 | fonte · 2ª fonte |
| RN | Verita | 18/09/26 | 1.220 | telefônico | 55,4 | 37,4 | BR-02399/2026 | fonte · 2ª fonte |
| RN | Quaest | 24/09/26 | 804 | presencial | 56,0 | 30,0 | RN-01492/2026 | fonte · 2ª fonte |
| RN | Quaest | 12/10/26 | 1.098 | presencial | 59,0 | 33,0 | RN-03210/2026 | fonte · 2ª fonte |
| RO | Verita | 19/09/26 | 1.220 | telefônico | 30,5 | 63,9 | BR-07923/2026 | fonte · 2ª fonte |
| RR | AtlasIntel | 09/09/26 | 1.200 | online | 25,6 | 66,4 | RR-07280/2026 | fonte · 2ª fonte |
| RR | Verita | 19/09/26 | 1.030 | telefônico | 23,5 | 70,8 | BR-07242/2026 | fonte · 2ª fonte |
| RR | Quaest | 24/09/26 | 804 | presencial | 22,0 | 67,0 | — | fonte · 2ª fonte |
| RR | AtlasIntel | 02/10/26 | 800 | online | 29,3 | 65,3 | RR-07765/2026 | fonte (fonte única) |
| RS | AtlasIntel | 01/09/26 | 1.783 | online | 42,0 | 50,4 | RS-00652/2026 | fonte · 2ª fonte |
| RS | Real Time Big Data | 09/09/26 | 1.600 | telefônico | 42,0 | 53,0 | BR-00631/2026 | fonte · 2ª fonte |
| RS | Verita | 19/09/26 | 2.020 | telefônico | 37,9 | 55,7 | BR-06983/2026 | fonte · 2ª fonte |
| RS | AtlasIntel | 20/09/26 | 1.819 | online | 44,4 | 47,9 | BR-01816/2026 | fonte · 2ª fonte |
| RS | Real Time Big Data | 28/09/26 | 1.600 | telefônico | 43,0 | 53,0 | BR-08546/2026 | fonte (fonte única) |
| SC | AtlasIntel | 09/09/26 | 1.211 | online | 27,5 | 61,5 | SC-02036/2026 | fonte · 2ª fonte |
| SC | Real Time Big Data | 16/09/26 | 1.600 | telefônico | 30,0 | 62,0 | BR-09582/2026 | fonte · 2ª fonte |
| SE | Verita | 19/09/26 | 1.220 | telefônico | 56,7 | 37,6 | BR-01212/2026 | fonte · 2ª fonte |
| SE | Quaest | 23/09/26 | 804 | presencial | 56,0 | 30,0 | — | fonte · 2ª fonte |
| SP | Datafolha | 19/08/26 | 1.610 | presencial | 42,0 | 47,0 | SP-01806/2026 | fonte · 2ª fonte |
| SP | Datafolha | 10/09/26 | 1.610 | presencial | 42,0 | 47,0 | — | fonte · 2ª fonte |
| SP | Real Time Big Data | 12/09/26 | 2.000 | telefônico | 44,0 | 50,0 | BR-05824/2026 | fonte (fonte única) |
| SP | Real Time Big Data | 19/09/26 | 2.000 | telefônico | 45,0 | 51,0 | BR-09702/2026 | fonte · 2ª fonte |
| SP | Verita | 19/09/26 | 3.025 | telefônico | 40,6 | 52,3 | BR-04470/2026 | fonte · 2ª fonte |
| SP | AtlasIntel | 20/09/26 | 1.800 | online | 42,8 | 48,8 | SP-02676/2026 | fonte · 2ª fonte |
| SP | Quaest | 22/09/26 | 1.800 | presencial | 36,0 | 42,0 | BR-08886/2026 | fonte · 2ª fonte |
| SP | Datafolha | 23/09/26 | 1.610 | presencial | 41,0 | 49,0 | BR-09355/2026 | fonte · 2ª fonte |
| SP | Real Time Big Data | 28/09/26 | 2.000 | telefônico | 43,0 | 51,0 | — | fonte · 2ª fonte |
| SP | Datafolha | 30/09/26 | 1.610 | presencial | 45,0 | 48,0 | BR-02676/2026 | fonte · 2ª fonte |
| TO | Verita | 19/09/26 | 1.220 | telefônico | 33,6 | 61,1 | BR-09946/2026 | fonte · 2ª fonte |
| TO | Quaest | 13/10/26 | 1.098 | não informado | 42,0 | 48,0 | TO-03829/2026 | fonte · 2ª fonte |
| TO | Paraná Pesquisas | 17/10/26 | 1.500 | telefônico | 41,9 | 52,9 | BR-09051/2026 | fonte (fonte única) |
| Instituto | Contratante | Campo até | n | Método | Lula | Flávio | Cury | Renan | Caiado | Zema | Outros | Br/nulo | Indec. | Registro TSE | Fontes |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Quaest | Globo/O Globo | 03/10 | 3.702 | presencial | 40 | 38 | 3 | 3 | 3 | 0 | 0 | 10 | 3 | BR-02197/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 03/10 | 4.006 | presencial | 42 | 40 | 3 | 3 | 4 | 0 | 0 | 4 | 3 | BR-01708/2026 | fonte · 2ª |
| Palver | próprio | 03/10 | 5.000 | online | 43 | 47 | 1 | 7 | 1 | 0 | 0 | 0 | 0 | BR-00198/2026 | fonte · 2ª |
| PoderData | Aya | 02/10 | 4.000 | telefônico | 42 | 41 | 3 | 3 | 2 | 1 | 2 | 4 | 2 | BR-03519/2026 | fonte · 2ª |
| CNT/MDA | CNT | 02/10 | 2.007 | presencial | 43,1 | 38 | 3,2 | 1,5 | 3 | 0,8 | 0,5 | 5 | 4,7 | BR-04756/2026 | fonte · 2ª |
| Vox Brasil | próprio | 01/10 | 2.100 | não informado | 40,4 | 41,2 | 1,2 | 2,2 | 3,3 | 1,3 | 0,2 | 3,2 | 7 | BR-00148/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 01/10 | 2.506 | presencial | 42 | 38 | 4 | 3 | 3 | 1 | 2 | 5 | 2 | BR-08039/2026 | fonte · 2ª |
| Real Time Big Data | Real Time Big Data (recursos próprios) | 30/09 | 2.000 | telefônico | 43 | 39 | 3 | 4 | 3 | 1 | 1 | 3 | 3 | BR-09503/2026 | fonte · 2ª |
| DataTrends | próprio | 30/09 | 2.000 | telefônico | 41 | 39 | 4 | 3 | 3 | — | 1 | 4 | 4 | BR-04414/2026 | fonte · 2ª |
| Indexa | Broadcast/Estadão | 29/09 | 2.000 | telefônico | 39 | 34 | 4 | 3 | 4 | 1 | 0 | 4 | 7 | BR-00698/2026 | fonte · 2ª |
| Futura Inteligencia | Apex/100% Cidades | 29/09 | 2.000 | telefônico | 39,4 | 42,2 | 3,8 | 3,2 | 4 | 1,6 | 1,2 | 2,4 | 2,6 | BR-01122/2026 | fonte · 2ª |
| Vox Brasil | próprio | 28/09 | 2.100 | presencial | 41,1 | 37,8 | 2,2 | 2,7 | 3,8 | 1,8 | 0,3 | 3,5 | 6,8 | BR-00895/2026 | fonte · 2ª |
| Meio/Ideia | Canal Meio | 28/09 | 2.000 | telefônico | 39,4 | 38,4 | 6,7 | 4,5 | 4,4 | 3,5 | 1,4 | 0,7 | 1,5 | BR-08706/2026 | fonte · 2ª |
| Gerp | próprio | 28/09 | 2.400 | telefônico | 40 | 42 | 5 | 3 | 3 | 1 | 1 | 2 | 3 | BR-03929/2026 | fonte · 2ª |
| AtlasIntel | Bloomberg (AtlasIntel recursos próprios) | 28/09 | 5.005 | online | 45,3 | 42,2 | 2 | 5,2 | 1,8 | 0,9 | 0,5 | 0,9 | 1,2 | BR-04391/2026 | fonte · 2ª |
| Palver | próprio | 27/09 | 5.000 | online | 44 | 44 | 1 | 8 | 1 | 1 | 0 | 1 | 1 | BR-02990/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 27/09 | 2.000 | telefônico | 42 | 37 | 5 | 4 | 5 | 1 | 1 | 3 | 2 | BR-07557/2026 | fonte · 2ª |
| Quaest | Globo/O Globo | 27/09 | 2.004 | presencial | 39 | 34 | 4 | 3 | 4 | 1 | 0 | 10 | 5 | BR-06520/2026 | fonte · 2ª |
| Real Time Big Data | Real Time Big Data (recursos próprios) | 23/09 | 2.000 | telefônico | 41 | 37 | 6 | 6 | 2 | 1 | 1 | 3 | 3 | BR-04202/2026 | fonte · 2ª |
| PoderData | PoderData (recursos próprios)/Aya | 23/09 | 3.000 | telefônico | 41 | 39 | 6 | 3 | 2 | 1 | 3 | 4 | 2 | BR-01739/2026 | fonte · 2ª |
| Palver | próprio | 23/09 | 5.000 | online | 43 | 43 | 2 | 8 | 1 | 1 | 1 | 1 | 1 | BR-09587/2026 | fonte · 2ª |
| Futura Inteligencia | Apex/100% Cidades | 23/09 | 2.000 | telefônico | 38,4 | 40,4 | 5,6 | 2,4 | 5,7 | 0,9 | 1,2 | 2,1 | 3,2 | BR-05268/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 23/09 | 2.002 | presencial | 40 | 36 | 5 | 3 | 4 | 1 | 1 | 5 | 2 | BR-00304/2026 | fonte · 2ª |
| Alfa Inteligencia | TMC | 23/09 | 2.700 | presencial | 40 | 33 | 6 | 3 | 5 | 1 | 1 | 6 | 5 | BR-02512/2026 | fonte · 2ª |
| AtlasIntel | Bloomberg (AtlasIntel recursos próprios) | 22/09 | 5.015 | online | 45,8 | 43,4 | 2,1 | 4,5 | 1,3 | 0,9 | 0,8 | 0,7 | 0,5 | BR-04739/2026 | fonte · 2ª |
| American Analytics | Times Brasil/CNBC | 20/09 | 2.000 | não informado | 38 | 34 | 5 | 4 | 4 | 1 | 0 | 6 | 8 | BR-032587/26 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 20/09 | 2.006 | telefônico | 40 | 37 | 6 | 3 | 5 | 1 | 1 | 4 | 2 | BR-00485/2026 | fonte · 2ª |
| Quaest | Globo/O Globo | 20/09 | 2.004 | presencial | 37 | 33 | 6 | 3 | 4 | 1 | 1 | 7 | 8 | BR-06004/2026 | fonte · 2ª |
| Verita | próprio | 19/09 | 40.500 | telefônico | 40 | 43,6 | 5,4 | 2,9 | 1,8 | 0,6 | 1,3 | 0,7 | 3,6 | — | fonte · 2ª |
| Palver | próprio | 18/09 | 5.000 | online | 41 | 42 | 2 | 11 | 1 | 1 | — | — | 1 | BR-00860/2026 | fonte · 2ª |
| PoderData | PoderData (recursos próprios)/Aya | 16/09 | 3.000 | telefônico | 37 | 36 | 8 | 3 | 3 | 1 | 6 | 4 | 2 | BR-00360/2026 | fonte · 2ª |
| Gerp | próprio | 16/09 | 2.400 | telefônico | 37 | 40 | 7 | 2 | 3 | 1 | 1 | 4 | 4 | BR-00535/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 16/09 | 2.002 | presencial | 39 | 36 | 6 | 3 | 4 | 2 | 2 | 6 | 3 | BR-04029/2026 | fonte · 2ª |
| AtlasIntel | Bloomberg (AtlasIntel recursos próprios) | 16/09 | 5.018 | online | 44,1 | 41,7 | 3,5 | 5,1 | 1,7 | 1,1 | 0,7 | 1,6 | 0,5 | BR-06221/2026 | fonte · 2ª |
| Futura Inteligencia | Apex/100% Cidades | 15/09 | 2.000 | telefônico | 38,3 | 37,7 | 6,5 | 3,4 | 4,5 | 1 | 2,4 | 2,7 | 3,4 | BR-00749/2026 | fonte · 2ª |
| DataTrends | próprio | 14/09 | 2.000 | telefônico | 39 | 35 | 6 | 4 | 3 | 1 | 0 | 4 | 8 | BR-08691/2026 | fonte · 2ª |
| Quaest | Globo/O Globo | 13/09 | 2.004 | presencial | 36 | 31 | 7 | 4 | 4 | 1 | 0 | 7 | 10 | BR-03607/2026 | fonte · 2ª |
| CNT/MDA | CNT | 13/09 | 2.002 | presencial | 40,5 | 30,4 | 6 | 2,7 | 3 | 1 | 2,9 | 6 | 7,5 | BR-06902/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 13/09 | 2.003 | telefônico | 42 | 37 | 6 | 2 | 5 | 1 | 1 | 4 | 2 | BR-04076/2026 | fonte · 2ª |
| Indexa | Broadcast/Estadão | 13/09 | 2.000 | telefônico | 38 | 34 | 6 | 3 | 4 | 1 | 0 | 10 | 4 | BR-03482/2026 | fonte · 2ª |
| Futura Inteligencia | Apex/100% Cidades | 10/09 | 2.000 | telefônico | 39,2 | 33,6 | 6,4 | 4,3 | 4,4 | 0,7 | 3 | 3,1 | 5,2 | BR-02322/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 10/09 | 2.002 | presencial | 39 | 35 | 6 | 3 | 4 | 2 | 4 | 6 | 4 | BR-01833/2026 | fonte · 2ª |
| PoderData | PoderData (recursos próprios)/Aya | 09/09 | 3.000 | telefônico | 38 | 36 | 10 | 3 | 2 | 1 | 5 | 3 | 1 | BR-04914/2026 | fonte · 2ª |
| AtlasIntel | Bloomberg (AtlasIntel recursos próprios) | 09/09 | 5.000 | online | 43 | 37,4 | 6,6 | 6,5 | 1,1 | 2,1 | 2,3 | 0,6 | 0,4 | BR-01452/2026 | fonte · 2ª |
| Gerp | próprio | 08/09 | 2.400 | telefônico | 34 | 37 | 6 | 4 | 5 | 1 | 2 | 4 | 8 | BR-00251/2026 | fonte · 2ª |
| Meio/Ideia | Canal Meio | 07/09 | 1.500 | telefônico | 38,4 | 37,3 | 6,6 | 4 | 4 | 1,5 | 3,1 | 2,5 | 2,8 | BR-07935/2026 | fonte · 2ª |
| Palver | próprio | 07/09 | 5.000 | online | 40 | 40 | 4 | 11 | 1 | 1 | 2 | 0 | 1 | BR-05420/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 07/09 | 2.002 | telefônico | 39 | 35 | 9 | 4 | 4 | 1 | 2 | 3 | 2 | BR-06790/2026 | fonte · 2ª |
| Quaest | Globo/O Globo | 06/09 | 2.004 | presencial | 36 | 29 | 8 | 3 | 3 | 2 | 1 | 8 | 10 | BR-01720/2026 | fonte · 2ª |
| Verita | próprio | 04/09 | 3.804 | não informado | 38,4 | 38,9 | 12,2 | 3,7 | 1,9 | 0,3 | 2,6 | 1,1 | 0,9 | BR-09426/2026 | fonte · 2ª |
| PoderData | PoderData (recursos próprios)/Aya | 02/09 | 3.000 | telefônico | 37 | 34 | 10 | 3 | 2 | 1 | 8 | 5 | 2 | BR-07561/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 02/09 | 2.002 | presencial | 38 | 33 | 8 | 3 | 4 | 2 | 3 | 6 | 3 | BR-03669/2026 | fonte · 2ª |
| Futura Inteligencia | Apex/100% Cidades | 01/09 | 2.000 | telefônico | 38,7 | 33,6 | 9,9 | 2,8 | 3,5 | 1,6 | 3,8 | 2 | 4,1 | BR-02793/2026 | fonte · 2ª |
| Quaest | Globo/O Globo | 01/09 | 2.004 | presencial | 37 | 29 | 10 | 3 | 1 | 1 | 1 | 7 | 11 | BR-07065/2026 | fonte · 2ª |
| Real Time Big Data | Real Time Big Data (recursos próprios) | 31/08 | 2.000 | telefônico | 38 | 30 | 11 | 7 | 4 | 2 | 2 | 3 | 3 | BR-03490/2026 | fonte · 2ª |
| AtlasIntel | Bloomberg (AtlasIntel recursos próprios) | 30/08 | 5.014 | online | 43,4 | 33,7 | 7,8 | 7,6 | 3,3 | 1 | 3 | 0,1 | 0,2 | BR-07972/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 30/08 | 2.005 | telefônico | 39 | 33 | 11 | 3 | 5 | 1 | 1 | 3 | 3 | BR-08900/2026 | fonte · 2ª |
| Vox Brasil | próprio | 27/08 | 2.100 | presencial | 37,1 | 34,8 | 2,6 | 3,3 | 5 | 2,8 | 4,1 | 3,5 | 6,8 | BR-05519/2026 | fonte · 2ª |
| PoderData | PoderData (recursos próprios)/Aya | 26/08 | 2.400 | telefônico | 38 | 35 | 4 | 4 | 4 | 2 | 7 | 5 | 2 | BR-04974/2026 | fonte · 2ª |
| Gerp | AESP | 25/08 | 2.400 | telefônico | 37 | 38 | 1 | 3 | 3 | 1 | 4 | 4 | 8 | BR-03547/2026 | fonte · 2ª |
| Indexa | Broadcast/Estadão | 23/08 | 2.000 | telefônico | 39 | 34 | 1 | 4 | 5 | 1 | 0 | 10 | 6 | BR-06366/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 23/08 | 2.006 | telefônico | 41 | 37 | 2 | 3 | 5 | 3 | 1 | 5 | 3 | BR-09028/2026 | fonte · 2ª |
| Verita | próprio | 20/08 | 3.840 | telefônico | 39,3 | 39,1 | 2,2 | 3,8 | 3,3 | 1,3 | 7 | 1,8 | 2,4 | BR-04006/2026 | fonte · 2ª |
| Datafolha | Folha de S.Paulo/Globo | 20/08 | 2.058 | presencial | 39 | 33 | 2 | 4 | 5 | 3 | 4 | 6 | 4 | BR-04496/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 16/08 | 2.003 | telefônico | 41 | 36 | 1 | 4 | 4 | 4 | 1 | 5 | 2 | BR-03317/2026 | fonte · 2ª |
| Quaest | Globo/O Globo | 13/08 | 2.004 | presencial | 38 | 31 | 2 | 4 | 4 | 2 | 1 | 8 | 10 | BR-06773/2026 | fonte · 2ª |
| PoderData | PoderData (recursos próprios)/Aya | 12/08 | 2.400 | telefônico | 41 | 35 | 3 | 4 | 4 | 3 | 3 | 4 | 2 | BR-06868/2026 | fonte · 2ª |
| Gerp | próprio | 10/08 | 2.400 | telefônico | 38 | 38 | 1 | 5 | 4 | 2 | 1 | 4 | 7 | BR-08045/2026 | fonte · 2ª |
| CNT/MDA | CNT | 09/08 | 2.002 | presencial | 42,4 | 28,7 | 1,2 | 2,8 | 4 | 3,3 | 3,4 | 6,8 | 7,2 | BR-06935/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 09/08 | 2.001 | telefônico | 40 | 35 | 1 | 4 | 5 | 3 | 3 | 5 | 3 | BR-08428/2026 | fonte · 2ª |
| Palver | próprio | 07/08 | 5.000 | online | 44 | 40 | 0 | 10 | 2 | 1 | 1 | 1 | 0 | BR-06596/2026 | fonte · 2ª |
| Futura Inteligencia | Apex/100% Cidades | 07/08 | 2.000 | telefônico | 38,8 | 34,1 | 1,2 | 3,6 | 5,9 | 3,6 | 2 | 6,2 | 4,8 | BR-08109/2026 | fonte · 2ª |
| Quaest | Genial (Banco Genial S.A.) | 03/08 | 2.004 | presencial | 39 | 30 | 1 | 4 | 4 | 2 | 2 | 8 | 10 | BR-06591/2026 | fonte · 2ª |
| Meio/Ideia | Canal Meio | 03/08 | 1.500 | telefônico | 43 | 35 | 1,7 | 4,7 | 5,7 | 2,6 | 1,3 | 2 | 4 | BR-04579/2026 | fonte · 2ª |
| BTG/Nexus | BTG Pactual | 02/08 | 2.002 | telefônico | 41 | 37 | 1 | 4 | 5 | 3 | 2 | 4 | 3 | BR-02874/2026 | fonte · 2ª |
Depois do 1º turno, voto declarado no 2º turno por voto no 1º (PoderData/Aya 10–12/out, via TMC), % Flávio / Lula: Cury 45/43 · Caiado 44/30 · Renan Santos 38/33 · outro candidato 57/17 (Zema sem linha própria); brancos/nulos: Renan 27%, Caiado 14%, Cury 8%. Datafolha 6–7/out, só o agregado dos quatro: Flávio 46, Lula 32, branco/nulo 15. Antes do 1º turno, 2ª opção declarada (Quaest 3–6/set), % dos eleitores de cada candidato que migram para Flávio / Lula no 2º turno: Zema 59/4 · Renan Santos 58/14 · Marçal 47/19 · Cury 46/24 · Caiado 36/25. O modelo usa a matriz pós-1º turno (PoderData) e confere o agregado com o Datafolha.
modelo_eleicao_2026.py (recalcula efeitos de casa, simulações e SVGs; após 4/out, preencha resultado_1turno.csv para ativar o regime pós-1º turno) e depois montar_painel.py / montar_standalone.py. Os arquivos abaixo estão embutidos neste HTML.# -*- coding: utf-8 -*-
"""
Espaço de estados para agregação de pesquisas (núcleo do modelo v3).
y_i = x_{t(i)} + h_{p(i)} + ε_i ε_i ~ N(0, se_i² + σ_{p(i)}²)
x_t = x_{t-1} + η_t η_t ~ N(0, σ_rw²)
h_p ~ N(0, τ²) com Σ_{p ∈ âncora} q_p·h_p = 0 (identificação pelos institutos calibrados)
Estimação por ECM: (1) dado h e σ_p, σ_rw por MAP (prior log-normal) e suavizador RTS para x_t;
(2) dado x_t, atualiza h_p (média posterior ridge) e σ_p² (empírico-Bayes, encolhido para a mediana).
Referências: Jackman (2005), Linzer (2013), Heidemanns–Gelman–Morris (2020).
"""
import math
import numpy as np
from scipy.optimize import minimize_scalar
def _kalman(y, day, R, sig_rw, n_days, x0, keep=False, q_extra=None):
"""Filtro para uma série diária. y, day, R ordenados por dia. Retorna loglik e, se keep, trajetórias.
q_extra: dict dia → variância adicional do passeio nesse dia (quebra estrutural, ex.: o dia seguinte ao 1º turno)."""
x, P = x0, 100.0
ll = 0.0
xf = np.empty(n_days); Pf = np.empty(n_days); xp = np.empty(n_days); Pp = np.empty(n_days)
obs_by_day = {}
for i, d in enumerate(day):
obs_by_day.setdefault(int(d), []).append(i)
q = sig_rw ** 2
q_extra = q_extra or {}
for d in range(n_days):
if d > 0:
P += q + q_extra.get(d, 0.0)
xp[d], Pp[d] = x, P
for i in obs_by_day.get(d, []):
F = P + R[i]
v = y[i] - x
ll += -0.5 * (math.log(2 * math.pi * F) + v * v / F)
K = P / F
x += K * v
P *= (1 - K)
xf[d], Pf[d] = x, P
if not keep:
return ll
# suavizador RTS
xs = xf.copy(); Ps = Pf.copy()
for d in range(n_days - 2, -1, -1):
C = Pf[d] / Pp[d + 1] if Pp[d + 1] > 0 else 0.0
xs[d] = xf[d] + C * (xs[d + 1] - xp[d + 1])
Ps[d] = Pf[d] + C * C * (Ps[d + 1] - Pp[d + 1])
return ll, xs, Ps, xf, Pf
def ajustar(y, day, se2, inst, ancora, q_ancora=None, tau_h=1.5, prior_rw=(math.log(0.25), 0.6),
k_sigma=4.0, sigma_floor=0.8, n_iter=15, tol=0.01, sig_rw_fixo=None, prior_h=None, tau_prior_h=None,
n_days=None, q_extra=None, a_h=None):
"""
y, day, se2: arrays (observação, dia inteiro desde a origem, variância amostral).
inst: array de rótulos. ancora: conjunto de institutos que identificam o nível.
q_ancora: pesos dos institutos da âncora (default 1). prior_h: dict instituto → média a priori do efeito de casa
(usado no regime pós-1º turno); tau_prior_h: desvio desse prior (default tau_h). q_extra: ver _kalman.
a_h: fator por observação com que o efeito de casa entra (y_i = x + a_i·h_p + ε); default 1. Serve à persistência parcial do efeito
de casa depois do 1º turno (a = λ < 1 nas pesquisas posteriores a 4/out): a observação identifica a_i·h, não h.
Retorna dict com x (nível suavizado por dia), P (variância), house, house_var (variância posterior de cada efeito de casa),
var_ancora (variância da média da âncora, que desloca o nível), sigma_inst, sig_rw, sig_rw_ml, loglik, resid.
"""
y = np.asarray(y, float); day = np.asarray(day, int); se2 = np.asarray(se2, float); inst = np.asarray(inst)
a_h = np.ones(len(y)) if a_h is None else np.asarray(a_h, float)
order = np.argsort(day, kind="stable")
y, day, se2, inst, a_h = y[order], day[order], se2[order], inst[order], a_h[order]
n_days = max(int(day.max()) + 1, n_days or 0) # o estado é propagado até n_days−1 (ex.: hoje) sem observações
insts = list(dict.fromkeys(inst))
idx = {p: np.where(inst == p)[0] for p in insts}
q_ancora = q_ancora or {}
prior_h = prior_h or {}
tau_prior_h = tau_prior_h or tau_h
h = {p: float(prior_h.get(p, 0.0)) for p in insts}
sig_p = {p: 2.0 for p in insts}
sig_rw = 0.25 if sig_rw_fixo is None else sig_rw_fixo
x0 = float(y[0])
hist_ll = []
for it in range(n_iter):
h_arr = a_h * np.array([h[p] for p in inst])
R = se2 + np.array([sig_p[p] ** 2 for p in inst])
y_adj = y - h_arr
if sig_rw_fixo is None:
def neg_post(z):
ll = _kalman(y_adj, day, R, math.exp(z), n_days, x0, q_extra=q_extra)
lp = -0.5 * ((z - prior_rw[0]) / prior_rw[1]) ** 2
return -(ll + lp)
opt = minimize_scalar(neg_post, bounds=(math.log(0.02), math.log(2.0)), method="bounded",
options={"xatol": 1e-3})
sig_rw = math.exp(opt.x)
ll, xs, Ps, xf, Pf = _kalman(y_adj, day, R, sig_rw, n_days, x0, keep=True, q_extra=q_extra)
hist_ll.append(ll)
# (2) efeitos de casa: média posterior sob h_p ~ N(prior, τ²) dado x suavizado
x_at = xs[day]
h_new = {}
for p in insts:
ii = idx[p]
w = 1.0 / R[ii]
tau2 = (tau_prior_h if p in prior_h else tau_h) ** 2
m0 = prior_h.get(p, 0.0)
h_new[p] = float((np.sum(w * a_h[ii] * (y[ii] - x_at[ii])) + m0 / tau2) / (np.sum(w * a_h[ii] ** 2) + 1.0 / tau2))
# identificação: média ponderada da âncora = 0 (o deslocamento vai para o nível)
anc = [p for p in insts if p in ancora]
if anc:
wq = np.array([q_ancora.get(p, 1.0) for p in anc])
c = float(np.sum(wq * np.array([h_new[p] for p in anc])) / wq.sum())
else:
c = float(np.mean(list(h_new.values())))
h_new = {p: v - c for p, v in h_new.items()}
xs = xs + c; xf = xf + c; x0 = x0 + c
delta = max(abs(h_new[p] - h[p]) for p in insts)
h = h_new
# (3) variância extra por instituto (EB): resíduo² − variância amostral − variância do estado, encolhido para a mediana
resid = y - xs[day] - a_h * np.array([h[p] for p in inst])
excess = {}
for p in insts:
ii = idx[p]
e2 = float(np.mean(resid[ii] ** 2 + Ps[day[ii]] - se2[ii]))
excess[p] = (len(ii), max(e2, 0.0))
med = float(np.median([v for _, v in excess.values()]))
sig_p = {p: math.sqrt(max((n * e2 + k_sigma * med) / (n + k_sigma), sigma_floor ** 2)) for p, (n, e2) in excess.items()}
if delta < tol and it >= 3:
break
# verossimilhança sem prior, para transparência sobre σ_rw
h_arr = a_h * np.array([h[p] for p in inst]); R = se2 + np.array([sig_p[p] ** 2 for p in inst]); y_adj = y - h_arr
ml = minimize_scalar(lambda z: -_kalman(y_adj, day, R, math.exp(z), n_days, x0, q_extra=q_extra), bounds=(math.log(0.02), math.log(2.0)),
method="bounded", options={"xatol": 1e-3})
ll, xs, Ps, xf, Pf = _kalman(y_adj, day, R, sig_rw, n_days, x0, keep=True, q_extra=q_extra)
resid = y - xs[day] - h_arr
# incerteza dos efeitos de casa (tratados como conhecidos no filtro): variância posterior de cada h_p e da média da âncora
house_var = {p: float(1.0 / (np.sum(a_h[idx[p]] ** 2 / R[idx[p]]) + 1.0 / ((tau_prior_h if p in prior_h else tau_h) ** 2))) for p in insts}
anc = [p for p in insts if p in ancora]
if anc:
wq = np.array([q_ancora.get(p, 1.0) for p in anc])
var_ancora = float(np.sum(wq ** 2 * np.array([house_var[p] for p in anc])) / wq.sum() ** 2)
else:
var_ancora = float(np.mean(list(house_var.values())) / len(insts))
return {"x": xs, "P": Ps, "x_filtro": xf, "P_filtro": Pf, "house": h, "house_var": house_var, "var_ancora": var_ancora, "sigma_inst": sig_p, "sig_rw": sig_rw,
"sig_rw_ml": math.exp(ml.x), "loglik": ll, "resid": resid, "order": order, "R": R, "iter": it + 1,
"n_days": n_days, "insts": insts}
def validacao_rolling(y, day, se2, inst, ancora, q_ancora, dias, horizonte=7, a_h=None, **kw):
"""Origem móvel: para cada dia d em `dias`, ajusta com observações até d e prevê as de (d, d+horizonte].
`a_h` é a persistência do efeito de casa por observação (λ nas pesquisas pós-1º turno), a mesma do ajuste validado.
Retorna lista de (dia, y_obs, y_prev, sd_prev, instituto, instituto_ja_visto)."""
y = np.asarray(y, float); day = np.asarray(day, int); se2 = np.asarray(se2, float); inst = np.asarray(inst)
a_h = np.ones(len(y)) if a_h is None else np.asarray(a_h, float)
out = []
for d in dias:
m = day <= d
if m.sum() < 20:
continue
fit = ajustar(y[m], day[m], se2[m], inst[m], ancora, q_ancora, n_iter=8, n_days=d + 1, a_h=a_h[m], **kw) # estado propagado até o dia d
x_d, P_d = fit["x"][d], fit["P"][d]
f = (day > d) & (day <= d + horizonte)
for i in np.where(f)[0]:
hp = fit["house"].get(inst[i], 0.0)
sp = fit["sigma_inst"].get(inst[i], float(np.median(list(fit["sigma_inst"].values()))))
k = day[i] - d
sd = math.sqrt(P_d + k * fit["sig_rw"] ** 2 + se2[i] + sp ** 2)
out.append((int(d), float(y[i]), float(x_d + a_h[i] * hp), sd, str(inst[i]), bool(inst[i] in fit["house"])))
return out
# -*- coding: utf-8 -*-
"""
Modelo do 2º turno estimado por MCMC (Markov chain Monte Carlo): mesmo modelo de espaco_estados.py, com o posterior completo.
y_i = x_{t(i)} + μ_{r(i)} + β + h_{p(i)} + ε_i ε_i ~ N(0, se_i² + σ_{p(i)}²)
x_t = x_{t-1} + η_t η_t ~ N(0, σ_rw²) até o 1º turno · + SD_QUEBRA² no dia seguinte · N(0, σ_entre²) depois
h_p ~ N(prior_p, τ²) com Σ_{p ∈ âncora} q_p·h_p = 0
β ~ t₅(0, sd_vies) erro sistemático comum das pesquisas (μ_r = desconto histórico do regime: antes/depois do 1º turno)
obs. extras: nível estrutural (voto contado + transferência) = x_{T1+1} + e, sem viés de pesquisa
log σ_rw ~ N(log 0,25; 0,6) · log σ_entre ~ N(log σ_entre_hist; 0,5)
log σ_p ~ N(m, 0,5²) truncada em σ_p ≥ 0,8 · m ~ N(log 2; 0,5²)
previsão: x_{T2} = x_{último dia} + deriva, deriva ~ t₅ com desvio sqrt(Σ σ²_dia) (como em simular())
O modelo atual (espaco_estados.ajustar + simular) fixa σ_rw, σ_p, os efeitos de casa e o nível em estimativas pontuais e sorteia
a deriva e o viés por fora. Aqui tudo é sorteado em conjunto. Amostrador (Gibbs com passos de Metropolis):
1. σ_rw e σ_entre por Metropolis com (x, β, h) integrados: dado o resto, o modelo é linear-gaussiano e a verossimilhança marginal
sai da fatoração de Cholesky da precisão conjunta — mistura muito melhor que condicionar na trajetória;
2. (x, β, h) num bloco só, sorteio exato da normal conjunta (a restrição da âncora entra como observação de variância ~0);
3. σ_p por Metropolis, dado o resíduo de cada instituto; m por Metropolis; 4. escala da t do viés (ω) por Gama conjugada.
Diagnósticos: R̂ dividido e ESS multicadeia (Vehtari et al. 2021, sem normalização por postos).
"""
import math
import numpy as np
from scipy.linalg import cholesky, solve_triangular
from scipy.stats import norm
DF = 5 # graus de liberdade da t (deriva e viés), o mesmo DF_ERRO do modelo atual
K_T = (DF - 2) / DF # escala da t com o mesmo desvio da normal (como _t_unit)
SIG_P_PISO, SIG_P_SD, M_PRIOR = 0.8, 0.5, (math.log(2.0), 0.5)
KAPPA = 1e-6 # variância da "observação" Σ q_p h_p = 0
def _monta(y, dia, se2, inst, n_dias, mu_obs, ancora, q_ancora, prior_h, tau_h, extras, sd_vies, x0_sd, a_h=None):
"""Pré-calcula a matriz de desenho e as partes fixas da precisão. Vetor z = [x_0 … x_{D−1}, β, h_1 … h_P]."""
insts = list(dict.fromkeys(inst))
P, D = len(insts), n_dias
com_vies = sd_vies > 0
iB = D if com_vies else None
i0h = D + (1 if com_vies else 0)
K = i0h + P
pidx = np.array([insts.index(p) for p in inst])
n, ne = len(y), len(extras)
H = np.zeros((n + ne, K))
H[np.arange(n), dia] = 1.0
if com_vies:
H[:n, iB] = 1.0
a_h = np.ones(n) if a_h is None else np.asarray(a_h, float) # persistência do efeito de casa (λ nas pesquisas pós-1º turno)
H[np.arange(n), i0h + pidx] = a_h
yy = np.concatenate([np.asarray(y, float) - mu_obs, [e["valor"] - e.get("mu", 0.0) for e in extras]])
r_extra = np.array([e["sd"] ** 2 for e in extras])
for j, e in enumerate(extras):
H[n + j, e["dia"]] = 1.0
if e.get("com_vies") and com_vies:
H[n + j, iB] = 1.0
# prior fixo: efeitos de casa, restrição da âncora, x_0
Q_fix = np.zeros((K, K)); b_fix = np.zeros(K)
m_h = np.array([prior_h.get(p, 0.0) for p in insts]); t2 = tau_h ** 2
Q_fix[i0h + np.arange(P), i0h + np.arange(P)] += 1.0 / t2
b_fix[i0h:] += m_h / t2
a = np.zeros(K)
for j, p in enumerate(insts):
if p in ancora:
a[i0h + j] = q_ancora.get(p, 1.0)
if not a.any():
a[i0h:] = 1.0
Q_fix += np.outer(a, a) / KAPPA
x0_m = float(np.mean(np.sort(np.asarray(y, float)[np.argsort(dia)][:10]))) # média das primeiras ondas, prior difuso
Q_fix[0, 0] += 1.0 / x0_sd ** 2; b_fix[0] += x0_m / x0_sd ** 2
return {"H": H, "yy": yy, "n": n, "r_extra": r_extra, "pidx": pidx, "insts": insts, "P": P, "D": D, "K": K, "iB": iB,
"i0h": i0h, "Q_fix": Q_fix, "b_fix": b_fix, "se2": np.asarray(se2, float), "dia": np.asarray(dia, int), "x0_sd": x0_sd, "a_h": a_h}
def _var_passos(D, dia_t1, sd_quebra, s_rw, s_entre):
"""Variância de η_d para d = 1 … D−1: σ_rw² até o 1º turno, + quebra no dia seguinte, σ_entre² depois."""
d = np.arange(1, D)
v = np.where(d <= dia_t1 + 1, s_rw ** 2, s_entre ** 2)
if sd_quebra > 0 and 1 <= dia_t1 + 1 < D:
v[dia_t1] += sd_quebra ** 2
return v
def _posterior(M, v, sig_p, omega, s_b):
"""Precisão e vetor canônico da normal conjunta de z dados os hiperparâmetros; devolve (L, b, log p(y|θ) a menos de constante)."""
D, n = M["D"], M["n"]
r = np.concatenate([M["se2"] + sig_p[M["pidx"]] ** 2, M["r_extra"]])
w = 1.0 / r
Hw = M["H"] * w[:, None]
Q = M["Q_fix"] + M["H"].T @ Hw
b = M["b_fix"] + Hw.T @ M["yy"]
iv = 1.0 / v
di = np.arange(D)
diag = np.zeros(D); diag[1:] += iv; diag[:-1] += iv
Q[di, di] += diag
Q[di[1:], di[:-1]] -= iv; Q[di[:-1], di[1:]] -= iv
if M["iB"] is not None:
Q[M["iB"], M["iB"]] += omega / s_b ** 2
L = cholesky(Q, lower=True, check_finite=False)
u = solve_triangular(L, b, lower=True, check_finite=False)
# log p(y|θ) = ½log|Q0| − ½log|Q| + ½ bᵀQ⁻¹b − ½ Σ log r − ½ yᵀR⁻¹y (+ constantes); |Q0| só tem a parte de x e de β que varia
ll = (-0.5 * np.sum(np.log(v)) - np.sum(np.log(np.diag(L))) + 0.5 * u @ u - 0.5 * np.sum(np.log(r)) - 0.5 * np.sum(w * M["yy"] ** 2))
if M["iB"] is not None:
ll += 0.5 * math.log(omega)
return L, u, ll
def _lp_logsig_p(ls, m):
"""Log-prior de log σ_p: normal truncada em σ_p ≥ piso (o termo de truncamento depende de m)."""
lo = math.log(SIG_P_PISO)
if np.any(ls < lo):
return -np.inf
return float(np.sum(-0.5 * ((ls - m) / SIG_P_SD) ** 2) - len(ls) * norm.logsf((lo - m) / SIG_P_SD))
def _cadeia(M, rng, n_aquec, n_amostras, dia_t1, sd_quebra, prior_rw, prior_entre, sd_vies, n_fut_rw, n_fut_entre, entre_tem_dados, prior_mu_der=(0.0, 0.0)):
D, P = M["D"], M["P"]
s_b = sd_vies * math.sqrt(K_T) if sd_vies > 0 else 1.0
lrw = prior_rw[0] + 0.3 * rng.standard_normal()
lent = prior_entre[0] + 0.3 * rng.standard_normal()
ls = np.log(np.maximum(2.0 * np.exp(0.2 * rng.standard_normal(P)), SIG_P_PISO + 0.05))
m = M_PRIOR[0]; omega = 1.0
passo = {"rw": 0.25, "entre": 0.3, "m": 0.3}; passo_p = np.full(P, 0.4)
ac = {"rw": [], "entre": [], "m": []}; ac_p = []
tot = n_aquec + n_amostras
out = {k: np.empty(n_amostras) for k in ["x_hoje", "x_ultimo", "x_final", "sig_rw", "sig_entre", "beta", "deriva", "m_sigp", "mu_der"]}
out["h"] = np.empty((n_amostras, P)); out["sig_p"] = np.empty((n_amostras, P)); out["x"] = np.empty((n_amostras, D))
lp_rw = lambda z: -0.5 * ((z - prior_rw[0]) / prior_rw[1]) ** 2
lp_en = lambda z: -0.5 * ((z - prior_entre[0]) / prior_entre[1]) ** 2
for it in range(tot):
sp = np.exp(ls)
L, u, ll = _posterior(M, _var_passos(D, dia_t1, sd_quebra, math.exp(lrw), math.exp(lent)), sp, omega, s_b)
# 1a. σ_rw (z integrado)
z_ = lrw + passo["rw"] * rng.standard_normal()
L2, u2, ll2 = _posterior(M, _var_passos(D, dia_t1, sd_quebra, math.exp(z_), math.exp(lent)), sp, omega, s_b)
aceita = math.log(rng.random()) < ll2 + lp_rw(z_) - ll - lp_rw(lrw)
if aceita:
lrw, L, u, ll = z_, L2, u2, ll2
ac["rw"].append(aceita)
# 1b. σ_entre: só tem verossimilhança se houver dias observados depois do 1º turno; senão, sorteio direto do prior
if entre_tem_dados:
z_ = lent + passo["entre"] * rng.standard_normal()
L2, u2, ll2 = _posterior(M, _var_passos(D, dia_t1, sd_quebra, math.exp(lrw), math.exp(z_)), sp, omega, s_b)
aceita = math.log(rng.random()) < ll2 + lp_en(z_) - ll - lp_en(lent)
if aceita:
lent, L, u, ll = z_, L2, u2, ll2
ac["entre"].append(aceita)
else:
lent = prior_entre[0] + prior_entre[1] * rng.standard_normal()
# 2. (x, β, h) em bloco: z = Q⁻¹b + L⁻ᵀε
zv = solve_triangular(L.T, u + rng.standard_normal(M["K"]), lower=False, check_finite=False)
x = zv[:D]; beta = zv[M["iB"]] if M["iB"] is not None else 0.0; h = zv[M["i0h"]:]
# 3. σ_p por instituto (Metropolis vetorizado, dado o resíduo)
res = M["yy"][:M["n"]] - x[M["dia"]] - beta - M["a_h"] * h[M["pidx"]]
def ll_p(lsv):
r_ = M["se2"] + np.exp(lsv)[M["pidx"]] ** 2
return np.bincount(M["pidx"], weights=-0.5 * np.log(r_) - 0.5 * res ** 2 / r_, minlength=P)
lo = math.log(SIG_P_PISO)
prop = ls + passo_p * rng.standard_normal(P)
lp_c = -0.5 * ((ls - m) / SIG_P_SD) ** 2; lp_n = np.where(prop >= lo, -0.5 * ((prop - m) / SIG_P_SD) ** 2, -np.inf)
a_p = np.log(rng.random(P)) < ll_p(prop) + lp_n - ll_p(ls) - lp_c
ls = np.where(a_p, prop, ls); ac_p.append(a_p)
# 3b. m (centro hierárquico de log σ_p)
m_ = m + passo["m"] * rng.standard_normal()
lm = lambda mm: _lp_logsig_p(ls, mm) - 0.5 * ((mm - M_PRIOR[0]) / M_PRIOR[1]) ** 2
aceita = math.log(rng.random()) < lm(m_) - lm(m)
if aceita:
m = m_
ac["m"].append(aceita)
# 4. ω | β (t₅ como mistura de escala de normais)
if M["iB"] is not None:
omega = rng.gamma(DF / 2 + 0.5, 1.0 / (DF / 2 + beta ** 2 / (2 * s_b ** 2)))
# adaptação dos passos no aquecimento (alvo ~0,44 em uma dimensão)
if it < n_aquec and (it + 1) % 50 == 0:
for k in passo:
if ac[k]:
passo[k] *= math.exp(np.mean(ac[k][-50:]) - 0.44)
passo_p *= np.exp(np.mean(ac_p[-50:], axis=0) - 0.44)
if it >= n_aquec:
j = it - n_aquec
# previsão: deriva até 25/out com caudas t₅ e o mesmo desvio da versão normal
v_fut = n_fut_rw * math.exp(lrw) ** 2 + n_fut_entre * math.exp(lent) ** 2
om_d = rng.gamma(DF / 2, 2.0 / DF)
# média da deriva (variante com momentum): sorteada do prior a cada iteração, sem verossimilhança própria (como σ_entre sem dados)
mu_d = prior_mu_der[0] + prior_mu_der[1] * rng.standard_normal() if prior_mu_der[1] > 0 else prior_mu_der[0]
der = mu_d + math.sqrt(v_fut * K_T / om_d) * rng.standard_normal()
out["x_ultimo"][j] = x[-1]; out["x_final"][j] = x[-1] + der; out["deriva"][j] = der; out["mu_der"][j] = mu_d
out["sig_rw"][j] = math.exp(lrw); out["sig_entre"][j] = math.exp(lent); out["beta"][j] = beta
out["m_sigp"][j] = math.exp(m); out["h"][j] = h; out["sig_p"][j] = np.exp(ls); out["x"][j] = x
taxa = {k: round(float(np.mean(v[n_aquec:])), 3) for k, v in ac.items() if len(v) > n_aquec}
taxa["sig_p_media"] = round(float(np.mean(ac_p[n_aquec:])), 3)
return out, taxa
def amostrar(y, dia, se2, inst, n_dias, ancora, q_ancora=None, mu_obs=None, sd_vies=0.0, prior_h=None, tau_h=1.5,
dia_t1=10 ** 9, sd_quebra=0.0, extras=(), prior_rw=(math.log(0.25), 0.6), prior_entre=(math.log(0.25), 0.5),
n_fut_rw=0, n_fut_entre=0, dia_hoje=None, n_cadeias=4, n_aquec=1500, n_amostras=4000, seed=20261025, x0_sd=10.0, a_h=None,
prior_mu_der=(0.0, 0.0)):
"""
y, dia, se2, inst: pesquisas (dia inteiro desde a origem da janela). n_dias: nº de dias do estado (0 … n_dias−1).
mu_obs: desconto de viés esperado de cada pesquisa (antes/depois do 1º turno). sd_vies: desvio do erro comum β (0 = sem β).
dia_t1: índice do dia do 1º turno; dia_t1+1 recebe a quebra sd_quebra² e os dias seguintes usam σ_entre.
extras: observações diretas do estado, dicts {dia, valor, sd, com_vies, mu} (nível estrutural, leitura estadual).
n_fut_rw / n_fut_entre: dias entre o último dia do estado e 25/out em cada regime de deriva. dia_hoje: dia reportado como "hoje".
prior_mu_der: (média, desvio) da média da deriva até 25/out; (0, 0) = deriva centrada em zero (modelo principal).
Retorna dict com as amostras (cadeias × sorteios), taxas de aceitação e diagnósticos.
"""
q_ancora = q_ancora or {}; prior_h = prior_h or {}
mu_obs = np.zeros(len(y)) if mu_obs is None else np.asarray(mu_obs, float)
M = _monta(np.asarray(y, float), np.asarray(dia, int), se2, np.asarray(inst), n_dias, mu_obs, set(ancora), q_ancora, prior_h,
tau_h, list(extras), sd_vies, x0_sd, a_h)
entre_tem_dados = n_dias - 1 > dia_t1 + 1
dia_hoje = n_dias - 1 if dia_hoje is None else dia_hoje
cad, taxas = [], []
for c in range(n_cadeias):
o, t = _cadeia(M, np.random.default_rng([seed, c]), n_aquec, n_amostras, dia_t1, sd_quebra, prior_rw, prior_entre, sd_vies,
n_fut_rw, n_fut_entre, entre_tem_dados, prior_mu_der)
o["x_hoje"] = o["x"][:, dia_hoje]
cad.append(o); taxas.append(t)
S = {k: np.stack([o[k] for o in cad]) for k in cad[0]}
diag = {k: {"rhat": round(rhat(S[k]), 4), "ess": int(ess(S[k]))} for k in ["x_hoje", "x_final", "sig_rw", "sig_entre", "beta", "m_sigp"] + (["mu_der"] if prior_mu_der[1] > 0 else [])}
for j, p in enumerate(M["insts"]):
diag[f"h[{p}]"] = {"rhat": round(rhat(S["h"][:, :, j]), 4), "ess": int(ess(S["h"][:, :, j]))}
diag[f"sig_p[{p}]"] = {"rhat": round(rhat(S["sig_p"][:, :, j]), 4), "ess": int(ess(S["sig_p"][:, :, j]))}
return {"amostras": S, "insts": M["insts"], "aceitacao": taxas, "diagnostico": diag,
"rhat_max": max(d["rhat"] for d in diag.values()), "ess_min": min(d["ess"] for d in diag.values()),
"n_cadeias": n_cadeias, "n_aquec": n_aquec, "n_amostras": n_amostras, "entre_tem_dados": bool(entre_tem_dados)}
# ---------------- diagnósticos ----------------
def _split(x):
x = np.asarray(x, float)
h = x.shape[1] // 2
return np.concatenate([x[:, :h], x[:, h:2 * h]])
def rhat(x):
"""R̂ dividido (Gelman et al., BDA3): cadeias × sorteios."""
s = _split(x)
m, n = s.shape
W = s.var(axis=1, ddof=1).mean()
B = n * s.mean(axis=1).var(ddof=1)
if W <= 0:
return 1.0
return float(math.sqrt(((n - 1) / n * W + B / n) / W))
def ess(x):
"""Tamanho efetivo de amostra multicadeia (Geyer: soma de pares positivos e monótonos de autocorrelação)."""
s = _split(x)
m, n = s.shape
if s.var() == 0:
return float(m * n)
xc = s - s.mean(axis=1, keepdims=True)
f = np.fft.rfft(xc, n=2 * n, axis=1)
acov = np.fft.irfft(f * np.conj(f), axis=1)[:, :n] / n
W = s.var(axis=1, ddof=1).mean()
var_mais = (n - 1) / n * W + s.mean(axis=1).var(ddof=1)
rho = 1.0 - (W - acov.mean(axis=0)) / var_mais
rho[0] = 1.0
soma, prev = 0.0, np.inf
for t in range(0, n - 1, 2):
par = rho[t] + rho[t + 1]
if par < 0:
break
par = min(par, prev); prev = par
soma += par
tau = -1.0 + 2.0 * soma
return float(m * n / max(tau, 1.0 / math.log10(m * n)))
# ---------------- verificação ----------------
if __name__ == "__main__":
import sys, os, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from espaco_estados import _kalman
# (a) com hiperparâmetros fixos, sem β e sem efeitos de casa, a normal conjunta coincide com o suavizador RTS do filtro de Kalman
rng = np.random.default_rng(1)
D = 80; xs_true = np.cumsum(np.r_[2.0, 0.3 * rng.standard_normal(D - 1)])
dd = np.sort(rng.integers(0, D, 40)); se2 = np.full(40, 4.0); yv = xs_true[dd] + 2 * rng.standard_normal(40)
M = _monta(yv, dd, se2, np.array(["A"] * 40), D, np.zeros(40), {"A"}, {}, {}, 1.5, [], 0.0, 10.0)
M["b_fix"][0] = yv[0] / 100.0 # mesmo x_0 do filtro: N(y_0, 100)
L, u, _ = _posterior(M, np.full(D - 1, 0.3 ** 2), np.array([1.0]), 1.0, 1.0)
mean = solve_triangular(L.T, u, lower=False)[:D]
Linv = solve_triangular(L, np.eye(M["K"]), lower=True); var = (Linv ** 2).sum(axis=0)[:D]
_, xs_k, Ps_k, _, _ = _kalman(yv, dd, se2 + 1.0, 0.3, D, yv[0], keep=True)
assert np.allclose(mean, xs_k, atol=1e-4), np.abs(mean - xs_k).max()
assert np.allclose(var, Ps_k, atol=1e-4), np.abs(var - Ps_k).max()
print(f"[ok] normal conjunta = suavizador RTS (dif. máx. média {np.abs(mean - xs_k).max():.1e}, variância {np.abs(var - Ps_k).max():.1e})")
# (b) recuperação de parâmetros em dados simulados: casas, σ_rw, σ_p e o nível final
rng = np.random.default_rng(7)
D = 120; srw = 0.2; casas = {"A": 0.0, "B": 0.0, "C": 2.5, "D": -2.0, "E": 1.0}; sigp = {"A": 1.0, "B": 1.0, "C": 3.0, "D": 1.5, "E": 1.0}
xt = np.cumsum(np.r_[-1.0, srw * rng.standard_normal(D - 1)])
rows = [(d, p) for d in range(0, D, 2) for p in casas if rng.random() < 0.35]
dd = np.array([d for d, _ in rows]); ii = np.array([p for _, p in rows]); se2 = np.full(len(rows), 2.2 ** 2)
yv = xt[dd] + np.array([casas[p] for p in ii]) + np.sqrt(se2 + np.array([sigp[p] ** 2 for p in ii])) * rng.standard_normal(len(rows))
t0 = time.time()
R = amostrar(yv, dd, se2, ii, D, {"A", "B"}, n_cadeias=4, n_aquec=800, n_amostras=1500, seed=3)
S = R["amostras"]
def cobre(a, v, lo=5, hi=95): q_ = np.percentile(a, [lo, hi]); return q_[0] <= v <= q_[1]
assert cobre(S["sig_rw"], srw), np.percentile(S["sig_rw"], [5, 95])
assert cobre(S["x_hoje"], xt[-1]), (np.percentile(S["x_hoje"], [5, 95]), xt[-1])
for j, p in enumerate(R["insts"]):
assert cobre(S["h"][:, :, j], casas[p] - (casas["A"] + casas["B"]) / 2), (p, np.percentile(S["h"][:, :, j], [5, 95]))
assert R["rhat_max"] < 1.05, R["rhat_max"]
print(f"[ok] recuperação: σ_rw {np.median(S['sig_rw']):.3f} (verdadeiro {srw}), nível final {np.median(S['x_hoje']):+.2f} "
f"(verdadeiro {xt[-1]:+.2f}), R̂ máx {R['rhat_max']:.3f}, ESS mín {R['ess_min']}, {len(rows)} pesquisas, {time.time() - t0:.1f} s")
# -*- coding: utf-8 -*-
"""
Migração de ausentes, terceira via e brancos/nulos entre o 1º e o 2º turno: cadeia de Markov + Monte Carlo (análise paralela).
Pergunta: para onde vão, até 25/out, os 33,5 mi que não votaram em 4/out, os 9,3 mi que votaram na terceira via e os 6,0 mi de
brancos e nulos, e quanto uma campanha de conversa pessoal do PT com esses grupos (guia "Três nomes, três semanas") poderia mudar.
Estrutura (tudo em votos absolutos, por UF + exterior, a partir do resultado oficial em resultado_1turno_uf.csv):
1. Estado inicial n_u = (Lula, Flávio, Cury, Renan, Caiado, Zema+outros, brancos+nulos, abstenção), Σ = eleitorado_u.
2. Matriz de um passo M_u (8 origens × 4 destinos: Lula, Flávio, branco/nulo, não vota), uma por sorteio e por UF: cada linha é
Dirichlet(κ·m) em torno de um centro m sorteado por fora (2ª opção declarada da Quaest; decisão dos "sem 2ª opção" e dos
brancos/nulos; fluxos brutos de abstenção: r_out sai, r_in entra, com inclinação de perfil τ). destinos_u = n_u · M_u.
3. Cadeia de Markov da campanha (5 a 24/out, não homogênea, fases do guia): por bolsão (ausentes, terceira via + brancos/nulos,
eleitor brando de Flávio), estados S0 não alcançado · S1 alcançado · S2 convertido (plano de voto) · S3 confirmado · S4 recusou.
O alcance vem do nº de voluntários (V_0 inicial, cada um convida 3 com aceitação p_ac, até 3 gerações, teto ψ·votos de Lula);
a conversão por contato q vem da literatura de mobilização (Gerber & Green; Nickerson & Rogers; Kalla & Broockman). O estoque
convertido é repartido pelas UFs ∝ voto de Lula e aplicado como fluxo entre destinos, com teto na massa disponível.
4. Cenários: escada de alcance (sem campanha dirigida / moderada / forte / máxima) × resposta de Flávio (nenhuma / simétrica, rede
ρ× a do PT e os mesmos priors de eficácia) + referência "metas do guia cumpridas". Números aleatórios comuns entre células.
5. Saídas: margem Lula − Flávio em válidos, abstenção e brancos/nulos do 2º turno, saldo por frente, trajetória diária, por região
e por UF, matriz de transição mediana; P(Lula) "só migração" e "com deriva" (a deriva t₅ do modelo principal, para comparar).
Não é previsão: é análise condicional. A manchete continua sendo o modelo principal. Autoverificações: python migracao_markov.py
"""
import json, math, os, time, html as H
import numpy as np
import pandas as pd
ORIG = ["lula", "flavio", "cury", "renan", "caiado", "zo", "bn", "abs"]
DEST = ["lula", "flavio", "bn", "abs"]
TV = ["cury", "renan", "caiado", "zo"]
ROT_ORIG = {"lula": "Votou em Lula", "flavio": "Votou em Flávio", "cury": "Votou em Cury", "renan": "Votou em Renan", "caiado": "Votou em Caiado",
"zo": "Votou em Zema ou outros", "bn": "Votou branco ou nulo", "abs": "Não votou"}
ROT_DEST = {"lula": "Lula", "flavio": "Flávio", "bn": "Branco/nulo", "abs": "Não vota"}
REGIAO_NOME = {"N": "Norte", "NE": "Nordeste", "CO": "Centro-Oeste", "SE": "Sudeste", "S": "Sul", "EXT": "Exterior"}
KAPPA = {"lula": 400, "flavio": 400, "cury": 100, "renan": 100, "caiado": 100, "zo": 100, "bn": 150, "abs": 150}
INICIO, FIM, T2 = pd.Timestamp("2026-10-05"), pd.Timestamp("2026-10-24"), pd.Timestamp("2026-10-25")
DIAS = pd.date_range(INICIO, FIM) # 20 dias de campanha
# fases do guia: (nome, início, fim, multiplicador do hazard de conversão, hazard de confirmação/dia, hazard de encerramento/dia, geração recrutada)
FASES = [("Largada", "2026-10-05", "2026-10-06", 0.0, 0.0, 0.0, 0),
("1ª conversa", "2026-10-07", "2026-10-11", 0.8, 0.0, 0.0, 1),
("Pausa (Aparecida)", "2026-10-12", "2026-10-12", 0.0, 0.0, 0.0, None),
("2ª conversa", "2026-10-13", "2026-10-18", 1.4, 0.0, 0.0, 2),
("Fechamento", "2026-10-19", "2026-10-22", 0.8, 0.15, 0.3, 3),
("Confirmação", "2026-10-23", "2026-10-24", 0.0, 0.4, 0.3, None)]
BOLSOES = ["ausentes", "terceira_via", "flavio_brando"]
REALIZA = {"ausentes": (0.85, 0.55), "terceira_via": (0.90, 0.55), "flavio_brando": (0.90, 0.55)} # P(vota como combinado | S3), (| S2)
CENARIOS = [("sem", "Sem campanha dirigida", 0.0, 0.0), ("moderada", "Moderada", 100_000, 0.5),
("forte", "Forte", 500_000, 1.0), ("maxima", "Máxima", 2_000_000, 1.5)]
RESPOSTAS = [("nenhuma", "Nenhuma"), ("simetrica", "Simétrica")]
GUIA_METAS = {"terceira_via": 2.4e6, "eleitores_flavio": 1.2e6, "brancos_nulos": 1.0e6, "ausentes": 0.5e6} # saldo (L − F) por frente
DF_T = 5
BINS = np.arange(-8.0, 4.01, 0.25) # histograma da margem só pela migração (caudas somadas nas pontas)
# (nome, símbolo, distribuição, média, desvio, fonte)
PRIORS = [
("Troca direta entre Lula e Flávio", "ε_x", "Beta", 0.005, 0.003, "prior explícito: troca direta rara entre os dois"),
("Eleitor de Lula ou Flávio que vota branco/nulo", "ε_bn", "Beta", 0.01, 0.005, "prior explícito"),
("Votou no 1º turno e falta no 2º", "r_out", "Beta", 0.05, 0.015, "cerca de 6 mi; em 2022 a abstenção quase não mudou entre os turnos, com troca de pessoas"),
("Variação líquida nacional da abstenção (p.p.)", "δ_nac", "Normal", 0.5, 1.0, "a abstenção subiu no 2º turno em 2006–2018 e caiu em 2022"),
("Variação líquida por UF além do padrão de 2022 (p.p.)", "δ_u", "Normal", 0.0, 0.7, "padrão 1º→2º turno de 2022 por UF encolhido 50% e cortado em ±3 (abstencao_2022_uf.csv)"),
("Inclinação de perfil de quem entra ou sai (p.p. de margem pró-Lula)", "τ", "Normal, ≥ 0", 10.0, 5.0, "abstenção por escolaridade 2022 (27/17/13%) × voto por escolaridade (Quaest 3/out) ≈ +6; K = 0,25 da seção Abstenção ≈ 20"),
("\"Sem 2ª opção\" da 3ª via que escolhem um dos dois", "d_3", "Beta", 0.50, 0.12, "modelo estrutural: pool decidido a 55%"),
("Parcela de Lula entre os que escolhem", "s_3", "Beta", 0.50, 0.08, "faixa 35–65% do tornado"),
("Brancos e nulos do 1º turno que escolhem um dos dois", "d_bn", "Beta", 0.35, 0.10, "TSE 2022: brancos + nulos caíram de 7,0 mi no 1º turno para 5,7 mi no 2º"),
("Brancos e nulos que faltam no 2º turno", "a_bn", "Beta", 0.08, 0.03, "prior explícito"),
("Aceitação do convite para montar a própria lista", "p_ac", "Beta", 0.25, 0.10, "organização relacional: adesão de 1 em 4 convidados"),
("Teto de voluntários (fração dos votos de Lula)", "ψ", "Beta", 0.05, 0.02, "2,7 mi no centro, ordem de grandeza dos filiados do PT"),
("Conversão por contato: ausentes", "q_aus", "Beta", 0.12, 0.05, "Gerber & Green (porta a porta: 1 voto por ~14 contatos); Nickerson & Rogers (plano de voto: +4 p.p.); organização relacional: +5 a +10 p.p."),
("Conversão por contato: 3ª via e brancos/nulos", "q_tv", "Beta", 0.05, 0.03, "Kalla & Broockman 2018: persuasão ≈ 0 em eleição geral, não nula entre indecisos; só quem não tem 2ª opção é permeável"),
("Conversão por contato: eleitor brando de Flávio", "q_fb", "Beta", 0.02, 0.015, "idem, com o voto já dado ao adversário"),
("Eleitores brandos (fração do voto de cada candidato)", "f_br", "Beta", 0.12, 0.04, "guia: 600 mil \"pouco convictos\" é o piso"),
("Reversão diária de convertido para alcançado", "δ_rev", "Beta", 0.03, 0.01, "plano de voto que não se cumpre"),
("Parcela de Lula entre ausentes trazidos nominalmente", "ν", "Beta", 0.80, 0.07, "trabalho nominal: só a lista do voluntário"),
("Chamado aberto: queda dos ausentes nas UFs de Lula (× intensidade)", "ω", "Beta", 0.015, 0.01, "Ipea 2023: passe livre no 2º turno de 2022 sem efeito mensurável; teto baixo"),
("Rede de Flávio ÷ rede do PT (resposta simétrica)", "ρ", "Log-normal", 0.6, 0.4, "mesmos priors de eficácia, menos capilaridade"),
("Voluntários iniciais (dispersão em torno do cenário)", "V_0", "Log-normal", None, 0.5, "desconhecido: a escada de cenários vai de 0 a 2 mi"),
]
def priors():
return [{"nome": n, "simbolo": s, "dist": d, "media": m, "sd": sd, "fonte": f} for n, s, d, m, sd, f in PRIORS]
def _p(simbolo):
for n, s, d, m, sd, f in PRIORS:
if s == simbolo: return m, sd
raise KeyError(simbolo)
def _beta(rng, mu, sd, size=None):
k = mu*(1 - mu)/sd**2 - 1
assert k > 0, (mu, sd)
return rng.beta(mu*k, (1 - mu)*k, size)
def _dirichlet(rng, centro, kappa):
g = rng.gamma(np.maximum(kappa*centro, 1e-6))
return g/g.sum(axis=-1, keepdims=True)
def _t_unit(rng, n):
return rng.standard_t(DF_T, n)*math.sqrt((DF_T - 2)/DF_T)
# ------------------------------------------------------------------ dados
def disponivel(DIR):
return all(os.path.exists(DIR + a) for a in ("resultado_1turno_uf.csv", "referencia_uf.csv", "abstencao_2022_uf.csv"))
def carregar(DIR):
r = pd.read_csv(DIR + "resultado_1turno_uf.csv")
br = r[r.uf == "BR"].iloc[0]
r = r[r.uf != "BR"].reset_index(drop=True)
ref = pd.read_csv(DIR + "referencia_uf.csv")[["uf", "nome", "regiao"]]
ab = pd.read_csv(DIR + "abstencao_2022_uf.csv")[["uf", "abst_1t_2022", "abst_2t_2022"]]
r = r.merge(ref, on="uf", how="left").merge(ab, on="uf", how="left")
r["regiao"] = r.regiao.fillna("EXT"); r["nome"] = r.nome.fillna("Exterior")
n = np.column_stack([r.lula, r.flavio, r.cury, r.renan, r.caiado, r.zema + r.outros, r.brancos + r.nulos, r.abstencao]).astype(float)
return {"tab": r, "n": n, "br": br, "ufs": list(r.uf), "regiao": list(r.regiao), "nome": list(r.nome),
"eleitorado": n.sum(1), "eleitorado_tse": r.eleitorado.values.astype(float), "comparecimento": r.comparecimento.values.astype(float), # Σ das caixas (a tabela do TSE difere do eleitorado em poucas centenas em algumas UFs)
"pad22": np.clip(0.5*(r.abst_2t_2022 - r.abst_1t_2022).fillna(0.0).values, -3, 3),
"zema": r.zema.values.astype(float), "outros": r.outros.values.astype(float), "ext": np.array([u == "EXT" for u in r.uf])}
def _transf_uf(D, transf):
"""(pF, pL) por origem; Zema e outros fundidos com peso pelos votos de cada UF. Retorna dict origem → (pF (U,), pL (U,))."""
U = len(D["ufs"]); out = {}
for o in ("cury", "renan", "caiado"):
pf, pl = transf[o]; out[o] = (np.full(U, pf), np.full(U, pl))
z, x = D["zema"], D["outros"]; w = np.where(z + x > 0, z/np.maximum(z + x, 1), 0.5)
out["zo"] = (w*transf["zema"][0] + (1 - w)*transf["outros"][0], w*transf["zema"][1] + (1 - w)*transf["outros"][1])
for o, (pf, pl) in out.items():
assert np.all(pf + pl <= 1.0 + 1e-9), (o, "2ª opção soma mais de 100%")
return out
def estrutural_check(D, transf, decis=0.55, split=0.5):
"""Reprodução determinística do modelo estrutural: 2ª opção nos centros, pool (sem 2ª opção + brancos/nulos) decidido a 55% e dividido 50/50."""
n, tr = D["n"], _transf_uf(D, transf)
L, F = n[:, 0].sum(), n[:, 1].sum(); pool = n[:, 6].sum()
for j, o in enumerate(TV):
pf, pl = tr[o]; v = n[:, 2 + j]
L += (v*pl).sum(); F += (v*pf).sum(); pool += (v*(1 - pf - pl)).sum()
L += pool*decis*split; F += pool*decis*(1 - split)
return 100*(L - F)/(L + F)
# ------------------------------------------------------------------ matriz de um passo
def sortear_base(rng, N, U):
"""Parâmetros nacionais por sorteio (comuns a todas as UFs e a todas as células)."""
B = {}
for k, s in [("eps_x", "ε_x"), ("eps_bn", "ε_bn"), ("r_out", "r_out"), ("d3", "d_3"), ("s3", "s_3"), ("d_bn", "d_bn"), ("a_bn", "a_bn"),
("p_ac", "p_ac"), ("psi", "ψ"), ("q_aus", "q_aus"), ("q_tv", "q_tv"), ("q_fb", "q_fb"), ("f_br", "f_br"), ("d_rev", "δ_rev"),
("nu", "ν"), ("omega", "ω")]:
m, sd = _p(s); B[k] = _beta(rng, m, sd, N)
for k, s in [("q_aus_F", "q_aus"), ("q_tv_F", "q_tv"), ("q_fb_F", "q_fb")]: # eficácia da resposta de Flávio: mesmos priors, sorteio próprio
m, sd = _p(s); B[k] = _beta(rng, m, sd, N)
m, sd = _p("δ_nac"); B["delta_nac"] = rng.normal(m, sd, N)
m, sd = _p("δ_u"); B["delta_u"] = rng.normal(m, sd, (N, U))
m, sd = _p("τ"); B["tau"] = np.clip(rng.normal(m, sd, N), 0.0, None)
m, sd = _p("ρ"); B["rho"] = np.exp(math.log(m) + sd*rng.standard_normal(N))
B["z_v0"] = rng.standard_normal(N)
return B
def matriz(D, B, transf, rng, dirichlet=True):
"""Centros m (N, U, 8, 4) e, se dirichlet, linhas Dirichlet(κ·m) em torno deles. Devolve (M, extras)."""
n = D["n"]; U, N = n.shape[0], len(B["r_out"])
L, F, BN, ABS = n[:, 0], n[:, 1], n[:, 6], n[:, 7]
s_u = L/(L + F)
tr = _transf_uf(D, transf)
bc = lambda x: np.broadcast_to(np.asarray(x, float).reshape(N, 1), (N, U))
r = bc(B["r_out"]); th = bc(np.minimum(B["tau"]/100, 0.9)); ex, eb = bc(B["eps_x"]), bc(B["eps_bn"])
d3, s3, dbn, abn = bc(B["d3"]), bc(B["s3"]), bc(B["d_bn"]), bc(B["a_bn"])
rL, rF = r*(1 + th), r*(1 - th) # quem sai é mais pró-Lula (mesma τ da entrada): sem variação líquida, efeito ≈ 0
M = np.zeros((N, U, 8, 4))
M[:, :, 0] = np.stack([1 - ex - eb - rL, ex, eb, rL], -1)
M[:, :, 1] = np.stack([ex, 1 - ex - eb - rF, eb, rF], -1)
perm = {} # massas permeáveis (centros) por origem e destino, para tetos e fluxos da campanha
for j, o in enumerate(TV):
pf, pl = tr[o]; res = np.clip(1 - pf - pl, 0, 1)[None, :]
M[:, :, 2 + j] = np.stack([(pl[None, :] + res*d3*s3)*(1 - r), (pf[None, :] + res*d3*(1 - s3))*(1 - r), res*(1 - d3)*(1 - r), r], -1)
base = n[None, :, 2 + j]*res*(1 - r)
perm[o] = {"lula": base*d3*s3, "flavio": base*d3*(1 - s3), "bn": base*(1 - d3)}
M[:, :, 6] = np.stack([dbn*s3*(1 - abn), dbn*(1 - s3)*(1 - abn), (1 - dbn)*(1 - abn), abn], -1)
base = n[None, :, 6]*(1 - abn)
perm["bn"] = {"lula": base*dbn*s3, "flavio": base*dbn*(1 - s3), "bn": base*(1 - dbn)}
# abstenção: meta líquida por UF (padrão 2022 encolhido + nacional + UF) e taxa bruta de entrada derivada
dA = D["pad22"][None, :] + bc(B["delta_nac"]) + B["delta_u"]
r_in = np.clip((r*D["comparecimento"][None, :] - dA*D["eleitorado"][None, :]/100)/ABS[None, :], 0.02, 0.6)
pi = np.clip(s_u[None, :] + bc(B["tau"])/200, 0.0, 1.0)
M[:, :, 7] = np.stack([r_in*0.95*pi, r_in*0.95*(1 - pi), r_in*0.05, 1 - r_in], -1)
assert np.allclose(M.sum(-1), 1.0, atol=1e-9)
if dirichlet:
for j, o in enumerate(ORIG):
M[:, :, j] = _dirichlet(rng, M[:, :, j], KAPPA[o])
return M, {"s_u": s_u, "pi": pi, "perm": perm, "r_in": r_in, "dA": dA}
# ------------------------------------------------------------------ cadeia de Markov da campanha
def _calendario(inicio):
"""Vetores diários: multiplicador de conversão normalizado (Σ = 1 nos dias ativos), hazards de confirmação e encerramento, dias de cada geração."""
T = len(DIAS); mult = np.zeros(T); conf = np.zeros(T); enc = np.zeros(T); ger = {}
for nome, a, b, m, c, e, g in FASES:
idx = [i for i, d in enumerate(DIAS) if pd.Timestamp(a) <= d <= pd.Timestamp(b) and d >= inicio]
for i in idx: mult[i], conf[i], enc[i] = m, c, e
if g is not None: ger[g] = idx
ativos = [i for i, d in enumerate(DIAS) if d >= inicio]
for g in range(4): # geração sem dias ativos (início tardio): entra no primeiro dia ativo
if not ger.get(g): ger[g] = ativos[:1]
w = mult/mult.sum() if mult.sum() > 0 else mult
return w, conf, enc, ger
def voluntarios(V0, p_ac, teto, ger):
"""Voluntários acumulados por dia (N, T): V0 na largada, cada geração = 3·p_ac × a anterior, até 3 gerações, teto ψ·votos."""
N, T = len(V0), len(DIAS); arr = np.zeros((N, T)); g = V0.copy()
for k in range(4):
if k > 0: g = g*3*p_ac
for i in ger[k]: arr[:, i] += g/len(ger[k])
return np.minimum(np.cumsum(arr, axis=1), teto[:, None])
def cadeia(V_t, P, q, d_rev, w, conf, enc):
"""Fracções da cadeia por dia: (T, N, B, 5) com estados S0 não alcançado · S1 alcançado · S2 convertido · S3 confirmado · S4 recusou.
V_t (N, T) voluntários acumulados = nomes por bolsão; P (N, B) tamanho dos bolsões; q (N, B) conversão por contato."""
N, T = V_t.shape; Bn = P.shape[1]
x = np.zeros((N, Bn, 5)); x[:, :, 0] = 1.0
traj = np.zeros((T, N, Bn, 5)); D_prev = np.zeros((N, Bn)); Pp = np.maximum(P, 1.0)
for t in range(T):
Dt = Pp*(1 - np.exp(-V_t[:, t][:, None]/Pp)) # nomes distintos alcançados até t (sorteio com reposição)
novo = np.clip((Dt - D_prev)/Pp, 0, None); D_prev = Dt
f12 = x[:, :, 1]*q*w[t]; f21 = x[:, :, 2]*d_rev[:, None]; f23 = x[:, :, 2]*conf[t]; f14 = x[:, :, 1]*enc[t]
x[:, :, 0] -= novo; x[:, :, 1] += novo - f12 + f21 - f14; x[:, :, 2] += f12 - f21 - f23; x[:, :, 3] += f23; x[:, :, 4] += f14
traj[t] = x
return traj
# ------------------------------------------------------------------ simulação
def _q(a, ps):
return [round(float(v), 3) for v in np.percentile(a, ps)]
def simular(DIR, transf, hoje, sd_deriva, comparacao=None, regiao_uf=None, transf_fonte="", seed=20261025, n_sorteios=20_000, inicio_cadeia=None):
t0 = time.time()
rng = np.random.default_rng(seed)
D = carregar(DIR); n = D["n"]; U = n.shape[0]; N = n_sorteios
hoje = pd.Timestamp(hoje); inicio = max(INICIO, pd.Timestamp(inicio_cadeia)) if inicio_cadeia is not None else INICIO
if regiao_uf:
D["regiao"] = [regiao_uf.get(u, r) for u, r in zip(D["ufs"], D["regiao"])]
B = sortear_base(rng, N, U)
M, X = matriz(D, B, transf, rng)
T_base = n[None, :, :, None]*M # (N, U, 8, 4): massa de cada origem em cada destino
elei = D["eleitorado"]; ext = D["ext"]; dom = ~ext
L1, F1 = n[:, 0], n[:, 1]
w_L = np.where(dom, L1, 0.0); w_L = w_L/w_L.sum(); w_F = np.where(dom, F1, 0.0); w_F = w_F/w_F.sum()
lula_lidera = (X["s_u"] > 0.5) & dom
s3, dbn = B["s3"], B["d_bn"]
# bolsões nacionais (sem o exterior)
P_aus = float(n[dom, 7].sum())
P_tv = float(sum(sum(v[:, dom].sum(1) for v in X["perm"][o].values()).mean() for o in TV + ["bn"])) # resíduo "sem 2ª opção" + brancos/nulos (médio)
_tru = _transf_uf(D, transf)
sem_2a = float(sum((n[:, 2 + j]*np.clip(1 - _tru[o][0] - _tru[o][1], 0, 1)).sum() for j, o in enumerate(TV))) # 3ª via sem 2ª opção declarada
P_fb = B["f_br"]*float(F1[dom].sum()); P_lb = B["f_br"]*float(L1[dom].sum())
w, conf, enc, ger = _calendario(inicio)
deriva = sd_deriva*_t_unit(rng, N) # a mesma deriva (t₅) em todas as células, para comparar com o modelo principal
bins = BINS
PS = [5, 10, 25, 50, 75, 90, 95]
def frentes_lula(T, V_t, qs, I_cen, saldo):
"""Aplica a campanha do PT sobre T (in place). Devolve trajetória da cadeia e nomes alcançados por bolsão."""
P = np.column_stack([np.full(N, P_aus), np.full(N, P_tv), P_fb])
tr = cadeia(V_t, P, np.column_stack(qs), B["d_rev"], w, conf, enc)
fim = tr[-1]
conv = np.column_stack([P[:, b]*(REALIZA[BOLSOES[b]][0]*fim[:, b, 3] + REALIZA[BOLSOES[b]][1]*fim[:, b, 2]) for b in range(3)]) # votos realizados
# ausentes trazidos nominalmente: ABS→L (ν) e ABS→F (1−ν), ∝ voto de Lula, teto na massa que ficaria em casa
mov = np.minimum(conv[:, 0][:, None]*w_L[None, :], 0.9*T[:, :, 7, 3])
T[:, :, 7, 3] -= mov; T[:, :, 7, 0] += B["nu"][:, None]*mov; T[:, :, 7, 1] += (1 - B["nu"])[:, None]*mov
saldo["ausentes"] += ((2*B["nu"] - 1)[:, None]*mov).sum(1)
# terceira via + brancos/nulos: sai dos destinos Flávio e branco/nulo das linhas permeáveis, ∝ massa permeável, → Lula
permF = sum(X["perm"][o]["flavio"] for o in TV + ["bn"]); permB = sum(X["perm"][o]["bn"] for o in TV + ["bn"])
avail = permF + permB
mov = np.minimum(conv[:, 1][:, None]*w_L[None, :], 0.9*avail)
for o in TV + ["bn"]:
j = ORIG.index(o)
mF = np.minimum(mov*X["perm"][o]["flavio"]/np.maximum(avail, 1), 0.9*T[:, :, j, 1]); mB = np.minimum(mov*X["perm"][o]["bn"]/np.maximum(avail, 1), 0.9*T[:, :, j, 2])
T[:, :, j, 1] -= mF; T[:, :, j, 2] -= mB; T[:, :, j, 0] += mF + mB
saldo["terceira_via" if o != "bn" else "brancos_nulos"] += (2*mF + mB).sum(1)
# eleitor brando de Flávio: F→L, teto f_br da massa que ficaria com Flávio
mov = np.minimum(conv[:, 2][:, None]*w_L[None, :], B["f_br"][:, None]*T[:, :, 1, 1])
T[:, :, 1, 1] -= mov; T[:, :, 1, 0] += mov; saldo["eleitores_flavio"] += (2*mov).sum(1)
# chamado aberto: nas UFs em que Lula liderou, ω·I_cen dos ausentes votam (composição π_u); nas de Flávio, 0,3× (sai pela culatra)
inten = np.where(lula_lidera, 1.0, 0.3)[None, :]*np.where(dom, 1.0, 0.0)[None, :]*(B["omega"]*I_cen)[:, None]
e = inten*T[:, :, 7, 3]
T[:, :, 7, 3] -= e; T[:, :, 7, 0] += e*X["pi"]; T[:, :, 7, 1] += e*(1 - X["pi"])
saldo["chamado_aberto"] += (e*(2*X["pi"] - 1)).sum(1)
nomes = np.column_stack([P[:, b]*(1 - np.exp(-V_t[:, -1]/np.maximum(P[:, b], 1))) for b in range(3)])
return tr, nomes, conv
def frentes_flavio(T, V_t, qs, I_cen, saldo):
"""Resposta simétrica de Flávio: mesmos bolsões espelhados, ∝ voto de Flávio, com os mesmos priors de eficácia."""
P = np.column_stack([np.full(N, P_aus), np.full(N, P_tv), P_lb])
tr = cadeia(V_t, P, np.column_stack(qs), B["d_rev"], w, conf, enc)
fim = tr[-1]
conv = np.column_stack([P[:, b]*(REALIZA[BOLSOES[b]][0]*fim[:, b, 3] + REALIZA[BOLSOES[b]][1]*fim[:, b, 2]) for b in range(3)])
s = np.zeros(N)
mov = np.minimum(conv[:, 0][:, None]*w_F[None, :], 0.9*T[:, :, 7, 3])
T[:, :, 7, 3] -= mov; T[:, :, 7, 1] += B["nu"][:, None]*mov; T[:, :, 7, 0] += (1 - B["nu"])[:, None]*mov
s -= ((2*B["nu"] - 1)[:, None]*mov).sum(1)
permL = sum(X["perm"][o]["lula"] for o in TV + ["bn"]); permB = sum(X["perm"][o]["bn"] for o in TV + ["bn"]); avail = permL + permB
mov = np.minimum(conv[:, 1][:, None]*w_F[None, :], 0.9*avail)
for o in TV + ["bn"]:
j = ORIG.index(o)
mL = np.minimum(mov*X["perm"][o]["lula"]/np.maximum(avail, 1), 0.9*T[:, :, j, 0]); mB = np.minimum(mov*X["perm"][o]["bn"]/np.maximum(avail, 1), 0.9*T[:, :, j, 2])
T[:, :, j, 0] -= mL; T[:, :, j, 2] -= mB; T[:, :, j, 1] += mL + mB; s -= (2*mL + mB).sum(1)
mov = np.minimum(conv[:, 2][:, None]*w_F[None, :], B["f_br"][:, None]*T[:, :, 0, 0])
T[:, :, 0, 0] -= mov; T[:, :, 0, 1] += mov; s -= (2*mov).sum(1)
inten = np.where(lula_lidera, 0.3, 1.0)[None, :]*np.where(dom, 1.0, 0.0)[None, :]*(B["omega"]*I_cen)[:, None]
e = inten*T[:, :, 7, 3]
T[:, :, 7, 3] -= e; T[:, :, 7, 0] += e*X["pi"]; T[:, :, 7, 1] += e*(1 - X["pi"])
s += (e*(2*X["pi"] - 1)).sum(1)
saldo["resposta_flavio"] += s
return tr
def agrega(T, cid, nome, rid, rnome, saldo, tr=None, nomes=None, conv=None, V_t=None, V_F=None):
dest = T.sum(axis=2) # (N, U, 4)
Lv, Fv, BNv, ABSv = (dest[:, :, k].sum(1) for k in range(4))
marg = 100*(Lv - Fv)/(Lv + Fv)
md = marg + deriva
c = {"id": cid, "nome": nome, "resposta": rid, "resposta_nome": rnome,
"margem_q": {str(p): round(float(v), 2) for p, v in zip(PS, np.percentile(marg, PS))},
"p_lula_so_migracao": round(float((marg > 0).mean()), 3), "p_lula_com_deriva": round(float((md > 0).mean()), 3),
"margem_com_deriva_q10_50_90": _q(md, [10, 50, 90]),
"abstencao_q10_50_90": _q(100*ABSv/elei.sum(), [10, 50, 90]), "bn_pct_q10_50_90": _q(100*BNv/(Lv + Fv + BNv), [10, 50, 90]),
"lula_mi_q50": round(float(np.median(Lv))/1e6, 3), "flavio_mi_q50": round(float(np.median(Fv))/1e6, 3),
"hist_margem": [round(float(v), 4) for v in np.histogram(np.clip(marg, bins[0] + 1e-6, bins[-1] - 1e-6), bins=bins, density=True)[0]]}
c["saldo_frentes"] = {k: {"q10": round(float(np.percentile(v, 10))), "q50": round(float(np.median(v))), "q90": round(float(np.percentile(v, 90)))} for k, v in saldo.items()}
tot = sum(saldo.values())
c["saldo_frentes"]["total"] = {"q10": round(float(np.percentile(tot, 10))), "q50": round(float(np.median(tot))), "q90": round(float(np.percentile(tot, 90)))}
if nomes is not None:
for b, k in enumerate(BOLSOES):
c["saldo_frentes"][{"ausentes": "ausentes", "terceira_via": "terceira_via", "flavio_brando": "eleitores_flavio"}[k]].update(
{"contatos_q50": round(float(np.median(nomes[:, b]))), "convertidos_q50": round(float(np.median(conv[:, b])))})
if V_t is not None:
c["voluntarios_q10_50_90"] = [round(float(v)) for v in np.percentile(V_t[:, -1], [10, 50, 90])]
c["nomes_distintos_q50"] = round(float(np.median(nomes.sum(1)))) if nomes is not None else 0
c["trajetoria"] = {"dias": [str(d.date()) for d in DIAS],
"voluntarios": [[round(float(v)) for v in np.percentile(V_t[:, t], [10, 50, 90])] for t in range(len(DIAS))]}
if V_F is not None: c["trajetoria"]["voluntarios_flavio"] = [[round(float(v)) for v in np.percentile(V_F[:, t], [10, 50, 90])] for t in range(len(DIAS))]
if tr is not None:
P = np.column_stack([np.full(N, P_aus), np.full(N, P_tv), P_fb])
for b, k in enumerate(BOLSOES):
est = (tr[:, :, b, 2] + tr[:, :, b, 3])*P[None, :, b] # (T, N) estoque convertido (plano de voto ou confirmado)
c["trajetoria"][k] = [[round(float(v)) for v in np.percentile(est[t], [10, 50, 90])] for t in range(len(DIAS))]
c["estados_finais_q50"] = {k: [round(float(np.median(tr[-1, :, b, s])), 4) for s in range(5)] for b, k in enumerate(BOLSOES)}
# por região e por UF
dL, dF, dAbs = dest[:, :, 0], dest[:, :, 1], dest[:, :, 3]
sal = (dL - dF) - (L1 - F1)[None, :]
novos = T[:, :, 7, :3].sum(-1)
regs = sorted(set(D["regiao"]), key=lambda r_: ["NE", "SE", "S", "N", "CO", "EXT"].index(r_) if r_ in ["NE", "SE", "S", "N", "CO", "EXT"] else 9)
c["por_regiao"] = []
for r_ in regs:
m_ = np.array([x == r_ for x in D["regiao"]])
c["por_regiao"].append({"regiao": r_, "nome": REGIAO_NOME.get(r_, r_), "ausentes_1t": int(n[m_, 7].sum()), "eleitorado": int(elei[m_].sum()),
"lula_1t": int(L1[m_].sum()), "flavio_1t": int(F1[m_].sum()),
"novos_eleitores_q50": round(float(np.median(novos[:, m_].sum(1)))),
"saldo_q10": round(float(np.percentile(sal[:, m_].sum(1), 10))), "saldo_q50": round(float(np.median(sal[:, m_].sum(1)))),
"saldo_q90": round(float(np.percentile(sal[:, m_].sum(1), 90))),
"abst_1t": round(100*float(n[m_, 7].sum()/elei[m_].sum()), 2), "abst_2t_q50": round(float(np.median(100*dAbs[:, m_].sum(1)/elei[m_].sum())), 2),
"margem_1t": round(100*float((L1[m_].sum() - F1[m_].sum())/(L1[m_].sum() + F1[m_].sum())), 2),
"margem_2t_q50": round(float(np.median(100*(dL[:, m_].sum(1) - dF[:, m_].sum(1))/(dL[:, m_].sum(1) + dF[:, m_].sum(1)))), 2)})
c["por_uf"] = [{"uf": u, "nome": D["nome"][i], "regiao": D["regiao"][i], "saldo_q50": round(float(np.median(sal[:, i]))),
"margem_1t": round(100*float((L1[i] - F1[i])/(L1[i] + F1[i])), 2),
"margem_2t_q50": round(float(np.median(100*(dL[:, i] - dF[:, i])/(dL[:, i] + dF[:, i]))), 2),
"abst_1t": round(100*float(n[i, 7]/elei[i]), 2), "abst_2t_q50": round(float(np.median(100*dAbs[:, i]/elei[i])), 2)}
for i, u in enumerate(D["ufs"])]
return c, marg, dest
def matriz_mediana(T):
Mn = T.sum(axis=1)/n.sum(0)[None, :, None] # (N, 8, 4) matriz nacional agregada por sorteio
return {o: {d: [round(100*float(v), 2) for v in np.percentile(Mn[:, i, j], [10, 50, 90])] for j, d in enumerate(DEST)} for i, o in enumerate(ORIG)}
cenarios, margens, checks = [], {}, {}
mats = {}
V_sem = None
for rid, rnome in RESPOSTAS:
for cid, cnome, V0_med, I_cen in CENARIOS:
T = T_base.copy()
saldo = {k: np.zeros(N) for k in ["ausentes", "terceira_via", "brancos_nulos", "eleitores_flavio", "chamado_aberto", "resposta_flavio"]}
V0 = V0_med*np.exp(0.5*B["z_v0"])
V_t = voluntarios(V0, B["p_ac"], B["psi"]*L1[dom].sum(), ger)
tr, nomes, conv = frentes_lula(T, V_t, [B["q_aus"], B["q_tv"], B["q_fb"]], I_cen, saldo)
V_F = None
if rid == "simetrica" and V0_med > 0:
V_F = voluntarios(B["rho"]*V0, B["p_ac"], B["psi"]*F1[dom].sum(), ger)
frentes_flavio(T, V_F, [B["q_aus_F"], B["q_tv_F"], B["q_fb_F"]], I_cen, saldo)
c, marg, dest = agrega(T, cid, cnome, rid, rnome, saldo, tr, nomes, conv, V_t, V_F)
assert np.allclose(dest.sum(-1), elei[None, :], rtol=1e-9), "massa não conservada"
cenarios.append(c); margens[(cid, rid)] = marg
if rid == "nenhuma" and cid in ("sem", "forte"):
mats[cid] = matriz_mediana(T)
if cid == "forte" and rid == "nenhuma":
nomes_forte = nomes; tr_forte = tr
if cid == "sem" and rid == "nenhuma":
dest_sem = dest
del T
# referência: metas do guia cumpridas (saldo imposto sobre o cenário sem campanha; o que precisaria acontecer)
T = T_base.copy(); saldo = {k: np.zeros(N) for k in ["ausentes", "terceira_via", "brancos_nulos", "eleitores_flavio", "chamado_aberto", "resposta_flavio"]}
def _move(T, j_from, d_from, d_to, total, pesos, cap_frac=0.95):
mov = np.minimum(total*pesos[None, :], cap_frac*T[:, :, j_from, d_from]); T[:, :, j_from, d_from] -= mov; T[:, :, j_from, d_to] += mov; return mov.sum(1)
mF = sum(_move(T, ORIG.index(o), 1, 0, GUIA_METAS["terceira_via"]/2*X["perm"][o]["flavio"].mean(0).sum()/sum(X["perm"][o_]["flavio"].mean(0).sum() for o_ in TV), w_L) for o in TV)
saldo["terceira_via"] += 2*mF
saldo["brancos_nulos"] += _move(T, 6, 2, 0, GUIA_METAS["brancos_nulos"], w_L)
saldo["eleitores_flavio"] += 2*_move(T, 1, 1, 0, GUIA_METAS["eleitores_flavio"]/2, w_L)
a_l = _move(T, 7, 3, 0, 1.0e6, w_L); a_f = _move(T, 7, 3, 1, 0.5e6, w_L); saldo["ausentes"] += a_l - a_f
c_guia, marg_guia, _ = agrega(T, "guia", "Metas do guia cumpridas (referência)", "nenhuma", "Nenhuma", saldo)
del T
med_nomes = np.median(nomes_forte, axis=0) # alcance mediano do cenário forte, por bolsão
lit = {"ausentes": _p("q_aus")[0], "terceira_via": _p("q_tv")[0], "brancos_nulos": _p("q_tv")[0], "eleitores_flavio": _p("q_fb")[0]}
nec = {"ausentes": GUIA_METAS["ausentes"]/(2*_p("ν")[0] - 1)/REALIZA["ausentes"][0]/med_nomes[0],
"terceira_via": GUIA_METAS["terceira_via"]/2/REALIZA["terceira_via"][0]/med_nomes[1],
"brancos_nulos": GUIA_METAS["brancos_nulos"]/REALIZA["terceira_via"][0]/med_nomes[1],
"eleitores_flavio": GUIA_METAS["eleitores_flavio"]/2/REALIZA["flavio_brando"][0]/med_nomes[2]}
guia = {"metas": {k: int(v) for k, v in GUIA_METAS.items()}, "metas_total": int(sum(GUIA_METAS.values())),
"alcance_forte_q50": {"ausentes": int(med_nomes[0]), "terceira_via": int(med_nomes[1]), "eleitores_flavio": int(med_nomes[2])},
"taxa_necessaria": {k: {"literatura": round(lit[k], 3), "necessaria": round(float(nec[k]), 3), "razao": round(float(nec[k]/lit[k]), 1)} for k in nec},
"margem_se_cumprido": c_guia["margem_q"]["50"], "p_lula_se_cumprido": c_guia["p_lula_so_migracao"],
"p_lula_se_cumprido_com_deriva": c_guia["p_lula_com_deriva"]}
# ---- verificações
br = D["br"]
checks["br_ok"] = bool(all(abs(float(br[c]) - float(D["tab"][c].sum())) < 1 for c in ["eleitorado", "comparecimento", "abstencao", "votos_validos", "brancos", "nulos", "lula", "flavio", "cury", "renan", "caiado", "zema", "outros"]))
checks["massa_ok"] = bool(np.allclose(dest_sem.sum(-1), elei[None, :], rtol=1e-9))
checks["linhas_ok"] = bool(np.allclose(M.sum(-1), 1.0, atol=1e-9))
fr = tr_forte
checks["cadeia_ok"] = bool(np.all(fr >= -1e-12) and np.allclose(fr.sum(-1), 1.0, atol=1e-9)
and np.all((fr[-1, :, :, 2] + fr[-1, :, :, 3])*1.0 <= (1 - fr[-1, :, :, 0]) + 1e-9))
esp = (comparacao or {}).get("estrutural_base_margem")
rec = estrutural_check(D, transf)
checks["estrutural_ok"] = bool(esp is None or abs(rec - esp) <= 0.3)
ordem = [margens[(c, "nenhuma")] for c, *_ in CENARIOS]
med = [float(np.median(m)) for m in ordem]; pl = [float((m > 0).mean()) for m in ordem]
checks["monotonico_ok"] = bool(all(med[i] <= med[i + 1] + 1e-9 for i in range(3)) and all(pl[i] <= pl[i + 1] + 1e-9 for i in range(3))
and all(float(np.median(margens[(c, "simetrica")])) <= float(np.median(margens[(c, "nenhuma")])) + 1e-9 for c, *_ in CENARIOS))
seg = round(time.time() - t0, 1)
checks["tempo_ok"] = bool(seg < 60)
return {"versao": 1, "usado": True, "seed": seed, "n_sorteios": N, "segundos": seg, "hoje": str(hoje.date()),
"inicio_cadeia": str(inicio.date()), "dias": [str(d.date()) for d in DIAS],
"fases": [{"nome": a, "inicio": b, "fim": c_, "mult_conversao": m, "confirmacao_dia": cf, "encerramento_dia": e} for a, b, c_, m, cf, e, g in FASES],
"transf_fonte": transf_fonte, "transf": {k: [round(v[0], 3), round(v[1], 3)] for k, v in transf.items()},
"estoques_1t": {"lula": int(n[:, 0].sum()), "flavio": int(n[:, 1].sum()), "terceira_via": int(n[:, 2:6].sum()), "sem_2a_opcao": int(round(sem_2a)),
"brancos_nulos": int(n[:, 6].sum()), "abstencao": int(n[:, 7].sum()), "eleitorado": int(elei.sum()), "comparecimento": int(D["comparecimento"].sum()),
"margem_1t_votos": int(n[:, 0].sum() - n[:, 1].sum()), "margem_1t": round(100*float((n[:, 0].sum() - n[:, 1].sum())/(n[:, 0].sum() + n[:, 1].sum())), 2),
"por_regiao": [{"regiao": r_, "nome": REGIAO_NOME.get(r_, r_), "ausentes": int(n[[x == r_ for x in D["regiao"]], 7].sum()),
"terceira_via": int(n[[x == r_ for x in D["regiao"]], 2:6].sum()), "brancos_nulos": int(n[[x == r_ for x in D["regiao"]], 6].sum())}
for r_ in ["NE", "SE", "S", "N", "CO", "EXT"] if r_ in D["regiao"]]},
"bolsoes": {"ausentes": int(P_aus), "terceira_via_bn": int(P_tv), "flavio_brando_q50": int(np.median(P_fb))},
"priors": priors(), "cenarios": cenarios + [c_guia], "guia": guia, "matriz_mediana": mats, "hist_bins": [float(b) for b in BINS],
"comparacao": {**(comparacao or {}), "sd_deriva": round(float(sd_deriva), 2), "margem_sem_campanha_q50": cenarios[0]["margem_q"]["50"],
"p_lula_sem_campanha_com_deriva": cenarios[0]["p_lula_com_deriva"],
"check_estrutural": {"recalculado": round(float(rec), 2), "esperado": esp, "dif": (None if esp is None else round(float(rec - esp), 2))}},
"abstencao_bruta": {"r_out_q10_50_90": _q(B["r_out"], [10, 50, 90]), "r_in_q10_50_90": _q(X["r_in"].mean(1), [10, 50, 90]),
"saem_mi_q50": round(float(np.median(B["r_out"]*D["comparecimento"].sum()))/1e6, 2),
"entram_mi_q50": round(float(np.median((X["r_in"]*n[None, :, 7]).sum(1)))/1e6, 2),
"tau_q10_50_90": _q(B["tau"], [10, 50, 90]), "delta_nac_q10_50_90": _q(B["delta_nac"], [10, 50, 90])},
"checks": checks}
# ------------------------------------------------------------------ HTML
def f(v, d=1, sinal=False):
v = round(float(v), d) + 0.0
s = f"{abs(v) if sinal else v:.{d}f}".replace(".", ",")
if sinal: return ("+" if v > 0 else "−" if v < 0 else "") + s
return s.replace("-", "−")
def fs(v, d=1): return f(v, d, True)
def pct(p, d=0): return f(100*p, d) + "%"
def mil(n): return f"{int(round(n)):,}".replace(",", ".")
def mi(v, d=1, sinal=False):
"""Votos em milhões (ou mil, abaixo de 1 mi)."""
v = float(v)
if abs(v) >= 950_000: return f(v/1e6, d, sinal) + " mi"
return f(v/1e3, 0, sinal) + " mil"
def _cel(R, cid, rid):
for c in R["cenarios"]:
if c["id"] == cid and c["resposta"] == rid: return c
raise KeyError((cid, rid))
def svg_trajetoria(R, c):
W, Hh = 900, 300; x0, x1, y0, y1 = 70, 880, 44, 236
dias = R["dias"]; T = len(dias)
series = [("ausentes", "Ausentes trazidos às urnas", "var(--accent)"), ("terceira_via", "3ª via e brancos/nulos", "var(--amber)"), ("flavio_brando", "Eleitores brandos de Flávio", "var(--flavio)")]
vmax = max(max(v[2] for v in c["trajetoria"][k]) for k, _, _ in series)*1.08 or 1
X = lambda t: x0 + t/(T - 1)*(x1 - x0)
Y = lambda v: y1 - v/vmax*(y1 - y0)
s = [f'<svg viewBox="0 0 {W} {Hh}" role="img" aria-label="Trajetória diária do estoque de eleitores convertidos por bolsão, de 5 a 24 de outubro, no cenário forte">']
for fase in R["fases"]:
a = dias.index(fase["inicio"]); b = dias.index(fase["fim"])
op = 0.0 if fase["mult_conversao"] == 0 and fase["confirmacao_dia"] == 0 and fase["nome"] != "Largada" else min(0.05 + 0.08*fase["mult_conversao"], 0.18)
if fase["nome"].startswith("Pausa"): op = 0.0
s.append(f'<rect x="{X(a) - (x1 - x0)/(T - 1)/2:.1f}" y="{y0}" width="{(b - a + 1)*(x1 - x0)/(T - 1):.1f}" height="{y1 - y0}" fill="var(--accent)" opacity="{op:.2f}"/>')
s.append(f'<text class="axis-label" x="{(X(a) + X(b))/2:.1f}" y="{y0 - 8}" text-anchor="middle">{H.escape(fase["nome"])}</text>')
passo = 10**math.floor(math.log10(vmax)) if vmax > 0 else 1
if vmax/passo < 3: passo /= 2
v = 0
while v <= vmax:
s.append(f'<line class="grid-line" x1="{x0}" y1="{Y(v):.1f}" x2="{x1}" y2="{Y(v):.1f}"/>')
s.append(f'<text class="axis-label" x="{x0 - 8}" y="{Y(v) + 4:.1f}" text-anchor="end">{mi(v, 1) if v else "0"}</text>')
v += passo
for k, rot, cor in series:
tr = c["trajetoria"][k]
band = " ".join(f"{X(t):.1f},{Y(tr[t][2]):.1f}" for t in range(T)) + " " + " ".join(f"{X(t):.1f},{Y(tr[t][0]):.1f}" for t in range(T - 1, -1, -1))
s.append(f'<polygon points="{band}" fill="{cor}" opacity="0.14"/>')
s.append(f'<polyline class="series-line" points="{" ".join(f"{X(t):.1f},{Y(tr[t][1]):.1f}" for t in range(T))}" stroke="{cor}"/>')
s.append(f'<text class="dot-label" x="{x1 + 4}" y="{Y(tr[-1][1]) + 4:.1f}" fill="{cor}" font-size="10">{mi(tr[-1][1])}</text>')
if R["hoje"] in dias:
th = dias.index(R["hoje"])
s.append(f'<line x1="{X(th):.1f}" y1="{y0}" x2="{X(th):.1f}" y2="{y1}" stroke="var(--ink)" stroke-width="1.2" stroke-dasharray="4,3"/>')
s.append(f'<text class="axis-label" x="{X(th) + 4:.1f}" y="{y1 - 6}" fill="var(--ink)">hoje</text>')
for t in range(0, T, 2):
s.append(f'<text class="axis-label" x="{X(t):.1f}" y="{y1 + 16}" text-anchor="middle">{dias[t][8:10]}/{dias[t][5:7]}</text>')
s.append(f'<text class="axis-label" x="{(x0 + x1)/2:.0f}" y="{y1 + 34}" text-anchor="middle">eleitores com plano de voto ou confirmados, por bolsão (mediana e faixa de 80%); fundo mais escuro = fase de conversa mais intensa</text>')
lx = x0
for k, rot, cor in series:
s.append(f'<rect x="{lx}" y="{Hh - 12}" width="14" height="4" fill="{cor}"/><text class="dot-label" x="{lx + 18}" y="{Hh - 7}" fill="var(--ink-soft)" font-weight="400">{rot}</text>')
lx += 230
s.append("</svg>")
return "\n".join(s)
def svg_margem(R):
W, Hh = 900, 330; x0, x1, y0, y1 = 60, 880, 106, 276
bins = np.array(R["hist_bins"])
cells = [(c, cor) for c, cor in zip([_cel(R, cid, "nenhuma") for cid, *_ in CENARIOS] + [_cel(R, "guia", "nenhuma")], ["var(--ink-3)", "var(--accent-med)", "var(--accent)", "var(--amber)", "var(--lula)"])]
hmax = max(max(c["hist_margem"]) for c, _ in cells) or 1
X = lambda v: x0 + (v - bins[0])/(bins[-1] - bins[0])*(x1 - x0)
Y = lambda d: y1 - d/hmax*(y1 - y0)
s = [f'<svg viewBox="0 0 {W} {Hh}" role="img" aria-label="Distribuição da margem Lula − Flávio no 2º turno por cenário de campanha, só pela migração">']
h0 = cells[0][0]["hist_margem"]
bw = (x1 - x0)/len(h0)
for i, d in enumerate(h0):
s.append(f'<rect x="{X(bins[i]) + 0.5:.1f}" y="{Y(d):.1f}" width="{max(bw - 1, 0.5):.1f}" height="{y1 - Y(d):.1f}" fill="var(--ink-3)" opacity="0.28"><title>margem {f(bins[i], 2, True)} a {f(bins[i + 1], 2, True)} p.p. · sem campanha dirigida: {d*(bins[1] - bins[0])*100:.1f}% dos sorteios</title></rect>')
for c, cor in cells[1:]:
pts = []
for i, d in enumerate(c["hist_margem"]):
pts += [f"{X(bins[i]):.1f},{Y(d):.1f}", f"{X(bins[i + 1]):.1f},{Y(d):.1f}"]
s.append(f'<polyline points="{" ".join(pts)}" fill="none" stroke="{cor}" stroke-width="2.2" stroke-linejoin="round"{" stroke-dasharray=\"5,3\"" if c["id"] == "guia" else ""}/>')
s.append(f'<line x1="{X(0):.1f}" y1="{y0 - 6}" x2="{X(0):.1f}" y2="{y1}" stroke="var(--ink)" stroke-width="1.2"/>')
mp = R["comparacao"].get("mediana_principal")
if mp is not None:
s.append(f'<line x1="{X(mp):.1f}" y1="{y0 - 6}" x2="{X(mp):.1f}" y2="{y1}" stroke="var(--flavio)" stroke-width="1.4" stroke-dasharray="4,3"/>')
s.append(f'<text class="axis-label" x="{X(mp) + 4:.1f}" y="{y0 + 4}" fill="var(--flavio)">mediana do modelo principal {fs(mp)}</text>')
for v in range(int(bins[0]), int(bins[-1]) + 1):
s.append(f'<text class="axis-label" x="{X(v):.1f}" y="{y1 + 18}" text-anchor="middle">{"+" if v > 0 else ""}{v}</text>')
s.append(f'<text class="axis-label" x="{(x0 + x1)/2:.0f}" y="{y1 + 36}" text-anchor="middle">margem Lula − Flávio em 25/out só pela migração, sem a deriva das pesquisas (p.p. de votos válidos; caudas além do eixo somadas nas pontas)</text>')
for k, (c, cor) in enumerate(cells):
xx, yy = x0, 14 + k*15
if k == 0: s.append(f'<rect x="{xx}" y="{yy - 8}" width="14" height="9" fill="var(--ink-3)" opacity="0.28"/>')
else: s.append(f'<line x1="{xx}" y1="{yy - 4}" x2="{xx + 14}" y2="{yy - 4}" stroke="{cor}" stroke-width="2.2"{" stroke-dasharray=\"5,3\"" if c["id"] == "guia" else ""}/>')
s.append(f'<text class="dot-label" x="{xx + 20}" y="{yy}" fill="var(--ink-soft)" font-weight="400">{H.escape(c["nome"])}: mediana {fs(c["margem_q"]["50"], 2)} (80% de {fs(c["margem_q"]["10"])} a {fs(c["margem_q"]["90"])}) · Lula {pct(c["p_lula_so_migracao"], 1)} só migração · {pct(c["p_lula_com_deriva"], 1)} com deriva</text>')
s.append("</svg>")
return "\n".join(s)
def _q3(d, k="q50", lo="q10", hi="q90", fn=mi):
return f'{fn(d[k], 2, True)} <span class="muted">({fn(d[lo], 1, True)} a {fn(d[hi], 1, True)})</span>'
def build(R, DIR=None):
"""Seção #migracao a partir de R["migracao"] (resultados.json). Vazio se a análise não rodou."""
Mg = R.get("migracao") or {}
if not Mg.get("usado"): return ""
E, G, C = Mg["estoques_1t"], Mg["guia"], Mg["comparacao"]
sem, mod, forte, maxi = (_cel(Mg, k, "nenhuma") for k in ("sem", "moderada", "forte", "maxima"))
forte_s = _cel(Mg, "forte", "simetrica"); guia = _cel(Mg, "guia", "nenhuma")
sf = forte["saldo_frentes"]
p_prin = C.get("p_lula_principal"); p_mc = C.get("p_lula_mcmc")
css = ("<style>"
"#migracao .mg-tab th,#migracao .mg-tab td{white-space:nowrap}"
"#migracao .mg-tab td.txt{white-space:normal;min-width:180px}"
"#migracao .mg-tab tr.ref td{background:var(--surface-2)}"
"#migracao .mg-tab tr.ref td.txt{font-weight:600}"
"#migracao .mg-mat td.lula{color:var(--lula);font-weight:600}#migracao .mg-mat td.flavio{color:var(--flavio);font-weight:600}"
"#migracao .nota{font-size:12.5px;color:var(--ink-soft);margin:8px 0 0;font-family:var(--sans);line-height:1.55}"
"#migracao h3.sub{font-size:19px;margin:30px 0 6px}"
"#migracao .estados{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0 0;font:12.5px var(--sans);color:var(--ink-soft)}"
"#migracao .estados span{border:1px solid var(--line);border-radius:999px;padding:3px 10px;background:var(--surface)}"
"@media(max-width:680px){#migracao .mg-tab td.txt{min-width:140px}}"
"</style>")
def tile(eb, val, cap):
return f'<div class="tile"><span class="eyebrow">{eb}</span><div class="val" style="font-size:22px;line-height:1.3">{val}</div><div class="cap">{cap}</div></div>'
sal_tot = sf["total"]
tiles = ('<div class="tiles">'
+ tile("Saldo da campanha forte", f'{mi(sal_tot["q50"], 2, True)} <small>votos</small>',
f'80% entre {mi(sal_tot["q10"], 1, True)} e {mi(sal_tot["q90"], 1, True)}; 500 mil voluntários iniciais, {mi(forte["voluntarios_q10_50_90"][1])} ao fim. O guia pede +{mi(G["metas_total"], 1)}.')
+ tile("Chance de Lula: sem → forte", f'{pct(sem["p_lula_com_deriva"], 1)} → {pct(forte["p_lula_com_deriva"], 1)}',
f'com a deriva das pesquisas, como no modelo principal ({pct(p_prin, 1) if p_prin is not None else "—"}; MCMC {pct(p_mc, 1) if p_mc is not None else "—"}). Só pela migração, sem deriva: {pct(sem["p_lula_so_migracao"], 1)} → {pct(forte["p_lula_so_migracao"], 1)}.')
+ tile("Abstenção esperada no 2º turno", f'{f(sem["abstencao_q10_50_90"][1])}%',
f'80% entre {f(sem["abstencao_q10_50_90"][0])}% e {f(sem["abstencao_q10_50_90"][2])}%; foi {f(100*E["abstencao"]/E["eleitorado"])}% no 1º turno. Saem {f(Mg["abstencao_bruta"]["saem_mi_q50"])} mi, entram {f(Mg["abstencao_bruta"]["entram_mi_q50"])} mi.')
+ tile("Conversão necessária × literatura", f'{f(G["taxa_necessaria"]["terceira_via"]["razao"], 0)}× <small>na 3ª via</small>',
f'para cumprir a meta do guia com o alcance forte, 1 em {f(1/G["taxa_necessaria"]["terceira_via"]["necessaria"], 1) if G["taxa_necessaria"]["terceira_via"]["necessaria"] < 1 else "menos de 1"} contato teria de virar voto; a literatura dá 1 em {f(1/G["taxa_necessaria"]["terceira_via"]["literatura"], 0)}.')
+ '</div>')
# tabela 1: cenários
rows1 = []
for c in Mg["cenarios"]:
s_ = c["saldo_frentes"]; ref = c["id"] == "guia"
vol = ("—" if ref else "0" if c["voluntarios_q10_50_90"][1] == 0 else f'{mi(c["voluntarios_q10_50_90"][1])} <span class="muted">({mi(c["voluntarios_q10_50_90"][0])}–{mi(c["voluntarios_q10_50_90"][2])})</span>')
nomes = "—" if ref else "0" if c["nomes_distintos_q50"] == 0 else mi(c["nomes_distintos_q50"])
rows1.append(f'<tr{" class=ref" if ref else ""}><td class="txt">{H.escape(c["nome"])}</td><td>{H.escape(c["resposta_nome"])}</td><td class="num">{vol}</td><td class="num">{nomes}</td>'
+ "".join(f'<td class="num">{mi(s_[k]["q50"], 2, True) if s_[k]["q50"] else "0"}</td>' for k in ["ausentes", "terceira_via", "brancos_nulos", "eleitores_flavio", "chamado_aberto", "resposta_flavio"])
+ f'<td class="num"><b>{mi(s_["total"]["q50"], 2, True)}</b> <span class="muted">({mi(s_["total"]["q10"], 1, True)} a {mi(s_["total"]["q90"], 1, True)})</span></td>'
+ f'<td class="pnum {"lead" if c["margem_q"]["50"] > 0 else "trail"}">{fs(c["margem_q"]["50"])} <span class="muted">({fs(c["margem_q"]["10"])} a {fs(c["margem_q"]["90"])})</span></td>'
+ f'<td class="pnum">{pct(c["p_lula_so_migracao"], 1)}</td><td class="pnum">{pct(c["p_lula_com_deriva"], 1)}</td></tr>')
# tabela 2: frente a frente com o guia
ROT_FRENTE = {"terceira_via": ("Terceira via", "dividir meio a meio os votos de Cury, Caiado e Renan"), "eleitores_flavio": ("Votos de Flávio", "trazer 600 mil eleitores pouco convictos"),
"brancos_nulos": ("Brancos e nulos", "convencer 1 milhão a votar em Lula"), "ausentes": ("Quem não foi votar", "levar às urnas 1,5 milhão de ausentes em redutos de Lula")}
rows2 = []
for k in ["terceira_via", "eleitores_flavio", "brancos_nulos", "ausentes"]:
tn = G["taxa_necessaria"][k]; alc = G["alcance_forte_q50"]["terceira_via" if k == "brancos_nulos" else k]
rows2.append(f'<tr><td class="txt"><b>{ROT_FRENTE[k][0]}</b><br><span class="muted">{ROT_FRENTE[k][1]}</span></td><td class="num">{mi(G["metas"][k], 1, True)}</td>'
f'<td class="num">{mi(sf[k]["q50"], 2, True)} <span class="muted">({mi(sf[k]["q10"], 2, True)} a {mi(sf[k]["q90"], 2, True)})</span></td>'
f'<td class="num">{mi(alc)}</td><td class="num">{pct(tn["literatura"], 0)}</td><td class="num">{pct(tn["necessaria"], 0) if tn["necessaria"] < 1 else "acima de 100%"}</td><td class="num"><b>{f(tn["razao"], 0)}×</b></td></tr>')
rows2.append(f'<tr class="ref"><td class="txt">Soma</td><td class="num">{mi(G["metas_total"], 1, True)}</td><td class="num">{mi(sal_tot["q50"], 2, True)} <span class="muted">({mi(sal_tot["q10"], 1, True)} a {mi(sal_tot["q90"], 1, True)})</span></td><td class="num">{mi(forte["nomes_distintos_q50"])}</td><td></td><td></td><td></td></tr>')
# tabela 3: matriz mediana
mat_s, mat_f = Mg["matriz_mediana"]["sem"], Mg["matriz_mediana"]["forte"]
rows3 = []
for o in ORIG:
rows3.append(f'<tr><td class="txt">{ROT_ORIG[o]}<br><span class="muted">{mi(E[{"lula": "lula", "flavio": "flavio", "bn": "brancos_nulos", "abs": "abstencao"}[o]] if o in ("lula", "flavio", "bn", "abs") else 0) if o in ("lula", "flavio", "bn", "abs") else ""}</span></td>'
+ "".join(f'<td class="num {d if d in ("lula", "flavio") else ""}">{f(mat_s[o][d][1])}% <span class="muted">({f(mat_s[o][d][0])}–{f(mat_s[o][d][2])})</span></td>' for d in DEST)
+ "".join(f'<td class="num">{fs(mat_f[o][d][1] - mat_s[o][d][1], 2)}</td>' for d in DEST) + "</tr>")
# tabela 4: por região (cenário forte, sem resposta)
rows4 = []
for r_, r1 in zip(forte["por_regiao"], sem["por_regiao"]):
rows4.append(f'<tr><td class="txt">{H.escape(r_["nome"])}</td><td class="num">{mi(r_["ausentes_1t"], 2)}</td><td class="num">{f(r_["abst_1t"])}%</td><td class="num">{f(r1["abst_2t_q50"])}%</td>'
f'<td class="num">{mi(r1["novos_eleitores_q50"], 2)}</td><td class="num">{mi(r1["saldo_q50"], 2, True)}</td><td class="num">{mi(r_["novos_eleitores_q50"], 2)}</td>'
f'<td class="num">{mi(r_["saldo_q50"], 2, True)} <span class="muted">({mi(r_["saldo_q10"], 2, True)} a {mi(r_["saldo_q90"], 2, True)})</span></td>'
f'<td class="pnum {"lead" if r_["margem_1t"] > 0 else "trail"}">{fs(r_["margem_1t"])}</td><td class="pnum {"lead" if r_["margem_2t_q50"] > 0 else "trail"}">{fs(r_["margem_2t_q50"])}</td></tr>')
priors_rows = "".join(f'<tr><td class="txt">{H.escape(p["nome"])}</td><td class="num">{H.escape(p["simbolo"])}</td><td>{H.escape(p["dist"])}</td><td class="num">{"cenário" if p["media"] is None else (f(p["media"], 3) if p["media"] < 0.1 else f(p["media"], 2))}</td><td class="num">{f(p["sd"], 3) if p["sd"] < 0.1 else f(p["sd"], 2)}</td><td class="txt muted">{H.escape(p["fonte"])}</td></tr>' for p in Mg["priors"])
ab = Mg["abstencao_bruta"]
ch = Mg["checks"]
dif_forte = forte["margem_q"]["50"] - sem["margem_q"]["50"]
dif_max = maxi["margem_q"]["50"] - sem["margem_q"]["50"]
tldr = (f'Sem campanha dirigida, a migração leva a margem para {fs(sem["margem_q"]["50"])} ponto{"s" if abs(sem["margem_q"]["50"]) >= 2 else ""} (80% entre {fs(sem["margem_q"]["10"])} e {fs(sem["margem_q"]["90"])}). '
f'Uma campanha forte de conversa pessoal, com 500 mil voluntários iniciais e a eficácia que a literatura mede, rende {mi(sal_tot["q50"], 1, True)} votos de saldo '
f'e move a margem em {fs(dif_forte)} ponto{"s" if abs(dif_forte) >= 2 else ""}; a máxima, {fs(dif_max)}. A virada exigiria {mi(G["metas_total"], 1, True)}: '
f'com o alcance forte, a conversão por contato teria de ser {f(G["taxa_necessaria"]["terceira_via"]["razao"], 0)} vezes a da literatura na terceira via. '
f'A chance de Lula, com a deriva das pesquisas, vai de {pct(sem["p_lula_com_deriva"], 1)} sem campanha a {pct(forte["p_lula_com_deriva"], 1)} com a forte. A manchete não muda: isto é análise condicional, não previsão.')
hoje_txt = f'{Mg["hoje"][8:10]}/{Mg["hoje"][5:7]}'
return f"""
<section id="migracao">
{css}
<div class="kicker">Migração</div>
<h2>Para onde vão os {f(E["abstencao"]/1e6, 0)} milhões que faltaram, os {f(E["terceira_via"]/1e6, 0)} milhões da terceira via e os {f(E["brancos_nulos"]/1e6, 0)} milhões de brancos e nulos</h2>
<p class="tldr"><b>Em uma frase:</b> {tldr}</p>
<div class="howto"><b>Como ler.</b> O resultado oficial por UF é o estado inicial: cada eleitor está numa de oito caixas (votou em Lula, em Flávio, em cada um dos candidatos da 3ª via, branco/nulo ou não votou). Uma [[markov|cadeia de Markov]] leva cada caixa aos quatro destinos do 2º turno (Lula, Flávio, branco/nulo, não vota) com uma matriz de transição sorteada: cada linha é uma [[dirichlet|Dirichlet]] em torno da 2ª opção declarada (Quaest, 3–6/set) e dos fluxos de comparecimento. Por cima, uma segunda cadeia, diária, acompanha a campanha de conversa pessoal do guia "Três nomes, três semanas" (5 a 24/out): quem é alcançado, quem faz um plano de voto, quem confirma, quem desiste. <b>"Sem campanha dirigida"</b> não é "sem campanha": a 2ª opção e os fluxos de 2022 já contêm a campanha normal dos dois lados; o que a escada mede é o acréscimo de uma mobilização organizada junto a esses três grupos. {Mg["n_sorteios"]//1000} mil sorteios por célula, os mesmos números aleatórios em todas (comparação pareada).</div>
{tiles}
<h3 class="sub">1. A campanha dia a dia: quantos eleitores a conversa alcança e converte</h3>
<div class="howto"><b>Como ler.</b> Cenário forte, sem resposta de Flávio: {mi(forte["voluntarios_q10_50_90"][1])} voluntários ao fim (80% entre {mi(forte["voluntarios_q10_50_90"][0])} e {mi(forte["voluntarios_q10_50_90"][2])}), cada um com três nomes (um ausente, um da 3ª via ou branco/nulo, um indeciso). A linha é o estoque de eleitores com plano de voto ou confirmados; a faixa, 80% dos sorteios. As fases seguem o calendário do guia: largada (5–6), 1ª conversa (7–11), pausa de Aparecida (12), 2ª conversa (13–18), fechamento (19–22), confirmação (23–24). A cadeia começa em {Mg["inicio_cadeia"][8:10]}/{Mg["inicio_cadeia"][5:7]}; hoje é {hoje_txt}.</div>
<div class="trend-card">{svg_trajetoria(Mg, forte)}</div>
<div class="estados"><span>S0 · não alcançado</span><span>S1 · alcançado</span><span>S2 · plano de voto</span><span>S3 · confirmado</span><span>S4 · recusou ou foi deixado</span></div>
<p class="nota">Ao fim da campanha forte, nos ausentes: {pct(forte["estados_finais_q50"]["ausentes"][1] + forte["estados_finais_q50"]["ausentes"][2] + forte["estados_finais_q50"]["ausentes"][3] + forte["estados_finais_q50"]["ausentes"][4], 1)} do bolsão de {mi(Mg["bolsoes"]["ausentes"])} alcançados, {pct(forte["estados_finais_q50"]["ausentes"][2] + forte["estados_finais_q50"]["ausentes"][3], 2)} com plano de voto ou confirmados. Os contatos são poucos diante do bolsão: o gargalo é o alcance, não a conversa.</p>
<h3 class="sub">2. A margem do 2º turno em cada cenário</h3>
<div class="howto"><b>Como ler.</b> Distribuição da margem Lula − Flávio só pela migração (resultado oficial + matriz de transição + campanha), sem a deriva das pesquisas: é o que o voto contado e a 2ª opção declarada permitem dizer. Barras: sem campanha dirigida. Linhas: moderada (100 mil voluntários iniciais), forte (500 mil) e máxima (2 milhões, teto de {pct(0.05)} dos votos de Lula). A linha tracejada é a mediana do modelo principal, que parte das pesquisas e do voto contado e inclui a deriva até 25/out.</div>
<div class="trend-card">{svg_margem(Mg)}</div>
<div class="table-card" style="margin-top:14px"><table class="mg-tab" style="min-width:1100px">
<thead><tr><th>Cenário</th><th>Resposta de Flávio</th><th>Voluntários ao fim</th><th>Nomes alcançados</th><th>Ausentes</th><th>3ª via</th><th>Brancos/nulos</th><th>Eleitores de Flávio</th><th>Chamado aberto</th><th>Resposta de Flávio</th><th>Saldo total <span class="muted">(80%)</span></th><th>Margem <span class="muted">(80%)</span></th><th>Lula só migração</th><th>Lula com deriva</th></tr></thead>
<tbody>{"".join(rows1)}</tbody></table></div>
<p class="nota">Saldo em votos (Lula − Flávio), mediana dos sorteios; a soma das frentes pode diferir do total por arredondamento e pelos tetos de massa disponível por UF. "Com deriva" soma à margem da migração a mesma deriva t<sub>5</sub> do modelo principal (σ = {f(C["sd_deriva"])} p.p. até 25/out): é a chance comparável à manchete ({pct(p_prin) if p_prin is not None else "—"} principal, {pct(p_mc) if p_mc is not None else "—"} MCMC), mas sem as pesquisas. A última linha impõe as metas do guia ({mi(G["metas_total"], 1, True)}) sobre o cenário sem campanha: mostra onde a margem iria se elas fossem cumpridas, não quanto é provável cumpri-las.</p>
<h3 class="sub">3. Frente a frente com o guia: o que precisaria acontecer</h3>
<div class="howto"><b>Como ler.</b> O guia soma +{mi(G["metas_total"], 1)} em quatro frentes. Para cada uma, o saldo que o modelo espera no cenário forte, o nº de nomes que a campanha forte alcança nesse bolsão e a conversão por contato que seria necessária para cumprir a meta com esse alcance, comparada com a que a literatura de mobilização mede (Gerber & Green; Nickerson & Rogers; Kalla & Broockman).</div>
<div class="table-card" style="margin-top:14px"><table class="mg-tab" style="min-width:820px">
<thead><tr><th>Frente</th><th>Meta do guia</th><th>Modelo, campanha forte <span class="muted">(80%)</span></th><th>Nomes alcançados</th><th>Conversão na literatura</th><th>Conversão necessária</th><th>Razão</th></tr></thead>
<tbody>{"".join(rows2)}</tbody></table></div>
<p class="nota">Se as quatro metas fossem cumpridas, a margem mediana iria a {fs(G["margem_se_cumprido"])} ponto e a chance de Lula, só pela migração, a {pct(G["p_lula_se_cumprido"], 1)} ({pct(G["p_lula_se_cumprido_com_deriva"], 1)} com deriva). Cada eleitor da 3ª via ou de Flávio que muda vale dois votos no saldo; cada branco/nulo ou ausente trazido, um (os ausentes trazidos nominalmente votam {pct(0.8)} em Lula, por isso rendem 0,6 cada). Brancos e nulos dividem com a 3ª via o mesmo bolsão de nomes.</p>
<h3 class="sub">4. A matriz de transição: de onde vem e para onde vai cada voto</h3>
<div class="howto"><b>Como ler.</b> Parcela de cada origem que termina em cada destino no 2º turno (mediana e faixa de 80% dos sorteios), no cenário sem campanha dirigida; à direita, quanto a campanha forte muda cada célula, em pontos. A 2ª opção declarada é o centro das linhas da 3ª via; quem não declarou 2ª opção decide com probabilidade d<sub>3</sub> e se divide s<sub>3</sub>; os brancos e nulos decidem com d<sub>bn</sub>. Quem votou e falta (r<sub>out</sub>) é compensado por quem faltou e vota (r<sub>in</sub>), com a variação líquida da abstenção sorteada em torno do padrão de 2022 e perfil mais pró-Lula nos dois sentidos (τ).</div>
<div class="table-card" style="margin-top:14px"><table class="mg-tab mg-mat" style="min-width:900px">
<thead><tr><th rowspan="2">Origem (1º turno)</th><th colspan="4">Destino no 2º turno, sem campanha dirigida</th><th colspan="4">Campanha forte: diferença (p.p.)</th></tr>
<tr><th>Lula</th><th>Flávio</th><th>Branco/nulo</th><th>Não vota</th><th>Lula</th><th>Flávio</th><th>Branco/nulo</th><th>Não vota</th></tr></thead>
<tbody>{"".join(rows3)}</tbody></table></div>
<p class="nota">Fluxos brutos de comparecimento: r<sub>out</sub> mediano {pct(ab["r_out_q10_50_90"][1])} dos que votaram ({f(ab["saem_mi_q50"])} mi), r<sub>in</sub> mediano {pct(ab["r_in_q10_50_90"][1])} dos ausentes ({f(ab["entram_mi_q50"])} mi); variação líquida nacional da abstenção {fs(ab["delta_nac_q10_50_90"][1])} p.p. (80% entre {fs(ab["delta_nac_q10_50_90"][0])} e {fs(ab["delta_nac_q10_50_90"][2])}); inclinação de perfil τ mediana {f(ab["tau_q10_50_90"][1], 0)} p.p. de margem. Com entrada e saída iguais e a mesma inclinação nos dois sentidos, o efeito líquido do comparecimento é próximo de zero; só o desvio em relação a 2022 importa, como na seção Abstenção.</p>
<h3 class="sub">5. Por região: onde a campanha rende e onde sai pela culatra</h3>
<div class="howto"><b>Como ler.</b> Ausentes do 1º turno, abstenção esperada no 2º, novos eleitores (quem não votou em 4/out e vota em 25/out) e saldo de votos (variação de Lula − Flávio em relação ao 1º turno) sem campanha e com a campanha forte. O chamado aberto a votar só entra com força nas UFs em que Lula liderou; nas de Flávio, o modelo supõe que um apelo genérico traz 30% do efeito, com a composição local, que favorece Flávio, como o guia avisa.</div>
<div class="table-card" style="margin-top:14px"><table class="mg-tab" style="min-width:980px">
<thead><tr><th rowspan="2">Região</th><th rowspan="2">Ausentes 1º turno</th><th rowspan="2">Abstenção 1º turno</th><th rowspan="2">Abstenção 2º turno esperada</th><th colspan="2">Sem campanha dirigida</th><th colspan="2">Campanha forte</th><th rowspan="2">Margem 1º turno</th><th rowspan="2">Margem 2º turno (forte)</th></tr>
<tr><th>Novos eleitores</th><th>Saldo</th><th>Novos eleitores</th><th>Saldo <span class="muted">(80%)</span></th></tr></thead>
<tbody>{"".join(rows4)}</tbody></table></div>
<p class="nota">Saldo em votos e margem em p.p. de válidos, medianas. O exterior entra na massa e na margem, mas fica fora da campanha e da inclinação de perfil.</p>
<div class="variables" style="margin-top:18px">
<div class="variable"><h4>Como a cadeia funciona</h4><p>Duas cadeias encaixadas. A primeira é de um passo: cada eleitor do 1º turno, classificado por UF e pelo que fez em 4/out, vai para um dos quatro destinos do 2º turno segundo uma matriz 8 × 4 sorteada; a massa é conservada (a soma dos destinos é o eleitorado de cada UF). A segunda é diária e não homogênea: em cada bolsão (ausentes, 3ª via e brancos/nulos, eleitores brandos de Flávio), um eleitor passa de não alcançado a alcançado quando entra na lista de um voluntário; de alcançado a convertido com probabilidade proporcional à conversão por contato q, distribuída pelas fases do guia (a 2ª conversa pesa 1,4; o fechamento e a 1ª, 0,8; a pausa, 0); de convertido a confirmado no fechamento e na confirmação; e volta a alcançado com δ<sub>rev</sub> por dia. Quem confirma vota como combinado em {pct(0.85)} (ausentes) ou {pct(0.9)} dos casos; quem só tem plano de voto, em {pct(0.55)}. O estoque convertido é repartido pelas UFs em proporção ao voto de Lula e aplicado sobre os destinos da primeira cadeia, com teto na massa disponível em cada caixa.</p></div>
<div class="variable"><h4>Resposta de Flávio e chamado aberto</h4><p>Na coluna "simétrica", Flávio monta uma rede ρ vezes a do PT (mediana {f(0.6, 1)}) com os mesmos priors de eficácia e bolsões espelhados: ausentes trazidos nominalmente ({pct(0.8)} para ele), 3ª via e brancos/nulos permeáveis, eleitores brandos de Lula. O chamado aberto a votar (rádio, feira, igreja) tira ω × intensidade dos ausentes das UFs em que cada lado liderou, com a composição de quem falta (perfil mais pró-Lula, τ); na UF do adversário, o apelo genérico rende 30% e tem a composição local. O saldo líquido da resposta aparece em coluna própria. Como as duas redes usam os mesmos priors, a diferença entre as colunas mede só a assimetria de tamanho (ρ) e de base (Lula tem mais ausentes nos seus redutos; Flávio tem mais votos a proteger).</p></div>
<div class="variable"><h4>De onde vêm os parâmetros</h4><p>Os priors estão na tabela abaixo, com a fonte de cada um. Os que mais pesam são o número de voluntários (desconhecido: por isso a escada de cenários) e a conversão por contato, tomada da literatura de experimentos de campo: cerca de um voto a cada 14 contatos porta a porta para comparecimento, efeito quase nulo de persuasão em eleição geral e efeitos maiores quando o contato vem de alguém próximo. A 2ª opção da Quaest é de setembro; quando sair uma onda de 2º turno com a pergunta refeita, ela substitui os centros e a cadeia da campanha passa a começar no dia seguinte ao campo, para não contar duas vezes a conversa já feita.</p></div>
<div class="variable"><h4>O que isso muda na manchete</h4><p>Nada. A chance de Lula do painel continua vindo do modelo principal ({pct(p_prin) if p_prin is not None else "—"}) e do MCMC ({pct(p_mc) if p_mc is not None else "—"}), que partem das pesquisas e do voto contado. Esta seção responde a outra pergunta: dado o resultado de 4/out, quanto uma campanha de conversa pessoal com os três grupos pode mover, e o que precisaria acontecer para a virada. A versão "com deriva" da chance existe só para pôr as duas contas na mesma régua: sem campanha dirigida ela dá {pct(sem["p_lula_com_deriva"], 1)}, perto do modelo principal, um pouco abaixo porque não carrega a incerteza das pesquisas e dos efeitos de casa; o nível estrutural dos dois é o mesmo (reprodução do modelo de transferência: {fs(C["check_estrutural"]["recalculado"])} contra {fs(C["check_estrutural"]["esperado"]) if C["check_estrutural"]["esperado"] is not None else "—"}).</p></div>
</div>
<div class="table-card" style="margin-top:14px"><table class="mg-tab" style="min-width:900px">
<thead><tr><th>Parâmetro</th><th>Símbolo</th><th>Distribuição</th><th>Média</th><th>Desvio</th><th>Fonte ou justificativa</th></tr></thead>
<tbody>{priors_rows}</tbody></table></div>
<p class="nota">Concentrações das linhas Dirichlet: κ = 400 (Lula e Flávio), 100 (3ª via: subamostras da Quaest de 100 a 150 entrevistas, desvio de 4 a 5 p.p.), 150 (brancos/nulos e ausentes). Verificações do script: massa conservada em todos os sorteios ({"ok" if ch["massa_ok"] else "falhou"}); linhas somam 1 ({"ok" if ch["linhas_ok"] else "falhou"}); cadeia em [0, 1] e convertidos ≤ alcançados ({"ok" if ch["cadeia_ok"] else "falhou"}); reprodução do modelo estrutural ({"ok" if ch["estrutural_ok"] else "falhou"}); escada monotônica e resposta simétrica ≤ sem resposta ({"ok" if ch["monotonico_ok"] else "falhou"}); {f(Mg["segundos"], 0)} s.</p>
<h3 class="sub">Limites desta análise</h3>
<ul class="nota" style="padding-left:18px;font-size:13px">
<li><b>Não é previsão.</b> É uma análise condicional: dado o voto contado e uma campanha com estas características, para onde a margem iria. Não usa as pesquisas de 2º turno, que são a base da manchete, e não substitui as rodadas pós-1º turno.</li>
<li><b>Literatura americana.</b> As taxas de conversão vêm de experimentos de campo nos EUA, com voto facultativo; no Brasil, com voto obrigatório e 2º turno três semanas depois, o efeito de mobilização pode ser outro, e não há experimento comparável publicado.</li>
<li><b>Número de voluntários desconhecido.</b> Não existe medida da adesão a uma campanha sem coordenação central; a escada de 0 a 2 milhões cobre as ordens de grandeza, e o teto ψ impede que a corrente de convites cresça sem limite.</li>
<li><b>Transferência declarada.</b> Os centros das linhas da 3ª via vêm da matriz mais recente da base: a PoderData/Aya de 10–12/out, depois do 1º turno (a 2ª opção da Quaest, de setembro, vale só enquanto não há onda posterior). Zema não tem linha própria na PoderData e herda a do grupo "outro candidato". O Datafolha de 6–7/out publicou só o agregado dos quatro (Flávio 46, Lula 32), que dá a Lula 41% dos decididos, contra 45% na PoderData: reescalar a matriz para o agregado do Datafolha baixaria a chance de Lula no modelo principal de 15,9% para 14,3%.</li>
<li><b>Sem abstenção por município ou perfil.</b> A inclinação τ é um número nacional, calibrado na abstenção por escolaridade de 2022 e no voto por escolaridade das pesquisas; a composição real de quem entra e sai varia por lugar.</li>
<li><b>Resposta de Flávio suposta simétrica.</b> A rede dele é um múltiplo da do PT com os mesmos priors; uma campanha de outro tipo (mídia, igrejas, lideranças locais) não está modelada.</li>
<li><b>Exterior fora da campanha.</b> Os {mi(E["eleitorado"] - sum(r["eleitorado"] for r in sem["por_regiao"] if r["regiao"] != "EXT"))} eleitores no exterior entram na massa, mas não nos bolsões nem na inclinação de perfil.</li>
</ul>
<p class="nota">Fontes: TSE, resultado oficial do 1º turno por UF (100% das seções); PoderData/Aya 10–12/out (transferência declarada; antes, Quaest 3–6/set); TSE 2022 via CNN Brasil (abstenção por UF nos dois turnos); TSE, perfil do comparecimento 2022 por escolaridade; Gerber & Green, <i>Get Out the Vote</i>; Nickerson & Rogers (2010), <i>Psychological Science</i>; Kalla & Broockman (2018), <i>American Political Science Review</i>; Pereira et al. (Ipea, 2023) sobre o passe livre de 2022. Código: migracao_markov.py (cálculo, autoverificações e esta seção), chamado por modelo_eleicao_2026.py (bloco 5b).</p>
</section>
"""
def metodologia(R):
"""Passo 8 da metodologia (div.variable), no formato dos passos 1 a 7 de montar_painel.py."""
Mg = R.get("migracao") or {}
if not Mg.get("usado"): return ""
sem, forte = _cel(Mg, "sem", "nenhuma"), _cel(Mg, "forte", "nenhuma")
return f"""
<div class="variable">
<h4>8 · Migração por cadeia de Markov</h4>
<div class="formula">destinos<sub>u</sub> = n<sub>u</sub> · M<sub>u</sub> · M<sub>u</sub>[o, ·] ~ Dirichlet(κ<sub>o</sub> · m<sub>o</sub>) · m<sub>3ª via</sub> = 2ª opção + resíduo decidido (d<sub>3</sub>, s<sub>3</sub>) · m<sub>ausente</sub> = (r<sub>in</sub>π<sub>u</sub>, r<sub>in</sub>(1−π<sub>u</sub>), ·, 1−r<sub>in</sub>)<br>campanha: x<sub>t+1</sub> = x<sub>t</sub> · P<sub>p,t</sub> sobre S0…S4 · alcance D<sub>p</sub> = P<sub>p</sub>(1 − e<sup>−N/P<sub>p</sub></sup>) · conversão q<sub>p</sub> por contato, fases do guia · margem = 100(ΣL − ΣF)/(ΣL + ΣF)</div>
<p>Análise paralela (<code>migracao_markov.py</code>), condicional e sem efeito na manchete: a partir do resultado oficial por UF, uma [[markov|cadeia de Markov]] leva cada eleitor do 1º turno (Lula, Flávio, cada candidato da 3ª via, branco/nulo, ausente) a um dos quatro destinos do 2º turno por uma matriz sorteada linha a linha ([[dirichlet|Dirichlet]] em torno da 2ª opção declarada da Quaest, dos fluxos brutos de comparecimento e de uma inclinação de perfil). Uma segunda cadeia, diária e não homogênea, segue a campanha de conversa pessoal de 5 a 24/out (alcance por voluntários e recrutamento em cadeia; conversão por contato tomada dos experimentos de Gerber & Green, Nickerson & Rogers e Kalla & Broockman; reversão; confirmação) e aplica o estoque convertido como fluxo entre destinos, com teto na massa disponível. {Mg["n_sorteios"]//1000} mil sorteios por célula, em 4 cenários de alcance × 2 respostas de Flávio, mais a referência "metas do guia cumpridas". Sem campanha dirigida a margem mediana é {fs(sem["margem_q"]["50"])}; com a forte, {fs(forte["margem_q"]["50"])}. A chance "com deriva" soma a mesma deriva t<sub>5</sub> do passo 4, só para comparar com o modelo principal.</p>
</div>"""
# ------------------------------------------------------------------ verificação
if __name__ == "__main__":
import sys
DIR = os.path.dirname(os.path.abspath(__file__)) + "/"
tr = pd.read_csv(DIR + "transferencia_2turno.csv")
transf = {k: (float(g.pct_flavio.mean())/100, float(g.pct_lula.mean())/100) for k, g in tr.groupby("origem")}
comp = {}
if os.path.exists(DIR + "resultados.json"):
Rj = json.load(open(DIR + "resultados.json", encoding="utf-8"))
comp = {"estrutural_base_margem": Rj.get("estrutural_base_margem"), "p_lula_principal": Rj.get("p_lula"), "p_lula_mcmc": (Rj.get("mcmc") or {}).get("p_lula"),
"mediana_principal": (Rj.get("quantis_margem_final") or {}).get("50"), "sd_deriva_principal": Rj.get("sd_deriva")}
n_s = int(sys.argv[1]) if len(sys.argv) > 1 else 20_000
t0 = time.time()
A = simular(DIR, transf, "2026-10-06", comp.get("sd_deriva_principal") or 4.24, comp, seed=7, n_sorteios=n_s)
A2 = simular(DIR, transf, "2026-10-06", comp.get("sd_deriva_principal") or 4.24, comp, seed=7, n_sorteios=n_s)
ch = A["checks"]
print("[checks] " + " · ".join(f"{k}={'ok' if v else 'FALHOU'}" for k, v in ch.items()))
print(f"[estrutural] recalculado {A['comparacao']['check_estrutural']['recalculado']:+.2f} esperado {A['comparacao']['check_estrutural']['esperado']}")
assert A["cenarios"][0]["margem_q"] == A2["cenarios"][0]["margem_q"] and A["cenarios"][2]["saldo_frentes"] == A2["cenarios"][2]["saldo_frentes"], "semente fixa deveria reproduzir"
print("[ok] semente fixa reproduz")
for c in A["cenarios"]:
s = c["saldo_frentes"]
print(f" {c['nome']:<40} {c['resposta_nome']:<9} vol {c.get('voluntarios_q10_50_90', ['—'] * 3)[1]:>9} | saldo {s['total']['q50']/1e6:+.2f} mi "
f"(aus {s['ausentes']['q50']/1e6:+.2f}, 3v {s['terceira_via']['q50']/1e6:+.2f}, bn {s['brancos_nulos']['q50']/1e6:+.2f}, fl {s['eleitores_flavio']['q50']/1e6:+.2f}, "
f"ch {s['chamado_aberto']['q50']/1e6:+.2f}, resp {s['resposta_flavio']['q50']/1e6:+.2f}) | margem {c['margem_q']['50']:+.2f} [{c['margem_q']['10']:+.2f}, {c['margem_q']['90']:+.2f}] "
f"| P(Lula) {c['p_lula_so_migracao']:.3f} / com deriva {c['p_lula_com_deriva']:.3f} | abst {c['abstencao_q10_50_90'][1]:.1f}%")
print("[guia]", json.dumps(A["guia"]["taxa_necessaria"], ensure_ascii=False), "margem se cumprido", A["guia"]["margem_se_cumprido"])
print(f"[tempo] {A['segundos']} s por chamada ({time.time() - t0:.1f} s as duas)")
assert all(ch.values()), ch
# -*- coding: utf-8 -*-
"""
Agregador de pesquisas + modelo probabilístico — Eleição presidencial 2026 (Brasil). Versão 3 (auditoria de 30/set/2026).
Componentes
1. Dados: pesquisas de 2º turno (Lula × Flávio) e de 1º turno (todos os candidatos), calibração histórica 2006–2022
2. Calibração: qualidade por instituto (MAE histórico), viés sistemático das pesquisas por ciclo (âncora), movimento entre turnos
3. Tendência + efeitos de casa: espaço de estados único (espaco_estados.py) — passeio aleatório, efeitos de casa ridge
identificados pela âncora (institutos com histórico verificado), variância extra por instituto estimada nos dados
3c. Pesquisas estaduais → leitura nacional independente (swing sobre 2022)
3d. 1º turno: um estado por candidato (votos válidos), viés histórico do 1º turno, simulação → P(decide no 1º turno), P(Flávio no 2º)
4. Monte Carlo do 2º turno: nível + deriva em duas fases (até 4/out; entre turnos, calibrada em 2014/18/22) − viés + choques regionais
4b. Ensemble com pesos por regime (antes/depois do 1º turno) e tabela "o que mudou" (v2 → v3)
5. Tornado das variáveis decisivas · 6. O que o 1º turno dirá sobre o 2º · 6b. Diagnóstico por instituto · 7. Saídas
8. Regime pós-1º turno: ativado por resultado_1turno.csv (erro por instituto → priors do 2º turno)
5b. Migração (migracao_markov.py): ausentes, 3ª via e brancos/nulos por cadeia de Markov + Monte Carlo, análise paralela e condicional
Reprodutível: python3 modelo_eleicao_2026.py (HOJE pode ser fixado por variável de ambiente ELEICAO_HOJE=YYYY-MM-DD)
"""
import json, math, os, sys
import numpy as np
import pandas as pd
from scipy.stats import norm, beta as beta_dist
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from espaco_estados import ajustar, validacao_rolling
rng = np.random.default_rng(20261025)
HOJE = pd.Timestamp(os.environ.get("ELEICAO_HOJE") or pd.Timestamp.today().normalize())
T1 = pd.Timestamp("2026-10-04")
T2 = pd.Timestamp("2026-10-25")
DATA = os.path.dirname(os.path.abspath(__file__)) + "/" # bases (CSV) e previsão congelada: no repositório
OUT = os.environ.get("ELEICAO_OUT", "/tmp/outputs/modelo/") # saídas intermediárias (JSON, SVGs) para montar o painel
os.makedirs(OUT, exist_ok=True)
# Variantes do toggle do herói (19/out): com ELEICAO_SO_POS1T a base de 2º turno fica só com as ondas de campo depois de 4/out.
# ELEICAO_SO_POS1T=1 → sem o voto contado: o nível NÃO é combinado ao resultado do 1º turno + transferência (W_ESTRUT = 0);
# grava resultados_pos1t.json.
# ELEICAO_SO_POS1T=estrut → com o voto contado: mesma combinação do modelo principal; grava resultados_pos1t_estrut.json.
# As duas rodadas terminam logo depois do ensemble (sem MCMC, abstenção, tornado, migração, congelados, CSVs ou SVGs).
SO_POS1T = bool(os.environ.get("ELEICAO_SO_POS1T"))
POS1T_COM_ESTRUT = os.environ.get("ELEICAO_SO_POS1T") == "estrut"
# Variante "com momentum" (21/out): com ELEICAO_MOMENTUM=1 a deriva até 25/out ganha uma média, vinda do agregado das diferenças entre
# ondas consecutivas do mesmo instituto (mudanca_2turno.resumo, em p.p./dia), encolhida por ENCOLHE_MOMENTUM; o desvio dessa média é o
# erro-padrão do agregado (piso SD_MOMENTUM_PISO por dia), encolhido na mesma proporção, para não inflar a variância da deriva. O MCMC recebe a mesma prior. Grava resultados_momentum.json e termina
# depois do MCMC. No modelo principal (flag ausente) a deriva continua com média zero: nenhuma saída muda.
MOMENTUM = bool(os.environ.get("ELEICAO_MOMENTUM"))
ENCOLHE_MOMENTUM, SD_MOMENTUM_PISO = 0.5, 0.1
T0 = pd.Timestamp("2025-12-01") # origem do índice diário
INICIO_JANELA = pd.Timestamp("2026-06-01") # janela de campanha usada para o nível de hoje e para σ_rw
def dia(ts): return int((pd.Timestamp(ts) - T0).days)
t_now = dia(HOJE)
# ----------------------------------------------------------------------------
# 1. DADOS — 2º turno
# ----------------------------------------------------------------------------
df = pd.read_csv(DATA + "pesquisas_2turno_2026.csv", parse_dates=["campo_fim"])
df = df[df.campo_fim <= HOJE].copy()
df["margem_total"] = df.lula - df.flavio
df["lula_valid"] = 100 * df.lula / (df.lula + df.flavio)
df["margem_valid"] = 100 * (df.lula - df.flavio) / (df.lula + df.flavio)
df["idade_dias"] = (HOJE - df.campo_fim).dt.days
df["t"] = (df.campo_fim - T0).dt.days.astype(float)
df["dia"] = df.t.astype(int)
def _se_amostral(l_, f_, n_):
"""Erro padrão da margem em votos válidos m = 100(L−F)/(L+F) sob amostragem aleatória simples (multinomial)."""
pl, pf = l_/100.0, f_/100.0
return 100*math.sqrt((pl + pf - (pl - pf)**2/(pl + pf))/n_)/(pl + pf) # = 100·sqrt(4s(1−s)/(n·V)), s = L/(L+F), V = L+F
N_MAX_EFETIVO = 5000 # teto do tamanho de amostra efetivo: acima disso o erro não amostral domina (e uma onda não deve pesar 20× outra)
# Auditoria de 16/out: (a) a coluna opcional n_efetivo da base traz o n efetivo declarado pelo instituto (Palver: Kish, efeito dos pesos 2,8
# a 4,3 — o n bruto de 5.000 subestimava o erro em até 2×); (b) AtlasIntel (recrutamento digital aleatório, não probabilístico) declara
# ±1,0 p.p. com n≈5.000, mas a amostragem aleatória simples já dá ±1,4 e o efeito de desenho não é reportado: teto de 2.500.
N_EFETIVO_INST = {"AtlasIntel": 2500}
df["n_ef"] = (df["n_efetivo"] if "n_efetivo" in df.columns else df["n"]).fillna(df["n"]).astype(float)
df["n_ef"] = [min(r.n_ef, N_EFETIVO_INST.get(r.instituto, N_MAX_EFETIVO), N_MAX_EFETIVO) for r in df.itertuples()]
df["se_margem_teorico"] = [_se_amostral(r.lula, r.flavio, r.n_ef) for r in df.itertuples()]
df["se2"] = df.se_margem_teorico**2
# ----------------------------------------------------------------------------
# 2. CALIBRAÇÃO HISTÓRICA (calibracao_historica.csv: erro na margem esq−dir em válidos; + = superestimou o PT)
# ----------------------------------------------------------------------------
cal = pd.read_csv(DATA + "calibracao_historica.csv")
hist = cal.rename(columns={"ciclo": "eleicao", "erro_margem": "erro_pp"})[["instituto", "eleicao", "turno", "erro_pp", "referencia"]]
mae_inst = hist.groupby("instituto").erro_pp.apply(lambda s: s.abs().mean())
bias_inst = hist.groupby("instituto").erro_pp.mean()
n_inst = hist.groupby("instituto").size()
bias_el = hist.groupby(["eleicao", "turno"]).erro_pp.mean()
# Qualidade do instituto: erro absoluto médio EM RELAÇÃO À MÉDIA DO CICLO/TURNO (auditoria de 3/out). O MAE bruto mistura o erro
# próprio do instituto com o erro comum do ciclo (em 2014 e 2022 todos erraram o 1º turno em ~9 pontos), que já é tratado pelo
# desconto de viés; quem só tem histórico em ciclos "bons" parecia melhor. Só entram ciclos/turnos com 3 ou mais institutos.
_grp = hist.groupby(["eleicao", "turno"]).erro_pp
hist["erro_rel"] = hist.erro_pp - _grp.transform("mean")
_rel = hist[_grp.transform("size") >= 3]
mae_rel = _rel.groupby("instituto").erro_rel.apply(lambda s: s.abs().mean())
n_rel = _rel.groupby("instituto").size()
MAE_REF, Q_DEFAULT, K_SHRINK = round(float(mae_rel.median()), 2), 0.8, 2.0
def peso_qualidade(inst):
"""Qualidade = MAE relativo mediano / MAE relativo do instituto, encolhida para 0,8 conforme poucas eleições sustentam o MAE."""
if inst not in mae_rel.index:
return Q_DEFAULT
q_ = float(np.clip(MAE_REF / max(mae_rel[inst], 0.5), 0.6, 1.5))
k_ = float(n_rel[inst])
return float((k_ * q_ + K_SHRINK * Q_DEFAULT) / (k_ + K_SHRINK))
# Viés sistemático do 2º turno: média por ciclo dos institutos de referência (referencia=1), depois média entre ciclos
# com recência suave (meia-vida de dois ciclos = 8 anos). σ_b = desvio entre ciclos, com piso 2,0 pela mistura de métodos de 2026.
_h2 = hist[(hist.turno == 2) & (hist.referencia == 1)]
bias_2t_ciclos = _h2.groupby("eleicao").erro_pp.mean().to_dict()
MEIA_VIDA_CICLOS = 8.0
pesos_ciclo = {c_: 0.5 ** ((2022 - c_) / MEIA_VIDA_CICLOS) for c_ in bias_2t_ciclos}
BIAS_MEAN = round(sum(bias_2t_ciclos[c_]*pesos_ciclo[c_] for c_ in bias_2t_ciclos) / sum(pesos_ciclo.values()), 2)
# Desvio preditivo para um ciclo novo: desvio entre ciclos × sqrt(1 + 1/n) (a média também é estimada), com piso 2,0.
# As caudas são t de Student (DF_ERRO graus de liberdade, reescalada para o mesmo desvio): com 3 a 5 ciclos, a normal é otimista.
DF_ERRO = 5
def _t_unit(r_, n_): return r_.standard_t(DF_ERRO, n_) * math.sqrt((DF_ERRO - 2) / DF_ERRO)
def _sd_pred(v_): v_ = list(v_); return float(np.std(v_, ddof=1) * math.sqrt(1 + 1/len(v_)))
BIAS_SD = round(max(2.0, _sd_pred(bias_2t_ciclos.values())), 2)
BIAS_MEAN_IGUAL = round(float(np.mean(list(bias_2t_ciclos.values()))), 2)
# 1º turno: os ciclos verificáveis (2010, 2014, 2018, 2022) subestimaram o principal adversário do PT. Prior = 60% da média com
# recência: é uma HIPÓTESE (os institutos ajustam método entre ciclos — em 2026 vários ponderam pelo voto declarado de 2022 — e o
# sinal do erro pode mudar), por isso o desvio é o preditivo entre ciclos (≈ 3,8), não mais 3,0 fixo.
_h1 = hist[(hist.turno == 1) & (hist.referencia == 1)]
bias_1t_ciclos = _h1.groupby("eleicao").erro_pp.mean().to_dict()
_p1 = {c_: 0.5 ** ((2022 - c_) / MEIA_VIDA_CICLOS) for c_ in bias_1t_ciclos}
BIAS_1T_HIST = round(sum(bias_1t_ciclos[c_]*_p1[c_] for c_ in bias_1t_ciclos) / sum(_p1.values()), 2)
ENCOLHIMENTO_1T = 0.6
BIAS_1T_MEAN, BIAS_1T_SD = round(ENCOLHIMENTO_1T * BIAS_1T_HIST, 2), round(max(3.0, _sd_pred(bias_1t_ciclos.values())), 2)
# Viés por método — só diagnóstico (poucas observações de 2º turno para telefônico e online: não se aplica)
METODO_HIST = {"Datafolha": "presencial", "Quaest": "presencial", "Ipec": "presencial", "Ibope": "presencial", "MDA": "presencial",
"Vox Populi": "presencial", "Parana Pesquisas": "presencial", "AtlasIntel": "online", "PoderData": "telefonico",
"Ipespe": "telefonico", "Verita": "nao_informado", "Futura Inteligencia": "nao_informado"}
_h2m = hist[hist.turno == 2].assign(metodo=lambda d: d.instituto.map(METODO_HIST))
BIAS_METODO = {m_: round(float(g.erro_pp.mean()), 2) for m_, g in _h2m.groupby("metodo")}
BIAS_N_METODO = _h2m.groupby("metodo").size().to_dict()
# Movimento entre turnos (pesquisas de 2º turno do mesmo instituto: primeira após o 1º turno → última antes do 2º)
mov = pd.read_csv(DATA + "movimento_entre_turnos.csv")
_se_pol = 2.2 # erro amostral típico da margem (n≈2.000) — cada delta observado carrega 2 erros de pesquisa
_var_obs = float((mov.delta_margem**2).mean()); _dias_med = float(mov.dias.mean())
SIG_ENTRE_DIA_BRUTO = math.sqrt(max(_var_obs - 2*_se_pol**2, 1.0) / _dias_med)
# Âncora: institutos com histórico presidencial verificado (referencia=1 no 2º turno) presentes na base de 2026
ANCORA = sorted(set(_h2.instituto) & set(df.instituto))
Q_ANCORA = {p_: peso_qualidade(p_) for p_ in ANCORA}
if SO_POS1T:
# Variante pós-1º turno: o filtro entra DEPOIS da âncora e dos pesos de qualidade, que ficam os do modelo principal (ajustar ignora
# âncoras sem observação), de modo que a seção 3d (1º turno) sai idêntica; só a série de 2º turno muda. Sem ondas pré-4/out,
# _inst_pre1t fica vazio e a_casa = 1 em todas: cada onda identifica o efeito de casa inteiro, encolhido ao prior do erro do 1º turno.
assert HOJE >= T1, "variante pós-1º turno só faz sentido depois de 4/out"
df = df[df.campo_fim > T1].copy()
assert len(df) >= 5, "variante pós-1º turno: menos de 5 pesquisas depois de 4/out"
assert set(ANCORA) & set(df.instituto), "variante pós-1º turno: nenhuma pesquisa da âncora depois de 4/out"
# ----------------------------------------------------------------------------
# 3. TENDÊNCIA + EFEITOS DE CASA — espaço de estados único (Jackman 2005; Linzer 2013; Economist 2020)
# ----------------------------------------------------------------------------
# Regime pós-1º turno (seção 8): priors de efeito de casa vindos do erro observado no 1º turno
res1t_path = DATA + "resultado_1turno.csv"
RESULTADO_1T = None
if os.path.exists(res1t_path):
_r1 = pd.read_csv(res1t_path)
_r1["votos_validos_pct"] = pd.to_numeric(_r1.votos_validos_pct, errors="coerce")
if len(_r1) and _r1.votos_validos_pct.notna().any():
RESULTADO_1T = dict(zip(_r1.candidato.str.lower(), _r1.votos_validos_pct))
POS_1T = RESULTADO_1T is not None and HOJE >= T1
if HOJE > T1 and not POS_1T:
print("[AVISO] já passou o 1º turno e resultado_1turno.csv está vazio: o modelo segue no regime pré-1º turno.")
prior_h_2t, tau_prior_h = {}, None
# Persistência do efeito de casa depois do 1º turno (auditoria de 16/out, estimada nas 14 pesquisas pós-4/out que havia então). A inclinação
# da margem observada sobre o efeito de casa estimado antes do 1º turno era +0,58 ± 0,68 (+0,13 ± 1,0 sem Gerp e Palver; persistência
# total = 1, nula = 0), e a dispersão entre institutos AUMENTAVA depois da correção (DP 0,8 → 2,1 na semana de 10–14/out, sem a Gerp):
# vários institutos passaram a pesar pelo voto declarado no 1º turno (Palver v3, Ipespe). λ = 0,5 entra como compromisso entre a
# persistência total do modelo antigo e o zero que os dados também comportam; a observação pós-1º turno identifica λ·h, não h.
# λ só faz sentido para quem tem efeito de casa estimado ANTES do 1º turno: uma série nova (Palver v3, MDA v2, só com ondas pós-4/out)
# não tem efeito anterior para "persistir", e nela a observação identifica h inteiro (a_casa = 1).
LAMBDA_CASA_POS1T = 0.5
LAMBDA_CASA = LAMBDA_CASA_POS1T if (RESULTADO_1T is not None and HOJE >= T1) else 1.0
_inst_pre1t = set(df.loc[df.campo_fim <= T1, "instituto"])
df["a_casa"] = np.where((df.campo_fim > T1) & df.instituto.isin(_inst_pre1t), LAMBDA_CASA, 1.0)
FLAVIO_TOP2_REAL = None
ERRO_COMUM_1T = None # erro médio da âncora na margem Lula − Flávio do 1º turno (+ = superestimou Lula)
if POS_1T:
# Seção 8: o erro de cada instituto na margem Lula − Flávio do 1º turno (última onda até 4/out, em válidos), medido em relação
# à média da âncora, vira prior do efeito de casa no 2º turno, com metade do peso (o erro de um turno só prevê em parte o do outro).
_real = RESULTADO_1T.get("lula", np.nan) - RESULTADO_1T.get("flavio", np.nan)
_ord = sorted(RESULTADO_1T, key=lambda k_: -(RESULTADO_1T[k_] if pd.notna(RESULTADO_1T[k_]) else -1))
FLAVIO_TOP2_REAL = "flavio" in _ord[:2]
_p1e = pd.read_csv(DATA + "pesquisas_1turno_2026.csv", parse_dates=["campo_fim"])
_p1e = _p1e[_p1e.campo_fim <= T1].sort_values("campo_fim").groupby("instituto").tail(1)
_cand_cols = ["lula", "flavio", "cury", "renan", "caiado", "zema", "outros"]
_val = _p1e[_cand_cols].sum(axis=1)
_err = dict(zip(_p1e.instituto, 100*(_p1e.lula - _p1e.flavio)/_val - _real))
_anc = [v for k_, v in _err.items() if k_ in set(_h2.instituto)]
_c0 = float(np.mean(_anc)) if _anc else float(np.mean(list(_err.values())))
if np.isfinite(_real):
ERRO_COMUM_1T = _c0
prior_h_2t = {k_: 0.5*(v - _c0) for k_, v in _err.items() if np.isfinite(v)}
tau_prior_h = 1.5
# As séries novas do 2º turno ("Palver v3", "MDA v2", "Futura Inteligencia v2") não recebem este prior (a chave é o nome do 1º turno):
# entram com prior 0 e τ = 1,5, de propósito, porque mudaram a ponderação e o erro do 1º turno foi medido com a régua antiga.
# Quebra estrutural: as simulações de 2º turno feitas antes do 1º turno e as pesquisas feitas depois não são a mesma série (em
# 2014, 2018 e 2022 o salto foi de 9 a 14 pontos). No regime pós-1º turno o passeio recebe variância extra de 6² no dia seguinte
# a 4/out, de modo que o nível passa a seguir as pesquisas novas em vez de levar semanas para se afastar das antigas.
SD_QUEBRA_1T = 6.0
Q_EXTRA = {dia(T1) + 1: SD_QUEBRA_1T**2} if POS_1T else {}
fit_full = ajustar(df.margem_valid.values, df.dia.values, df.se2.values, df.instituto.values, set(ANCORA), Q_ANCORA, n_days=t_now + 1, q_extra=Q_EXTRA,
a_h=df.a_casa.values)
jan = df[df.campo_fim >= INICIO_JANELA].copy()
d0_jan = int(jan.dia.min())
Q_EXTRA_WIN = {k_ - d0_jan: v for k_, v in Q_EXTRA.items()}
def _ajusta_janela(sub_):
return ajustar(sub_.margem_valid.values, (sub_.dia - d0_jan).values, sub_.se2.values, sub_.instituto.values, set(ANCORA), Q_ANCORA,
n_days=t_now - d0_jan + 1, prior_h=prior_h_2t, tau_prior_h=tau_prior_h, q_extra=Q_EXTRA_WIN, a_h=sub_.a_casa.values)
fit_win = _ajusta_janela(jan)
def x_win(d_): # nível suavizado da janela no dia absoluto d_
k_ = int(np.clip(d_ - d0_jan, 0, fit_win["n_days"] - 1)); return float(fit_win["x"][k_]), float(fit_win["P"][k_])
level_nac, P_nac = x_win(t_now)
se_nac = math.sqrt(P_nac + fit_win["var_ancora"]) # o filtro trata os efeitos de casa como conhecidos; soma a incerteza da média da âncora
house = dict(fit_full["house"]); house.update(fit_win["house"]) # janela prevalece; institutos só antigos ficam com o ajuste completo
sigma_inst = dict(fit_full["sigma_inst"]); sigma_inst.update(fit_win["sigma_inst"])
SIG_RW_KF, SIG_RW_ML = fit_win["sig_rw"], fit_win["sig_rw_ml"]
insts = list(df.instituto.unique())
# casa (instituto sem o sufixo " vN"): "Palver v3" e "MDA v2" são séries novas só para o efeito de casa; nos contadores onda a onda
# e na "última pesquisa por instituto" continuam a Palver e a CNT/MDA. "Futura Inteligencia v2" (21/out): a partir da onda de 19–20/out a Futura
# pondera escolaridade pela PNAD Contínua em vez do cadastro do TSE (mudança declarada no relatório), série nova pelo mesmo critério
df["inst_base"] = df.instituto.str.replace(r" v\d$", "", regex=True)
df["house"] = df.instituto.map(house)
df["house_aplicado"] = df.house * df.a_casa # o que de fato desconta a margem de cada pesquisa (λ·h depois do 1º turno)
df["sigma_inst"] = df.instituto.map(sigma_inst)
df["w_precisao"] = 1.0 / (df.se2 + df.sigma_inst**2)
df["w_recencia"] = np.exp(-np.log(2) * df.idade_dias / 21.0) # só para exibição (a recência entra pelo passeio aleatório)
df["w_qualidade"] = df.instituto.map(peso_qualidade)
df["w_tamanho"] = np.clip(np.sqrt(df.n / 2000.0), 0.7, 1.5)
df["w"] = df.w_recencia * df.w_precisao / df.w_precisao.mean()
print(f"[Espaço de estados] janela desde {INICIO_JANELA.date()}: {len(jan)} pesquisas | σ_rw MAP={SIG_RW_KF:.3f} (ML {SIG_RW_ML:.3f}) p.p./dia | "
f"nível hoje={level_nac:+.2f} ± {se_nac:.2f} | âncora={ANCORA} | iterações {fit_win['iter']}")
level_kf, se_kf = level_nac, se_nac
level_pspline, se_pspline = float(fit_full["x"][t_now]), math.sqrt(float(fit_full["P"][t_now])) # ajuste completo (dez–set), para comparação
# média ponderada simples (última pesquisa por casa, ajustada pelo efeito de casa da série a que ela pertence) — para comparação
last = df.sort_values("campo_fim").groupby("inst_base").tail(1).copy()
last["margem_adj"] = last.margem_valid - last.house_aplicado
last["w_last"] = last.w
media_simples_adj = float((last.margem_adj * last.w_last).sum() / last.w_last.sum())
media_simples_bruta = float((last.margem_valid * last.w_last).sum() / last.w_last.sum())
# validação de origem móvel (últimos 60 dias, passo 3, horizonte 7 dias) — cobertura e viés das previsões do próprio modelo
# Mesmo modelo do ajuste da janela (prior do efeito de casa vindo do erro do 1º turno e λ nas ondas pós-1º turno): antes a validação
# rodava sem os dois e media um modelo diferente do que o painel usa.
_rv = [] if SO_POS1T else validacao_rolling(jan.margem_valid.values, (jan.dia - d0_jan).values, jan.se2.values, jan.instituto.values, set(ANCORA), Q_ANCORA,
dias=range(max(t_now - d0_jan - 60, 25), t_now - d0_jan - 6, 3), q_extra=Q_EXTRA_WIN,
prior_h=prior_h_2t, tau_prior_h=tau_prior_h, a_h=jan.a_casa.values)
_z = np.array([(o_ - p_)/s_ for _, o_, p_, s_, _, _ in _rv]) if _rv else np.array([0.0])
# viés separado: institutos que o ajuste já conhecia × institutos novos (entram com efeito de casa 0). Se o viés está só nos novos,
# é composição da base; se está também nos conhecidos, é tendência que o passeio aleatório (sem inclinação) não antecipa.
_ev = [o_ - p_ for _, o_, p_, _, _, v_ in _rv if v_]; _en = [o_ - p_ for _, o_, p_, _, _, v_ in _rv if not v_]
VALIDACAO = {"n_previsoes": len(_rv), "vies_medio": round(float(np.mean([o_ - p_ for _, o_, p_, _, _, _ in _rv])), 2) if _rv else None,
"vies_inst_vistos": round(float(np.mean(_ev)), 2) if _ev else None, "n_inst_vistos": len(_ev),
"vies_inst_novos": round(float(np.mean(_en)), 2) if _en else None, "n_inst_novos": len(_en),
"cobertura_80": round(float(np.mean(np.abs(_z) < 1.2816)), 3), "cobertura_95": round(float(np.mean(np.abs(_z) < 1.96)), 3),
"erro_abs_medio": round(float(np.mean([abs(o_ - p_) for _, o_, p_, _, _, _ in _rv])), 2) if _rv else None,
"sd_z": round(float(_z.std()), 2), "horizonte_dias": 7}
print(f"[Validação] {VALIDACAO}")
# curva para o gráfico (ajuste completo dez→hoje)
tt_grid = np.arange(int(df.dia.min()), t_now + 1)
f_grid = fit_full["x"][tt_grid]
se_grid = np.sqrt(fit_full["P"][tt_grid])
# ----------------------------------------------------------------------------
# 3c. PESQUISAS ESTADUAIS → estimativa nacional independente (swing sobre o 2º turno de 2022 por UF)
# ----------------------------------------------------------------------------
MARGEM_2022 = 1.80
SIG_EXTRA_UF, SIG_SIST_UF, RHO_EST_NAC, COBERTURA_MIN, HALF_LIFE = 3.0, 1.5, 0.3, 0.30, 21.0
ref_uf = pd.read_csv(DATA + "referencia_uf.csv") if os.path.exists(DATA + "referencia_uf.csv") else None
est = pd.read_csv(DATA + "pesquisas_estaduais_2026.csv", parse_dates=["campo_fim"]) if os.path.exists(DATA + "pesquisas_estaduais_2026.csv") else None
estadual = {"usado": False, "motivo": "base estadual ausente"}
REGIAO_UF, PESO_UF = {}, {}
if ref_uf is not None:
ref_uf["peso"] = ref_uf.eleitorado / ref_uf.eleitorado.sum()
ref_uf["peso_v"] = ref_uf.votos_validos_2022 / ref_uf.votos_validos_2022.sum() # a margem nacional é média ponderada por votos válidos
ref_uf["m22"] = ref_uf.lula_2022 - ref_uf.bolsonaro_2022
REGIAO_UF = dict(zip(ref_uf.uf, ref_uf.regiao)); PESO_UF = dict(zip(ref_uf.uf, ref_uf.peso))
if ref_uf is not None and est is not None:
e2 = est[(est.turno == 2) & ((HOJE - est.campo_fim).dt.days <= 45) & (est.campo_fim <= HOJE)].copy()
if POS_1T:
e2 = e2[e2.campo_fim > T1] # a quebra de 4/out vale também para as estaduais: só entram as feitas depois do 1º turno
e2 = e2.merge(ref_uf[["uf", "m22", "peso", "peso_v", "regiao"]], on="uf", how="inner")
if len(e2):
e2["margem_bruta"] = 100*(e2.lula - e2.flavio)/(e2.lula + e2.flavio)
e2["t"] = (e2.campo_fim - T0).dt.days.astype(float)
e2["ajuste_casa"] = -e2.instituto.map(house).fillna(0.0)
e2["ajuste_tend"] = [level_nac - x_win(int(tt))[0] for tt in e2.t]
e2["margem_valid"] = e2.margem_bruta + e2.ajuste_casa + e2.ajuste_tend
e2["swing"] = e2.margem_valid - e2.m22
e2["w"] = (np.exp(-np.log(2)*(HOJE - e2.campo_fim).dt.days/HALF_LIFE) * np.clip(np.sqrt(e2.n/1000), 0.6, 1.5)
* e2.instituto.map(lambda i_: peso_qualidade(i_)))
e2["var_i"] = [_se_amostral(r.lula, r.flavio, r.n)**2 + SIG_EXTRA_UF**2 for r in e2.itertuples()]
por_uf = []
for uf_, g in e2.groupby("uf"):
wv = g.w/g.var_i
s_raw = float((g.swing*wv).sum()/wv.sum())
v_raw = float(1/(g.w/g.var_i).sum()*g.w.sum()/len(g)) if len(g) > 1 else float(g.var_i.iloc[0])
por_uf.append({"uf": uf_, "n_pesq": int(len(g)), "swing_bruto": s_raw, "var": v_raw, "peso": float(g.peso.iloc[0]), "peso_v": float(g.peso_v.iloc[0]),
"m22": float(g.m22.iloc[0]), "margem_2026": float((g.margem_valid*g.w).sum()/g.w.sum()),
"institutos": ", ".join(sorted(g.instituto.unique())), "ultimo_campo": str(g.campo_fim.max().date())})
pu = pd.DataFrame(por_uf)
wi = 1/pu["var"]; mu_fe = float((pu.swing_bruto*wi).sum()/wi.sum())
Qh = float((wi*(pu.swing_bruto - mu_fe)**2).sum()); c_ = float(wi.sum() - (wi**2).sum()/wi.sum())
tau2 = max(0.0, (Qh - (len(pu) - 1))/c_) if len(pu) > 1 and c_ > 0 else 0.0
wr = 1/(pu["var"] + tau2); mu_s = float((pu.swing_bruto*wr).sum()/wr.sum()); var_mu = float(1/wr.sum())
pu["regiao"] = pu.uf.map(REGIAO_UF)
mu_reg = {}
for r_, g_ in pu.groupby("regiao"):
wg = 1/(g_["var"] + tau2); m_r = float((g_.swing_bruto*wg).sum()/wg.sum()); v_r = float(1/wg.sum())
k_r = (tau2/2)/((tau2/2) + v_r) if tau2 > 0 else 0.0
mu_reg[r_] = mu_s + k_r*(m_r - mu_s)
pu["mu_regiao"] = pu.regiao.map(mu_reg)
pu["swing"] = pu.mu_regiao + (tau2/2)/((tau2/2) + pu["var"])*(pu.swing_bruto - pu.mu_regiao)
pu["var_post"] = 1/(1/max(tau2/2, 1e-6) + 1/pu["var"])
cobertura = float(pu.peso.sum()); cobertura_v = float(pu.peso_v.sum())
_sem = ref_uf[~ref_uf.uf.isin(pu.uf)]
swing_sem = float(sum(r_.peso_v*mu_reg.get(r_.regiao, mu_s) for r_ in _sem.itertuples()))
swing_nac = float((pu.swing*pu.peso_v).sum() + swing_sem)
var_est = float((pu.peso_v**2*pu.var_post).sum() + (1 - cobertura_v)**2*(var_mu + tau2/2) + SIG_SIST_UF**2)
nivel_est, se_est = MARGEM_2022 + swing_nac, math.sqrt(var_est)
estadual = {"usado": cobertura >= COBERTURA_MIN, "motivo": "" if cobertura >= COBERTURA_MIN else f"cobertura {cobertura:.0%} < {COBERTURA_MIN:.0%}",
"nivel": nivel_est, "se": se_est, "mu_swing": mu_s, "tau": math.sqrt(tau2), "cobertura": cobertura,
"n_pesq": int(len(e2)), "n_uf": int(len(pu)), "por_uf": pu.sort_values("peso", ascending=False),
"mu_regiao": {k_: round(v, 2) for k_, v in mu_reg.items()},
"ajuste_medio_tend": float((e2.ajuste_tend).mean()), "ajuste_medio_casa": float(e2.ajuste_casa.mean())}
print(f"[Estadual] {len(e2)} pesquisas em {len(pu)} UFs (cobertura {cobertura:.0%}) | swing médio {mu_s:+.2f} τ={math.sqrt(tau2):.2f} | nacional {nivel_est:+.2f} ± {se_est:.2f}")
level_final, se_final = level_nac, se_nac
w_est_comb = 0.0
if estadual["usado"]:
s1, s2 = se_nac, estadual["se"]
den = s1**2 + s2**2 - 2*RHO_EST_NAC*s1*s2
w_est_comb = float(np.clip((s1**2 - RHO_EST_NAC*s1*s2)/den, 0, 1))
level_final = (1 - w_est_comb)*level_nac + w_est_comb*estadual["nivel"]
se_final = math.sqrt(s1**2*s2**2*(1 - RHO_EST_NAC**2)/den)
estadual["peso_na_combinacao"] = w_est_comb
# ----------------------------------------------------------------------------
# 3d. 1º TURNO — um estado por candidato (votos válidos), efeitos de casa por instituto × candidato, viés histórico, simulação
# ----------------------------------------------------------------------------
CANDS = ["lula", "flavio", "cury", "renan", "caiado", "zema", "outros"]
CAND_LABEL = {"lula": "Lula", "flavio": "Flávio Bolsonaro", "cury": "Augusto Cury", "renan": "Renan Santos", "caiado": "Ronaldo Caiado",
"zema": "Romeu Zema", "outros": "Outros"}
p1 = pd.read_csv(DATA + "pesquisas_1turno_2026.csv", parse_dates=["campo_fim"])
p1 = p1[(p1.campo_fim <= HOJE)].sort_values("campo_fim").reset_index(drop=True)
# Ondas em que só Lula e Flávio foram publicados: os demais candidatos são imputados (última onda completa do mesmo instituto ou,
# na falta, média recente entre institutos) apenas para calcular os válidos; o candidato imputado NÃO entra no seu próprio ajuste.
_terc = ["cury", "renan", "caiado", "zema", "outros"]
_obs = {c_: p1[c_].notna().values.copy() for c_ in CANDS} # máscara antes de qualquer imputação (inclusive de "outros")
p1["imputada"] = p1[["cury", "renan", "caiado", "zema"]].isna().any(axis=1)
for i_, r_ in p1.iterrows():
for c_ in _terc:
if pd.isna(r_[c_]):
prev = p1[(p1.instituto == r_.instituto) & (p1.index < i_) & p1[c_].notna()]
ref = p1[(p1.index < i_) & p1[c_].notna()].tail(8)
p1.loc[i_, c_] = float(prev[c_].iloc[-1]) if len(prev) else (float(ref[c_].mean()) if len(ref) else 0.0)
p1["validos"] = p1[CANDS].sum(axis=1)
p1["n_valid"] = np.minimum(p1.n, N_MAX_EFETIVO) * p1.validos / 100.0 # teto de n: amostras gigantes (Veritá 40.500) não dominam
for c_ in CANDS: p1[c_ + "_v"] = 100 * p1[c_] / p1.validos
p1["dia"] = (p1.campo_fim - T0).dt.days.astype(int)
p1["margem_v"] = p1.lula_v - p1.flavio_v
d0_1t = int(p1.dia.min())
JANELA_MENORES = 28 # dias: o nível dos candidatos menores usa só as ondas recentes (depois da quebra de Cury no início de setembro)
def _fit_cand(c_, sig_rw_fixo=None, desde=None):
m_ = _obs[c_] & ((p1.campo_fim >= desde).values if desde is not None else True)
sub_ = p1[m_]
pv = sub_[c_ + "_v"].values / 100.0
se2c = np.maximum(pv * (1 - pv) / sub_.n_valid.values, 1e-6) * 1e4
d0_ = int(sub_.dia.min())
fit_ = ajustar(sub_[c_ + "_v"].values, (sub_.dia - d0_).values, se2c, sub_.instituto.values, set(ANCORA), Q_ANCORA,
n_days=t_now - d0_ + 1, tau_h=1.2, sigma_floor=0.6, sig_rw_fixo=sig_rw_fixo)
fit_["d0"] = d0_
return fit_
fit1 = {c_: _fit_cand(c_) for c_ in CANDS}
# Candidatos menores: σ_rw comum (mediana das estimativas individuais). Com σ próprio, uma quebra estrutural (Cury: 1–2% → 10% → 3%)
# leva o passeio de um candidato a ~1 p.p./dia e o nível passa a copiar a última pesquisa, o que fazia o 3º lugar oscilar a cada onda.
MENORES = ["cury", "renan", "caiado", "zema", "outros"]
SIG_RW_MENORES_LIVRE = {c_: round(float(fit1[c_]["sig_rw"]), 3) for c_ in MENORES}
SIG_RW_MENORES = float(np.median(list(SIG_RW_MENORES_LIVRE.values())))
for c_ in MENORES:
fit1[c_] = _fit_cand(c_, SIG_RW_MENORES, desde=min(HOJE, T1) - pd.Timedelta(days=JANELA_MENORES)) # depois de 4/out não há pesquisa de 1º turno: a janela fica ancorada em T1
share_now = {c_: float(fit1[c_]["x"][t_now - fit1[c_]["d0"]]) for c_ in CANDS}
se_share = {c_: math.sqrt(float(fit1[c_]["P"][t_now - fit1[c_]["d0"]])) for c_ in CANDS}
# Lula e Flávio (auditoria de 3/out): em vez de duas séries independentes, o espaço de estados é ajustado na MARGEM Lula − Flávio
# (como no 2º turno: o efeito de casa e o viés histórico são da margem) e na SOMA dos dois (quanto sobra para os demais).
# Lula = (soma + margem)/2, Flávio = (soma − margem)/2. Duas séries independentes ignoravam que os dois disputam o mesmo voto.
_pl, _pf = p1.lula_v/100.0, p1.flavio_v/100.0
p1["soma_v"] = p1.lula_v + p1.flavio_v
def _fit_par(col_, se2_, tau_, piso_, sub_=None):
sub_ = p1 if sub_ is None else sub_
f_ = ajustar(sub_[col_].values, (sub_.dia - d0_1t).values, se2_[sub_.index.values], sub_.instituto.values, set(ANCORA), Q_ANCORA,
n_days=t_now - d0_1t + 1, tau_h=tau_, sigma_floor=piso_)
return f_
_se2_m1 = ((_pl + _pf - (_pl - _pf)**2) / p1.n_valid * 1e4).values
_se2_s1 = (np.maximum((_pl + _pf)*(1 - _pl - _pf), 1e-4) / p1.n_valid * 1e4).values
fit_m1 = _fit_par("margem_v", _se2_m1, 1.5, 0.8)
fit_s1 = _fit_par("soma_v", _se2_s1, 1.2, 0.6)
_m1, _s1 = float(fit_m1["x"][-1]), float(fit_s1["x"][-1])
SE_M1 = math.sqrt(float(fit_m1["P"][-1]) + fit_m1["var_ancora"]); SE_S1 = math.sqrt(float(fit_s1["P"][-1]) + fit_s1["var_ancora"])
share_now["lula"], share_now["flavio"] = (_s1 + _m1)/2, (_s1 - _m1)/2
se_share["lula"] = se_share["flavio"] = math.sqrt(SE_M1**2 + SE_S1**2)/2
_norm = sum(share_now.values())
share_now = {c_: 100 * v / _norm for c_, v in share_now.items()} # renormaliza (compositional)
house_1t = {c_: fit1[c_]["house"] for c_ in CANDS}
sig_rw_1t = float(np.median([fit1[c_]["sig_rw"] for c_ in CANDS]))
# última pesquisa por instituto (totais e válidos) — para a tabela do painel
last1 = p1.sort_values("campo_fim").groupby("instituto").tail(1).sort_values("campo_fim", ascending=False)
# agregado em totais (para o modelo de transferência): brancos/indecisos = média ponderada por recência das últimas ondas
_w1 = np.exp(-np.log(2) * (HOJE - last1.campo_fim).dt.days / 14.0)
agg1 = {c_: float((last1[c_] * _w1).sum() / _w1[last1[c_].notna()].sum()) for c_ in CANDS + ["branco_nulo", "indecisos"]} # NaN fora do numerador e do denominador
_tot = sum(agg1[c_] for c_ in CANDS)
_resto = 100 - agg1["branco_nulo"] - agg1["indecisos"]
agg1.update({c_: share_now[c_] * _resto / 100 for c_ in CANDS}) # totais coerentes com o nível de hoje em válidos
agg1["L"], agg1["F"], agg1["branco"], agg1["indec"] = agg1["lula"], agg1["flavio"], agg1["branco_nulo"], agg1["indecisos"]
if POS_1T:
# Depois do 1º turno o modelo de transferência parte do RESULTADO OFICIAL (votos válidos), não mais do agregado de pesquisas.
_rv1 = {c_: float(RESULTADO_1T.get(c_, np.nan)) for c_ in CANDS}
_conh = sum(v for v in _rv1.values() if np.isfinite(v))
_rv1 = {c_: (v if np.isfinite(v) else (max(100 - _conh, 0.0) if c_ == "outros" else 0.0)) for c_, v in _rv1.items()}
agg1.update({c_: _rv1[c_] * _resto / 100 for c_ in CANDS})
agg1["L"], agg1["F"] = agg1["lula"], agg1["flavio"]
for k_, v_ in [("Cury", "cury"), ("Renan", "renan"), ("Caiado", "caiado"), ("Zema", "zema")]:
agg1[k_] = agg1[v_]
L_valid_now = share_now["lula"]; gap_valid = 50.0 - L_valid_now
# matriz de transferência declarada (2ª opção, Quaest 3–6/set/2026): % dos eleitores de cada candidato que migram p/ (Flávio, Lula)
# Lida de transferencia_2turno.csv (instituto, campo_fim, origem, pct_flavio, pct_lula). Havendo ondas com campo depois do 1º turno,
# só elas entram (média por origem); antes disso vale a 2ª opção declarada.
_tr = pd.read_csv(DATA + "transferencia_2turno.csv", parse_dates=["campo_fim"])
_tr = _tr[_tr.campo_fim <= HOJE]
if (_tr.campo_fim > T1).any(): _tr = _tr[_tr.campo_fim > T1]
TRANSF_FONTE = "; ".join(sorted(set(f"{r_.instituto} {r_.campo_fim:%d/%m}" for r_ in _tr.itertuples())))
transf = {k_: (float(g_.pct_flavio.mean())/100, float(g_.pct_lula.mean())/100) for k_, g_ in _tr.groupby("origem")}
assert set(transf) == {"cury", "renan", "caiado", "zema", "outros"}, "transferencia_2turno.csv precisa cobrir as cinco origens"
share_lula_3via = {k_: transf[k_][1]/(transf[k_][0]+transf[k_][1]) for k_ in transf}
T3 = {k_: share_now[k_] for k_ in transf}; T3_total = sum(T3.values())
# Simulação do 1º turno (200 mil): nível + incerteza + deriva até 4/out + colapso tardio da 3ª via (voto útil) + viés histórico,
# renormalizado a 100 (compositional). O viés entra na margem Lula−Flávio: −b/2 em Lula, +b/2 em Flávio.
M1 = 200_000
r1 = np.random.default_rng(41)
dias_1t = max((T1 - HOJE).days, 0)
sim1 = {}
_m_sim = r1.normal(share_now["lula"] - share_now["flavio"], math.sqrt(SE_M1**2 + dias_1t * fit_m1["sig_rw"]**2), M1)
_s_sim = r1.normal(share_now["lula"] + share_now["flavio"], math.sqrt(SE_S1**2 + dias_1t * fit_s1["sig_rw"]**2), M1)
sim1["lula"], sim1["flavio"] = (_s_sim + _m_sim)/2, (_s_sim - _m_sim)/2
for c_ in CANDS:
_sig_dia = fit1[c_]["sig_rw"]
if c_ in ("lula", "flavio"):
continue
else:
# menores: incerteza extra de reta final proporcional ao tamanho (1,5 p.p. para quem tem 5%), sorteada em escala
# log-normal com a mesma média e o mesmo desvio, para não gerar parcelas negativas nem exigir corte e renormalização
_sd = math.sqrt(se_share[c_]**2 + dias_1t * _sig_dia**2 + (1.5*math.sqrt(max(share_now[c_], 0.1)/5.0))**2)
_mu = max(share_now[c_], 0.05); _s = math.sqrt(math.log(1 + (_sd/_mu)**2))
sim1[c_] = _mu * np.exp(r1.normal(-_s**2/2, _s, M1))
colapso = beta_dist.rvs(2, 8, size=M1, random_state=r1) # fração da 3ª via que migra na reta final (média 0,20)
mig_l = np.zeros(M1); mig_f = np.zeros(M1)
for k_ in transf:
m_ = sim1[k_] * colapso
sim1[k_] = sim1[k_] - m_
mig_l += m_ * share_lula_3via[k_]; mig_f += m_ * (1 - share_lula_3via[k_])
# O viés histórico do 1º turno compara as pesquisas finais com o resultado e por isso JÁ inclui a migração tardia da 3ª via
# (2014: Marina → Aécio; 2022: Ciro/Tebet → Bolsonaro). Para não contar duas vezes, o colapso redistribui votos com efeito
# médio nulo na margem Lula − Flávio: mantém a dispersão (quanto e para quem migra varia), mas o deslocamento médio fica no viés.
_desloc_colapso = float((mig_l - mig_f).mean())
mig_l -= _desloc_colapso/2; mig_f += _desloc_colapso/2
b1 = BIAS_1T_MEAN + BIAS_1T_SD*_t_unit(r1, M1) if not POS_1T else np.zeros(M1)
sim1["lula"] = sim1["lula"] + mig_l - b1/2
sim1["flavio"] = sim1["flavio"] + mig_f + b1/2
for c_ in ("lula", "flavio"): sim1[c_] = np.clip(sim1[c_], 0.1, None)
_tot1 = sum(sim1[c_] for c_ in CANDS)
for c_ in CANDS: sim1[c_] = 100 * sim1[c_] / _tot1
lula_valid_mc = sim1["lula"]
margem_1t_mc = sim1["lula"] - sim1["flavio"]
_mat = np.column_stack([sim1[c_] for c_ in CANDS])
_rank = np.argsort(-_mat, axis=1)
p_1t = float((lula_valid_mc > 50).mean())
p_flavio_1t = float((sim1["flavio"] > 50).mean())
P_FLAVIO_TOP2_VOTOS = float(((_rank[:, 0] == 1) | (_rank[:, 1] == 1)).mean())
p_lula_lidera_1t = float((margem_1t_mc > 0).mean())
# 3º lugar entre candidatos (o agregado "outros" soma vários nomes e não concorre a posição)
_cn = [c_ for c_ in CANDS if c_ != "outros"]
_rank_c = np.argsort(-np.column_stack([sim1[c_] for c_ in _cn]), axis=1)
p_terceiro = {c_: float((_rank_c[:, 2] == i_).mean()) for i_, c_ in enumerate(_cn) if c_ not in ("lula", "flavio")}
q_1t = np.percentile(lula_valid_mc, [5, 25, 50, 75, 95])
q_margem_1t = np.percentile(margem_1t_mc, [5, 10, 25, 50, 75, 90, 95])
q_share = {c_: [round(float(v), 1) for v in np.percentile(sim1[c_], [10, 50, 90])] for c_ in CANDS}
# sensibilidade do 1º turno ao viés (0 / prior / histórico cheio)
_r_sens = np.random.default_rng(43)
def _p1t_sens(bm):
# troca o viés sorteado por outro com média alternativa e a mesma incerteza (a linha "prior" reproduz a manchete)
mm = margem_1t_mc + b1 - (bm + BIAS_1T_SD*_t_unit(_r_sens, M1) if bm else 0.0)
return {"p_lula_lidera": round(float((mm > 0).mean()), 3), "margem_mediana": round(float(np.median(mm)), 1)}
sens_1t = {"sem viés": _p1t_sens(0.0), f"prior ({BIAS_1T_MEAN:+.1f})": _p1t_sens(BIAS_1T_MEAN), f"histórico cheio ({BIAS_1T_HIST:+.1f})": _p1t_sens(BIAS_1T_HIST)}
print(f"[1º turno] {len(p1)} pesquisas | válidos hoje: " + ", ".join(f"{CAND_LABEL[c_]} {share_now[c_]:.1f}" for c_ in CANDS)
+ f" | viés prior {BIAS_1T_MEAN:+.1f}±{BIAS_1T_SD} | P(Lula>50%)={p_1t:.3f} P(Flávio top-2)={P_FLAVIO_TOP2_VOTOS:.3f} P(Lula lidera)={p_lula_lidera_1t:.3f}")
def estrutural_2t(L=agg1["L"], F=agg1["F"], terceiros=None, indec=agg1["indec"]+agg1["branco"],
mult_lula=1.0, mult_flavio=1.0, split_indec=0.5, decis_indec=0.55, abst_dif=0.0):
terceiros = terceiros or {k_: agg1[k_] for k_ in transf}
l, f = L, F
for k_, share in terceiros.items():
pf, pl = transf[k_]
l += share * min(pl*mult_lula, 1-pf*mult_flavio)
f += share * pf*mult_flavio
pool = indec * decis_indec
l += pool*split_indec; f += pool*(1-split_indec)
l -= abst_dif
return 100*(l-f)/(l+f), l, f
# Nível estrutural pós-1º turno: resultado oficial + transferência. Nos primeiros dias depois de 4/out é a melhor informação que
# existe (voto contado, sem erro de pesquisa); é combinado ao nível das pesquisas por precisão e perde peso sozinho conforme as
# pesquisas pós-1º turno se acumulam. O desvio cobre a incerteza da transferência declarada e da mudança de comparecimento.
# É uma foto de 4/out: como medida do nível de HOJE, à incerteza da transferência soma-se a deriva do eleitorado desde o 1º turno
# (SIG_ENTRE_DIA por dia). Sem essa parcela o peso do voto contado ficava preso em ~50% independentemente de quantas pesquisas
# novas houvesse (auditoria de 18/out), ao contrário do que o parágrafo acima promete e do que o MCMC faz (observação no dia 5/out).
SD_ESTRUT = 2.5
SD_ESTRUT_HOJE = SD_ESTRUT
NIVEL_ESTRUT = float(estrutural_2t()[0]) if POS_1T else None
W_ESTRUT = 0.0
level_pesq, se_pesq = level_final, se_final
# Fração do nível de hoje que vem de pesquisas de 2º turno feitas DEPOIS do 1º turno (revisão de 4/out, com o resultado na mão).
# Depois da quebra de SD_QUEBRA_1T no passeio, o filtro pesa as ondas novas por precisão contra o nível antigo com variância
# P + 6²; essa é a mesma conta, feita fora do filtro, para saber qual desconto de viés vale para o nível.
F_POS1T = 0.0
if POS_1T:
_wpos = float(df.loc[df.campo_fim > T1, "w_precisao"].sum())
F_POS1T = _wpos / (_wpos + 1.0/(se_pesq**2 + SD_QUEBRA_1T**2)) if _wpos > 0 else 0.0
N_POS1T = int((df.campo_fim > T1).sum()) if POS_1T else 0
RISCO_CANDIDATURA = 0.0 if HOJE >= T2 else 0.01 # com as urnas abertas não há mais substituição de candidato
P_FLAVIO_2T_MERC = 0.88 # referência histórica (v24): mercado "Flávio em 2º lugar"
P_FLAVIO_2T = ((1.0 if FLAVIO_TOP2_REAL else 0.0) if POS_1T else P_FLAVIO_TOP2_VOTOS) * (1 - RISCO_CANDIDATURA)
# ----------------------------------------------------------------------------
# 4. MONTE CARLO — 2º TURNO
# ----------------------------------------------------------------------------
N = 400_000
dias = max((T2 - HOJE).days, 0)
dias_ate_t1, dias_t1_t2 = max((T1 - HOJE).days, 0), max((T2 - max(T1, HOJE)).days, 0)
# Deriva em duas fases: até 4/out, σ_rw estimado; entre turnos, σ calibrado no movimento observado em 2014/18/22 (descontado o
# erro de pesquisa) e encolhido 50% para σ_rw, porque são só três ciclos.
SIG_ENTRE_DIA = math.sqrt(0.5*SIG_ENTRE_DIA_BRUTO**2 + 0.5*SIG_RW_KF**2)
SD_DIARIO = SIG_RW_KF
MULT_ENTRE_TURNOS = round(SIG_ENTRE_DIA / SIG_RW_KF, 2)
sd_deriva = math.sqrt(dias_ate_t1*SIG_RW_KF**2 + dias_t1_t2*SIG_ENTRE_DIA**2)
sd_deriva_antigo = 0.55*math.sqrt(max(dias, 0))
# Média da deriva (só na variante com momentum): metade do agregado das diferenças intra-instituto, em p.p./dia, vezes os dias que faltam.
MU_DERIVA, SD_MU_DERIVA, MOM = 0.0, 0.0, None
if MOMENTUM and POS_1T:
import mudanca_2turno
MOM = mudanca_2turno.resumo(DATA, hoje=HOJE)
if MOM:
MU_DERIVA = ENCOLHE_MOMENTUM * MOM["m_dia"] * dias_t1_t2
SD_MU_DERIVA = ENCOLHE_MOMENTUM * max(MOM["se_dia"], SD_MOMENTUM_PISO) * dias_t1_t2 # o encolhimento vale para o erro também
BIAS_APLICADO = BIAS_MEAN
BIAS_SD_FINAL = BIAS_SD
# Antes do 1º turno, as pesquisas de 2º turno ainda não passaram pelo resultado do 1º turno. O desconto de BIAS_MEAN vem da rodada
# final (véspera do 2º turno) e subestima o erro das rodadas feitas antes do 1º turno: sem ajuste, o 2º turno partia das pesquisas
# de hoje com desconto de ~1 ponto, enquanto o 1º turno, com as mesmas pesquisas, recebia ~3,5, e as duas previsões não
# conversavam (a tabela "1º turno → 2º turno" partia de outra margem). Regra até 4/out: o 2º turno recebe o mesmo desconto do
# 1º turno (BIAS_1T_MEAN ± BIAS_1T_SD), porque um erro na margem do 1º turno se transmite quase um para um ao ponto de partida do
# 2º (inclinação da tabela 1º → 2º turno ≈ 1). O histórico das rodadas de 2º turno publicadas antes do 1º turno
# (vies_2t_pre_1turno.csv: 2014, 2018, 2022) tem erros ainda maiores (+3 a +16); ele fica como referência, não como regra,
# porque 2022 sozinho domina a média e porque em 2026 a distância entre 1º e 2º turno nas pesquisas já é pequena.
BIAS_PRE1T_HIST, BIAS_PRE1T_CICLOS = None, {}
_pre_path = DATA + "vies_2t_pre_1turno.csv"
if os.path.exists(_pre_path):
_pre = pd.read_csv(_pre_path)
BIAS_PRE1T_CICLOS = _pre.groupby("ciclo").erro_margem.mean().to_dict()
_pp = {c_: 0.5 ** ((2022 - c_) / MEIA_VIDA_CICLOS) for c_ in BIAS_PRE1T_CICLOS}
BIAS_PRE1T_HIST = round(sum(BIAS_PRE1T_CICLOS[c_]*_pp[c_] for c_ in BIAS_PRE1T_CICLOS) / sum(_pp.values()), 2)
BIAS_PRE1T_MEAN, BIAS_PRE1T_SD = BIAS_1T_MEAN, round(max(BIAS_SD, BIAS_1T_SD), 2)
# Auditoria de 3/out: a troca de regime dependia só da data (em 4/out o desconto caía de +3,5 para +0,95 sem resultado nem
# pesquisa nova). Agora depende do resultado: enquanto resultado_1turno.csv estiver vazio vale o desconto pré-1º turno. Com o
# resultado, o desconto das pesquisas de 2º turno é o histórico das rodadas finais mais uma fração do erro COMUM observado no 1º
# turno (nos três ciclos com dados, o erro do 2º turno acompanhou o do 1º com inclinação ≈ 0,25), e só incide sobre a parcela
# do nível que vem de pesquisas (a parcela estrutural vem de voto contado).
# Revisão de 22/out (v56): RHO_1T_2T passou de 0,25 para 0. O erro de 2026 no 1º turno teve a assinatura de voto útil: a média
# final acertou Lula (45,8 contra 45,16) e errou Flávio (43,1 contra 47,03) pelo que os demais perderam (11,0 contra 7,8). Esse
# mecanismo não existe num 2º turno com dois nomes; o erro próprio de 2º turno segue no desconto das rodadas finais (BIAS_MEAN).
# Com 0,25, o 2º turno herdava ~0,9 ponto (1,19 × a parcela de pesquisas) e a chance de Lula era 12,8% (ensemble 15,0%) em 22/out.
RHO_1T_2T, BIAS_POS1T_SD = 0.0, 2.5
# Revisão de 4/out (com o resultado): a regra acima vale para pesquisas feitas DEPOIS do 1º turno. As ondas de 2º turno feitas
# antes dele são as mesmas que, no 1º turno, erraram a margem em ERRO_COMUM_1T; antes do resultado elas recebiam o desconto
# suposto do 1º turno (BIAS_PRE1T_MEAN), e agora recebem o erro observado. Sem isso, na manhã de 5/out, sem nenhuma pesquisa nova,
# o desconto caía de +3,4 para +2,0 e a chance de Lula subia justamente no dia em que o resultado mostrou o erro contra ele.
# O desconto do nível é a média dos dois, pesada pela fração do nível que vem de ondas pós-1º turno (F_POS1T).
# A combinação com o nível estrutural (voto contado) usa o erro TOTAL do nível das pesquisas (amostral + sistemático): antes
# usava só o amostral (± 0,7 contra ± 2,5) e o resultado oficial pesava 8%.
BIAS_PRE1T_OBS = BIAS_POS_REGRA = None
if not POS_1T:
BIAS_APLICADO, BIAS_SD = BIAS_PRE1T_MEAN, BIAS_PRE1T_SD
else:
BIAS_PRE1T_OBS = float(ERRO_COMUM_1T if ERRO_COMUM_1T is not None else BIAS_PRE1T_MEAN)
BIAS_POS_REGRA = BIAS_MEAN + RHO_1T_2T*(ERRO_COMUM_1T or 0.0)
_b_pesq = F_POS1T*BIAS_POS_REGRA + (1 - F_POS1T)*BIAS_PRE1T_OBS
_var_pesq = se_pesq**2 + BIAS_POS1T_SD**2
_dias_desde_t1 = max((HOJE - T1).days, 0)
SD_ESTRUT_HOJE = math.sqrt(SD_ESTRUT**2 + _dias_desde_t1 * SIG_ENTRE_DIA**2) # foto de 4/out + deriva até hoje
W_ESTRUT = float(_var_pesq / (_var_pesq + SD_ESTRUT_HOJE**2))
level_final = (1 - W_ESTRUT)*level_pesq + W_ESTRUT*NIVEL_ESTRUT
se_final = math.sqrt((1 - W_ESTRUT)**2*se_pesq**2 + W_ESTRUT**2*SD_ESTRUT_HOJE**2)
BIAS_APLICADO = round((1 - W_ESTRUT)*_b_pesq, 2)
BIAS_SD = round((1 - W_ESTRUT)*BIAS_POS1T_SD, 2)
if SO_POS1T and not POS1T_COM_ESTRUT:
# Variante: nível = só as pesquisas pós-1º turno, sem parcela de voto contado; todo o desconto é a regra pós-1º turno.
F_POS1T, W_ESTRUT = 1.0, 0.0
level_final, se_final = level_pesq, se_pesq
BIAS_APLICADO, BIAS_SD = round(BIAS_POS_REGRA, 2), round(BIAS_POS1T_SD, 2)
SIG_REGIAO = 2.5
if REGIAO_UF:
PESO_REGIAO = {r_: sum(PESO_UF[u_] for u_ in PESO_UF if REGIAO_UF[u_] == r_) for r_ in sorted(set(REGIAO_UF.values()))}
else:
PESO_REGIAO = {"N": 0.085, "NE": 0.265, "CO": 0.075, "SE": 0.43, "S": 0.145}
# Choques regionais: retirados da simulação na auditoria de 3/out (ver simular); as constantes ficam só para o histórico do painel.
SD_RUIDO = math.sqrt(sum((SIG_REGIAO*w_)**2 for w_ in PESO_REGIAO.values()))
def simular(level, bias_mean, n=N, seed=1, se_level=None, sdd=None, pflav=None, bias_sd=None):
r = np.random.default_rng(seed)
# Deriva e erro sistemático com caudas t de Student (mesmo desvio da versão normal). O sorteio de choques por macrorregião foi
# retirado: somava normais sem alterar nenhuma saída nacional (o erro total já era BIAS_SD).
est = r.normal(level, se_final if se_level is None else se_level, n)
deriva = (sd_deriva if sdd is None else sdd) * _t_unit(r, n)
if SD_MU_DERIVA > 0: # variante com momentum: a deriva tem média (incerta); fora dela nada é sorteado e o fluxo aleatório não muda
deriva = deriva + r.normal(MU_DERIVA, SD_MU_DERIVA, n)
_bsd = BIAS_SD if bias_sd is None else bias_sd
vies = bias_mean + _bsd * _t_unit(r, n)
adv = r.random(n) < (P_FLAVIO_2T if pflav is None else pflav)
est_outro = r.normal(7.0, 4.0, n)
m = np.where(adv, est, est_outro) + deriva - vies
return m, adv
margem_final, adv_flavio = simular(level_final, BIAS_APLICADO)
p_lula = float((margem_final > 0).mean())
p_lula_vs_flavio = float((margem_final[adv_flavio] > 0).mean())
p_empate = float((np.abs(margem_final) < 1.5).mean())
q = np.percentile(margem_final, [5, 10, 25, 50, 75, 90, 95])
var_parts = {"Agregado hoje (amostral+casas)": se_final**2, "Deriva até 25/out": sd_deriva**2,
"Erro sistemático das pesquisas": BIAS_SD**2}
tot_var = sum(var_parts.values())
var_share = {k_: v/tot_var for k_, v in var_parts.items()}
sens = {}
for lab_l, lv in [("nowcast combinado", level_final), ("média ponderada últimas", media_simples_adj)]:
for lab_b, bm in [("viés recalibrado", BIAS_APLICADO), ("sem correção de viés", 0.0)]:
m_, _ = simular(lv, bm, n=200_000, seed=7)
sens[f"{lab_l} | {lab_b}"] = round(float((m_ > 0).mean()), 3)
sens_bias = {f"{bm:+.1f}": round(float((simular(level_final, bm, n=200_000, seed=7)[0] > 0).mean()), 3) for bm in sorted({0.0, BIAS_MEAN, BIAS_APLICADO, 2.1})}
# "O que mudou no modelo (v3)": parte do resultado do modelo anterior (v2, valores do último resultados.json publicado) e liga um
# fator por vez. O nível "v2" é recomposto com a identificação antiga (média de todos os institutos, peso qualidade × nº de ondas).
_wq_all = {i_: peso_qualidade(i_) * int((jan.instituto == i_).sum()) for i_ in jan.instituto.unique()}
_shift_v2 = sum(fit_win["house"][i_]*_wq_all[i_] for i_ in _wq_all) / sum(_wq_all.values()) # média dos efeitos de casa sob a âncora (v2 forçava = 0)
level_v2_like = level_nac + _shift_v2
def _p_cfg(level, se_l, bias, bsd, sdd, pflav):
m_, _ = simular(level, bias, n=200_000, seed=11, se_level=se_l, sdd=sdd, pflav=pflav, bias_sd=bsd)
return round(float((m_ > 0).mean()), 3)
efeitos_fatores = {"Modelo anterior (v2): âncora em todos os institutos, viés +2,1, deriva 4,3": _p_cfg(level_v2_like, se_nac, 2.1, 2.0, 4.3, P_FLAVIO_2T_MERC)}
efeitos_fatores["+ âncora nos institutos calibrados (identificação dos efeitos de casa)"] = _p_cfg(level_nac, se_nac, 2.1, 2.0, 4.3, P_FLAVIO_2T_MERC)
efeitos_fatores[f"+ viés recalibrado ({BIAS_MEAN:+.1f} ± {BIAS_SD_FINAL})"] = _p_cfg(level_nac, se_nac, BIAS_MEAN, BIAS_SD_FINAL, 4.3, P_FLAVIO_2T_MERC)
efeitos_fatores[f"+ deriva em duas fases calibrada ({sd_deriva:.1f})"] = _p_cfg(level_nac, se_nac, BIAS_MEAN, BIAS_SD_FINAL, sd_deriva, P_FLAVIO_2T_MERC)
efeitos_fatores[f"+ P(Flávio no 2º turno) do modelo do 1º turno ({P_FLAVIO_2T:.2f})"] = _p_cfg(level_nac, se_nac, BIAS_MEAN, BIAS_SD_FINAL, sd_deriva, P_FLAVIO_2T)
if estadual["usado"]:
efeitos_fatores["+ pesquisas estaduais"] = _p_cfg(level_final, se_final, BIAS_MEAN, BIAS_SD_FINAL, sd_deriva, P_FLAVIO_2T)
if BIAS_APLICADO != BIAS_MEAN:
efeitos_fatores[f"+ viés das pesquisas de 2º turno anteriores ao 1º turno ({BIAS_APLICADO:+.1f} ± {BIAS_SD})"] = _p_cfg(level_final, se_final, BIAS_APLICADO, BIAS_SD, sd_deriva, P_FLAVIO_2T)
# ENSEMBLE — 75% modelo / 25% mercados / 0% fundamentos quando há preço de mercado válido; senão 100% modelo (a versão até 3/out usava
# 65/25/10 antes do 1º turno)
MERC = json.load(open(DATA + "mercados.json", encoding="utf-8")) # leitura do mercado (valor, data, volume): fonte única para modelo e painel
P_MERC, SD_ALT = float(MERC["p_lula"]), 4.5
# Fundamentos: ótimo+bom do governo no Datafolha durante a campanha, antes do 1º turno (série homogênea; a versão anterior
# misturava a aprovação pessoal de Lula na CNT/Sensus de 2008, 77,7, com outras medidas). Margem = 2º turno do incumbente.
FUND_HIST = [(2006, 47.0, 21.66), (2014, 39.0, 3.28), (2022, 31.0, -1.80)]
FUND_MEDIDA = "Datafolha, durante a campanha"
APROVACAO_ATUAL = {"Datafolha 22–23/set": 34} # mesma medida da série histórica (ótimo + bom)
aprovacao_media = float(np.mean(list(APROVACAO_ATUAL.values())))
_fx = np.array([a_ for _, a_, _ in FUND_HIST]); _fy = np.array([m_ for _, _, m_ in FUND_HIST])
FUND_B, FUND_A = np.polyfit(_fx, _fy, 1)
FUND_EMPATE = -FUND_A/FUND_B
mu_fund = float(FUND_A + FUND_B*aprovacao_media)
P_FUND = float(norm.cdf(mu_fund/SD_ALT))
mu_merc = SD_ALT * norm.ppf(P_MERC)
# Pesos (auditoria de 3/out). Fundamentos: reta com três eleições e desvio preditivo real acima de 8 pontos — a componente dizia
# ~50% e não informava; fica calculada para exibição, com peso 0. Mercado: só entra se o preço for recente (até 5 dias) e, no
# regime pós-1º turno, posterior ao resultado; um preço velho com 25% do peso contaminaria o resultado sem aviso. Um preço lido
# na própria noite de 4/out, depois da apuração, vale se mercados.json trouxer "pos_resultado": true.
_md = pd.Timestamp(MERC["data_iso"]) if MERC.get("data_iso") else None
MERC_VALIDO = bool(_md is not None and (HOJE - _md).days <= 5 and not (POS_1T and _md < T1)
and not (POS_1T and _md == T1 and not MERC.get("pos_resultado")))
W_ENS = {"modelo": 0.75, "mercados": 0.25, "fundamentos": 0.0} if MERC_VALIDO else {"modelo": 1.0, "mercados": 0.0, "fundamentos": 0.0}
r2 = np.random.default_rng(99)
comp = r2.random(N)
ens = np.where(comp < W_ENS["modelo"], margem_final,
np.where(comp < W_ENS["modelo"] + W_ENS["mercados"], r2.normal(mu_merc, SD_ALT, N), r2.normal(mu_fund, SD_ALT, N)))
p_ens = float((ens > 0).mean())
cen = {"Lula por mais de 4 p.p.": float((ens > 4).mean()), "Lula por 0 a 4 p.p.": float(((ens > 0) & (ens <= 4)).mean()),
"Flávio por 0 a 4 p.p.": float(((ens <= 0) & (ens > -4)).mean()), "Flávio por mais de 4 p.p.": float((ens <= -4).mean())}
p_ens_empate = float((np.abs(ens) < 1.5).mean())
q_ens = np.percentile(ens, [10, 50, 90])
def _saida_variante(variante):
"""Saída das variantes do herói: só o que o herói e a metodologia precisam."""
return {
"data_base": str(HOJE.date()), "atualizado_em": pd.Timestamp.now(tz="America/Sao_Paulo").strftime("%Y-%m-%dT%H:%M"), "variante": variante,
"regime_pos_1t": POS_1T, "n_pesquisas": int(len(df)), "n_institutos": int(df.inst_base.nunique()), "n_series_casa": len(insts),
"primeira_pesquisa": str(df.campo_fim.min().date()), "ultima_pesquisa": str(df.campo_fim.max().date()),
"ancora_presente": sorted(set(ANCORA) & set(df.instituto)), "n_ondas_ancora": int(df.instituto.isin(ANCORA).sum()),
"nivel_ajustado_hoje_margem_valid": round(level_final, 2), "se_total": round(se_final, 2),
"nivel_pesquisas": round(level_pesq, 2), "se_pesquisas": round(se_pesq, 2), "var_ancora": round(fit_win["var_ancora"], 3),
"nivel_estrutural": round(NIVEL_ESTRUT, 2), "sd_estrutural": round(SD_ESTRUT_HOJE, 2), "peso_estrutural": round(W_ESTRUT, 3), "f_pos1t": round(F_POS1T, 3), "lambda_casa_pos1t": None,
"bias_aplicado": round(BIAS_APLICADO, 2), "bias_sd_aplicado": round(BIAS_SD, 2), "bias_pos_regra": round(BIAS_POS_REGRA, 2),
"sigma_passeio_estimado": round(SIG_RW_KF, 3), "sigma_passeio_ml": round(SIG_RW_ML, 3), "sd_deriva": round(sd_deriva, 2), "sig_entre_turnos_dia": round(SIG_ENTRE_DIA, 3),
"house_effects": {i: round(house[i], 2) for i in insts}, "sigma_inst": {i: round(sigma_inst[i], 2) for i in insts},
"media_ponderada_ultimas_ajustada": round(media_simples_adj, 2),
"p_lula": round(p_lula, 3), "p_lula_vs_flavio": round(p_lula_vs_flavio, 3), "p_empate_1p5": round(p_empate, 3), "sensibilidade": sens,
"quantis_margem_final": {str(p): round(float(v), 1) for p, v in zip([5, 10, 25, 50, 75, 90, 95], q)},
"p_ensemble": round(p_ens, 3), "p_ensemble_empate": round(p_ens_empate, 3), "pesos_ensemble": W_ENS, "mercado_valido": MERC_VALIDO, "p_mercados": P_MERC,
"cenarios_ensemble": {k_: round(v, 3) for k_, v in cen.items()}, "q_ensemble_10_50_90": [round(float(v), 1) for v in q_ens],
"mu_deriva": round(MU_DERIVA, 2), "sd_mu_deriva": round(SD_MU_DERIVA, 2),
}
if SO_POS1T:
# termina aqui para não tocar nenhum arquivo do modelo principal.
res_pos = _saida_variante("pos1t_estrut" if POS1T_COM_ESTRUT else "so_pos1t")
json.dump(res_pos, open(OUT + ("resultados_pos1t_estrut.json" if POS1T_COM_ESTRUT else "resultados_pos1t.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(f"[Pós-1º turno{' + voto contado' if POS1T_COM_ESTRUT else ''}] {len(df)} pesquisas de {res_pos['n_institutos']} institutos desde {df.campo_fim.min():%d/%m} | nível {level_final:+.2f} ± {se_final:.2f} "
f"(estrutural {NIVEL_ESTRUT:+.2f}, peso {W_ESTRUT:.0%}) | viés {BIAS_APLICADO:+.2f} ± {BIAS_SD} | σ_rw {SIG_RW_KF:.3f} | P(Lula)={p_lula:.3f} ensemble {p_ens:.3f}")
sys.exit(0)
def p_dado_lead(lead):
sd = math.sqrt(BIAS_SD**2 + 0.8**2)
return 1 - norm.cdf(0, loc=lead - BIAS_APLICADO, scale=sd)
leads = np.arange(-2, 9, 1)
tabela_lead = {int(l): round(p_dado_lead(l), 3) for l in leads}
lead_breakeven = BIAS_APLICADO
lead_70 = BIAS_APLICADO + norm.ppf(0.70) * math.sqrt(BIAS_SD**2 + 0.64)
lead_90 = BIAS_APLICADO + norm.ppf(0.90) * math.sqrt(BIAS_SD**2 + 0.64)
# ----------------------------------------------------------------------------
# 4c. MODELO PARALELO POR MCMC (mcmc_2turno.py) — mesmas pesquisas da janela, mesmos priors e as mesmas peças do Monte Carlo acima
# (efeitos de casa ancorados, prior do erro de cada instituto no 1º turno, quebra de 4/out, nível estrutural, desconto de viés por
# regime com caudas t, deriva em duas fases), mas com o posterior completo: σ_rw, σ_entre, σ_p, efeitos de casa, viés e trajetória
# sorteados em conjunto, em vez de fixados em estimativas pontuais. Não altera nenhuma saída do modelo principal (manchete).
# ----------------------------------------------------------------------------
import time as _time
import mcmc_2turno
_t0_mc = _time.time()
_ult = max(t_now, dia(T1) + 1) if POS_1T else t_now # último dia do estado: inclui o dia seguinte ao 1º turno (voto contado)
_mu_obs = (np.where(jan.campo_fim > T1, BIAS_POS_REGRA, BIAS_PRE1T_OBS) if POS_1T else np.full(len(jan), BIAS_PRE1T_MEAN))
_extras = []
if POS_1T:
_extras.append({"dia": dia(T1) + 1 - d0_jan, "valor": NIVEL_ESTRUT, "sd": SD_ESTRUT, "com_vies": False})
if estadual["usado"]:
_extras.append({"dia": t_now - d0_jan, "valor": estadual["nivel"], "sd": estadual["se"], "com_vies": True,
"mu": BIAS_POS_REGRA if POS_1T else BIAS_PRE1T_MEAN})
_fut = np.arange(_ult + 1, dia(T2) + 1)
MC = mcmc_2turno.amostrar(jan.margem_valid.values, (jan.dia - d0_jan).values, jan.se2.values, jan.instituto.values, _ult - d0_jan + 1,
set(ANCORA), Q_ANCORA, mu_obs=_mu_obs, sd_vies=(BIAS_POS1T_SD if POS_1T else BIAS_PRE1T_SD),
prior_h=prior_h_2t, tau_h=tau_prior_h or 1.5, dia_t1=dia(T1) - d0_jan,
sd_quebra=(SD_QUEBRA_1T if POS_1T else 0.0), extras=_extras,
prior_entre=(math.log(SIG_ENTRE_DIA), 0.5), n_fut_rw=int((_fut <= dia(T1)).sum()),
n_fut_entre=int((_fut > dia(T1)).sum()), dia_hoje=t_now - d0_jan, a_h=jan.a_casa.values,
prior_mu_der=(MU_DERIVA, SD_MU_DERIVA))
_S = MC["amostras"]
margem_mcmc = _S["x_final"].ravel()
_rm = np.random.default_rng(5)
_adv_m = _rm.random(len(margem_mcmc)) < P_FLAVIO_2T
margem_mcmc = np.where(_adv_m, margem_mcmc, _rm.normal(7.0, 4.0, len(margem_mcmc)))
p_lula_mcmc = float((margem_mcmc > 0).mean())
q_mcmc = np.percentile(margem_mcmc, [5, 10, 25, 50, 75, 90, 95])
_comp_m = _rm.random(len(margem_mcmc))
ens_mcmc = np.where(_comp_m < W_ENS["modelo"], margem_mcmc,
np.where(_comp_m < W_ENS["modelo"] + W_ENS["mercados"], _rm.normal(mu_merc, SD_ALT, len(margem_mcmc)),
_rm.normal(mu_fund, SD_ALT, len(margem_mcmc))))
def _qs(a_, ps=(10, 50, 90)): return [round(float(v), 2) for v in np.percentile(a_, ps)]
_xh = _S["x_hoje"].ravel()
# variância da margem final do MCMC decomposta como na seção 4 (nível de hoje, deriva até 25/out; o viés já está dentro do nível)
_var_der = float(_S["deriva"].var()); _var_niv = float(_xh.var())
MCMC_CONVERGIU = MC["rhat_max"] < 1.05
mcmc_res = {
"p_lula": round(p_lula_mcmc, 3), "p_lula_vs_flavio": round(float((margem_mcmc[_adv_m] > 0).mean()), 3),
"p_empate_1p5": round(float((np.abs(margem_mcmc) < 1.5).mean()), 3),
"p_ensemble": round(float((ens_mcmc > 0).mean()), 3), "q_ensemble_10_50_90": _qs(ens_mcmc),
"quantis_margem_final": {str(p): round(float(v), 1) for p, v in zip([5, 10, 25, 50, 75, 90, 95], q_mcmc)},
"nivel_hoje": round(float(_xh.mean()), 2), "se_nivel_hoje": round(float(_xh.std()), 2),
"nivel_hoje_q": _qs(_xh), "sd_deriva": round(float(_S["deriva"].std()), 2),
"var_share": {"Nível de hoje (pesquisas, casas, viés, voto contado)": round(_var_niv/(_var_niv + _var_der), 3),
"Deriva até 25/out": round(_var_der/(_var_niv + _var_der), 3)},
"sig_rw": _qs(_S["sig_rw"]), "sig_entre": _qs(_S["sig_entre"]), "beta": _qs(_S["beta"]), "m_sigma_inst": _qs(_S["m_sigp"]),
"house_effects": {p_: _qs(_S["h"][:, :, j_]) for j_, p_ in enumerate(MC["insts"])},
"sigma_inst": {p_: _qs(_S["sig_p"][:, :, j_]) for j_, p_ in enumerate(MC["insts"])},
"trajetoria": {"inicio": str((T0 + pd.Timedelta(days=d0_jan)).date()),
"q10_50_90": [[round(float(v), 2) for v in col] for col in np.percentile(_S["x"].reshape(-1, _S["x"].shape[-1]), [10, 50, 90], axis=0).T]},
"desconto_obs": {"antes_1t": round(float(_mu_obs[(jan.campo_fim <= T1).values].mean()), 2) if (jan.campo_fim <= T1).any() else None,
"depois_1t": (round(float(BIAS_POS_REGRA), 2) if POS_1T else None), "sd_vies": BIAS_POS1T_SD if POS_1T else BIAS_PRE1T_SD},
"obs_extras": [{k_: (round(v, 2) if isinstance(v, float) else v) for k_, v in e_.items()} for e_ in _extras],
"rhat_max": round(MC["rhat_max"], 3), "ess_min": MC["ess_min"], "convergiu": MCMC_CONVERGIU,
"diagnostico": {k_: MC["diagnostico"][k_] for k_ in ["x_hoje", "x_final", "sig_rw", "sig_entre", "beta", "m_sigp"]},
"aceitacao": MC["aceitacao"], "n_cadeias": MC["n_cadeias"], "n_aquec": MC["n_aquec"], "n_amostras": MC["n_amostras"],
"sig_entre_tem_dados": MC["entre_tem_dados"], "segundos": round(_time.time() - _t0_mc, 1),
# comparação com o modelo principal: o nível dele é o das pesquisas (antes do desconto); o "verdadeiro" é nível − viés aplicado
"comparacao": {"p_lula_principal": round(p_lula, 3), "p_ensemble_principal": round(p_ens, 3),
"nivel_verdadeiro_principal": round(level_final - BIAS_APLICADO, 2),
"se_nivel_verdadeiro_principal": round(math.sqrt(se_final**2 + BIAS_SD**2), 2),
"mediana_principal": round(float(q[3]), 1), "sd_deriva_principal": round(sd_deriva, 2),
"sig_rw_principal": round(SIG_RW_KF, 3), "sig_entre_principal": round(SIG_ENTRE_DIA, 3)},
}
print(f"[MCMC] {MC['n_cadeias']}×{MC['n_amostras']} sorteios | hoje {mcmc_res['nivel_hoje']:+.2f} ± {mcmc_res['se_nivel_hoje']:.2f} | "
f"σ_rw {mcmc_res['sig_rw'][1]:.3f} | P(Lula)={p_lula_mcmc:.3f} (principal {p_lula:.3f}) | ensemble {mcmc_res['p_ensemble']:.3f} "
f"(principal {p_ens:.3f}) | R̂ máx {MC['rhat_max']:.3f} ESS mín {MC['ess_min']} | {mcmc_res['segundos']} s")
if not MCMC_CONVERGIU:
print(f"[AVISO] MCMC sem convergência (R̂ máx {MC['rhat_max']:.3f} > 1,05): resultado do modelo paralelo não confiável.")
if MOMENTUM:
# Variante com momentum: sai depois do MCMC (que recebeu a mesma prior de deriva), sem tocar nenhum arquivo do modelo principal.
res_mom = _saida_variante("momentum")
res_mom["momentum"] = ({k_: (round(v_, 4) if isinstance(v_, float) else v_) for k_, v_ in MOM.items() if k_ != "lento"} if MOM else None)
res_mom["encolhimento_momentum"] = ENCOLHE_MOMENTUM; res_mom["sd_momentum_piso_dia"] = SD_MOMENTUM_PISO
res_mom["mcmc"] = {k_: mcmc_res[k_] for k_ in ["p_lula", "p_ensemble", "quantis_margem_final", "q_ensemble_10_50_90", "nivel_hoje", "se_nivel_hoje",
"sd_deriva", "rhat_max", "ess_min", "convergiu", "segundos"]}
res_mom["mcmc"]["mu_der"] = _qs(_S["mu_der"]) if "mu_der" in _S else None
json.dump(res_mom, open(OUT + "resultados_momentum.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
print(f"[Momentum] deriva com média {MU_DERIVA:+.2f} ± {SD_MU_DERIVA:.2f} (agregado {MOM['taxa']:+.2f} ± {MOM['se_taxa']:.2f} p.p./semana, {MOM['n_dif']} diferenças de {MOM['n_inst']} séries, encolhido {ENCOLHE_MOMENTUM:.0%})"
if MOM else "[Momentum] sem diferenças suficientes: deriva com média zero")
print(f"[Momentum] nível {level_final:+.2f} ± {se_final:.2f} | P(Lula)={p_lula:.3f} ensemble {p_ens:.3f} | MCMC {p_lula_mcmc:.3f} ensemble {mcmc_res['p_ensemble']:.3f}")
sys.exit(0)
# ----------------------------------------------------------------------------
# 4b. ABSTENÇÃO — quanto a margem nacional muda se o comparecimento de 2026 se afastar do padrão de 2022.
# Dois canais: (i) geográfico, entre UFs: cada UF pesa no total pelo seu comparecimento, então faltar mais onde um
# candidato lidera reduz o peso dessa UF; (ii) composicional, dentro de cada UF: quem falta é mais pobre e menos
# escolarizado (abstenção de 52% entre analfabetos × 12% no superior, 1T 2022), grupos em que Lula lidera; calibrado em
# K = 0,25 p.p. de margem por p.p. de abstenção (Laureno: +4 p.p. de abstenção ≈ −1 ponto na vantagem de Lula), faixa 0–0,4.
# O padrão de 2022 já está embutido no desconto histórico (as finais são comparadas com resultados que incluem a abstenção
# daquele ano), então os cenários medem só o DESVIO em relação a 2022, sem dupla contagem.
# ----------------------------------------------------------------------------
abst = {"usado": False}
_abst_path = DATA + "abstencao_2022_uf.csv"
if ref_uf is not None and os.path.exists(_abst_path):
K_COMP, K_COMP_LO, K_COMP_HI = 0.25, 0.0, 0.40
au = ref_uf.merge(pd.read_csv(_abst_path), on="uf", how="inner").copy()
_sw = {}
if estadual.get("por_uf") is not None:
_sw = dict(zip(estadual["por_uf"].uf, estadual["por_uf"].swing))
_mu_reg = estadual.get("mu_regiao", {}); _mu_s = estadual.get("mu_swing", 0.0)
au["swing"] = [_sw.get(u_, _mu_reg.get(r_, _mu_s)) for u_, r_ in zip(au.uf, au.regiao)]
au["a0"] = au.abst_2t_2022/100
def _nac(a_vec, marg):
V_ = au.eleitorado*(1 - a_vec)
return float((V_*marg).sum()/V_.sum())
_m_raw = au.m22 + au.swing
_c = level_final - _nac(au.a0, _m_raw) # alinha o nível por UF ao placar nacional do modelo
au["m26"] = _m_raw + _c
base_nac = _nac(au.a0, au.m26)
def cenario(delta_reg=None, delta_uf=None, k=K_COMP):
d_ = np.zeros(len(au))
if delta_reg: d_ = np.array([delta_reg.get(r_, 0.0) for r_ in au.regiao])/100
if delta_uf is not None: d_ = np.asarray(delta_uf)/100
geo = _nac(au.a0 + d_, au.m26) - base_nac
V_ = au.eleitorado*(1 - au.a0)
comp = -k*100*float((V_*d_).sum()/V_.sum()) # média ponderada do aumento de abstenção × K
return geo, comp
_todas = {r_: 1.0 for r_ in au.regiao.unique()}
cen_defs = [
("Abstenção igual à de 2022 (20,6%)", {}, None),
("Sobe para cerca de 22% em todo o país (projeção de analistas)", {r_: 1.4 for r_ in _todas}, None),
("Sobe para cerca de 25% em todo o país (cenário de desinteresse)", {r_: 4.4 for r_ in _todas}, None),
("Nordeste falta mais: +3 p.p. só no NE", {"NE": 3.0}, None),
("Sul e Centro-Oeste faltam mais: +3 p.p. só em S e CO", {"S": 3.0, "CO": 3.0}, None),
("Mobilização no Nordeste: −2 p.p. só no NE", {"NE": -2.0}, None),
("Repete a variação 1º→2º turno de 2022, UF por UF", None, (au.abst_2t_2022 - au.abst_1t_2022).values),
]
_cen_out = []
for nome_, dreg_, duf_ in cen_defs:
geo_, comp_ = cenario(dreg_ or None, duf_)
_, comp_lo = cenario(dreg_ or None, duf_, K_COMP_LO)
_, comp_hi = cenario(dreg_ or None, duf_, K_COMP_HI)
dm_ = geo_ + comp_
m_, _ = simular(level_final + dm_, BIAS_APLICADO, n=200_000, seed=7)
_cen_out.append({"cenario": nome_, "geo": round(geo_, 2), "comp": round(comp_, 2), "total": round(dm_, 2),
"total_faixa": [round(geo_ + comp_lo, 2), round(geo_ + comp_hi, 2)], "p_lula": round(float((m_ > 0).mean()), 3)})
_p_base = _cen_out[0]["p_lula"]
for c_ in _cen_out: c_["dp_lula"] = round(c_["p_lula"] - _p_base, 3)
_geo_ne1, _comp_ne1 = cenario({"NE": 1.0}); _geo_u1, _comp_u1 = cenario({r_: 1.0 for r_ in _todas})
reg_tab = []
for r_, g_ in au.groupby("regiao"):
V_ = g_.eleitorado*(1 - g_.a0)
reg_tab.append({"regiao": r_, "eleitorado_pct": round(100*float(g_.eleitorado.sum()/au.eleitorado.sum()), 1),
"abst_1t_2022": round(float((g_.abst_1t_2022*g_.eleitorado).sum()/g_.eleitorado.sum()), 1),
"abst_2t_2022": round(float((g_.abst_2t_2022*g_.eleitorado).sum()/g_.eleitorado.sum()), 1),
"m22": round(float((V_*g_.m22).sum()/V_.sum()), 1), "m26": round(float((V_*g_.m26).sum()/V_.sum()), 1)})
abst = {"usado": True, "k_comp": K_COMP, "k_faixa": [K_COMP_LO, K_COMP_HI], "base_nac": round(base_nac, 2),
"check_m22": round(_nac(au.a0, au.m22), 2), "abst_base_pond": round(float((au.abst_2t_2022*au.eleitorado).sum()/au.eleitorado.sum()), 2),
"cenarios": _cen_out, "por_regiao": sorted(reg_tab, key=lambda d_: -d_["eleitorado_pct"]),
"por_pp_ne": round(_geo_ne1 + _comp_ne1, 2), "por_pp_nacional": round(_geo_u1 + _comp_u1, 2)}
print(f"[Abstenção] base {base_nac:+.2f} (check 2022 {abst['check_m22']:+.2f}) | +1 p.p. NE: {abst['por_pp_ne']:+.2f} | +1 p.p. nacional: {abst['por_pp_nacional']:+.2f}")
# ----------------------------------------------------------------------------
# 5. MODELO ESTRUTURAL DE TRANSFERÊNCIA (1º → 2º turno) E TORNADO
# ----------------------------------------------------------------------------
base_m, base_l, base_f = estrutural_2t()
tornado = []
def add(nome, lo_kwargs, hi_kwargs, obs):
lo = estrutural_2t(**lo_kwargs)[0] - base_m
hi = estrutural_2t(**hi_kwargs)[0] - base_m
tornado.append({"variavel": nome, "baixo": lo, "alto": hi, "obs": obs})
add("Aprovação do governo (−3 / +3 p.p. → base de Lula)", dict(L=agg1["L"]-2.4), dict(L=agg1["L"]+2.4),
"elasticidade ≈0,8 voto/p.p. de aprovação (Lavareda: aprovação ≈ voto no 2º turno)")
add("Transferência da 3ª via para Lula (−1/3 / +50% vs. Quaest)", dict(mult_lula=0.67), dict(mult_lula=1.5),
"hoje ~2:1 para Flávio; se Cury/Caiado dividissem 50/50, +3 p.p. de margem")
add("Transferência da 3ª via para Flávio (−20% / +15%)", dict(mult_flavio=1.15), dict(mult_flavio=0.80),
"consolidação antipetista vs. dispersão em branco/nulo")
add("Indecisos/brancos que decidem: divisão (35% / 65% Lula)", dict(split_indec=0.35), dict(split_indec=0.65),
"estoque de 10–17%; Atlas: independentes Lula 38 × Flávio 26")
if abst["usado"]:
_tots = [c_["total"] for c_ in abst["cenarios"]]
tornado.append({"variavel": "Abstenção em relação a 2022 (cenários da seção Abstenção)", "baixo": min(_tots), "alto": max(_tots),
"obs": "geográfico + perfil de quem falta (K = 0,25 p.p. por p.p.); de 25% de abstenção a mobilização no NE"})
else:
add("Abstenção diferencial (Lula −1,5 p.p. / 0)", dict(abst_dif=1.5), dict(abst_dif=0.0),
"abstenção 21%→>22%; eleitor de menor renda/NE falta mais (Ibmec)")
add("Base de Flávio (evangélicos/segurança: +2 / −2 p.p.)", dict(F=agg1["F"]+2), dict(F=agg1["F"]-2),
"evangélicos ≈30% do eleitorado; Flávio 62×29 (Datafolha)")
tornado.append({"variavel": f"Erro sistemático das pesquisas ({-(BIAS_APLICADO+BIAS_SD):.0f} / {-(BIAS_APLICADO-BIAS_SD):+.0f} p.p.)",
"baixo": -(BIAS_APLICADO + BIAS_SD), "alto": -(BIAS_APLICADO - BIAS_SD),
"obs": "medição, não causa: pesquisas finais superestimaram o PT em 3 dos 5 últimos 2º turnos"})
tornado_df = pd.DataFrame(tornado)
tornado_df["amplitude"] = (tornado_df.alto - tornado_df.baixo).abs()
tornado_df = tornado_df.sort_values("amplitude", ascending=False)
# ----------------------------------------------------------------------------
# 6. O QUE O 1º TURNO DIRÁ SOBRE O 2º — margem inicial do 2º turno implicada por cada resultado do 1º (transferência declarada)
# ----------------------------------------------------------------------------
def margem_2t_dado_1t(margem_1t):
"""Desloca Lula e Flávio simetricamente até a margem observada e aplica a transferência da 3ª via + indecisos."""
d_ = (margem_1t - (share_now["lula"] - share_now["flavio"])) / 2
L_, F_ = agg1["L"] + d_*_resto/100, agg1["F"] - d_*_resto/100
return estrutural_2t(L=L_, F=F_)[0]
# A tabela parte do resultado oficial do 1º turno (não de pesquisa), então não leva erro de medição: a incerteza é a deriva entre
# os turnos e a da própria transferência declarada (2ª opção), da ordem da amplitude das variáveis de transferência no tornado.
SD_TRANSF = 2.0
_sd_t12 = math.sqrt(max(dias_t1_t2, 0)*SIG_ENTRE_DIA**2 + SD_TRANSF**2)
tabela_1t_2t = [{"margem_1t": m_, "margem_2t_inicial": round(margem_2t_dado_1t(m_), 1),
"p_lula_2t": round(float(1 - norm.cdf(0, loc=margem_2t_dado_1t(m_), scale=_sd_t12)), 3)}
for m_ in [-6, -4, -2, 0, 2, 4, 6, 8, 10]]
hist_1t = [("2006", "Lula", 56.0, 48.6, "Datafolha 12/set: 50% totais (≈56% válidos)"),
("2010", "Dilma", 55.0, 46.9, "Datafolha 21–22/set: 51% totais (≈55% válidos)"),
("2014", "Dilma", 46.0, 41.6, "Datafolha 29–30/set: 40% totais (≈46% válidos)"),
("2018", "Bolsonaro", 33.0, 46.0, "Datafolha 26–28/set: 28% totais (≈33% válidos) — onda antipetista tardia"),
("2022", "Lula", 50.0, 48.4, "Datafolha 1/out: 50% válidos")]
# ----------------------------------------------------------------------------
# 6a. INFLUÊNCIA — quanto cada pesquisa dos últimos 7 dias desloca o nível de hoje (reajuste sem ela). Em séries que terminam hoje, a
# última pesquisa pesa muito; a tabela mostra de onde vem cada movimento do placar.
# ----------------------------------------------------------------------------
_inf = {}
_corte = HOJE - pd.Timedelta(days=7)
for i_ in jan.index[jan.campo_fim > _corte]:
f_ = _ajusta_janela(jan.drop(i_))
_inf[(jan.instituto[i_], str(jan.campo_fim[i_].date()))] = {"d_nivel_2t": round(level_nac - float(f_["x"][-1]), 2)}
if not POS_1T:
for i_ in p1.index[p1.campo_fim > _corte]:
f_ = _fit_par("margem_v", _se2_m1, 1.5, 0.8, p1.drop(i_))
_inf.setdefault((p1.instituto[i_], str(p1.campo_fim[i_].date())), {})["d_margem_1t"] = round(_m1 - float(f_["x"][-1]), 2)
influencia = sorted([{"instituto": k_[0], "campo_fim": k_[1], **v} for k_, v in _inf.items()],
key=lambda d_: -max(abs(d_.get("d_nivel_2t", 0)), abs(d_.get("d_margem_1t", 0))))
# ----------------------------------------------------------------------------
# 6b. DIAGNÓSTICO DE VIÉS E DISPERSÃO POR INSTITUTO (resíduos do modelo único)
# ----------------------------------------------------------------------------
def moe95(n_): return 1.96 * math.sqrt(0.25 / n_) * 100
df["x_tend"] = [float(fit_full["x"][d_]) for d_ in df.dia]
df["resid_spline"] = df.margem_valid - df.x_tend # desvio da tendência comum (casa + ruído)
df["resid_full"] = df.resid_spline - df.house_aplicado # após remover o efeito de casa (o aplicado: λ·h depois do 1º turno)
df["moe_declarada"] = df.n.map(moe95)
df["moe_margem_95"] = 1.96 * df.se_margem_teorico
diag = []
for i_ in insts:
sub = df[df.instituto == i_]
n_i = len(sub)
# variância posterior do efeito de casa vinda do próprio ajuste (conta a_casa = λ nas ondas pós-1º turno, que identificam só λ·h);
# a soma simples das precisões superestimava a informação dessas ondas
_hv = (fit_win.get("house_var") or {}).get(i_, (fit_full.get("house_var") or {}).get(i_))
se_h = math.sqrt(_hv) if _hv else math.sqrt(1.0 / (sub.w_precisao.sum() + 1/1.5**2))
rms_tend = float(np.sqrt((sub.resid_spline ** 2).mean()))
rms_full = float(np.sqrt((sub.resid_full ** 2).mean()))
se_teo = float(sub.se_margem_teorico.mean())
diag.append({"instituto": i_, "n_ondas": int(n_i), "n_medio": int(sub.n.mean()),
"metodo": sub.sort_values("campo_fim").metodo.iloc[-1], "contratante": sub.sort_values("campo_fim").contratante.iloc[-1],
"house": round(house[i_], 2), "house_se": round(se_h, 2), "house_z": round(house[i_] / se_h, 1),
"sigma_extra": round(sigma_inst[i_], 2), "ancora": i_ in ANCORA,
"rms_vs_tendencia": round(rms_tend, 2), "rms_idiossincratico": round(rms_full, 2),
"moe_declarada": round(float(sub.moe_declarada.mean()), 2), "moe_margem_95": round(float(sub.moe_margem_95.mean()), 2),
"se_margem_teorico": round(se_teo, 2), "razao_erro_teorico": round(rms_tend / se_teo, 2),
"mae_hist": round(float(mae_inst[i_]), 2) if i_ in mae_inst.index else None,
"n_obs_hist": int(n_inst[i_]) if i_ in n_inst.index else 0, "peso_qualidade": round(peso_qualidade(i_), 2),
"ultima_onda": sub.campo_fim.max().strftime("%d/%m"), "diagnostico_confiavel": bool(n_i >= 3)})
diag = sorted(diag, key=lambda d: -d["n_ondas"])
rms_tend_global = float(np.sqrt((df.resid_spline ** 2).mean()))
se_teo_global = float(df.se_margem_teorico.mean())
moe_decl_global = float(df.moe_declarada.mean()); moe_margem_global = float(df.moe_margem_95.mean())
razao_global = rms_tend_global / se_teo_global
sigma2 = float((df.resid_full**2).mean())
recentes = df[df.idade_dias <= 21]
# Teste de manada (auditoria de 3/out): compara a dispersão dos RESÍDUOS (margem − tendência − efeito de casa) com o erro amostral.
# A versão anterior usava as margens brutas, que carregam as diferenças permanentes entre institutos: razão acima de 1 era certa
# e não dizia nada. Razão bem abaixo de 1 nos resíduos indicaria pesquisas mais parecidas entre si do que o acaso permite.
sd_bruto_recente = float(recentes.margem_valid.std(ddof=1))
sd_obs_recente = float(np.sqrt((recentes.resid_full ** 2).mean())); sd_esp_recente = float(np.sqrt((recentes.se_margem_teorico ** 2).mean()))
herding_ratio = sd_obs_recente / sd_esp_recente
# ----------------------------------------------------------------------------
# 5b. MIGRAÇÃO — ausentes, terceira via e brancos/nulos por cadeia de Markov + Monte Carlo (migracao_markov.py). Análise paralela e
# condicional: parte do resultado oficial por UF e da 2ª opção declarada, sorteia a matriz de transição (Dirichlet) e os fluxos
# brutos de comparecimento, simula dia a dia a campanha de conversa pessoal do 2º turno (guia "Três nomes, três semanas") e a
# resposta de Flávio, e compara com as metas do guia. Não altera nenhuma saída do modelo principal (manchete).
# ----------------------------------------------------------------------------
import migracao_markov
migracao_res = {"usado": False, "motivo": "só com o resultado oficial do 1º turno por UF"}
if POS_1T and migracao_markov.disponivel(DATA):
_ini_cad = str((_tr.campo_fim.max() + pd.Timedelta(days=1)).date()) if (_tr.campo_fim > T1).any() else None # 2ª opção pós-1º turno: a cadeia começa depois do campo
migracao_res = migracao_markov.simular(DATA, transf, HOJE, sd_deriva, regiao_uf=REGIAO_UF, transf_fonte=TRANSF_FONTE, inicio_cadeia=_ini_cad,
comparacao={"estrutural_base_margem": round(base_m, 2), "p_lula_principal": round(p_lula, 3), "p_lula_mcmc": round(p_lula_mcmc, 3),
"mediana_principal": round(float(q[3]), 1), "sd_deriva_principal": round(sd_deriva, 2)})
_mg = {c_["id"]: c_ for c_ in migracao_res["cenarios"] if c_["resposta"] == "nenhuma"}
print(f"[Migração] {migracao_res['n_sorteios']} sorteios × {len(migracao_res['cenarios'])} células | sem campanha {_mg['sem']['margem_q']['50']:+.2f} "
f"(P(Lula) com deriva {_mg['sem']['p_lula_com_deriva']:.3f}; principal {p_lula:.3f}) | forte {_mg['forte']['margem_q']['50']:+.2f} "
f"(saldo {_mg['forte']['saldo_frentes']['total']['q50']/1e6:+.2f} mi; P {_mg['forte']['p_lula_com_deriva']:.3f}) | metas do guia: margem "
f"{migracao_res['guia']['margem_se_cumprido']:+.2f}, conversão necessária {migracao_res['guia']['taxa_necessaria']['terceira_via']['razao']:.0f}× a literatura "
f"| checks {'ok' if all(migracao_res['checks'].values()) else 'FALHOU'} | {migracao_res['segundos']} s")
if not all(migracao_res["checks"].values()):
print(f"[AVISO] migração: verificação falhou: {migracao_res['checks']}")
# ----------------------------------------------------------------------------
# 7. SAÍDAS — JSON + CSVs + SVGs
# ----------------------------------------------------------------------------
res = {
"data_base": str(HOJE.date()), "atualizado_em": pd.Timestamp.now(tz="America/Sao_Paulo").strftime("%Y-%m-%dT%H:%M"), "versao_modelo": 3,
"influencia": influencia, "df_erro": DF_ERRO, "mae_rel": {i: round(float(mae_rel[i]), 2) for i in mae_rel.index}, "mae_ref": MAE_REF,
"erro_comum_1t": (None if ERRO_COMUM_1T is None else round(ERRO_COMUM_1T, 2)), "rho_1t_2t": RHO_1T_2T, "sd_quebra_1t": SD_QUEBRA_1T,
"nivel_pesquisas": round(level_pesq, 2), "se_pesquisas": round(se_pesq, 2), "nivel_estrutural": (None if NIVEL_ESTRUT is None else round(NIVEL_ESTRUT, 2)),
"sd_estrutural": round(SD_ESTRUT_HOJE, 2), "sd_estrutural_base": SD_ESTRUT, "peso_estrutural": round(W_ESTRUT, 3), "f_pos1t": round(F_POS1T, 3), "n_pos1t": N_POS1T, "bias_pre1t_obs": None if BIAS_PRE1T_OBS is None else round(BIAS_PRE1T_OBS, 2), "bias_pos_regra": None if BIAS_POS_REGRA is None else round(BIAS_POS_REGRA, 2), "mercado_valido": MERC_VALIDO, "transf_fonte": TRANSF_FONTE,
"transf": {k_: [round(v[0], 2), round(v[1], 2)] for k_, v in transf.items()},
"t1_margem_nivel": round(share_now["lula"] - share_now["flavio"], 2), "t1_margem_se": round(SE_M1, 2), "t1_sig_rw_margem": round(float(fit_m1["sig_rw"]), 3),
"t1_house_margem": {i_: round(v, 2) for i_, v in fit_m1["house"].items()}, "var_ancora": round(fit_win["var_ancora"], 3), "regime_pos_1t": POS_1T, "n_pesquisas": int(len(df)), "n_institutos": int(df.inst_base.nunique()), "n_series_casa": len(insts),
"ancora": ANCORA, "q_ancora": {k_: round(v, 2) for k_, v in Q_ANCORA.items()}, "inicio_janela": str(INICIO_JANELA.date()), "n_pesquisas_janela": int(len(jan)),
"diagnostico_institutos": diag, "rms_vs_tendencia_global": round(rms_tend_global, 2),
"moe_declarada_media": round(moe_decl_global, 2), "moe_margem_95_media": round(moe_margem_global, 2),
"se_margem_teorico_medio": round(se_teo_global, 2), "razao_erro_teorico_global": round(razao_global, 2),
"herding": {"sd_observado": round(sd_obs_recente, 2), "sd_esperado_amostral": round(sd_esp_recente, 2), "razao": round(herding_ratio, 2), "n_ondas_21d": int(len(recentes)), "sd_bruto": round(sd_bruto_recente, 2)},
"validacao_rolling": VALIDACAO,
"nivel_ajustado_hoje_margem_valid": round(level_final, 2), "se_total": round(se_final, 2),
"nivel_pspline": round(level_pspline, 2), "se_pspline": round(se_pspline, 2), "nivel_kalman": round(level_kf, 2), "se_kalman": round(se_kf, 2),
"nivel_nacional_combinado": round(level_nac, 2), "se_nacional_combinado": round(se_nac, 2), "nivel_v2_like": round(level_v2_like, 2), "shift_ancora": round(_shift_v2, 2),
"sigma_passeio_estimado": round(SIG_RW_KF, 3), "sigma_passeio_ml": round(SIG_RW_ML, 3), "sigma_extra_kalman": round(float(np.median(list(sigma_inst.values()))), 2), "sd_diario_usado": round(SD_DIARIO, 3),
"sig_entre_turnos_dia": round(SIG_ENTRE_DIA, 3), "sig_entre_turnos_bruto": round(SIG_ENTRE_DIA_BRUTO, 3), "mult_entre_turnos": MULT_ENTRE_TURNOS, "sd_deriva_antigo": round(sd_deriva_antigo, 2),
"movimento_entre_turnos": mov.to_dict(orient="records"),
"bias_metodo": BIAS_METODO, "bias_n_metodo": {k_: int(v) for k_, v in BIAS_N_METODO.items()}, "bias_aplicado": round(BIAS_APLICADO, 2), "bias_sd_aplicado": round(BIAS_SD, 2), "bias_pre1t_mean": BIAS_PRE1T_MEAN, "bias_pre1t_sd": BIAS_PRE1T_SD, "bias_pre1t_ciclos": {str(k_): round(v, 2) for k_, v in BIAS_PRE1T_CICLOS.items()}, "bias_pre1t_hist": BIAS_PRE1T_HIST, "bias_final_mean": BIAS_MEAN,
"bias_2t_ciclos": {str(k_): round(v, 2) for k_, v in bias_2t_ciclos.items()}, "pesos_ciclo": {str(k_): round(v, 2) for k_, v in pesos_ciclo.items()},
"bias_prior_mean_igual": BIAS_MEAN_IGUAL, "sens_bias": sens_bias,
"bias_1t_ciclos": {str(k_): round(v, 2) for k_, v in bias_1t_ciclos.items()}, "bias_1t_hist": BIAS_1T_HIST, "bias_1t_mean": BIAS_1T_MEAN, "bias_1t_sd": BIAS_1T_SD, "encolhimento_1t": ENCOLHIMENTO_1T,
"efeitos_fatores": efeitos_fatores, "abstencao": abst, "sd_ruido_regional": round(SD_RUIDO, 2), "sig_regiao": SIG_REGIAO, "peso_regiao": {k_: round(v, 3) for k_, v in PESO_REGIAO.items()},
"estadual": ({"usado": estadual["usado"], "motivo": estadual.get("motivo", ""), "nivel": round(estadual["nivel"], 2), "se": round(estadual["se"], 2),
"mu_swing": round(estadual["mu_swing"], 2), "mu_regiao": estadual["mu_regiao"],
"ajuste_medio_tend": round(estadual["ajuste_medio_tend"], 2), "ajuste_medio_casa": round(estadual["ajuste_medio_casa"], 2), "tau": round(estadual["tau"], 2), "cobertura": round(estadual["cobertura"], 3),
"n_pesq": estadual["n_pesq"], "n_uf": estadual["n_uf"], "peso_na_combinacao": round(estadual.get("peso_na_combinacao", 0.0), 3),
"por_uf": estadual["por_uf"][["uf","n_pesq","institutos","ultimo_campo","m22","margem_2026","swing_bruto","swing","peso"]].round(3).to_dict(orient="records")}
if "nivel" in estadual else {"usado": False, "motivo": estadual["motivo"]}),
"p_flavio_2t_usado": round(P_FLAVIO_2T, 3), "sig_rw_menores": round(SIG_RW_MENORES, 3), "sig_rw_menores_livre": SIG_RW_MENORES_LIVRE, "janela_menores_dias": JANELA_MENORES, "desloc_colapso_removido": round(_desloc_colapso, 2), "n_max_efetivo": N_MAX_EFETIVO, "n_efetivo_inst": N_EFETIVO_INST, "lambda_casa_pos1t": LAMBDA_CASA, "p_flavio_top2_votos": round(P_FLAVIO_TOP2_VOTOS, 4), "risco_candidatura": RISCO_CANDIDATURA, "p_flavio_2t_mercado": P_FLAVIO_2T_MERC,
"sigma_resid": round(math.sqrt(sigma2), 2),
"media_ponderada_ultimas_bruta": round(media_simples_bruta, 2), "media_ponderada_ultimas_ajustada": round(media_simples_adj, 2),
"house_effects": {i: round(house[i], 2) for i in insts}, "sigma_inst": {i: round(sigma_inst[i], 2) for i in insts},
"mae_hist": {i: round(float(mae_inst[i]), 2) for i in mae_inst.index}, "bias_hist": {i: round(float(bias_inst[i]), 2) for i in bias_inst.index},
"bias_prior_mean": BIAS_MEAN, "bias_prior_sd": BIAS_SD_FINAL, "bias_prior_sd_final": BIAS_SD_FINAL, "sd_deriva": round(sd_deriva, 2),
"p_lula": round(p_lula, 3), "p_lula_vs_flavio": round(p_lula_vs_flavio, 3), "p_empate_1p5": round(p_empate, 3),
"sensibilidade": sens, "p_ensemble": round(p_ens, 3), "p_ensemble_empate": round(p_ens_empate, 3), "pesos_ensemble": W_ENS,
"cenarios_ensemble": {k_: round(v, 3) for k_, v in cen.items()}, "q_ensemble_10_50_90": [round(float(v), 1) for v in q_ens],
"p_mercados": P_MERC, "mercado": MERC, "p_fundamentos": round(P_FUND, 3), "aprovacao_media_corrente": round(aprovacao_media, 1),
"fund_inclinacao": round(float(FUND_B), 2), "fund_empate": round(float(FUND_EMPATE), 1), "fund_margem_esperada": round(mu_fund, 1),
"fund_hist": [{"ciclo": c_, "aprovacao": a_, "margem": m_} for c_, a_, m_ in FUND_HIST], "fund_medida": FUND_MEDIDA, "aprovacao_atual": APROVACAO_ATUAL,
"quantis_margem_final": {str(p): round(float(v), 1) for p, v in zip([5,10,25,50,75,90,95], q)},
"var_share": {k_: round(v, 3) for k_, v in var_share.items()},
"p_dado_lead": tabela_lead, "lead_breakeven": round(lead_breakeven, 1), "lead_70": round(lead_70, 1), "lead_90": round(lead_90, 1),
"estrutural_base_margem": round(base_m, 2), "tornado": tornado_df.round(2).to_dict(orient="records"),
# 1º turno
"t1_n_pesquisas": int(len(p1)), "t1_n_institutos": int(p1.instituto.nunique()), "t1_sig_rw": round(sig_rw_1t, 3),
"t1_share_valid": {c_: round(v, 1) for c_, v in share_now.items()}, "t1_se_share": {c_: round(v, 2) for c_, v in se_share.items()},
"t1_q_share": q_share, "t1_house": {c_: {i_: round(v, 2) for i_, v in house_1t[c_].items()} for c_ in CANDS},
"t1_agregado": {k_: round(v, 1) for k_, v in agg1.items() if k_ in ["L","F","Cury","Renan","Caiado","Zema","outros","branco","indec"]},
"t1_lula_valid_now": round(L_valid_now, 1), "t1_gap": round(gap_valid, 1),
"t1_por_instituto": [{"instituto": r.instituto, "campo_fim": str(r.campo_fim.date()), "n": int(r.n), **{c_: round(float(getattr(r, c_)), 1) for c_ in CANDS},
"branco_nulo": (None if pd.isna(r.branco_nulo) else round(float(r.branco_nulo), 1)), "indecisos": (None if pd.isna(r.indecisos) else round(float(r.indecisos), 1)),
"L_valid": round(float(r.lula_v), 1), "F_valid": round(float(r.flavio_v), 1), "margem_v": round(float(r.margem_v), 1), "registro_tse": (r.registro_tse if isinstance(r.registro_tse, str) else "")}
for r in last1.itertuples()],
"p_1turno": round(p_1t, 4), "p_flavio_1turno": round(p_flavio_1t, 4), "p_lula_lidera_1t": round(p_lula_lidera_1t, 3), "q_1turno": [round(float(v), 1) for v in q_1t],
"q_margem_1t": {str(p): round(float(v), 1) for p, v in zip([5,10,25,50,75,90,95], q_margem_1t)}, "p_terceiro": {c_: round(v, 3) for c_, v in p_terceiro.items()},
"sens_1t": sens_1t, "tabela_1t_2t": tabela_1t_2t, "hist_1t": hist_1t,
"share_lula_3via": {k_: round(v, 2) for k_, v in share_lula_3via.items()}, "T3_total": round(T3_total, 1),
"resultado_1t": RESULTADO_1T,
}
# termômetro de tendência (últimas duas semanas): nível suavizado da janela dia a dia, com a leitura estadual na mesma proporção de hoje.
# É o nível SÓ das pesquisas (sem o voto contado), com o erro do filtro mais o da âncora; o painel o compara com o nível das pesquisas de
# hoje (nivel_pesquisas), não com a manchete mesclada (21/out: antes o erro da série era o do nível mesclado).
_serie = []
for _k in range(14, -1, -1):
_lv, _Pk = x_win(t_now - _k)
_lv = (1 - w_est_comb)*_lv + w_est_comb*(estadual["nivel"] if estadual["usado"] else 0.0)
_serie.append({"data": str((HOJE - pd.Timedelta(days=_k)).date()), "nivel": round(_lv, 2), "se": round(math.sqrt(_Pk + fit_win["var_ancora"]), 2)})
_d7 = _serie[-1]["nivel"] - _serie[-8]["nivel"]; _d14 = _serie[-1]["nivel"] - _serie[0]["nivel"]
_LIM = 0.5
_quem = lambda v: "Lula" if v > 0 else "Flávio"
_dir = (f"a favor de {_quem(_d7)}" if abs(_d7) >= _LIM else (f"a favor de {_quem(_d14)}, desacelerando" if abs(_d14) >= 2*_LIM else "estável"))
_mov = []
_dfs = df.sort_values("campo_fim")
for _r in _dfs[_dfs.campo_fim > HOJE - pd.Timedelta(days=14)].itertuples():
# onda anterior da mesma série; depois do 1º turno só pares pós-4/out, para não contar a quebra de 4/out como movimento
_prev = _dfs[(_dfs.instituto == _r.instituto) & (_dfs.campo_fim < _r.campo_fim) & ((_dfs.campo_fim > T1) if POS_1T else True)]
if len(_prev) == 0: continue
_p = _prev.iloc[-1]
_mov.append({"instituto": _r.instituto, "campo_fim": str(_r.campo_fim.date()), "anterior": str(_p.campo_fim.date()),
"lula": float(_r.lula), "flavio": float(_r.flavio), "lula_ant": float(_p.lula), "flavio_ant": float(_p.flavio),
"delta_margem": round(float(_r.margem_valid - _p.margem_valid), 1)})
res["nivel_ajustado_14d_atras"] = _serie[0]["nivel"]
# mesma linha desde o 1º turno, para o gráfico evolutivo das pesquisas pós-4/out (evolucao_pesquisas.py)
_serie_pos1t = []
for _k in range(max((HOJE - T1).days, 0), -1, -1):
_lv, _Pk = x_win(t_now - _k)
_lv = (1 - w_est_comb)*_lv + w_est_comb*(estadual["nivel"] if estadual["usado"] else 0.0)
_serie_pos1t.append({"data": str((HOJE - pd.Timedelta(days=_k)).date()), "nivel": round(_lv, 2), "se": round(math.sqrt(_Pk + fit_win["var_ancora"]), 2)})
res["serie_pos1t"] = _serie_pos1t
res["termometro"] = {"serie": _serie, "delta_7d": round(_d7, 2), "delta_14d": round(_d14, 2), "limiar": _LIM, "direcao": _dir, "movimentos": _mov,
"n_lula": sum(m["delta_margem"] >= 0.5 for m in _mov), "n_flavio": sum(m["delta_margem"] <= -0.5 for m in _mov),
"n_estavel": sum(abs(m["delta_margem"]) < 0.5 for m in _mov)}
# previsões do 1º turno congeladas (última rodada antes de 4/out fica gravada no repositório; a partir de 4/out nunca se reescreve,
# nem se o arquivo faltar — recriá-lo depois da eleição produziria uma "previsão" feita com o resultado conhecido)
frozen_path = DATA + "previsoes_1turno_congeladas.json"
if HOJE < T1 and not os.environ.get("ELEICAO_NAO_CONGELAR"):
json.dump({"data_base": str(HOJE.date()), "share_valid": res["t1_share_valid"], "q_share": q_share, "p_1turno": res["p_1turno"],
"p_flavio_top2": res["p_flavio_top2_votos"], "p_lula_lidera": res["p_lula_lidera_1t"], "q_margem_1t": res["q_margem_1t"],
"p_terceiro": res["p_terceiro"], "bias_1t_mean": BIAS_1T_MEAN, "ultimas_por_instituto": res["t1_por_instituto"],
"nivel_2t": res["nivel_ajustado_hoje_margem_valid"], "p_lula_2t": res["p_lula"], "p_ensemble_2t": res["p_ensemble"]},
open(frozen_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
if os.path.exists(frozen_path):
res["previsoes_congeladas"] = json.load(open(frozen_path, encoding="utf-8"))
res["mcmc"] = mcmc_res
res["migracao"] = migracao_res
# previsões do 2º turno congeladas, dos dois modelos em paralelo (mesma regra do 1º turno: reescritas a cada rodada até a véspera de
# 25/out e nunca depois), para comparar com o resultado oficial
frozen2_path = DATA + "previsoes_2turno_congeladas.json"
if HOJE < T2 and not os.environ.get("ELEICAO_NAO_CONGELAR"):
json.dump({"data_base": str(HOJE.date()),
"principal": {"p_lula": res["p_lula"], "p_ensemble": res["p_ensemble"], "quantis_margem_final": res["quantis_margem_final"],
"q_ensemble_10_50_90": res["q_ensemble_10_50_90"], "nivel_pesquisas": res["nivel_pesquisas"], "nivel_ajustado": res["nivel_ajustado_hoje_margem_valid"],
"nivel_verdadeiro": mcmc_res["comparacao"]["nivel_verdadeiro_principal"]},
"mcmc": {k_: mcmc_res[k_] for k_ in ["p_lula", "p_ensemble", "quantis_margem_final", "q_ensemble_10_50_90", "nivel_hoje",
"se_nivel_hoje", "rhat_max", "ess_min", "convergiu"]}},
open(frozen2_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
if os.path.exists(frozen2_path):
res["previsoes_2turno_congeladas"] = json.load(open(frozen2_path, encoding="utf-8"))
json.dump(res, open(OUT + "resultados.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)
def _csv(d_, path, nd=3):
d_ = d_.copy()
for c_ in d_.select_dtypes(include="number").columns: d_[c_] = d_[c_].round(nd)
d_.to_csv(path, index=False)
_csv(df, OUT + "pesquisas_2turno_2026_ponderadas.csv")
last["sigma_inst"] = last.instituto.map(sigma_inst)
_csv(last[["instituto","campo_fim","n","n_ef","lula","flavio","margem_valid","margem_adj","house_aplicado","w_recencia","w_tamanho","w_qualidade","w","sigma_inst","w_precisao"]], OUT + "ultimas_por_instituto.csv")
_csv(p1, OUT + "pesquisas_1turno_2026_processadas.csv", 2)
# ---------------- SVG helpers ----------------
def fmt(v, d=1): return f"{v:.{d}f}".replace(".", ",")
LABEL = {"Parana Pesquisas": "Paraná Pesquisas", "Futura Inteligencia": "Futura Inteligência", "MDA": "CNT/MDA", "Palver v3": "Palver (calibração v3)", "MDA v2": "CNT/MDA (ponderada pelo 1º turno)", "Futura Inteligencia v2": "Futura Inteligência (escolaridade pela PNAD)",
"Verita": "Veritá", "Alfa Inteligencia": "Alfa Inteligência", "Vetor": "Vetor/Arrow",
"Instituto Franca": "Instituto França"}
def svg_trend():
W, H = 900, 300
x0, x1, y0, y1 = 60, 880, 20, 250
tmin, tmax = int(df.dia.min()) - 1, (T2 - T0).days
ymin, ymax = -12, 20
def X(t): return x0 + (t - tmin)/(tmax - tmin)*(x1-x0)
def Y(v): return y1 - (v - ymin)/(ymax - ymin)*(y1-y0)
s = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Margem Lula menos Flávio em votos válidos, pesquisas e tendência ajustada por efeitos de casa">']
for v in range(-12, 21, 4):
s.append(f'<line class="grid-line" x1="{x0}" y1="{Y(v):.1f}" x2="{x1}" y2="{Y(v):.1f}"/>')
s.append(f'<text class="axis-label" x="{x0-8}" y="{Y(v)+4:.1f}" text-anchor="end">{"+" if v>0 else ""}{v}</text>')
s.append(f'<line x1="{x0}" y1="{Y(0):.1f}" x2="{x1}" y2="{Y(0):.1f}" stroke="var(--ink-soft)" stroke-width="1.2"/>')
MESES = {1:"jan",2:"fev",3:"mar",4:"abr",5:"mai",6:"jun",7:"jul",8:"ago",9:"set",10:"out",11:"nov",12:"dez"}
for m in pd.date_range("2025-12-01", "2026-11-01", freq="MS"):
tt = (m - T0).days
if tmin-15 <= tt <= tmax:
s.append(f'<text class="axis-label" x="{X(tt):.1f}" y="{y1+22}" text-anchor="middle">{MESES[m.month]}</text>')
up = " ".join(f"{X(t):.1f},{Y(f+1.96*e):.1f}" for t, f, e in zip(tt_grid, f_grid, se_grid))
lo = " ".join(f"{X(t):.1f},{Y(f-1.96*e):.1f}" for t, f, e in zip(tt_grid[::-1], f_grid[::-1], se_grid[::-1]))
s.append(f'<polygon points="{up} {lo}" fill="var(--accent)" opacity="0.13"/>')
s.append('<polyline class="series-line" stroke="var(--accent)" stroke-width="2.4" points="' + " ".join(f"{X(t):.1f},{Y(f):.1f}" for t, f in zip(tt_grid, f_grid)) + '"/>')
for _, r in df.iterrows():
s.append(f'<circle cx="{X(r.t):.1f}" cy="{Y(r.margem_valid):.1f}" r="4" fill="var(--other)" stroke="var(--surface)" stroke-width="1.2" opacity="0.85" class="iso-dot" tabindex="0"><title>{LABEL.get(r.instituto, r.instituto)} · {r.campo_fim.strftime("%d/%m/%y")} · Lula {fmt(r.lula)} × Flávio {fmt(r.flavio)} → margem válida {fmt(r.margem_valid)} · efeito de casa {fmt(r.house, 1)}</title></circle>')
marcos = [(t_now, "hoje", "end"), ((T1 - T0).days, "1º turno", "middle"), (tmax, "2º turno", "end")]
xs = [X(tt) for tt, _, _ in marcos]
rows = [0]
for i in range(1, len(xs)):
rows.append(1 if abs(xs[i] - xs[i-1]) < 58 and rows[i-1] == 0 else 0)
for (tt, lab, anchor), xr, row in zip(marcos, xs, rows):
s.append(f'<line x1="{xr:.1f}" y1="{y0}" x2="{xr:.1f}" y2="{y1}" stroke="var(--line)" stroke-dasharray="3,4"/>')
dx = -4 if anchor == "end" else 0
ry = (y0 - 6) - (14 if row else 0)
s.append(f'<text class="axis-label" x="{xr+dx:.1f}" y="{ry}" text-anchor="{anchor}">{lab}</text>')
tq = tmax; med = q[3]; p10, p90 = q[1], q[5]
cor_proj = "var(--lula)" if med > 0 else "var(--flavio)"
s.append(f'<line x1="{X(tq):.1f}" y1="{Y(p10):.1f}" x2="{X(tq):.1f}" y2="{Y(p90):.1f}" stroke="{cor_proj}" stroke-width="6" opacity="0.35"/>')
s.append(f'<circle cx="{X(tq):.1f}" cy="{Y(med):.1f}" r="5" fill="{cor_proj}"/>')
s.append(f'<text class="dot-label" x="{x1-4:.1f}" y="{y0+16}" text-anchor="end" fill="{cor_proj}">projeção 25/out · mediana {("+" if med>0 else "")}{fmt(med)}</text>')
s.append(f'<text class="dot-label" x="{x1-4:.1f}" y="{y0+30}" text-anchor="end" fill="{cor_proj}">intervalo 80%: {fmt(p10)} a {("+" if p90>0 else "")}{fmt(p90)}</text>')
s.append(f'<circle cx="{X(t_now):.1f}" cy="{Y(level_final):.1f}" r="5.5" fill="var(--accent)" stroke="var(--surface)" stroke-width="1.5"><title>Nível hoje: {fmt(level_final)} ± {fmt(1.96*se_final)}</title></circle>')
s.append(f'<text class="dot-label" x="{X(t_now)-8:.1f}" y="{Y(level_final)+26:.1f}" text-anchor="end" fill="var(--accent)">hoje {("+" if level_final>0 else "")}{fmt(level_final)} ±{fmt(1.96*se_final)}</text>')
s.append("</svg>")
return "\n".join(s)
def svg_hist():
W, H = 900, 260
x0, x1, y0, y1 = 60, 880, 44, 220
bins = np.arange(-14, 18.5, 1.0)
h, edges = np.histogram(margem_final, bins=bins, density=True)
hmax = h.max()
def X(v): return x0 + (v - bins[0])/(bins[-1]-bins[0])*(x1-x0)
def Y(d): return y1 - d/hmax*(y1-y0)
s = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Distribuição simulada da margem final Lula menos Flávio em 25 de outubro">']
for i, d in enumerate(h):
c = "var(--lula)" if edges[i] >= 0 else "var(--flavio)"
quem = "Lula" if edges[i] >= 0 else "Flávio"
s.append(f'<rect x="{X(edges[i])+1:.1f}" y="{Y(d):.1f}" width="{(x1-x0)/len(h)-2:.1f}" height="{y1-Y(d):.1f}" fill="{c}" opacity="{0.85 if abs(edges[i]+0.5)>1.5 else 0.45}" tabindex="0"><title>margem {fmt(edges[i])} a {fmt(edges[i]+1)} p.p. · {quem} vence nessa faixa · densidade {d/hmax*100:.0f}% do pico</title></rect>')
s.append(f'<line x1="{X(0):.1f}" y1="{y0}" x2="{X(0):.1f}" y2="{y1}" stroke="var(--ink)" stroke-width="1.2"/>')
for v in range(-12, 17, 4):
s.append(f'<text class="axis-label" x="{X(v):.1f}" y="{y1+18}" text-anchor="middle">{"+" if v>0 else ""}{v}</text>')
s.append(f'<text class="axis-label" x="{(x0+x1)/2:.0f}" y="{y1+34}" text-anchor="middle">margem Lula − Flávio em 25/out (p.p. de votos válidos)</text>')
# rótulos em cor de texto; a cor da série fica no quadrado ao lado (Ardósia: texto de gráfico nunca na cor da série)
s.append(f'<rect x="{X(6.5):.1f}" y="{y0+5}" width="9" height="9" rx="1" fill="var(--lula)"/><text class="dot-label" x="{X(6.5)+14:.1f}" y="{y0+14}" fill="var(--ink)">Lula vence · {p_lula*100:.0f}%</text>')
s.append(f'<rect x="{X(-6.5)-9:.1f}" y="{y0+5}" width="9" height="9" rx="1" fill="var(--flavio)"/><text class="dot-label" x="{X(-6.5)-14:.1f}" y="{y0+14}" text-anchor="end" fill="var(--ink)">Flávio vence · {(1-p_lula)*100:.0f}%</text>')
s.append(f'<text class="dot-label" x="{X(0):.1f}" y="{y0-16}" text-anchor="middle" fill="var(--ink-soft)">faixa clara = |margem| < 1,5 p.p. (resultado contestável): {p_empate*100:.0f}%</text>')
s.append("</svg>")
return "\n".join(s)
def svg_mcmc():
"""Os dois modelos lado a lado: barras = modelo principal (Monte Carlo com parâmetros fixos), linha = MCMC (posterior completo)."""
W, H = 900, 270
x0, x1, y0, y1 = 60, 880, 56, 220
bins = np.arange(-20, 20.5, 1.0)
h_a, edges = np.histogram(np.clip(margem_final, -19.99, 19.99), bins=bins, density=True)
h_m, _ = np.histogram(np.clip(margem_mcmc, -19.99, 19.99), bins=bins, density=True)
hmax = max(h_a.max(), h_m.max())
def X(v): return x0 + (v - bins[0])/(bins[-1]-bins[0])*(x1-x0)
def Y(d): return y1 - d/hmax*(y1-y0)
s = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Distribuição da margem final em 25 de outubro: modelo principal em barras, modelo MCMC em linha">']
bw = (x1-x0)/len(h_a)
for i, d in enumerate(h_a):
s.append(f'<rect x="{X(edges[i])+1:.1f}" y="{Y(d):.1f}" width="{bw-2:.1f}" height="{y1-Y(d):.1f}" fill="var(--ink-3)" opacity="0.32" tabindex="0">'
f'<title>margem {fmt(edges[i])} a {fmt(edges[i]+1)} p.p. · principal {d*100:.1f}% · MCMC {h_m[i]*100:.1f}% das simulações</title></rect>')
pts = []
for i, d in enumerate(h_m):
pts += [f"{X(edges[i]):.1f},{Y(d):.1f}", f"{X(edges[i+1]):.1f},{Y(d):.1f}"]
s.append(f'<polyline points="{" ".join(pts)}" fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linejoin="round"/>')
s.append(f'<line x1="{X(0):.1f}" y1="{y0-6}" x2="{X(0):.1f}" y2="{y1}" stroke="var(--ink)" stroke-width="1.2"/>')
for med, cor, dy in [(q[3], "var(--ink-3)", 0), (q_mcmc[3], "var(--accent)", 0)]:
s.append(f'<line x1="{X(med):.1f}" y1="{y0-6}" x2="{X(med):.1f}" y2="{y1}" stroke="{cor}" stroke-width="1.4" stroke-dasharray="4,3"/>')
for v in range(-20, 21, 4):
s.append(f'<text class="axis-label" x="{X(v):.1f}" y="{y1+18}" text-anchor="middle">{"+" if v>0 else ""}{v}</text>')
s.append(f'<text class="axis-label" x="{(x0+x1)/2:.0f}" y="{y1+36}" text-anchor="middle">margem Lula − Flávio em 25/out (p.p. de votos válidos; caudas além de ±20 somadas nas pontas)</text>')
s.append(f'<rect x="{x0}" y="{y0-44}" width="14" height="10" fill="var(--ink-3)" opacity="0.32"/>')
s.append(f'<text class="dot-label" x="{x0+20}" y="{y0-35}" fill="var(--ink-soft)">principal (Monte Carlo): Lula {p_lula*100:.0f}% · mediana {fmt(q[3])} · 80% de {fmt(q[1])} a {fmt(q[5])}</text>')
s.append(f'<line x1="{x0}" y1="{y0-19}" x2="{x0+14}" y2="{y0-19}" stroke="var(--accent)" stroke-width="2.4"/>')
s.append(f'<text class="dot-label" x="{x0+20}" y="{y0-15}" fill="var(--accent)">MCMC (posterior completo): Lula {p_lula_mcmc*100:.0f}% · mediana {fmt(q_mcmc[3])} · 80% de {fmt(q_mcmc[1])} a {fmt(q_mcmc[5])}</text>')
s.append("</svg>")
return "\n".join(s)
def svg_tornado():
W = 900; rowh = 34; H = 40 + rowh*len(tornado_df) + 30
x0, x1 = 400, 880; vmax = max(tornado_df.alto.abs().max(), tornado_df.baixo.abs().max()) + 0.5
def X(v): return (x0+x1)/2 + v/vmax*(x1-x0)/2
s = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Tornado: efeito de cada variável sobre a margem de Lula no 2º turno">']
for v in [-4, -2, 0, 2, 4]:
if abs(v) <= vmax:
s.append(f'<line class="grid-line" x1="{X(v):.1f}" y1="30" x2="{X(v):.1f}" y2="{H-30}"/>')
s.append(f'<text class="axis-label" x="{X(v):.1f}" y="{H-12}" text-anchor="middle">{"+" if v>0 else ""}{v} p.p.</text>')
s.append(f'<line x1="{X(0):.1f}" y1="30" x2="{X(0):.1f}" y2="{H-30}" stroke="var(--ink)" stroke-width="1.2"/>')
for i, (_, r) in enumerate(tornado_df.iterrows()):
y = 40 + i*rowh
lo, hi = sorted([r.baixo, r.alto])
s.append(f'<text x="{x0-16}" y="{y+13}" text-anchor="end" font-size="12.5" fill="var(--ink)">{r.variavel}</text>')
tt_lab = f'<title>{r.variavel}: {fmt(lo)} a +{fmt(hi)} p.p. na margem de Lula · {r.obs}</title>'
if lo < 0: s.append(f'<rect x="{X(lo):.1f}" y="{y}" width="{X(min(hi,0))-X(lo):.1f}" height="20" fill="var(--flavio)" opacity="0.8" tabindex="0">{tt_lab}</rect>')
if hi > 0: s.append(f'<rect x="{X(max(lo,0)):.1f}" y="{y}" width="{X(hi)-X(max(lo,0)):.1f}" height="20" fill="var(--lula)" opacity="0.8" tabindex="0">{tt_lab}</rect>')
s.append(f'<text class="dot-label" x="{X(lo)-6:.1f}" y="{y+14}" text-anchor="end" fill="var(--ink-soft)">{fmt(lo)}</text>')
s.append(f'<text class="dot-label" x="{X(hi)+6:.1f}" y="{y+14}" fill="var(--ink-soft)">+{fmt(hi)}</text>')
s.append("</svg>")
return "\n".join(s)
def svg_house():
dd = sorted([d for d in diag if d["diagnostico_confiavel"]], key=lambda d: d["house"])
W = 900; rowh = 26; H = 54 + rowh * len(dd) + 26
x0, x1 = 240, 870
vmax = max(abs(d["house"]) + 1.96 * d["house_se"] for d in dd) + 0.4
def X(v): return (x0 + x1) / 2 + v / vmax * (x1 - x0) / 2
s_ = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Efeito de casa de cada instituto na margem Lula menos Flávio, com intervalo de 95%">']
s_.append(f'<text class="axis-label" x="{X(-vmax*0.55):.0f}" y="20" text-anchor="middle">mede mais Flávio que a âncora</text>')
s_.append(f'<text class="axis-label" x="{X(vmax*0.55):.0f}" y="20" text-anchor="middle">mede mais Lula que a âncora</text>')
for v in range(-6, 7, 2):
if abs(v) <= vmax:
s_.append(f'<line class="grid-line" x1="{X(v):.1f}" y1="30" x2="{X(v):.1f}" y2="{H-26}"/>')
s_.append(f'<text class="axis-label" x="{X(v):.1f}" y="{H-8}" text-anchor="middle">{"+" if v>0 else ""}{v}</text>')
s_.append(f'<line x1="{X(0):.1f}" y1="30" x2="{X(0):.1f}" y2="{H-26}" stroke="var(--ink)" stroke-width="1.2"/>')
for i_, d in enumerate(dd):
yy = 44 + i_ * rowh
lo, hi = d["house"] - 1.96 * d["house_se"], d["house"] + 1.96 * d["house_se"]
cor = "var(--lula)" if d["house"] > 0 else "var(--flavio)"
signif = abs(d["house_z"]) >= 1.96
nome = LABEL.get(d["instituto"], d["instituto"]) + (" ◆" if d["ancora"] else "")
s_.append(f'<text x="{x0-14}" y="{yy+4}" text-anchor="end" font-size="12" font-weight="{"600" if d["ancora"] else "400"}" fill="var(--ink)">{nome}</text>')
s_.append(f'<line x1="{X(lo):.1f}" y1="{yy}" x2="{X(hi):.1f}" y2="{yy}" stroke="{cor}" stroke-width="2" opacity="0.45"/>')
s_.append(f'<circle cx="{X(d["house"]):.1f}" cy="{yy}" r="{5 if signif else 3.5}" fill="{cor}" opacity="{1 if signif else 0.55}" tabindex="0">'
f'<title>{nome}: efeito de casa {fmt(d["house"])} p.p. (IC 95%: {fmt(lo)} a {fmt(hi)}) · {d["n_ondas"]} ondas · variância extra estimada σ={fmt(d["sigma_extra"])} · '
f'erro típico {fmt(d["rms_vs_tendencia"])} p.p. vs. {fmt(d["se_margem_teorico"])} esperado pela amostragem</title></circle>')
s_.append(f'<text class="dot-label" x="{X(hi)+7:.1f}" y="{yy+4}" fill="var(--ink-3)">{fmt(d["house"], 1)}{"*" if signif else ""}</text>')
s_.append("</svg>")
return "\n".join(s_)
def svg_1turno():
"""Barras horizontais: share em válidos por candidato, mediana e intervalo 80% da simulação; marca a linha de 50%."""
cands = sorted(CANDS, key=lambda c_: -share_now[c_])
W = 900; rowh = 34; H = 40 + rowh*len(cands) + 34
x0, x1 = 180, 860; vmax = 60
def X(v): return x0 + v/vmax*(x1-x0)
s = [f'<svg viewBox="0 0 {W} {H}" role="img" aria-label="Votos válidos esperados por candidato no 1º turno, com intervalo de 80%">']
for v in range(0, 61, 10):
s.append(f'<line class="grid-line" x1="{X(v):.1f}" y1="30" x2="{X(v):.1f}" y2="{H-30}"/>')
s.append(f'<text class="axis-label" x="{X(v):.1f}" y="{H-12}" text-anchor="middle">{v}%</text>')
s.append(f'<line x1="{X(50):.1f}" y1="30" x2="{X(50):.1f}" y2="{H-30}" stroke="var(--ink)" stroke-width="1.2" stroke-dasharray="4,3"/>')
s.append(f'<text class="axis-label" x="{X(50)+4:.1f}" y="26">50% + 1 decide no 1º turno</text>')
for i, c_ in enumerate(cands):
y = 40 + i*rowh
lo, med, hi = q_share[c_]
cor = "var(--lula)" if c_ == "lula" else ("var(--flavio)" if c_ == "flavio" else "var(--other)")
s.append(f'<text x="{x0-12}" y="{y+14}" text-anchor="end" font-size="12.5" fill="var(--ink)">{CAND_LABEL[c_]}</text>')
s.append(f'<rect x="{X(0):.1f}" y="{y+2}" width="{X(med)-X(0):.1f}" height="18" fill="{cor}" opacity="0.75" tabindex="0"><title>{CAND_LABEL[c_]}: mediana {fmt(med)}% dos válidos · 80% entre {fmt(lo)} e {fmt(hi)} · nível nas pesquisas hoje {fmt(share_now[c_])}%</title></rect>')
s.append(f'<line x1="{X(lo):.1f}" y1="{y+11}" x2="{X(hi):.1f}" y2="{y+11}" stroke="var(--ink)" stroke-width="2"/>')
s.append(f'<line x1="{X(lo):.1f}" y1="{y+5}" x2="{X(lo):.1f}" y2="{y+17}" stroke="var(--ink)" stroke-width="1.5"/><line x1="{X(hi):.1f}" y1="{y+5}" x2="{X(hi):.1f}" y2="{y+17}" stroke="var(--ink)" stroke-width="1.5"/>')
s.append(f'<text class="dot-label" x="{X(hi)+8:.1f}" y="{y+15}" fill="var(--ink)">{fmt(med)}% <tspan fill="var(--ink-3)">({fmt(lo)}–{fmt(hi)})</tspan></text>')
s.append("</svg>")
return "\n".join(s)
open(OUT + "svg_trend.html", "w", encoding="utf-8").write(svg_trend())
open(OUT + "svg_hist.html", "w", encoding="utf-8").write(svg_hist())
open(OUT + "svg_mcmc.html", "w", encoding="utf-8").write(svg_mcmc())
open(OUT + "svg_house.html", "w", encoding="utf-8").write(svg_house())
open(OUT + "svg_tornado.html", "w", encoding="utf-8").write(svg_tornado())
open(OUT + "svg_1turno.html", "w", encoding="utf-8").write(svg_1turno())
print(json.dumps({k_: res[k_] for k_ in ["nivel_ajustado_hoje_margem_valid","se_total","nivel_v2_like","shift_ancora","house_effects","sigma_inst","bias_prior_mean","bias_prior_sd",
"bias_2t_ciclos","sd_deriva","sig_entre_turnos_dia","p_lula","p_lula_vs_flavio","p_empate_1p5","sensibilidade","sens_bias","p_ensemble",
"cenarios_ensemble","efeitos_fatores","var_share","t1_share_valid","p_1turno","p_flavio_top2_votos","p_lula_lidera_1t","q_margem_1t","p_terceiro","sens_1t","validacao_rolling"]},
ensure_ascii=False, indent=1))
print(tornado_df.round(2).to_string())
print("MAE hist:", mae_inst.round(2).to_dict())
print("bias por eleição/turno:", bias_el.round(2).to_dict())