Poder e múltiplos testes

Aula 15 — Módulo 2: Inferência Estatística

Heitor Ramos
heitor@dcc.ufmg.br

Departamento de Ciência da Computação — UFMG

Uma decisão com três métricas

Mover uma barreira do nível 30 para o nível 40 melhora a experiência dos jogadores?

Podemos medir retenção em 1 dia, retenção em 7 dias e número de rodadas. Qual resultado deve decidir?

Hoje

  • descrever o experimento Cookie Cats;
  • construir \(H_0\) por permutação e por bootstrap recentralizado;
  • construir naturalmente \(H_A\) por bootstrap;
  • estimar poder sob alternativas específicas;
  • compreender a inflação de falsos positivos;
  • comparar Bonferroni e Benjamini–Hochberg.

A unidade e as colunas

Coluna Significado
userid jogador único
version posição da primeira porta: 30 ou 40
sum_gamerounds rodadas nos primeiros 14 dias
retention_1 retornou após 1 dia
retention_7 retornou após 7 dias

Qual é a intervenção?

No jogo, uma porta interrompe o avanço até o jogador esperar ou realizar uma compra.

  • Controle: primeira porta no nível 30.
  • Tratamento: mover a porta para o nível 40.
  • Pergunta: adiar a interrupção aumenta engajamento e retenção?

Os grupos têm tamanhos semelhantes

Barras com 44.700 jogadores no gate 30 e 45.489 no gate 40.
Código do gráfico
counts = df["grupo"].value_counts().reindex(order)
counts.plot.bar(color=[ORANGE, BLUE])
plt.ylabel("Jogadores")
plt.title("Os grupos têm tamanhos muito semelhantes")

Três métricas, três histórias possíveis

Barras de retenção em 1 e 7 dias mostrando taxas ligeiramente maiores para a porta no nível 30.
Código do gráfico
ret = df.groupby("grupo")[["retention_1", "retention_7"]].mean().mul(100)
ret.T.plot.bar(color=[ORANGE, BLUE])
plt.ylabel("Retenção (%)")
plt.title("A porta no nível 30 retém um pouco mais")

A análise descritiva já orienta escolhas

Métrica Porta 30 Porta 40 Diferença 30 − 40
Retenção em 1 dia 44,82% 44,23% +0,59 p.p.
Retenção em 7 dias 19,02% 18,20% +0,82 p.p.
Média de rodadas 52,46 51,30 +1,16

Diferenças pequenas podem ser importantes — ou apenas ruído.

Rodadas não seguem uma distribuição simples

Histograma e boxplots mostrando forte assimetria à direita no número de rodadas.
Código do gráfico
sns.histplot(df.loc[df.sum_gamerounds <= 500, "sum_gamerounds"], bins=50)
sns.boxplot(data=df[df.sum_gamerounds <= df.sum_gamerounds.quantile(.999)],
            x="grupo", y="sum_gamerounds", showfliers=False)

Defina uma métrica primária

Usaremos retenção em 7 dias como métrica primária.

Motivos:

  • representa engajamento mais duradouro;
  • é binária e interpretável;
  • evita deixar a escolha depender do menor valor-p;
  • torna as demais métricas secundárias.

O efeito de interesse

\[ \Delta=p_{30}-p_{40} \]

Estimativa observada:

\[ \widehat\Delta=0{,}1902-0{,}1820=0{,}0082 \]

A porta no nível 30 teve 0,82 p.p. a mais de retenção em 7 dias.

Duas hipóteses, dois mundos

\[ H_0:\Delta=0 \qquad\text{e}\qquad H_A:\Delta\neq0 \]

  • O mundo nulo descreve o que observaríamos sem efeito.
  • O mundo alternativo exige escolher um efeito real específico ou estimá-lo.

Permutação é natural sob \(H_0\)

Se a posição da porta não altera retenção, os rótulos gate_30 e gate_40 são trocáveis.

Embaralhar os rótulos:

  • preserva todos os resultados;
  • preserva os tamanhos dos grupos;
  • remove a associação grupo–retenção.

O mundo nulo por permutação

Distribuição de diferenças por permutação centrada em zero, com a diferença observada na cauda.
Código do gráfico
pooled = np.r_[y30, y40]
labels_perm = rng.permutation(labels)
t_perm = pooled[labels_perm == "gate_30"].mean() \
       - pooled[labels_perm == "gate_40"].mean()

Bootstrap é natural sob \(H_A\)

Reamostrar separadamente dentro de cada grupo preserva:

  • a taxa observada no gate_30;
  • a taxa observada no gate_40;
  • a diferença observada entre elas.

