O desenho de experimentos é uma pedra angular da prática da estatística, com aplicações em praticamente todas as áreas de pesquisa.
O objetivo é projetar um experimento para confirmar ou rejeitar uma hipótese.
Os cientistas de dados geralmente precisam realizar experimentos contínuos, particularmente em relação à interface do usuário e ao marketing do produto.
Este capítulo revisa o desenho experimental tradicional e discute alguns desafios comuns em ciência de dados.
Ele também cobre alguns conceitos frequentemente citados em inferência estatística e explica seu significado e relevância (ou falta de relevância) para a ciência de dados.
Sempre que você vê referências a significância estatística, testes t ou valores p, normalmente é no contexto do “pipeline” de inferência estatística clássica (veja a Figura 3-1).
Este processo começa com uma hipótese (“o medicamento A é melhor que o padrão existente medicamento” ou “o preço A é mais lucrativo do que o preço B existente”).
Um experimento (que pode ser um teste A/B) é projetado para testar a hipótese - projetado de tal forma que esperamos que produza resultados conclusivos.
Os dados são coletados e analisados, e então uma conclusão é tirada.
O termo inferência reflete a intenção de aplicar o resultados de experimentos, que envolvem um conjunto limitado de dados, para um processo maior ou população.
from pathlib import Path
import random
import pandas as pd
import numpy as np
from scipy import stats
import statsmodels.api as sm
import statsmodels.formula.api as smf
from statsmodels.stats import power
import matplotlib.pylab as plt
O diretório DATA contém os arquivos .csv utilizados nos exemplos.
DATA = './'
Se você não mantiver seus dados no mesmo diretório que o código, adapte os nomes dos caminhos.
WEB_PAGE_DATA_CSV = DATA + 'web_page_data.csv'
FOUR_SESSIONS_CSV = DATA + 'four_sessions.csv'
CLICK_RATE_CSV = DATA + 'click_rates.csv'
IMANISHI_CSV = DATA + 'imanishi_data.csv'
A reamostragem em estatística significa amostrar repetidamente valores de dados observados, com um objetivo geral de avaliar a variabilidade aleatória em uma estatística.
Também pode ser usado para avaliar e melhorar a precisão de alguns modelos de aprendizado de máquina (por exemplo, as previsões de modelos de árvore de decisão construídos em vários conjuntos de dados bootstrap podem ser calculados em
um processo conhecido como ensacamento—consulte “Ensacando e a Floresta Aleatória” na página 259).
Existem dois tipos principais de procedimentos de reamostragem: o da alça de bota (bootstrap) e o de permutação de testes.
O bootstrap é usado para avaliar a confiabilidade de uma estimativa.
Os testes de permutação são usados para hipóteses de teste, geralmente envolvendo dois ou mais grupos.
session_times = pd.read_csv(WEB_PAGE_DATA_CSV)
session_times.Time = 100 * session_times.Time
#
ax = session_times.boxplot(
by='Page',
column='Time',
figsize=(4, 4))
ax.set_xlabel('')
ax.set_ylabel('Time (in seconds)')
plt.suptitle('')
#
plt.tight_layout()
plt.show()
mean_a = session_times[session_times.Page == 'Page A'].Time.mean()
mean_b = session_times[session_times.Page == 'Page B'].Time.mean()
print(mean_b - mean_a)
Exemplo de teste de permutação com aderência:
def perm_fun(x, nA, nB):
n = nA + nB
idx_B = set(random.sample(range(n), nB))
idx_A = set(range(n)) - idx_B
return x.loc[list(idx_B)].mean() - x.loc[list(idx_A)].mean()
nA = session_times[session_times.Page == 'Page A'].shape[0]
nB = session_times[session_times.Page == 'Page B'].shape[0]
print(perm_fun(session_times.Time, nA, nB))
Em Python, podemos criar um gráfico usando matplotlib:
random.seed(1)
perm_diffs = [perm_fun(session_times.Time, nA, nB) for _ in range(1000)]
fig, ax = plt.subplots(figsize=(5, 5))
ax.hist(perm_diffs, bins=11, rwidth=0.9)
ax.axvline(x = mean_b - mean_a, color='black', lw=2)
ax.text(50, 190, 'Observed\ndifference', bbox={'facecolor':'white'})
ax.set_xlabel('Session time differences (in seconds)')
ax.set_ylabel('Frequency')
plt.tight_layout()
plt.show()
Converta perm_diffs para matriz numpy para evitar problemas com algumas instalações do Python:
perm_diffs = np.array(perm_diffs)
print(np.mean(perm_diffs > mean_b - mean_a))
random.seed(1)
obs_pct_diff = 100 * (200 / 23739 - 182 / 22588)
print(f'Observed difference: {obs_pct_diff:.4f}%')
conversion = [0] * 45945
conversion.extend([1] * 382)
conversion = pd.Series(conversion)
#
perm_diffs = [100 * perm_fun(conversion, 23739, 22588)
for _ in range(1000)]
#
fig, ax = plt.subplots(figsize=(5, 5))
ax.hist(perm_diffs, bins=11, rwidth=0.9)
ax.axvline(x=obs_pct_diff, color='black', lw=2)
ax.text(0.06, 200, 'Observed\ndifference', bbox={'facecolor':'white'})
ax.set_xlabel('Conversion rate (percent)')
ax.set_ylabel('Frequency')
plt.tight_layout()
plt.show()
Se np.mean for aplicado a uma lista de booleanos, fornece a porcentagem de quantas vezes True foi encontrado na lista (#True / #Total).
print(np.mean([diff > obs_pct_diff for diff in perm_diffs]))
#
survivors = np.array([[200, 23739 - 200], [182, 22588 - 182]])
chi2, p_value, df, _ = stats.chi2_contingency(survivors)
#
print(f'p-value for single sided test: {p_value / 2:.4f}')
#
res = stats.ttest_ind(
session_times[session_times.Page == 'Page A'].Time,
session_times[session_times.Page == 'Page B'].Time,
equal_var=False)
#
print(f'p-value for single sided test: {res.pvalue / 2:.4f}')
#
tstat, pvalue, df = sm.stats.ttest_ind(
session_times[session_times.Page == 'Page A'].Time,
session_times[session_times.Page == 'Page B'].Time,
usevar='unequal', alternative='smaller')
#
print(f'p-value: {pvalue:.4f}')
four_sessions = pd.read_csv(FOUR_SESSIONS_CSV)
print(pd.read_csv(FOUR_SESSIONS_CSV).head())
ax = four_sessions.boxplot(
by='Page',
column='Time',
figsize=(4, 4))
ax.set_xlabel('Page')
ax.set_ylabel('Time (in seconds)')
plt.suptitle('')
plt.title('')
#
plt.tight_layout()
plt.show()
observed_variance = four_sessions.groupby('Page').mean().var()[0]
print('Observed means:', four_sessions.groupby('Page').mean().values.ravel())
print('Variance:', observed_variance)
def perm_test(df):
df = df.copy()
df['Time'] = np.random.permutation(df['Time'].values)
return df.groupby('Page').mean().var()[0]
#
print(perm_test(four_sessions))
#
random.seed(1)
perm_variance = [perm_test(four_sessions) for _ in range(3000)]
print('Pr(Prob)', np.mean([var > observed_variance for var in perm_variance]))
fig, ax = plt.subplots(figsize=(5,5))
ax.hist(
perm_variance,
bins=11,
rwidth=0.9)
ax.axvline(
x = observed_variance,
color='black', lw=2)
ax.text(
60, 200, 'Observed\nvariance',
bbox={'facecolor':'white'})
ax.set_xlabel('Variance')
ax.set_ylabel('Frequency')
plt.tight_layout()
plt.show()
Podemos calcular uma tabela ANOVA usando statsmodel.
model = smf.ols('Time ~ Page', data=four_sessions).fit()
#
aov_table = sm.stats.anova_lm(model)
print(aov_table)
#
res = stats.f_oneway(
four_sessions[four_sessions.Page == 'Page 1'].Time,
four_sessions[four_sessions.Page == 'Page 2'].Time,
four_sessions[four_sessions.Page == 'Page 3'].Time,
four_sessions[four_sessions.Page == 'Page 4'].Time)
print(f'F-Statistic: {res.statistic / 2:.4f}')
print(f'p-value: {res.pvalue / 2:.4f}')
# ```
# formula = 'len ~ C(supp) + C(dose) + C(supp):C(dose)'
# model = ols(formula, data).fit()
# aov_table = anova_lm(model, typ=2)
# ```
Tabela 3-4:
click_rate = pd.read_csv(CLICK_RATE_CSV)
clicks = click_rate.pivot(index='Click', columns='Headline', values='Rate')
print(clicks)
Tabela 3-5:
row_average = clicks.mean(axis=1)
pd.DataFrame({
'Headline A': row_average,
'Headline B': row_average,
'Headline C': row_average,
})
#
# Abordagem de reamostragem
#
box = [1] * 34
box.extend([0] * 2966)
random.shuffle(box)
#
def chi2(observed, expected):
pearson_residuals = []
for row, expect in zip(observed, expected):
pearson_residuals.append([(observe - expect) ** 2 / expect
for observe in row])
# return sum of squares
return np.sum(pearson_residuals)
#
expected_clicks = 34 / 3
expected_noclicks = 1000 - expected_clicks
expected = [34 / 3, 1000 - 34 / 3]
chi2observed = chi2(clicks.values, expected)
#
def perm_fun(box):
sample_clicks = [sum(random.sample(box, 1000)),
sum(random.sample(box, 1000)),
sum(random.sample(box, 1000))]
sample_noclicks = [1000 - n for n in sample_clicks]
return chi2([sample_clicks, sample_noclicks], expected)
#
perm_chi2 = [perm_fun(box) for _ in range(2000)]
#
resampled_p_value = sum(perm_chi2 > chi2observed) / len(perm_chi2)
print(f'Observed chi2: {chi2observed:.4f}')
print(f'Resampled p-value: {resampled_p_value:.4f}')
#
chisq, pvalue, df, expected = stats.chi2_contingency(clicks)
print(f'Observed chi2: {chisq:.4f}')
print(f'p-value: {pvalue:.4f}')
x = [1 + i * (30 - 1) / 99 for i in range(100)]
#
chi = pd.DataFrame({
'x': x,
'chi_1': stats.chi2.pdf(x, df=1),
'chi_2': stats.chi2.pdf(x, df=2),
'chi_5': stats.chi2.pdf(x, df=5),
'chi_10': stats.chi2.pdf(x, df=10),
'chi_20': stats.chi2.pdf(x, df=20),
})
fig, ax = plt.subplots(figsize=(4, 2.5))
ax.plot(chi.x, chi.chi_1, color='black', linestyle='-', label='1')
ax.plot(chi.x, chi.chi_2, color='black', linestyle=(0, (1, 1)), label='2')
ax.plot(chi.x, chi.chi_5, color='black', linestyle=(0, (2, 1)), label='5')
ax.plot(chi.x, chi.chi_10, color='black', linestyle=(0, (3, 1)), label='10')
ax.plot(chi.x, chi.chi_20, color='black', linestyle=(0, (4, 1)), label='20')
ax.legend(title='df')
#
plt.tight_layout()
plt.show()
# O Scipy possui apenas uma implementação do teste Exato de Fisher para matrizes 2x2.
# Existe um repositório github que fornece uma implementação Python que usa o mesmo código da versão R.
# A instalação requer um compilador Fortran.
# ```
# stats.fisher_exact(clicks)
# ```
# stats.fisher_exact(clicks.values)
imanishi = pd.read_csv(IMANISHI_CSV)
imanishi.columns = [c.strip() for c in imanishi.columns]
ax = imanishi.plot.bar(x='Digit', y=['Frequency'], legend=False,
figsize=(4, 4))
ax.set_xlabel('Digit')
ax.set_ylabel('Frequency')
#
plt.tight_layout()
plt.show()
statsmodels tem vários métodos para cálculo de potência.
Veja, por exemplo: https://machinelearningmastery.com/statistical-power-and-power-analysis-in-python/
effect_size = sm.stats.proportion_effectsize(0.0121, 0.011)
analysis = sm.stats.TTestIndPower()
result = analysis.solve_power(
effect_size=effect_size,
alpha=0.05,
power=0.8,
alternative='larger')
print('Sample Size: %.3f' % result)
#
effect_size = sm.stats.proportion_effectsize(0.0165, 0.011)
analysis = sm.stats.TTestIndPower()
result = analysis.solve_power(
effect_size=effect_size,
alpha=0.05, power=0.8, alternative='larger')
print('Sample Size: %.3f' % result)