Aula 12 — Módulo 2: Inferência Estatística
Como medir a incerteza quando só temos uma amostra e nenhuma fórmula conveniente?
Em 2019, quanto mais cara era uma acomodação inteira típica em Manhattan do que no Brooklyn?
Não queremos apenas uma diferença observada. Queremos saber quão precisa ela é.
Fonte: Kaggle, dgomonov/new-york-city-airbnb-open-data.
| Coluna | Significado |
|---|---|
neighbourhood_group |
distrito da cidade |
room_type |
acomodação inteira, quarto privado ou compartilhado |
price |
preço anunciado por noite, em dólares |
host_id |
identificador do anfitrião |
availability_365 |
dias disponíveis no ano |
O preço é anunciado: não observamos se a estadia realmente ocorreu.
Manhattan e Brooklyn somam aproximadamente 85% dos anúncios.
Limpar a cauda muda a população-alvo: agora estudamos anúncios entre US$ 1 e US$ 799.
Na base original, a mediana é US$ 106 e a média é US$ 153.
fig, axes = plt.subplots(1, 2, figsize=(11, 4.8))
sns.histplot(raw.loc[raw["price"] > 0, "price"], bins=70, color=BLUE, ax=axes[0])
axes[0].set_xlim(0, 1000)
axes[0].set_xlabel("Preço por noite (US$)")
axes[0].set_ylabel("Anúncios")
axes[0].set_title("Escala original")
sns.histplot(np.log10(raw.loc[raw["price"] > 0, "price"]), bins=45, color=ORANGE, ax=axes[1])
axes[1].set_xlabel("log10 do preço")
axes[1].set_ylabel("")
axes[1].set_title("Escala logarítmica")
fig.suptitle("Preços são fortemente assimétricos", fontweight="bold")
finish("distribuicao-precos.png")Mas comparar todos os anúncios mistura localização e tipo de acomodação.
order = ["Bronx", "Queens", "Staten Island", "Brooklyn", "Manhattan"]
plt.figure(figsize=(10, 5.4))
sns.boxplot(data=airbnb, x="neighbourhood_group", y="price", order=order,
showfliers=False, color=LIGHT, medianprops={"color": ORANGE, "linewidth": 3})
plt.xlabel("")
plt.ylabel("Preço por noite (US$)")
plt.title("A localização desloca toda a distribuição de preços")
finish("precos-distrito.png")Manhattan tem uma proporção maior de acomodações inteiras.
composition = pd.crosstab(raw["neighbourhood_group"], raw["room_type"], normalize="index")
composition = composition.loc[["Manhattan", "Brooklyn"], ["Entire home/apt", "Private room", "Shared room"]]
composition.plot.bar(stacked=True, figsize=(8.8, 5), color=[BLUE, ORANGE, PURPLE])
plt.xticks(rotation=0)
plt.xlabel("")
plt.ylabel("Proporção")
plt.title("Comparar distritos exige controlar o tipo de acomodação")
plt.legend(title="Tipo", bbox_to_anchor=(1.02, 1), loc="upper left")
finish("composicao-tipo.png")Vamos restringir a pergunta a:
Isso evita atribuir ao distrito uma diferença que vem apenas do tipo de quarto.
| Distrito | \(n\) | Média | Mediana |
|---|---|---|---|
| Brooklyn | 600 | US$ 167 | US$ 140 |
| Manhattan | 600 | US$ 211 | US$ 180 |
Estimativa observada:
\[ \widehat{\Delta}=180-140=\text{US\$ }40 \]
A cauda extrema afeta pouco o ponto em que a curva acumulada cruza 50%.
plt.figure(figsize=(9.5, 5.2))
for values, label, color in [(brooklyn, "Brooklyn", ORANGE), (manhattan, "Manhattan", BLUE)]:
sns.ecdfplot(values, label=label, color=color, lw=3)
plt.axvline(np.median(values), color=color, ls="--", lw=2)
plt.xlim(0, 500)
plt.xlabel("Preço por noite (US$)")
plt.ylabel("Proporção acumulada")
plt.title("A mediana compara o centro sem obedecer à cauda")
plt.legend()
finish("ecdf-amostra.png")Para a média, o TCL fornece uma aproximação conhecida.
Para a diferença de medianas:
O bootstrap oferece uma aproximação computacional.
Se repetíssemos a coleta, obteríamos exatamente US$ 40 outra vez?
Precisamos imaginar muitas amostras semelhantes — mas só observamos uma.
O bootstrap substitui a distribuição desconhecida \(F\) pela distribuição empírica:
\[ \widehat F_n(x)=\frac{1}{n}\sum_{i=1}^{n}\mathbb{1}(X_i\leq x) \]
\(X_i\) é a \(i\)-ésima observação, \(x\) é um ponto da escala e \(\mathbb 1(\cdot)\) vale 1 quando a condição é verdadeira e 0 caso contrário. Assim, \(\widehat F_n(x)\) é a proporção observada de valores menores ou iguais a \(x\); cada observação recebe massa \(1/n\).
Sortear \(n\) observações sem reposição apenas reorganiza a amostra.
Com reposição:
Essa variação imita a incerteza amostral.
Por que o bootstrap comum reamostra \(n\) observações com reposição?
Em média, uma réplica contém cerca de 63,2% das observações distintas.
toy = np.array([65, 80, 90, 110, 145, 190, 240, 320])
toy_boot = np.random.default_rng(7).choice(toy, len(toy), replace=True)
fig, axes = plt.subplots(2, 1, figsize=(10, 4.8), sharex=True)
for ax, values, title, color in [(axes[0], toy, "Amostra observada", BLUE),
(axes[1], toy_boot, "Amostra bootstrap", ORANGE)]:
unique, multiplicity = np.unique(values, return_counts=True)
for x, count in zip(unique, multiplicity):
ax.scatter([x] * count, np.arange(1, count + 1), s=130, color=color)
ax.set_yticks([])
ax.set_title(title, loc="left")
axes[1].set_xlabel("Preço (US$)")
fig.suptitle("Reamostrar com reposição cria uma nova amostra", fontweight="bold")
finish("reamostragem-reposicao.png")Repetir esse processo revela quais diferenças são plausíveis.
one_m = rng.choice(manhattan, len(manhattan), replace=True)
one_b = rng.choice(brooklyn, len(brooklyn), replace=True)
observed = [np.median(brooklyn), np.median(manhattan)]
replica = [np.median(one_b), np.median(one_m)]
xpos = np.arange(2)
plt.figure(figsize=(8.8, 4.8))
plt.plot(xpos, observed, "o-", color=BLUE, lw=3, ms=10, label="amostra observada")
plt.plot(xpos, replica, "o--", color=ORANGE, lw=3, ms=10, label="uma réplica bootstrap")
plt.xticks(xpos, ["Brooklyn", "Manhattan"])
plt.ylabel("Mediana do preço (US$)")
plt.title("Cada réplica produz uma nova diferença")
plt.legend()
finish("uma-replica.png")Se \(X_1,\ldots,X_n\overset{iid}{\sim}F\) e \(\widehat\theta=T(\widehat F_n)\), então:
\[ X_1^*,\ldots,X_n^*\mid X_1,\ldots,X_n \overset{iid}{\sim}\widehat F_n \]
e
\[ \widehat\theta^*=T(\widehat F_n^*). \]
O método aproxima a lei de \(\widehat\theta-\theta\) pela lei condicional de \(\widehat\theta^*-\widehat\theta\).
\(F\) é a distribuição populacional desconhecida; \(\widehat F_n\) é sua versão empírica; \(T\) é o funcional que define o parâmetro; \(\theta=T(F)\), \(\widehat\theta=T(\widehat F_n)\) e \(\widehat\theta^*\) é a estimativa em uma amostra bootstrap. O asterisco sempre identifica o mundo reamostrado.
Formalmente, esperamos que:
\[ \mathcal L\!\left(\sqrt n(\widehat\theta^*-\widehat\theta)\mid X_1,\ldots,X_n\right) \Rightarrow \mathcal L\!\left(\sqrt n(\widehat\theta-\theta)\right). \]
Para qualquer variável aleatória genérica \(Z\), \(\mathcal L(Z)\) denota sua distribuição de probabilidade. Na fórmula, aplicamos esse operador aos erros padronizados do estimador bootstrap e do estimador original. A barra \(\mid X_1,\ldots,X_n\) indica que condicionamos nos dados observados; \(\Rightarrow\) indica convergência em distribuição. A convergência não é automática para toda estatística ou processo de dados.
O raciocínio estatístico é mais importante que o laço.
Esses dois papéis não devem ser confundidos.
A distribuição não precisa ser perfeitamente Normal para ser útil.
plt.figure(figsize=(9.3, 5.2))
sns.histplot(boot, bins=np.arange(18, 65, 1), color=BLUE)
plt.axvline(theta_hat, color=ORANGE, lw=3, label=f"diferença observada = US$ {theta_hat:.0f}")
plt.xlabel("Mediana Manhattan − mediana Brooklyn (US$)")
plt.ylabel("Réplicas")
plt.title("As réplicas revelam a incerteza da estimativa")
plt.legend()
finish("distribuicao-bootstrap.png")Se \(\widehat\theta_1^*,\ldots,\widehat\theta_B^*\) são as réplicas:
\[ \widehat{\operatorname{SE}}_{boot}(\widehat\theta) =\sqrt{\frac{1}{B-1}\sum_{b=1}^{B} (\widehat\theta_b^*-\overline{\theta^*})^2}. \]
Aqui, o erro padrão bootstrap é aproximadamente US$ 5,5.
\(B\) é o número de réplicas, \(\widehat\theta_b^*\) é a estimativa da réplica \(b\) e \(\overline{\theta^*}=B^{-1}\sum_b\widehat\theta_b^*\) é a média das réplicas.
O viés de um estimador é
\[\operatorname{Bias}(\widehat\theta)=E(\widehat\theta)-\theta.\]
Como \(\theta\) e a esperança populacional são desconhecidos, o bootstrap aproxima:
\[\widehat{\operatorname{Bias}}_{boot} =\overline{\theta^*}-\widehat\theta,\]
onde \(\overline{\theta^*}=B^{-1}\sum_b\widehat\theta_b^*\) é a média das réplicas. Valor próximo de zero sugere pouco deslocamento capturável pela amostra.
O bootstrap pode diagnosticar viés do estimador; não detecta nem corrige viés de seleção que a amostra não revela.
Um intervalo bootstrap percentil de nível \(1-\alpha\) é:
\[ \left[ q_{\alpha/2}(\widehat\theta^*), q_{1-\alpha/2}(\widehat\theta^*) \right]. \]
Ele usa os quantis da própria distribuição bootstrap.
\(q_\gamma(\widehat\theta^*)\) é o quantil de ordem \(\gamma\) das réplicas e \(\alpha\) é a probabilidade total deixada fora do intervalo; para 95%, \(\alpha=0{,}05\).
O intervalo permanece totalmente acima de zero.
plt.figure(figsize=(9.3, 5.2))
sns.kdeplot(boot, fill=True, color=LIGHT, linewidth=0)
inside = (boot >= ci[0]) & (boot <= ci[1])
sns.kdeplot(boot[inside], fill=True, color=BLUE, linewidth=0)
plt.axvline(ci[0], color=ORANGE, ls="--", lw=3)
plt.axvline(ci[1], color=ORANGE, ls="--", lw=3)
plt.text(ci[0], .012, f"2,5%\nUS$ {ci[0]:.0f}", ha="right", color=ORANGE, fontweight="bold")
plt.text(ci[1], .012, f"97,5%\nUS$ {ci[1]:.0f}", ha="left", color=ORANGE, fontweight="bold")
plt.xlabel("Diferença de medianas (US$)")
plt.ylabel("Densidade")
plt.title("O intervalo percentil preserva os 95% centrais")
finish("intervalo-percentil.png")Em repetições do procedimento, cerca de 95% dos intervalos construídos dessa forma cobririam a diferença-alvo.
Depois de observar o intervalo, o parâmetro não passa a ter 95% de probabilidade frequentista de estar nele.
Para intervalos, alguns milhares de réplicas costumam ser um ponto de partida prático.
checkpoints = np.arange(100, 3001, 100)
lower = np.array([np.quantile(boot[:b], .025) for b in checkpoints])
upper = np.array([np.quantile(boot[:b], .975) for b in checkpoints])
plt.figure(figsize=(9.4, 5.2))
plt.plot(checkpoints, lower, color=ORANGE, lw=2, label="limite inferior")
plt.plot(checkpoints, upper, color=BLUE, lw=2, label="limite superior")
plt.xlabel("Número de réplicas B")
plt.ylabel("Limite do IC (US$)")
plt.title("Mais réplicas reduzem o ruído de Monte Carlo")
plt.legend()
finish("estabilidade-b.png")Com a amostra fixa, aumentar \(B\) reduz a oscilação de Monte Carlo. A proximidade com o TCL ocorre porque a diferença de médias tem distribuição aproximadamente Normal — não porque \(B\) obrigue os métodos a coincidir.
mean_hat = manhattan.mean() - brooklyn.mean()
se_tcl = np.sqrt(
manhattan.var(ddof=1) / len(manhattan)
+ brooklyn.var(ddof=1) / len(brooklyn)
)
ci_tcl = np.array([mean_hat - 1.96*se_tcl,
mean_hat + 1.96*se_tcl])
B_max = 5000
mean_boot = (
rng.choice(manhattan, (B_max, len(manhattan)), replace=True).mean(axis=1)
- rng.choice(brooklyn, (B_max, len(brooklyn)), replace=True).mean(axis=1)
)
checkpoints = np.array([25, 50, 100, 200, 400, 800,
1200, 2000, 3000, 5000])
ci_boot_B = np.array([
np.quantile(mean_boot[:B], [.025, .975]) for B in checkpoints
])
plt.plot(checkpoints, ci_boot_B[:, 0], "o-", label="Bootstrap: inferior")
plt.plot(checkpoints, ci_boot_B[:, 1], "o-", label="Bootstrap: superior")
plt.axhline(ci_tcl[0], ls="--", label="TCL: inferior")
plt.axhline(ci_tcl[1], ls="--", label="TCL: superior")
plt.xscale("log"); plt.legend()
finish("estabilidade-media-bootstrap-tcl.png")Depois de estabilizar \(B\), ampliar a amostra é o que realmente estreita o intervalo.
sizes = np.array([50, 100, 200, 400, 600])
widths = []
for n in sizes:
widths_n = []
for repeat in range(6):
m = rng.choice(manhattan, n, replace=False)
b = rng.choice(brooklyn, n, replace=False)
values = bootstrap_difference(m, b, B=250, seed=1000 + repeat + n)
q = np.quantile(values, [.025, .975])
widths_n.append(q[1] - q[0])
widths.append(widths_n)
plt.figure(figsize=(9.2, 5.2))
sns.boxplot(data=pd.DataFrame(widths, index=sizes).T, color=LIGHT,
medianprops={"color": ORANGE, "linewidth": 3})
plt.xlabel("Observações por distrito")
plt.ylabel("Largura do IC 95% (US$)")
plt.title("Mais dados estreitam o intervalo")
finish("tamanho-largura.png")| Método | Ideia | Atenção |
|---|---|---|
| Percentil | usa quantis de \(\widehat\theta^*\) | simples, mas sensível a viés |
| Básico | reflete os quantis em torno de \(\widehat\theta\) | corrige deslocamento de forma simples |
| Normal | \(\widehat\theta\pm z\,SE_{boot}\) | supõe simetria aproximada |
| BCa | corrige viés e aceleração | mais sofisticado e geralmente preferível |
Discordância relevante é um sinal para investigar assimetria, viés e tamanho amostral.
se_boot = boot.std(ddof=1)
intervals = {
"Percentil": ci,
"Básico": np.array([2 * theta_hat - ci[1], 2 * theta_hat - ci[0]]),
"Normal": np.array([theta_hat - 1.96 * se_boot, theta_hat + 1.96 * se_boot]),
}
plt.figure(figsize=(9, 4.7))
for y, (name, limits) in enumerate(intervals.items()):
plt.plot(limits, [y, y], color=BLUE, lw=5)
plt.scatter(theta_hat, y, color=ORANGE, s=90, zorder=3)
plt.yticks(range(3), intervals.keys())
plt.axvline(0, color=INK, ls="--")
plt.xlabel("Diferença de medianas (US$)")
plt.title("Métodos diferentes podem produzir limites diferentes")
finish("metodos-intervalo.png")Um intervalo é válido quando sua cobertura real se aproxima do nível nominal:
\[ P_F\{\theta\in CI(X_1,\ldots,X_n)\}\approx 1-\alpha. \]
Não basta que o histograma bootstrap pareça suave ou convincente.
Na prática, a população verdadeira não está disponível para essa verificação.
population_median = np.median(focus.loc[focus["neighbourhood_group"].eq("Manhattan"), "price"])
population_median -= np.median(focus.loc[focus["neighbourhood_group"].eq("Brooklyn"), "price"])
cover = []
for i in range(20):
m = rng.choice(focus.loc[focus["neighbourhood_group"].eq("Manhattan"), "price"], 250, replace=False)
b = rng.choice(focus.loc[focus["neighbourhood_group"].eq("Brooklyn"), "price"], 250, replace=False)
values = bootstrap_difference(m, b, B=300, seed=3000 + i)
lo, hi = np.quantile(values, [.025, .975])
cover.append((lo, hi, lo <= population_median <= hi))
plt.figure(figsize=(9, 6.3))
for i, (lo, hi, covered) in enumerate(cover):
plt.plot([lo, hi], [i, i], color=BLUE if covered else ORANGE, lw=2)
plt.axvline(population_median, color=INK, lw=3)
plt.xlabel("Diferença de medianas (US$)")
plt.ylabel("Amostras repetidas")
plt.title("Validade significa cobertura no longo prazo")
finish("cobertura-bootstrap.png")Muitas réplicas não compensam poucos dados ruins.
Anúncios do mesmo anfitrião podem compartilhar:
Reamostrar anúncios como se fossem independentes pode subestimar a incerteza.
Quando a dependência ocorre por anfitrião, reamostramos anfitriões inteiros.
host_counts = sample.groupby(["neighbourhood_group", "host_id"]).size()
fig, axes = plt.subplots(1, 2, figsize=(10.5, 4.8))
sns.histplot(host_counts, discrete=True, color=BLUE, ax=axes[0])
axes[0].set_xlim(.5, 6.5)
axes[0].set_xlabel("Anúncios do mesmo anfitrião na amostra")
axes[0].set_ylabel("Anfitriões")
axes[0].set_title("Há agrupamento por anfitrião")
naive = bootstrap_difference(manhattan, brooklyn, B=1000, seed=9)
cluster_estimates = []
for _ in range(100):
pieces = []
for borough in ["Manhattan", "Brooklyn"]:
group = sample[sample["neighbourhood_group"].eq(borough)]
hosts = group["host_id"].unique()
chosen = rng.choice(hosts, len(hosts), replace=True)
values = np.concatenate([group.loc[group["host_id"].eq(h), "price"].to_numpy() for h in chosen])
pieces.append(np.median(values))
cluster_estimates.append(pieces[0] - pieces[1])
sns.kdeplot(naive, color=BLUE, lw=3, label="por anúncio", ax=axes[1])
sns.kdeplot(cluster_estimates, color=ORANGE, lw=3, label="por anfitrião", ax=axes[1])
axes[1].set_xlabel("Diferença de medianas (US$)")
axes[1].set_ylabel("Densidade")
axes[1].set_title("A unidade de reamostragem importa")
axes[1].legend()
finish("dependencia-host.png")
print(f"linhas={len(raw)}; análise={len(airbnb)}; foco={len(focus)}")
print(f"medianas: Manhattan={np.median(manhattan):.0f}; Brooklyn={np.median(brooklyn):.0f}")
print(f"diferença={theta_hat:.0f}; IC95%=[{ci[0]:.0f}, {ci[1]:.0f}]")Vários anúncios pertencem ao mesmo anfitrião e podem ser dependentes. Qual estratégia é mais adequada?
| Estrutura dos dados | Alternativa |
|---|---|
| série temporal | block bootstrap |
| dados espaciais | blocos espaciais ou modelo explícito |
| grupos e turmas | cluster bootstrap |
| amostragem estratificada | reamostrar dentro dos estratos |
| eventos extremos raros | teoria de extremos ou modelo paramétrico |
| máximo da distribuição | métodos específicos; bootstrap comum pode falhar |
Na amostra didática, a acomodação inteira mediana foi:
O resultado descreve anúncios de 2019 após os filtros — não preços atuais nem reservas efetivas.
O bootstrap transforma uma amostra em um laboratório para estudar a estabilidade de uma estatística.
Ele é poderoso quando o laboratório preserva a estrutura que gerou os dados — e enganoso quando essa estrutura é ignorada.
Com a mesma amostra:
ICD: Aula 12 — Bootstrapvoltar para a Aula