Por isso, o bootstrap usual fica centrado em \(\widehat\Delta\), não em zero.

O mundo alternativo por bootstrap

Distribuição bootstrap da diferença de retenção centrada no efeito observado e afastada de zero.
Código do gráfico
p30_b = rng.choice(y30, size=len(y30), replace=True).mean()
p40_b = rng.choice(y40, size=len(y40), replace=True).mean()
delta_b = p30_b - p40_b

O bootstrap quantifica efeitos plausíveis

O intervalo percentil de 95% para \(\Delta\) é aproximadamente:

\[ [0{,}31;1{,}33]\text{ p.p.} \]

Ele responde melhor a:

Quais tamanhos de efeito são compatíveis com a amostra?

Bootstrap também pode construir \(H_0\)

Bootstrap não pertence exclusivamente a \(H_A\).

Para simular \(H_0\), precisamos impor a igualdade antes de reamostrar. Uma forma é recentralizar os dois grupos para uma média comum.

Recentrar remove o efeito observado

Para uma variável contínua:

\[ Y^{0}_{ij}=Y_{ij}-\bar Y_j+\bar Y_{pool} \]

Cada grupo perde sua própria média e recebe a média combinada. Assim, a diferença entre as médias recentralizadas é zero.

Código da recentralização

pooled_mean = np.r_[y30, y40].mean()

y30_null = y30 - y30.mean() + pooled_mean
y40_null = y40 - y40.mean() + pooled_mean

t_null = rng.choice(y30_null, len(y30), replace=True).mean() \
       - rng.choice(y40_null, len(y40), replace=True).mean()

Para Bernoulli, simulamos de uma proporção combinada comum.

Bootstrap recentralizado sob \(H_0\)

Distribuição bootstrap recentralizada em zero com a diferença observada na cauda.
Código do gráfico
pooled = (y30.sum() + y40.sum()) / (len(y30) + len(y40))
p30_null = rng.binomial(len(y30), pooled) / len(y30)
p40_null = rng.binomial(len(y40), pooled) / len(y40)
t_null = p30_null - p40_null

Qual distribuição o bootstrap representa?

Associe corretamente o uso do bootstrap à hipótese representada.

  • Bootstrap usual separado por grupo representa naturalmente efeitos próximos ao observado
  • Bootstrap usual separado por grupo sempre representa H0
  • Bootstrap recentralizado preserva necessariamente o efeito observado
  • Permutação e bootstrap respondem sempre à mesma pergunta

A hipótese determina a simulação

Três histogramas comparando permutação sob H0, bootstrap recentralizado sob H0 e bootstrap usual sob HA.
Código do gráfico
for values, title in [(perm, "Permutação: H0"),
                      (boot_null, "Bootstrap recentrado: H0"),
                      (boot_alt, "Bootstrap usual: HA")]:
    sns.histplot(values)

Qual método usar?

Objetivo Construção mais natural
testar ausência de associação com rótulos trocáveis permutação
testar \(H_0\) sem permutabilidade, impondo um parâmetro bootstrap recentralizado
estimar incerteza do efeito observado bootstrap usual
simular poder para um efeito planejado bootstrap/Monte Carlo sob alternativa definida

Poder começa com uma alternativa específica

Não existe “o poder do teste” sem dizer qual efeito é verdadeiro.

\[ \text{Poder}(\Delta^*)=P(\text{rejeitar }H_0\mid\Delta=\Delta^*) \]

Cada \(\Delta^*\) produz um poder diferente.

Como simular poder com bootstrap

  1. escolha taxas sob uma alternativa relevante;
  2. gere duas amostras com o tamanho planejado;
  3. aplique o teste completo;
  4. registre se \(H_0\) foi rejeitada;
  5. repita e calcule a proporção de rejeições.

Poder cresce com efeito e amostra

Curvas de poder para três efeitos e diferentes números de jogadores por grupo.
Código do gráfico
for effect in effects:
    control = rng.binomial(n, p0, size=R) / n
    treatment = rng.binomial(n, p0 + effect, size=R) / n
    power = np.mean(test_rejects(treatment - control))

Três maneiras de aumentar poder

  • aumentar a amostra;
  • reduzir a variabilidade com blocos ou covariáveis pré-tratamento;
  • aceitar um \(\alpha\) maior.

Aumentar o efeito não é uma opção estatística: ele pertence ao fenômeno e à intervenção.

Alfa e poder estão em tensão

