Capítulo 04 - Estatística
Os fatos são teimosos, mas as estatísticas são mais flexíveis. (Mark Twain)
Os fatos são teimosos, mas as estatísticas são mais flexíveis. (Mark Twain)
Estatísticas refere-se à matemática e técnicas com as quais entendemos dados. É um campo rico e enorme, mais adequado para uma prateleira (ou sala) em uma biblioteca do que um capítulo, e, portanto, nossa discussão necessariamente não será profunda. Em vez disso, tentarei ensiná-lo apenas o suficiente para despertar seu interesse apenas o suficiente para que você possa aprenda mais.
Descrevendo um único conjunto de dados
Através de uma combinação de boca a boca e sorte, o Datasciencester cresceu para dezenas de membros, e o vice-presidente de captação de recursos solicita algum tipo de descrição de quantos amigos seus membros têm que ele pode incluir como seus amigos.
Usando técnicas do Capítulo 1, você pode facilmente produzir esses dados. Mas agora você se depara com o problema de como descrevê-los.
Uma descrição óbvia de qualquer conjunto de dados é simplesmente os próprios dados:
num_amigos = [100, 49, 41, 40,
# ... e muito mais
]
Para um conjunto de dados pequeno o suficiente, essa pode ser a melhor descrição. Mas para um conjunto de dados maior, isso é pesado e provavelmente opaco. (Imagine olhar para uma lista de 1 milhão de números.) Por esse motivo, usamos estatísticas para destilar e comunicar recursos relevantes de nossos dados.
Como primeira abordagem, você coloca o amigo em um histograma usando o contador e o plt.bar (Figura 1):
from collections import Counter
import matplotlib.pyplot as plt
num_amigos = [100,49,41,40,25,21,21,19,19,18,18,16,15,15,15,15,14,
14,13,13,13,13,12,12,11,10,10,10,10,10,10,10,10,10,
10,10,10,10,10,10,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
9,8,8,8,8,8,8,8,8,8,8,8,8,8,7,7,7,7,7,7,7,7,7,7,7,
7,7,7,7,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4,4,
4,4,4,4,4,4,4,4,4,4,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
qtde_amigos = Counter(num_amigos)
xs = range(101) # o maior valor é 100, por isso o eixo X vai até 100
ys = [qtde_amigos[x] for x in xs] # a altura da barra é a quantidade de amigos
plt.bar(xs, ys)
plt.axis([0, 101, 0, 25])
plt.title('Histograma da quantidade de amigos')
plt.xlabel('Qtde. de amigos')
plt.ylabel('Qtde. de pessoas')
plt.show()
Figura 1: Um histograma da quantidade de amigos.
Infelizmente, esse gráfico ainda é muito difícil para entrar em conversas. Então você começa a gerar algumas estatísticas. Provavelmente a estatística mais simples é o número de pontos de dados:
num_pontos = len(num_amigos)
Você provavelmente também está interessado nos maiores e maiores valores:
maior_valor = max(num_amigos)
menor_valor = min(num_amigos)
Você também pode querer saber a posição específica de um certo dado:
valores_organizados = sorted(num_amigos)
menor_valor = valores_organizados[0]
segundo_menor_valor = valores_organizados[1]
segundo_maior_valor = valores_organizados[-2]
Estamos apenas começando.
Tendências centrais
Geralmente, vamos querer alguma noção de onde nossos dados estão centrados. Geralmente, usaremos a média, que é apenas a soma dos dados divididos por sua quantidade:
from typing import List
def media(x: List[float]) -> float:
return sum(x) / len(x)
media(num_amigos)
Se você tiver dois pontos de dados, a média é simplesmente o ponto a meio caminho entre eles. À medida que você adiciona mais pontos, a média muda, mas sempre depende do valor de todos os pontos. Por exemplo, se você tiver 10 pontos de dados e aumentar o valor de qualquer um deles em 1, aumentará a média de 0,1.
Às vezes, também estaremos interessados na mediana, que é o valor mais médio (se o número de pontos de dados for ímpar) ou a média dos dois valores intermediários (se o número de pontos de dados for par).
Por exemplo, se tivermos cinco pontos de dados em um vetor x, a mediana é x[5 // 2] ou x[2]. Se tivermos seis pontos de dados, queremos a média de x[2] (o terceiro ponto) e x[3] (o quarto ponto).
Observe que - como a média - a mediana não depende totalmente de todos os valores em seus dados. Por exemplo, se você aumentar o maior valor (ou o menor valor), os pontos do meio permanecem inalterados, o que significa que a mediana também.
Escreveremos funções diferentes para casos pares e ímpares e combiná-los:
# --- O underline (_) indica que é uma função privada , pois pretendem ser --- #
# --- chamadas por nossa função mediana, mas não por outras pessoas que usam --- #
# --- nossa biblioteca de estatísticas --- #
def _mediana_impar(x: List[float]) -> float:
# --- Se len(x) é ímpar, a mediana é o elemento do meio --- #
return sorted(x)[len(x) // 2]
def _mediana_par(x: List[float]) -> float:
# --- Se len(x) é par, a mediana é a média dos elementos centrais --- #
x_ordenado = sorted(x)
ponto_medio = len(x) // 2
return (x_ordenado[ponto_medio - 1] + x_ordenado[ponto_medio]) / 2
def mediana(v: List[float]) -> float:
# --- Encontrar a mediana do vetor --- #
return _mediana_par(v) if len(v) % 2 == 0 else _mediana_impar(v)
assert mediana([1, 10, 2, 9, 5]) == 5
assert mediana([1, 9, 2, 10]) == (2 + 9) / 2
E agora podemos calcular o a mediana dos amigos:
print(mediana(num_amigos))
Claramente, a média é mais simples de calcular e varia sem problemas à medida que nossos dados mudam. Se tivermos n pontos de dados e um deles aumenta em uma pequena quantidade m, necessariamente a média aumentará em m/n. Isso torna a média favorável a todos os tipos de truques de cálculo. Para encontrar a mediana, no entanto, temos que classificar nossos dados, e alterar um de nossos pontos de dados em uma pequena quantidade e pode aumentar a mediana por m, em algum número menor que m, ou não (dependendo do restante dos dados).
Ao mesmo tempo, a média é muito sensível a outliers em nossos dados. Se nosso usuário mais amigável tivesse 200 amigos (em vez de 100), a média subiria para 7,82, enquanto a mediana permaneceria a mesma. Se é provável que os valores extremos sejam dados ruins (ou não representativos de qualquer fenômeno que estamos tentando entender), a média às vezes pode nos dar uma imagem enganosa. Por exemplo, a história é frequentemente contada que, em meados da década de 1980, o curso da Universidade da Carolina do Norte com o maior salário inicial médio foi a geografia, principalmente por causa da estrela da NBA (e outlier) Michael Jordan.
Uma generalização da mediana é o quartil, que representa o valor sob o qual está um certo percentil dos dados (a mediana representa o valor sob o qual 50% dos dados estão):
def quartil(x: List[float], p: float) -> float:
# --- Retorna o quartil de x referente ao valor de p --- #
p_index = int(p * len(x))
return sorted(x)[p_index]
assert quartil(num_amigos, 0.1) == 1
assert quartil(num_amigos, 0.25) == 3
assert quartil(num_amigos, 0.75) == 9
assert quartil(num_amigos, 0.9) == 13
Menos comumente, você pode querer olhar para a moda, ou valor o mais comum:
def moda(x: List[float]) -> List[float]:
# --- Retorna uma lista, caso haja mais do que um valor --- #
contagem = Counter(x)
max_contagem = max(contagem.values())
return [x_i for x_i, c in contagem.items() if c == max_contagem]
assert set(moda(num_amigos)) == {1, 6}
Mas mais frequentemente usaremos a média.
Dispersão
Dispersão refere-se a medidas de como os dados são espalhados. Normalmente, são estatísticas para os quais os valores próximos a zero não se espalharam e para os quais grandes valores significa muito espalhado. Por exemplo, uma medida muito simples é o intervalo, que é apenas a diferença entre os elementos maiores e os menores:
def intervalo(x: List[float]) -> float:
return max(x) - min(x)
assert intervalo(num_amigos) == 99
O intervalo é zero quando o máximo e o mínimo são iguais, o que só pode acontecer se os elementos de x forem todos iguais, o que significa que os dados não são os mais indispersos possível. Por outro lado, se o intervalo for grande, o máximo é muito maior que o mínimo e os dados serão mais espalhados.
Como a mediana, o intervalo não depende realmente de todo o conjunto de dados. Um conjunto de dados cujos pontos são todos 0 ou 100 tem o mesmo intervalo de um conjunto de dados cujos valores são 0, 100 e muitos 50. Mas parece que o primeiro conjunto de dados "deveria" ser mais espalhado.
Uma medida mais complexa de dispersão é a variação, que é calculada como:
from scratch.linear_algebra import sum_of_squares
def media_dispersao(x: List[float]) -> List[float]:
# --- Traduzir x subtraindo sua média (então o resultado tem média 0) --- #
x_bar = media(x)
return [x_i - x_bar for x_i in x]
def variancia(x: List[float]) -> float:
# --- Quadrado da média menos o desvio padrão --- #
assert len(x) >= 2, 'A variância requer 2 ou mais valores'
n = len(x)
dispersao = media_dispersao(x)
return sum_of_squares(dispersao) / (n - 1)
assert 81.54 < variancia(num_amigos) < 81.55
Parece que é quase o desvio quadrático médio da média, exceto que estamos dividindo por n - 1 em vez de n. De fato, quando estamos lidando com uma amostra de uma população maior, o x_bar é apenas uma estimativa da média real, o que significa que, em média (x_i - x_bar) ** 2 é uma subestimação do desvio quadrado de x_i da média, e é por isso que dividimos por n - 1 em vez de n.
Agora, quaisquer que sejam as unidades que nossos dados estejam (por exemplo, "amigos"), todas as nossas medidas de tendência central estão nessa mesma unidade. O intervalo estará da mesma forma nessa mesma unidade. A variação, por outro lado, possui unidades que são o quadrado das unidades originais (por exemplo, "amigos quadrados"). Como pode ser difícil entender isso, frequentemente olhamos para o desvio padrão:
import math
def desvio_padrao(x: List[float]) -> float:
# --- O desvio padrão é a raiz quadrada da variância --- #
return math.sqrt(variancia(x))
assert 9.02 < desvio_padrao(num_amigos) < 9.04
Que não é claramente afetado por um pequeno número de outliers.
Correlação
O vice-presidente de crescimento de Datasciencester tem uma teoria de que a quantidade de tempo que as pessoas gastam no site está relacionada ao número de amigos que eles têm no site (ela não é vice presidente a toa) e pediu que você verificasse isso.
Depois de cavar logs de tráfego, você criou uma lista chamada minutos_diarios que mostra quantos minutos por dia cada usuário gasta no DataSciencester e o pediu para que seus elementos correspondam aos elementos da nossa lista de num_amigos anteriores. Gostaríamos de investigar o relacionamento entre essas duas métricas.
Primeiro, veremos a covariância, o análogo de variância emparelhado. Enquanto a variação mede como uma única variável se desvia de sua média, a covariância mede como duas variáveis variam em conjunto de seus meios:
from scratch.linear_algebra import dot
minutos_diarios = [1,68.77,51.25,52.08,38.36,44.54,57.13,51.4,41.42,31.22,34.76,
54.01,38.79,47.59,49.1,27.66,41.03,36.73,48.65,28.12,46.62,
35.57,32.98,35,26.07,23.77,39.73,40.57,31.65,31.21,36.32,
20.45,21.93,26.02,27.34,23.49,46.94,30.5,33.8,24.23,21.4,
27.94,32.24,40.57,25.07,19.42,22.39,18.42,46.96,23.72,26.41,
26.97,36.76,40.32,35.02,29.47,30.2,31,38.11,38.18,36.31,21.03,
30.86,36.07,28.66,29.08,37.28,15.28,24.17,22.31,30.17,25.53,
19.85,35.37,44.6,17.23,13.47,26.33,35.02,32.09,24.81,19.33,
28.77,24.26,31.98,25.73,24.86,16.28,34.51,15.23,39.72,40.8,
26.06,35.76,34.76,16.13,44.04,18.03,19.65,32.62,35.59,39.43,
14.18,35.24,40.13,41.82,35.45,36.07,43.67,24.61,20.9,21.9,
18.79,27.61,27.21,26.61,29.77,20.59,27.53,13.82,33.2,25,33.1,
36.65,18.63,14.87,22.2,36.81,25.53,24.62,26.25,18.21,28.08,
19.42,29.79,32.8,35.99,28.32,27.79,35.88,29.06,36.28,14.1,
36.63,37.49,26.9,18.58,38.48,24.48,18.95,33.55,14.24,29.04,
32.51,25.63,22.22,19,32.73,15.16,13.9,27.2,32.01,29.27,33,
13.74,20.42,27.32,18.23,35.35,28.48,9.08,24.62,20.12,35.26,
19.92,31.02,16.49,12.16,30.7,31.22,34.65,13.13,27.51,33.2,
31.57,14.1,33.42,17.44,10.12,24.42,9.82,23.39,30.93,15.03,
21.67,31.09,33.29,22.61,26.89,23.48,8.38,27.81,32.35,23.84]
horas_diarias = [md / 60 for md in minutos_diarios]
def covariancia(x: List[float], y: List[float]) -> float:
assert len(x) == len(y), 'x e y deve ter o mesmo tamanho'
return dot(media_dispersao(x), media_dispersao(y)) / (len(x) - 1)
assert 22.42 < covariancia(num_amigos, minutos_diarios) < 22.43
assert 22.42/60 < covariancia(num_amigos, horas_diarias) < 22.43/60
Lembre-se de que o dot resume os produtos de pares de elementos correspondentes. Quando os elementos correspondentes de x e y estão acima dos seus meios ou abaixo dos meios, um número positivo entra na soma. Quando um está acima da sua média e a outra abaixo, um número negativo entra na soma. Consequentemente, uma covariância positiva “grande” significa que x tende a ser grande quando y é grande e pequeno quando y é pequeno. Uma covariância negativa "grande" significa o oposto - que x tende a ser pequeno quando y é grande e vice-versa. Uma covariância próxima a zero significa que esse relacionamento não existe.
No entanto, esse número pode ser difícil de interpretar, por alguns motivos:
Suas unidades são o produto das unidades dos dados (por exemplo, amigo por minuto por dia), o que pode ser difícil de entender. O que é um amigo por minuto por dia?
Se cada usuário tivesse duas vezes mais amigos (mas o mesmo número de minutos), a covariância seria duas vezes maior. Mas, em certo sentido, as variáveis estariam igualmente inter-relacionadas. Disse de maneira diferente, é difícil dizer o que conta como uma covariância "grande".
Por esse motivo, é mais comum analisar a correlação, que divide os desvios padrão de ambas as variáveis:
def correlacao(x: List[float], y: List[float]) -> float:
# --- Mede o quanto x e y variam em conjunto sobre suas médias --- #
dp_x = desvio_padrao(x)
dp_y = desvio_padrao(y)
if dp_x > 0 and dp_y > 0:
return covariancia(x, y) / dp_x / dp_y
else:
return 0 # se não há variação, a correlação é 0
assert 0.24 < correlacao(num_amigos, minutos_diarios) < 0.25
assert 0.24 < correlacao(num_amigos, horas_diarias) < 0.25
A correlação é sem unidade e sempre fica entre –1 (anticorrelação perfeita) e 1 (correlação perfeita). Um número como 0,25 representa uma correlação positiva relativamente fraca.
No entanto, uma coisa que negligenciamos foi examinar nossos dados. Confira a Figura 2.
Figura 2: Correlação com outlier.
A pessoa com 100 amigos (que passa apenas 1 minuto por dia no local) é um grande outlier, e a correlação pode ser muito sensível a discrepantes. O que acontece se o ignoramos?:
outlier = num_amigos.index(100) # índice do outlier
num_amigos_bom = [x for i, x in enumerate(num_amigos) if i != outlier]
minutos_diarios_bom = [x for i, x in enumerate(minutos_diarios) if i!= outlier]
horas_diarias_bom = [md / 60 for md in minutos_diarios_bom]
assert 0.57 < correlacao(num_amigos_bom, minutos_diarios_bom) < 0.58
assert 0.57 < correlacao(num_amigos_bom, horas_diarias_bom) < 0.58
Sem o outlier, há uma correlação muito mais forte (Figura 3).
Figura 3: Correlação sem outlier.
Você investiga ainda mais e descobre que o outlier era na verdade uma conta de teste interna que ninguém se preocupou em remover. Então você se sente justificado em excluí-lo.
Paradoxo de Simpson
Uma surpresa não incomum ao analisar os dados é o paradoxo de Simpson, no qual as correlações podem ser enganosas quando as variáveis confusas são ignoradas.
Por exemplo, imagine que você pode identificar todos os seus membros como cientistas de dados da sul ou cientistas de dados do sudeste. Você decide examinar em qual região estão os cientistas mais amigáveis:
Certamente parece que os cientistas de dados do sul são mais amigáveis que os cientistas de dados do sudeste Seus colegas de trabalho avançam todos os tipos de teorias sobre por que isso pode ser: talvez seja o clima, o café, ou os produtos orgânicos, ou aquele frio gostoso do inverno?
Mas ao brincar com os dados, você descobre algo muito estranho. Se você olhar apenas para pessoas com doutorado, os cientistas de dados do sudeste têm mais amigos em média. E se você olhar apenas para pessoas sem doutorado, os cientistas de dados do sudeste também têm mais amigos em média!
Depois de dar conta dos graus dos usuários, a correlação vai na direção oposta! O agrupamento dos dados como sul/sudeste disfarçaram o fato de que os cientistas de dados da sudeste se inclinam muito mais fortemente em relação aos tipos de doutorado.
Esse fenômeno surge no mundo real com alguma regularidade. A questão principal é que a correlação está medindo a relação entre suas duas variáveis, tudo sendo igual. Se a classe dos seus dados forem atribuídas aleatoriamente, pois podem estar em um experimento bem projetado, "tudo sendo igual" pode não ser uma suposição terrível. Mas quando há um padrão mais profundo para as atribuições de classe, "tudo sendo igual" pode ser uma suposição terrível.
A única maneira real de evitar isso é conhecer seus dados e fazendo o que puder para garantir que você tenha verificado quanto a possíveis fatores de confusão. Obviamente, isso nem sempre é possível. Se você não tinha dados sobre a obtenção educacional desses 200 cientistas de dados, pode simplesmente concluir que havia algo inerentemente mais sociável no sul.
Algumas outras advertências correlacionais
Uma correlação de zero indica que não há relação linear entre as duas variáveis. No entanto, pode haver outros tipos de relacionamentos. Por exemplo, se:
x = [-2, -1, 0, 1, 2]
y = [2, 1, 0, 1, 2]
então x e y têm correlação zero. Mas eles certamente têm um relacionamento - cada elemento de y é igual ao valor absoluto do elemento correspondente de x. O que eles não têm é um relacionamento no qual saber como x_i se compara a media(x) nos fornece informações sobre como o y_i se compara a media(y). Esse é o tipo de relacionamento que a correlação procura.
Além disso, a correlação não diz nada sobre o tamanho do relacionamento. As variáveis:
x = [-2, -1, 0, 1, 2]
y = [99.98, 99.99, 100, 100.01, 100.02]
estão perfeitamente correlacionados, mas (dependendo do que você está medindo), é bem possível que esse relacionamento não seja tão interessante.
Correlação e causa
Você provavelmente já ouviu falar em algum momento que "a correlação não é causa", provavelmente de alguém que procura dados que representaram um desafio para partes de sua visão de mundo que ele estava relutante em questionar. No entanto, esse é um ponto importante, se x e y estiverem fortemente correlacionados, isso pode significar que x causa y, que y causa x, que cada um causa o outro, que algum terceiro fator causa ambos, ou nada.
Considere a relação entre num_amigos e minutos_diarios. É possível que ter mais amigos no site faça com que os usuários do Datasciencester gastem mais tempo no site. Pode ser esse o caso se cada amigo publicar uma certa quantidade de conteúdo a cada dia, o que significa que quanto mais amigos você tiver, mais tempo leva para ler as atualizações.
No entanto, também é possível que quanto mais tempo os usuários gastam discutindo nos fóruns do Datasciencester, mais eles encontram e fazem amizade com pessoas que pensam da mesma forma. Ou seja, passar mais tempo no site faz com que os usuários tenham mais amigos.
Uma terceira possibilidade é que os usuários mais apaixonados pela ciência de dados gastem mais tempo no site (porque acham mais interessante) e coletam mais ativamente amigos da ciência de dados (porque eles não querem se associar a mais ninguém).
Uma maneira de se sentir mais confiante em relação à causalidade é a realização de ensaios randomizados. Se você pode dividir aleatoriamente seus usuários em dois grupos com dados demográficos semelhantes e fornecer a um dos grupos uma experiência um pouco diferente, muitas vezes você pode se sentir muito bem com o fato de as diferentes experiências estão causando os diferentes resultados.
Para uma exploração adicional
Scipy, Pandas e StatsModels vêm com uma ampla variedade de funções estatísticas.
Estatísticas é importante. Se você deseja ser um melhor cientista de dados, seria uma boa ideia ler um livro de estatísticas. Muitos estão disponíveis gratuitamente online, incluindo:
Introductory Statistics, por Douglas Shafer and Zhiyi Zhang (Saylor Foundation);
OnlineStatBook, por David Lane (Rice University);
Introductory Statistics, por OpenStax (OpenStax College)