Data Science é a disciplina que extrai conhecimento e insights de dados — combinando estatística, programação e domínio do problema. Python se tornou a linguagem dominante nessa área por um motivo simples: seu ecossistema de bibliotecas é incomparável. NumPy, pandas, Matplotlib e Seaborn formam a base sobre a qual toda análise de dados em Python é construída. Neste artigo apresentamos esse ecossistema de forma sistemática e prática.
O Ecossistema de Data Science
Dados brutos
│
▼
┌─────────────┐ coleta e limpeza ┌──────────────┐
│ pandas │ ◄──────────────────── │ requests / │
│ (tabelas) │ │ scrapy / sql│
└──────┬──────┘ └──────────────┘
│ arrays numéricos
▼
┌─────────────┐ visualização ┌──────────────┐
│ NumPy │ ────────────────────► │ Matplotlib │
│ (vetores) │ │ Seaborn │
└──────┬──────┘ └──────────────┘
│ modelagem
▼
┌─────────────┐ avaliação ┌──────────────┐
│ scikit-learn│ ────────────────────► │ métricas / │
│ (ML) │ │ relatórios │
└─────────────┘ └──────────────┘
pip install numpy pandas matplotlib seaborn scipy jupyter
NumPy: Computação Numérica
NumPy é a fundação de todo o ecossistema científico Python — arrays multidimensionais com operações vetorizadas, muito mais rápidas que listas Python puras.
import numpy as np
# Criando arrays
a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])
print(a.dtype) # int64
print(a.shape) # (5,)
print(a.ndim) # 1
# Operações vetorizadas — sem loop explícito
print(a + b) # [11 22 33 44 55]
print(a * 2) # [ 2 4 6 8 10]
print(a ** 2) # [ 1 4 9 16 25]
print(np.sqrt(a)) # [1. 1.41421356 1.73205081 2. 2.23606798]
# Comparações vetorizadas
print(a > 3) # [False False False True True]
print(a[a > 3]) # [4 5] — indexação booleana
Arrays Multidimensionais
# Matriz 3x4
matriz = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
print(matriz.shape) # (3, 4)
print(matriz[1, 2]) # 7 — linha 1, coluna 2
print(matriz[:, 0]) # [1 5 9] — toda a coluna 0
print(matriz[0, :]) # [1 2 3 4] — toda a linha 0
print(matriz[1:, 2:]) # submatriz
# Criando arrays especiais
zeros = np.zeros((3, 4))
uns = np.ones((2, 5))
ident = np.eye(3) # matriz identidade
seq = np.arange(0, 20, 2) # [0 2 4 ... 18]
lin = np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1.]
rand = np.random.rand(3, 3) # valores aleatórios 0-1
Álgebra Linear e Estatística
a = np.array([2, 4, 4, 4, 5, 5, 7, 9])
print(f"Média: {np.mean(a):.2f}") # 5.00
print(f"Mediana: {np.median(a):.2f}") # 4.50
print(f"Desvio: {np.std(a):.2f}") # 2.00 — populacional (ddof=0)
print(f"Variância: {np.var(a):.2f}") # 4.00
print(f"Mínimo: {np.min(a)}") # 2
print(f"Máximo: {np.max(a)}") # 9
print(f"Soma: {np.sum(a)}") # 40
print(f"Percentil 75: {np.percentile(a, 75)}") # 5.5
# Desvio amostral (ddof=1), o padrão do .std() do pandas
print(np.std(a, ddof=1)) # 2.138...
# Produto matricial
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
print(A @ B) # produto matricial
print(np.dot(A, B))
# Autovalores e autovetores
vals, vecs = np.linalg.eig(A)
print(vals) # [-0.37228132+0.j 5.37228132+0.j] — o NumPy 2.5 devolve complexos
# Resolução de sistema linear Ax = b
A = np.array([[2, 1], [1, 3]], dtype=float)
b = np.array([5, 10], dtype=float)
x = np.linalg.solve(A, b)
print(x) # [1. 3.] — confere: 2·1 + 3 = 5 e 1 + 3·3 = 10
pandas: Análise de Dados Tabulares
O pandas organiza dados em tabelas — o DataFrame, com colunas nomeadas e tipadas — e faz em uma linha de código o que numa planilha pede fórmula arrastada ou tabela dinâmica.
import pandas as pd
# Criando DataFrame
df = pd.DataFrame({
"nome": ["Ana", "Bruno", "Carla", "Diego", "Elena"],
"idade": [23, 35, 28, 42, 31],
"cidade": ["Recife", "SP", "RJ", "SP", "Recife"],
"salario": [4500.0, 8200.0, 6300.0, 12000.0, 5800.0],
"ativo": [True, True, False, True, True]
})
# Informações básicas
print(df.shape) # (5, 5)
print(df.dtypes) # tipos de cada coluna
print(df.describe()) # estatísticas descritivas
df.info() # visão geral + memória (imprime sozinho e devolve None)
print(df.head(3)) # primeiras 3 linhas
print(df.tail(2)) # últimas 2 linhas
Seleção e Filtragem
# Selecionando colunas
print(df["nome"]) # Series
print(df[["nome", "salario"]]) # DataFrame
# loc — por rótulo
print(df.loc[0]) # linha 0
print(df.loc[1:3, ["nome", "idade"]]) # linhas 1 a 3 — no loc, o fim entra
# iloc — por posição inteira
print(df.iloc[0, 1]) # linha 0, coluna 1
print(df.iloc[:3, :2]) # primeiras 3 linhas, 2 colunas
# Filtrando
ativos = df[df["ativo"] == True]
sp = df[df["cidade"] == "SP"]
alto_sal = df[df["salario"] > 6000]
sp_ativo = df[(df["cidade"] == "SP") & (df["ativo"] == True)]
recife_ou_rj = df[df["cidade"].isin(["Recife", "RJ"])]
# query() — sintaxe SQL-like
resultado = df.query("salario > 5000 and ativo == True")
Transformações e Cálculos
# Novas colunas
df["bonus"] = df["salario"] * 0.1
df["faixa_etaria"] = pd.cut(
df["idade"],
bins=[0, 25, 35, 50, 100],
labels=["Jovem", "Adulto", "Meia-idade", "Sênior"]
)
# Aplicando funções
df["nome_upper"] = df["nome"].str.upper()
df["salario_k"] = df["salario"].apply(lambda x: f"R$ {x/1000:.1f}k")
# Operações sobre grupos
por_cidade = df.groupby("cidade").agg(
total_funcionarios=("nome", "count"),
salario_medio= ("salario", "mean"),
salario_total= ("salario", "sum"),
idade_media= ("idade", "mean")
).round(2)
print(por_cidade)
# Pivot table
pivot = df.pivot_table(
values="salario",
index="cidade",
columns="ativo",
aggfunc="mean"
)
print(pivot)
Limpeza de Dados
import numpy as np
# Dataset com problemas
df_sujo = pd.DataFrame({
"nome": ["Ana", "Bruno", None, "Diego", "Ana", "Elena"],
"idade": [23, 35, 28, None, 23, -5],
"salario": [4500, 8200, 6300, 12000, 4500, np.nan],
"email": ["ana@email.com", "bruno@email.com", "invalido",
"diego@email.com", "ana@email.com", "elena@email.com"]
})
print("=== Dados sujos ===")
print(df_sujo)
print(f"\nValores nulos:\n{df_sujo.isnull().sum()}")
print(f"\nDuplicatas: {df_sujo.duplicated().sum()}")
# 1. Removendo duplicatas
df_limpo = df_sujo.drop_duplicates()
# 2. Tratando valores nulos — sempre atribuindo o resultado.
# No pandas 3, df["col"].fillna(..., inplace=True) não altera o df.
df_limpo = df_limpo.fillna({
"nome": "Desconhecido",
"salario": df_limpo["salario"].median(),
})
df_limpo = df_limpo.dropna(subset=["idade"]) # remove linhas sem idade
# 3. Corrigindo valores inválidos
df_limpo = df_limpo[df_limpo["idade"] > 0]
# 4. Validando e-mails
df_limpo["email_valido"] = df_limpo["email"].str.fullmatch(
r"[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}"
)
# 5. Normalizando tipos
df_limpo["idade"] = df_limpo["idade"].astype(int)
df_limpo["salario"] = df_limpo["salario"].astype(float)
print("\n=== Dados limpos ===")
print(df_limpo)
Matplotlib: Visualização Fundamental
import matplotlib.pyplot as plt
import numpy as np
# Configuração global
plt.rcParams.update({
"figure.figsize": (10, 6),
"axes.spines.top": False,
"axes.spines.right": False,
"font.family": "DejaVu Sans"
})
def graficos_basicos():
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
fig.suptitle("Tipos de Gráficos", fontsize=16, fontweight="bold")
# 1. Linha
x = np.linspace(0, 10, 100)
ax = axes[0, 0]
ax.plot(x, np.sin(x), label="sen(x)", color="#2E75B6")
ax.plot(x, np.cos(x), label="cos(x)", color="#C00000", linestyle="--")
ax.set_title("Gráfico de Linha")
ax.legend()
ax.grid(alpha=0.3)
# 2. Barras
categorias = ["Jan", "Fev", "Mar", "Abr", "Mai"]
valores = [42, 58, 71, 65, 83]
ax = axes[0, 1]
bars = ax.bar(categorias, valores, color="#2E75B6", alpha=0.8)
ax.bar_label(bars, padding=2)
ax.set_title("Gráfico de Barras")
# 3. Dispersão
np.random.seed(42)
x = np.random.randn(100)
y = 2 * x + np.random.randn(100)
ax = axes[0, 2]
ax.scatter(x, y, alpha=0.6, color="#2E75B6")
ax.set_title("Dispersão")
# 4. Histograma
dados = np.random.normal(170, 10, 500)
ax = axes[1, 0]
ax.hist(dados, bins=30, color="#2E75B6", alpha=0.8, edgecolor="white")
ax.set_title("Histograma")
ax.set_xlabel("Altura (cm)")
# 5. Pizza
labels = ["Aprovados", "Reprovados", "Recuperação"]
sizes = [65, 20, 15]
cores = ["#375623", "#C00000", "#F5A623"]
ax = axes[1, 1]
ax.pie(sizes, labels=labels, colors=cores, autopct="%1.1f%%", startangle=90)
ax.set_title("Pizza")
# 6. Box Plot
dados_box = [np.random.normal(loc, 1, 100) for loc in [2, 3, 5, 8]]
ax = axes[1, 2]
ax.boxplot(dados_box, tick_labels=["A", "B", "C", "D"], patch_artist=True)
ax.set_title("Box Plot")
plt.tight_layout()
plt.savefig("graficos_basicos.png", dpi=150, bbox_inches="tight")
plt.show()
graficos_basicos()
Seaborn: Visualizações Estatísticas
import numpy as np
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
# Dataset de exemplo: a nota cresce com as horas de estudo, com ruído
np.random.seed(42)
horas = np.random.uniform(1, 8, 200)
df_vis = pd.DataFrame({
"horas_estudo": horas,
"nota": (3.5 + 0.6 * horas + np.random.normal(0, 1.2, 200)).clip(0, 10),
"turma": np.random.choice(["A", "B", "C"], 200),
})
df_vis["aprovado"] = (df_vis["nota"] >= 6).map({True: "Sim", False: "Não"})
sns.set_theme(style="whitegrid", palette="muted")
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. Distribuição de notas
sns.histplot(
data=df_vis, x="nota", hue="aprovado",
multiple="stack", bins=20, ax=axes[0, 0]
)
axes[0, 0].set_title("Distribuição de Notas")
axes[0, 0].axvline(6, color="red", linestyle="--", label="Mínimo")
# 2. Boxplot por turma
sns.boxplot(
data=df_vis, x="turma", y="nota",
hue="turma", palette="Blues", legend=False, ax=axes[0, 1]
)
axes[0, 1].set_title("Notas por Turma")
# 3. Scatter com regressão
sns.regplot(
data=df_vis,
x="horas_estudo", y="nota",
scatter_kws={"alpha": 0.4},
line_kws={"color": "red"},
ax=axes[1, 0]
)
axes[1, 0].set_title("Horas de Estudo vs Nota")
# 4. Heatmap de correlação
correlacao = df_vis[["nota", "horas_estudo"]].corr()
sns.heatmap(
correlacao,
annot=True, fmt=".2f",
cmap="coolwarm", vmin=-1, vmax=1, # correlação vai de -1 a 1
ax=axes[1, 1]
)
axes[1, 1].set_title("Correlação")
plt.suptitle("Análise de Desempenho Escolar", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.savefig("analise_escolar.png", dpi=150, bbox_inches="tight")
plt.show()
Análise Exploratória Completa
def analise_exploratoria(df: pd.DataFrame, coluna_alvo: str):
"""EDA — Exploratory Data Analysis padronizada."""
print("=" * 50)
print("ANÁLISE EXPLORATÓRIA DE DADOS")
print("=" * 50)
# 1. Visão geral
print(f"\n📊 Dimensões: {df.shape[0]} linhas × {df.shape[1]} colunas")
print(f"\n📋 Tipos de dados:\n{df.dtypes}")
print(f"\n🔍 Valores nulos:\n{df.isnull().sum()}")
print(f"\n🔁 Duplicatas: {df.duplicated().sum()}")
# 2. Estatísticas da variável alvo
alvo = df[coluna_alvo]
print(f"\n📈 Estatísticas de '{coluna_alvo}':")
print(f" Média: {alvo.mean():.2f}")
print(f" Mediana: {alvo.median():.2f}")
print(f" Desvio: {alvo.std():.2f}")
print(f" Assimetria: {alvo.skew():.2f}")
print(f" Curtose: {alvo.kurtosis():.2f}")
# 3. Correlações com a variável alvo
numericas = df.select_dtypes(include=[np.number])
correlacoes = numericas.corr()[coluna_alvo].sort_values(ascending=False)
print(f"\n🔗 Correlações com '{coluna_alvo}':\n{correlacoes}")
# 4. Distribuição de categorias
categoricas = df.select_dtypes(include=["object", "string", "category"]) # "string": texto no pandas 3
for col in categoricas.columns:
print(f"\n📌 {col}:")
print(df[col].value_counts().head(5))
analise_exploratoria(df_vis, "nota")
SciPy: Estatística Avançada
from scipy import stats
# Teste t — comparar médias de dois grupos
turma_a = df_vis[df_vis["turma"] == "A"]["nota"]
turma_b = df_vis[df_vis["turma"] == "B"]["nota"]
t_stat, p_valor = stats.ttest_ind(turma_a, turma_b)
print(f"Teste t: t={t_stat:.3f}, p={p_valor:.3f}")
print(f"Diferença significativa: {'Sim' if p_valor < 0.05 else 'Não'}")
# Teste de normalidade
stat, p = stats.shapiro(turma_a)
print(f"\nShapiro-Wilk: stat={stat:.3f}, p={p:.3f}")
# p alto não prova normalidade: só diz que o teste não achou evidência contra
print(f"Normalidade rejeitada: {'Sim' if p < 0.05 else 'Não'}")
# Intervalo de confiança
media = turma_a.mean()
ic = stats.t.interval(
confidence=0.95,
df=len(turma_a) - 1,
loc=media,
scale=stats.sem(turma_a)
)
print(f"\nIC 95%: [{ic[0]:.2f}, {ic[1]:.2f}]")
# Regressão linear
slope, intercept, r, p, se = stats.linregress(
df_vis["horas_estudo"], df_vis["nota"]
)
print(f"\nRegressão Linear:")
print(f" Inclinação: {slope:.3f}")
print(f" Intercepto: {intercept:.3f}")
print(f" R²: {r**2:.3f}")
As quatro bibliotecas deste artigo dividem o trabalho de um jeito estável: o NumPy guarda números em arrays e faz as contas sem laço explícito; o pandas põe esses arrays em tabelas com nome, tipo e índice, e é onde a maior parte do tempo vai — seleção, agrupamento e, sobretudo, limpeza; Matplotlib e Seaborn transformam o resultado em gráfico; e o SciPy responde se uma diferença entre grupos é real ou ruído. A análise exploratória junta tudo num roteiro que se repete antes de qualquer modelo. Quem já leu planilhas com o pandas em Automação de Planilhas e PDFs reconhece o DataFrame; aqui ele passa de meio de transporte a objeto de estudo.
Os erros que mais custam nesta área não levantam exceção. Um fillna(inplace=True) que não altera nada no pandas 3, um desvio padrão calculado com a fórmula populacional num lugar e a amostral no outro, um p-valor alto lido como prova — todos produzem um número plausível e errado. Vale conferir a tabela depois de cada passo de limpeza, dizer qual ddof foi usado e escrever a conclusão de um teste estatístico na forma que ele de fato permite, com os avisos de descontinuação ligados enquanto o código é escrito.
Fontes e leituras recomendadas
- NumPy — documentação oficial — https://numpy.org/doc/stable/
- pandas — documentação oficial — https://pandas.pydata.org/docs/
- Matplotlib — documentação oficial — https://matplotlib.org/stable/
- Seaborn — documentação oficial — https://seaborn.pydata.org/
- SciPy — documentação oficial — https://docs.scipy.org/doc/scipy/
- MCKINNEY, Wes. Python for Data Analysis. 3. ed. O'Reilly Media, 2022. — o livro definitivo sobre pandas, escrito pelo seu criador.
- VANDERPLAS, Jake. Python Data Science Handbook. O'Reilly Media, 2016. — guia completo do ecossistema científico Python, disponível gratuitamente em https://jakevdp.github.io/PythonDataScienceHandbook/
- GÉRON, Aurélien. Hands-On Machine Learning. 3. ed. O'Reilly Media, 2022. Cap. 2 — pipeline completo de Data Science do início ao fim.
Exercícios
Exercício 1
Um script de limpeza que rodava havia dois anos começou a entregar relatórios com nomes vazios depois que o servidor atualizou o pandas para a versão 3. O código não mudou: df["nome"].fillna("Desconhecido", inplace=True). Não há exceção, só um aviso no log que ninguém lê. O que aconteceu?
Ver resposta
✓ Resposta: O pandas 3 adotou o Copy-on-Write como único comportamento: df["nome"] devolve um objeto que se comporta como cópia, e o inplace=True preenche essa cópia, que é descartada em seguida. Medido no pandas 3.0.6 com o exemplo do artigo: a linha sem nome saiu da limpeza ainda com NaN, e o único sinal foi um ChainedAssignmentError — que, apesar do nome, é um aviso, não uma exceção — dizendo que o método "never works to update the original DataFrame". Em versões anteriores o mesmo código às vezes funcionava, o que explica os dois anos de silêncio. A correção é atribuir o resultado, e de preferência numa chamada só: df = df.fillna({"nome": "Desconhecido", "salario": df["salario"].median()}), como no artigo corrigido. Duas defesas para o futuro: rodar os scripts com -W error em homologação, para que avisos derrubem o teste, e conferir a contagem de nulos depois da limpeza, não só antes.
Exercício 2
Dois analistas calculam o desvio padrão das mesmas oito notas — 2, 4, 4, 4, 5, 5, 7 e 9. Um usa np.std(notas) e obtém 2,00; o outro usa df["nota"].std() e obtém 2,14. Os dois juram que o próprio código está certo. Quem tem razão, e o que deveria constar no relatório?
Ver resposta
✓ Resposta: Os dois calcularam certo coisas diferentes. O NumPy usa por padrão ddof=0, a fórmula populacional, que divide a soma dos quadrados dos desvios por n: 32 / 8 = 4, raiz 2,00. O pandas usa ddof=1, a fórmula amostral, que divide por n − 1: 32 / 7 ≈ 4,57, raiz 2,138 — medido nas duas bibliotecas, e np.std(notas, ddof=1) dá exatamente o valor do pandas. Se as oito notas são a turma inteira, a populacional descreve a turma; se são uma amostra usada para estimar algo maior, a amostral corrige o viés de medir o espalhamento em torno da média da própria amostra. A diferença encolhe com n grande e é de 7% com n = 8. O relatório deve dizer qual das duas foi usada, e o código deve passar o ddof explicitamente, para que ninguém dependa do padrão da biblioteca da vez.
Exercício 3
Antes de aplicar um teste t, uma analista roda stats.shapiro nas notas de uma turma de 10 alunos, obtém p = 0,40 e escreve no relatório: "as notas seguem distribuição normal, conforme o teste de Shapiro-Wilk". O revisor pede para reescrever a frase. Por quê?
Ver resposta
✓ Resposta: Porque o teste não pode afirmar isso. A hipótese nula do Shapiro-Wilk é a normalidade; p abaixo de 0,05 permite rejeitá-la, mas p alto só diz que os dados não trouxeram evidência suficiente contra ela. Com amostra pequena, o teste quase nunca tem força para rejeitar: sorteando 2.000 amostras de uma distribuição uniforme — plana, nada parecida com a normal —, o Shapiro deu p acima de 0,05 em 91% das amostras de 10 valores, em 63% das de 30 e em menos de 1% das de 100. A frase honesta é "o teste de Shapiro-Wilk não rejeitou a normalidade (p = 0,40)", e com dez alunos isso diz pouco; um histograma ou um gráfico Q-Q informa mais. Foi por isso que o código do artigo passou a imprimir "Normalidade rejeitada: Sim/Não" em vez de "Distribuição normal: Sim".
Exercício 4
Para uma aula sobre regressão, um professor gera dados sintéticos com np.random.normal para a nota e np.random.uniform para as horas de estudo, cada coluna sorteada por conta própria. O gráfico de dispersão sai como uma nuvem sem forma, a reta do regplot fica quase horizontal e um aluno conclui que "estudar não adianta". O que deu errado?
Ver resposta
✓ Resposta: Os dados não tinham relação nenhuma porque foram gerados assim: duas colunas sorteadas de forma independente. Com a versão original do exemplo deste artigo, a correlação medida entre horas e nota foi −0,03, a inclinação −0,021 ponto por hora e o R² 0,001 — o modelo reencontrou exatamente o que foi posto nos dados, ou seja, nada. A conclusão do aluno é sobre o gerador, não sobre o estudo. Para a aula mostrar uma relação, ela precisa estar no gerador: o artigo passou a calcular a nota a partir das horas, 3.5 + 0.6 * horas mais ruído, e a correlação subiu para 0,72, com inclinação de 0,587 e R² de 0,52. Dados sintéticos servem para testar se o código funciona, não para descobrir coisa alguma sobre o mundo — e vale a lição inversa também: com dados reais, uma correlação forte ainda não diz qual variável causa a outra.
Exercício 5
Um relatório deveria listar os três primeiros funcionários da tabela. O código df.loc[0:3] devolve quatro linhas, e o colega que "corrigiu" para df.loc[0:2] quebrou outra consulta, que usava índice de datas. Qual é a regra e qual é a forma certa de pedir "os três primeiros"?
Ver resposta
✓ Resposta: O loc seleciona por rótulo e inclui as duas pontas; o iloc seleciona por posição e exclui o fim, como o fatiamento de listas do Python. No DataFrame do artigo, df.loc[1:3] devolveu Bruno, Carla e Diego, as linhas 1, 2 e 3, enquanto df.iloc[:3] devolveu as posições 0, 1 e 2. A regra do loc faz sentido para rótulos que não são números — em df.loc["2024-01":"2024-03"], excluir março seria absurdo —, mas engana quando o índice por acaso é 0, 1, 2… e parece posição. "Os três primeiros" é uma pergunta sobre posição, então a resposta é df.iloc[:3] ou df.head(3), que funcionam com qualquer índice. O remendo loc[0:2] só funcionava enquanto o índice fosse 0, 1, 2…, e depois de um filtro ou de uma ordenação ele deixa de ser.