Curva mostrando aumento do poder conforme aumenta o nível de significância.
Código do gráfico
zcrit = norm.ppf(1 - alphas/2)
power = 1 - norm.cdf(zcrit-effect/se) + norm.cdf(-zcrit-effect/se)
plt.plot(100*alphas, power)

Um efeito detectável não é necessariamente útil

Com muitos jogadores, podemos detectar diferenças minúsculas.

Planejamento exige definir o menor efeito de interesse:

\[ \Delta_{min}=\text{menor mudança que alteraria a decisão} \]

O tamanho amostral deve ser planejado em torno dele.

Agora temos mais de uma métrica

Cookie Cats oferece pelo menos três testes naturais:

  1. retenção em 1 dia;
  2. retenção em 7 dias;
  3. rodadas em 14 dias.

Adicionar horas, segmentos ou cortes cria rapidamente dezenas de hipóteses.

Cem testes fabricam surpresas

Cem valores-p sob hipóteses nulas, com alguns abaixo de 0,05 apenas por acaso.
Código do gráfico
p_values = rng.uniform(size=100)
plt.scatter(range(100), p_values, c=np.where(p_values < .05, ORANGE, LIGHT))
plt.axhline(.05, color=INK, ls="--")

A contabilidade das decisões

Em uma família com \(m\) hipóteses:

símbolo significado
\(R\) número total de rejeições — as “descobertas”
\(V\) rejeições erradas — falsos positivos
\(R-V\) rejeições corretas — descobertas verdadeiras

\(V\) não é observado: não sabemos diretamente quais rejeições são falsas. Os procedimentos controlam propriedades de \(V\) ao longo de repetições do estudo.

FWER: nenhum falso positivo na família

FWER (family-wise error rate) é:

\[ \operatorname{FWER}=P(V\geq1). \]

Ela responde: “qual é a chance de cometer pelo menos um falso positivo nesta família?”

Se \(m\) testes independentes usam nível \(\alpha\), todos sob \(H_0\):

\[ \operatorname{FWER}=1-(1-\alpha)^m. \]

Com \(m=100\) e \(\alpha=0{,}05\), a FWER chega a aproximadamente 99,4%.

A inflação cresce depressa

Curva crescente da probabilidade de ao menos um falso positivo conforme aumenta o número de testes.
Código do gráfico
m = np.arange(1, 101)
fwer = 1 - (1 - .05)**m
plt.plot(m, fwer)

Bonferroni controla a família

Bonferroni divide o “orçamento de erro” \(\alpha\) entre os \(m\) testes:

\[ \alpha^*=\frac{\alpha}{m} \]

Rejeite \(H_i\) quando \(p_i\leq\alpha/m\). Equivalentemente:

\[ p_i^{adj}=\min(mp_i,1). \]

Exemplo: com \(m=10\) e \(\alpha=0{,}05\), cada teste precisa de \(p_i\leq0{,}005\).

Por que Bonferroni funciona?

Se \(A_i\) é o evento “o teste \(i\) produz um falso positivo”, então:

\[ P\left(\bigcup_{i=1}^{m}A_i\right) \leq\sum_{i=1}^{m}P(A_i) \leq m\frac{\alpha}{m}=\alpha. \]

É a desigualdade da união: o controle vale mesmo sem independência entre os testes.

O preço dessa proteção é poder menor, especialmente quando \(m\) é grande ou os testes são muito correlacionados.

Holm controla FWER com mais poder

Holm é um procedimento step-down:

  1. ordene \(p_{(1)}\leq\cdots\leq p_{(m)}\);
  2. compare \(p_{(1)}\) com \(\alpha/m\);
  3. se passar, compare \(p_{(2)}\) com \(\alpha/(m-1)\);
  4. continue com \(p_{(i)}\leq\alpha/(m-i+1)\);
  5. ao primeiro fracasso, pare e não rejeite as hipóteses restantes.

Ele controla FWER sob dependência arbitrária e nunca rejeita menos hipóteses que Bonferroni.

Exemplo: Bonferroni e Holm

Para \(m=5\), \(\alpha=0{,}05\) e valores-p ordenados:

\(i\) \(p_{(i)}\) Bonferroni limiar de Holm decisão de Holm
1 0,003 0,010 0,0100 rejeita
2 0,011 0,010 0,0125 rejeita
3 0,018 0,010 0,0167 para
4 0,041 0,010 não rejeita
5 0,200 0,010 não rejeita

Bonferroni rejeita apenas a primeira; Holm rejeita as duas primeiras sem abandonar o controle de FWER.

FDR responde a outra pergunta

FDR (false discovery rate) é a proporção esperada de falsos entre os resultados declarados significativos:

\[ \operatorname{FDR}=E\left[\frac{V}{\max(R,1)}\right]. \]

Ela responde: “entre as descobertas que eu anunciar, qual fração falsa aceito em média?”

Controlar FDR em 5% não significa que cada resultado tenha 5% de chance de ser falso, nem garante exatamente 5% de falsos em uma realização.

É apropriado quando buscamos várias descobertas e aceitamos algumas falsas em troca de maior poder.

O procedimento de Benjamini–Hochberg

Para controlar FDR no nível \(q\):

  1. ordene \(p_{(1)}\leq\cdots\leq p_{(m)}\);
  2. calcule os limiares \((i/m)q\);
  3. encontre o maior \(k\) tal que \(p_{(k)}\leq(k/m)q\);
  4. rejeite \(H_{(1)},\ldots,H_{(k)}\).

Não pare no primeiro valor-p que falha: BH procura o maior índice que satisfaz a desigualdade.

O controle clássico vale sob independência e certas formas de dependência positiva. Dependências arbitrárias exigem métodos mais conservadores, como Benjamini–Yekutieli.

O mesmo exemplo com BH

Com \(q=0{,}05\):

\(i\) \(p_{(i)}\) \((i/5)q\) satisfaz?
1 0,003 0,010 sim
2 0,011 0,020 sim
3 0,018 0,030 sim — maior \(k\)
4 0,041 0,040 não
5 0,200 0,050 não

BH rejeita as três primeiras hipóteses. Ele faz mais descobertas porque controla FDR, não a probabilidade de qualquer erro.

Qual erro cada método controla?

Selecione as associações corretas.

  • Bonferroni controla FWER dividindo \(\alpha\) pelo número de testes
  • Holm controla FWER com um procedimento sequencial
  • Benjamini–Hochberg controla FDR
  • Todos os três métodos controlam exatamente a mesma quantidade

Bonferroni, Holm e BH têm metas distintas

Valores-p ordenados comparados aos limiares de Bonferroni e Benjamini-Hochberg.
método controla perfil
Bonferroni FWER simples e conservador
Holm FWER prefere-se a Bonferroni quando aplicável
BH FDR mais poder para muitas descobertas
Código do gráfico
p = np.sort(p_values)
bh = .05 * np.arange(1, len(p)+1) / len(p)
plt.scatter(np.arange(1, len(p)+1), p)
plt.plot(np.arange(1, len(p)+1), bh, label="BH")
plt.axhline(.05/len(p), label="Bonferroni")

Qual família devemos corrigir?

A família deve ser definida pela pergunta científica, não pelo arquivo de código.

  • métricas usadas para uma única decisão pertencem à mesma família;
  • análises confirmatórias e exploratórias podem formar famílias distintas;
  • dividir artificialmente testes para evitar correção é p-hacking.

Métrica primária reduz multiplicidade

Um plano claro para Cookie Cats poderia definir:

  • primária: retenção em 7 dias;
  • secundária: retenção em 1 dia;
  • exploratória: rodadas e subgrupos;
  • regra de decisão baseada na primária e métricas de proteção.

Não corrigimos uma hipótese que foi realmente definida como única antes do estudo.

Selecionar significativos exagera efeitos

Distribuições mostrando que efeitos selecionados por significância são maiores que o efeito verdadeiro.
Código do gráfico
estimates = rng.normal(true_effect, se, 5000)
selected = estimates[estimates > 1.96*se]
sns.histplot(estimates)
sns.histplot(selected)

Correção protege erro, não corrige desenho

Bonferroni ou BH não resolvem:

  • escolha de hipóteses depois dos dados;
  • parada opcional;
  • exclusões seletivas;
  • métricas pós-tratamento;
  • falta de randomização;
  • publicação apenas de resultados favoráveis.

Um plano antes do experimento

Registre:

  • métrica primária e família de testes;
  • \(\alpha\) ou \(q\);
  • menor efeito relevante;
  • poder desejado e tamanho amostral;
  • teste, cauda e regra de parada;
  • exclusões, transformações e subgrupos.

O que aprendemos

  • permutação constrói \(H_0\) diretamente quando há trocabilidade;
  • bootstrap também constrói \(H_0\) se recentralizarmos os dados;
  • bootstrap usual é mais natural para \(H_A\) e incerteza do efeito;
  • poder depende de uma alternativa específica;
  • múltiplos testes exigem definir família e objetivo do controle.

Encerramento

Uma boa análise não busca apenas rejeitar \(H_0\): ela planeja quais efeitos consegue detectar e quantas oportunidades teve de se enganar.