A mentalidade do analista de dados
BásicoFerramenta nenhuma salva uma análise mal formulada. Antes do código, o método.
1.1 O ciclo de uma análise profissional
Toda análise de valor no mercado segue, explícita ou implicitamente, este ciclo:
Analistas júnior entregam números ("o churn foi 4,2%"). Plenos entregam comparações ("subiu 0,8 p.p. vs. abril, concentrado no plano básico"). Sêniores entregam decisões ("o aumento vem do reajuste de preço no plano básico; recomendo testar desconto de retenção segmentado — impacto estimado de R$ 120k/trimestre"). Esta apostila te move nessa escada.
1.2 Tipos de dados e níveis de medida
| Tipo | Exemplos | O que pode fazer | Estatísticas válidas |
|---|---|---|---|
| Nominal | UF, categoria de produto | Contar, comparar igualdade | Moda, frequência |
| Ordinal | NPS (detrator/neutro/promotor), escolaridade | Ordenar | Mediana, percentis |
| Intervalar | Temperatura em °C, datas | Somar/subtrair | Média, desvio-padrão |
| Razão | Receita, quantidade, idade | Todas as operações | Todas + coef. de variação |
Calcular média de dados ordinais (ex.: média de uma escala 1–5 tratada como razão) é aceito na prática, mas reporte também a distribuição. E jamais calcule média de códigos nominais (ex.: média de CEP) — parece óbvio, mas acontece quando colunas categóricas chegam como números.
1.3 Estatística descritiva: o vocabulário mínimo
- Média vs. mediana: com distribuições assimétricas (salários, ticket, tempo de sessão), a mediana representa melhor o "típico". Reporte as duas; a diferença entre elas já é um insight sobre assimetria.
- Desvio-padrão e IQR: dispersão. O IQR (Q3−Q1) é robusto a outliers.
- Percentis: a linguagem de SLAs e metas ("p95 do tempo de entrega").
- Correlação ≠ causalidade: o mantra. O Capítulo 11 mostra como se aproximar de causalidade de verdade.
Pegue um relatório real do seu trabalho. Identifique: (a) qual pergunta de negócio ele responde; (b) que decisão ele deveria influenciar; (c) se a métrica central deveria ser média ou mediana. Reescreva o título do relatório como uma pergunta.
Python essencial para dados
BásicoO mínimo de Python que um analista precisa dominar com fluência — e o ambiente profissional para trabalhar.
2.1 Ambiente profissional
# Ambiente isolado por projeto (padrão de mercado) python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install pandas numpy matplotlib seaborn jupyterlab pyarrow pip freeze > requirements.txt # reprodutibilidade
2.2 Estruturas que você usa todo dia
vendas = [1200, 980, 1450, 2100] # lista meta = {"jan": 1000, "fev": 1100} # dicionário # List/dict comprehension: a construção mais usada em dados acima = [v for v in vendas if v > 1000] pct_meta = {mes: v / 1000 for mes, v in meta.items()} # f-strings para relatórios total = sum(vendas) print(f"Total: R$ {total:,.2f} | Média: R$ {total/len(vendas):,.1f}")
Total: R$ 5,730.00 | Média: R$ 1,432.5
2.3 NumPy: a base de tudo
Pandas é construído sobre NumPy. Entender vetorização — operar sobre arrays inteiros em vez de laços — é o que separa código de analista profissional de script amador.
import numpy as np precos = np.array([19.9, 45.0, 12.5, 89.9]) qtd = np.array([100, 30, 250, 12]) receita = precos * qtd # vetorizado: sem for! receita_com_desc = np.where(receita > 2000, receita * 0.95, receita) print(receita) print(f"p90 da receita: {np.percentile(receita, 90):.0f}")
[1990. 1350. 3125. 1078.8] p90 da receita: 2784
Se você escreveu um for sobre linhas de dados numéricos, pare e procure a operação vetorizada equivalente (np.where, np.select, operações aritméticas diretas). Diferença típica: 10–100× mais rápido.
Dado notas = np.random.normal(7, 1.5, 10_000): (a) calcule % de notas ≥ 9; (b) crie um array de conceitos usando np.select (A ≥ 9, B ≥ 7, C ≥ 5, D abaixo); (c) compare o tempo com uma versão em for usando %timeit.
Pandas: fundamentos sólidos
BásicoSeries, DataFrame, indexação e o modelo mental correto — a base que evita 90% dos bugs de quem "aprendeu no improviso".
3.1 O modelo mental
Um DataFrame é um dicionário de Series (colunas) que compartilham um índice. Quase toda confusão em Pandas vem de ignorar o índice: operações alinham dados pelo índice, não pela posição.
import pandas as pd df = pd.read_csv("vendas.csv", parse_dates=["data"]) # O ritual de reconhecimento — sempre, em todo dataset novo: df.head() df.info() # tipos, nulos, memória df.describe(include="all") df.nunique() # cardinalidade de cada coluna
3.2 Seleção: loc, iloc e máscaras booleanas
# loc = por rótulo | iloc = por posição df.loc[df["uf"] == "SP", ["data", "receita"]] df.iloc[:5, 0:3] # Condições compostas: parênteses obrigatórios, & e | (não and/or) filtro = (df["receita"] > 1000) & (df["canal"].isin(["app", "site"])) df.loc[filtro] # query(): mais legível para filtros longos df.query("receita > 1000 and canal in ['app','site'] and uf != 'SP'")
Nunca encadeie seleção e atribuição: df[df.uf=='SP']['receita'] = 0 pode modificar uma cópia e silenciosamente não fazer nada. Sempre use uma única operação loc:
df.loc[df.uf=='SP', 'receita'] = 0
No Pandas moderno com Copy-on-Write (padrão a partir do Pandas 3), o encadeamento nunca modifica o original — o que torna o padrão correto ainda mais importante.
3.3 Criando e transformando colunas
df["ticket"] = df["receita"] / df["pedidos"] # assign() permite encadear (method chaining — estilo profissional) resumo = ( df .assign( ticket=lambda d: d["receita"] / d["pedidos"], faixa=lambda d: pd.cut(d["ticket"], bins=[0, 50, 150, np.inf], labels=["baixo", "médio", "alto"]), ) .query("receita > 0") .sort_values("receita", ascending=False) )
3.4 Tipos de dados (dtypes) importam
| dtype | Use para | Observação de mercado |
|---|---|---|
int64 / float64 | Números | Int64 (maiúsculo) aceita nulos em inteiros |
datetime64[ns] | Datas | Sempre converta com pd.to_datetime na leitura |
category | Texto repetitivo (UF, canal, status) | Economiza até 95% de memória e acelera groupby |
string | Texto livre | Prefira ao antigo object |
bool | Flags | True/False somam como 1/0: df.flag.mean() = proporção |
Baixe um CSV público (ex.: dados de voos da ANAC ou vendas do Kaggle). Faça o "ritual de reconhecimento", converta pelo menos duas colunas para category e uma para data, e meça a redução de memória com df.memory_usage(deep=True).sum() antes e depois.
Limpeza de dados como profissional
IntermediárioÉ aqui que se ganha o salário. Dados reais são sujos, e a qualidade da limpeza define a credibilidade de tudo que vem depois.
4.1 Diagnóstico sistemático de qualidade
def diagnostico(df): return pd.DataFrame({ "dtype": df.dtypes.astype(str), "nulos": df.isna().sum(), "pct_nulos": (df.isna().mean() * 100).round(1), "unicos": df.nunique(), "duplicados_col": [df[c].duplicated().sum() for c in df.columns], "exemplo": df.apply(lambda s: s.dropna().iloc[0] if s.notna().any() else None), }) diagnostico(df) df.duplicated(subset=["pedido_id"]).sum() # duplicatas de chave
4.2 Valores ausentes: decidir, não só preencher
Antes de tratar, pergunte por que o dado está ausente. Os três mecanismos clássicos:
- MCAR (aleatório): falha de sistema. Seguro remover ou imputar.
- MAR (depende de outras colunas): ex.: renda ausente mais em jovens. Imputar por grupo.
- MNAR (depende do próprio valor): ex.: quem ganha muito omite renda. Perigoso — a ausência é informação. Crie uma flag.
# Estratégias em ordem de sofisticação df["renda_ausente"] = df["renda"].isna() # flag: sempre útil df["renda"] = df["renda"].fillna(df["renda"].median()) # Imputação por grupo (MAR): muito mais defensável df["renda"] = df["renda"].fillna( df.groupby("faixa_etaria")["renda"].transform("median") ) # Séries temporais: propagação limitada df["estoque"] = df["estoque"].ffill(limit=3)
4.3 Outliers: investigar antes de remover
q1, q3 = df["ticket"].quantile([0.25, 0.75]) iqr = q3 - q1 limites = (q1 - 1.5 * iqr, q3 + 1.5 * iqr) outliers = df.query("ticket < @limites[0] or ticket > @limites[1]") # Winsorização: alternativa a remover (preserva a linha, limita o valor) df["ticket_w"] = df["ticket"].clip(*df["ticket"].quantile([0.01, 0.99]))
Outlier removido sem documentação é passivo de auditoria. Em vez de deletar, mantenha três artefatos: o critério (ex.: "1,5×IQR"), a contagem afetada ("removidas 214 linhas, 0,3%") e a análise de sensibilidade ("a conclusão não muda com/sem outliers"). Isso é o que um gestor sênior verifica primeiro.
4.4 Validação automática: dados que se defendem
# Asserts baratos que salvam análises inteiras assert df["pedido_id"].is_unique, "IDs duplicados!" assert (df["receita"] >= 0).all(), "Receita negativa!" assert df["data"].between("2020-01-01", pd.Timestamp.today()).all() # Em pipelines sérios: biblioteca pandera define schemas validáveis import pandera as pa schema = pa.DataFrameSchema({ "receita": pa.Column(float, pa.Check.ge(0)), "uf": pa.Column(str, pa.Check.isin(UFS_VALIDAS)), }) df = schema.validate(df)
Crie a função diagnostico() no seu ambiente e aplique num dataset do trabalho. Documente: 3 problemas de qualidade encontrados, o mecanismo provável de cada ausência (MCAR/MAR/MNAR) e a estratégia escolhida com justificativa.
GroupBy, merge e reshape
IntermediárioO trio que resolve 80% das demandas analíticas do dia a dia corporativo.
5.1 GroupBy: split → apply → combine
# Agregação nomeada: o padrão moderno e legível kpis = ( df.groupby(["uf", "canal"], observed=True) .agg( receita_total=("receita", "sum"), ticket_medio=("receita", "mean"), pedidos=("pedido_id", "nunique"), p90_ticket=("receita", lambda s: s.quantile(0.9)), ) .reset_index() )
transform: agregado com o shape original
A ferramenta mais subestimada do Pandas. Devolve o resultado do grupo alinhado a cada linha — perfeito para "% do total", rankings e comparações com a média do grupo.
df["pct_da_uf"] = df["receita"] / df.groupby("uf")["receita"].transform("sum") df["vs_media_canal"] = df["receita"] - df.groupby("canal")["receita"].transform("mean") df["rank_no_mes"] = df.groupby("mes")["receita"].rank(ascending=False) # Top 3 produtos por categoria — pergunta de entrevista clássica top3 = (df.sort_values("receita", ascending=False) .groupby("categoria") .head(3))
5.2 Merge: joins sem perder linhas sem querer
base = pedidos.merge(
clientes,
on="cliente_id",
how="left",
validate="m:1", # explode se clientes tiver ID duplicado!
indicator=True, # coluna _merge: auditoria do join
)
base["_merge"].value_counts()
# both 98_412
# left_only 188 ← pedidos órfãos: investigar antes de seguir| how= | Mantém | Uso típico |
|---|---|---|
left | Todas as linhas da esquerda | Enriquecer a tabela-fato (o mais comum) |
inner | Só correspondências | Quando ambos os lados são obrigatórios |
outer | Tudo dos dois lados | Reconciliação entre sistemas |
cross | Produto cartesiano | Grades de combinações (cuidado com o tamanho) |
Explosão de linhas em join m:m. Se a chave se repete nos dois lados, as linhas se multiplicam e sua receita "cresce" magicamente. Defesa: validate= sempre, e compare len(base) antes/depois do merge.
5.3 Reshape: pivot, melt e as tabelas que executivos pedem
# Longo → largo: a "tabela de gestor" (UF nas linhas, mês nas colunas) matriz = df.pivot_table( index="uf", columns="mes", values="receita", aggfunc="sum", fill_value=0, margins=True, margins_name="Total" ) # Largo → longo: formato ideal para análise e gráficos (tidy data) tidy = matriz.drop("Total").reset_index().melt( id_vars="uf", var_name="mes", value_name="receita" )
Para analisar: formato longo (1 linha = 1 observação). Para apresentar: formato largo. Dominar a ida e volta entre os dois é uma habilidade de entrevista técnica recorrente.
Com um dataset de vendas: (a) monte a tabela de KPIs por região com agregação nomeada; (b) calcule o % que cada produto representa dentro da sua categoria usando transform; (c) faça um left join com cadastro de clientes usando validate e indicator, e reporte quantos registros ficaram órfãos.
Datas, texto e SQL
IntermediárioOs três idiomas complementares do analista: tempo, strings e o SQL que nenhuma vaga dispensa.
6.1 Datas com o acessor .dt
df["data"] = pd.to_datetime(df["data"], format="%d/%m/%Y") df["ano_mes"] = df["data"].dt.to_period("M") df["dia_semana"] = df["data"].dt.day_name() df["fim_de_semana"] = df["data"].dt.dayofweek >= 5 # Aritmética de datas: idade da conta em dias df["dias_como_cliente"] = (df["data"] - df["data_cadastro"]).dt.days # Deslocamentos de negócio df["vencimento"] = df["data"] + pd.offsets.BusinessDay(5)
CSV nacional costuma vir dd/mm/aaaa. Sem format= (ou dayfirst=True), o Pandas pode interpretar 03/04 como 4 de março. Esse bug já derrubou relatórios de diretoria — seja explícito sempre.
6.2 Texto com o acessor .str
# Padronização — o kit básico de higiene df["cidade"] = (df["cidade"].str.strip().str.title() .str.normalize("NFKD") # remove acentos… .str.encode("ascii", "ignore").str.decode("ascii")) # Extração com regex: CNPJ no meio de texto livre df["cnpj"] = df["descricao"].str.extract(r"(\d{2}\.?\d{3}\.?\d{3}/?\d{4}-?\d{2})") df["contem_cancelamento"] = df["obs"].str.contains("cancel", case=False, na=False) df[["ddd", "numero"]] = df["telefone"].str.split(")", expand=True)
6.3 SQL: o idioma obrigatório
Em quase toda empresa, os dados moram num banco. O fluxo profissional é: SQL agrega e filtra no servidor → Pandas refina e analisa localmente. Puxar a tabela inteira para filtrar no Pandas é erro clássico de júnior.
import sqlalchemy as sa engine = sa.create_engine("postgresql://user:senha@host:5432/db") query = """ WITH mensal AS ( SELECT cliente_id, date_trunc('month', data) AS mes, SUM(receita) AS receita FROM pedidos WHERE data >= '2025-01-01' GROUP BY 1, 2 ) SELECT *, SUM(receita) OVER (PARTITION BY cliente_id ORDER BY mes) AS receita_acum, LAG(receita) OVER (PARTITION BY cliente_id ORDER BY mes) AS receita_ant, ROW_NUMBER() OVER (PARTITION BY mes ORDER BY receita DESC) AS rank_mes FROM mensal """ df = pd.read_sql(query, engine)
| Conceito SQL | Equivalente Pandas |
|---|---|
GROUP BY + HAVING | groupby().agg() + .query() |
ROW_NUMBER() OVER (PARTITION BY...) | groupby().cumcount() / rank() |
LAG / LEAD | groupby().shift(1) / shift(-1) |
SUM() OVER (ORDER BY...) | groupby().cumsum() |
LEFT JOIN ... ON | merge(how="left") |
Escreva a mesma análise duas vezes — "receita mensal por cliente com variação vs. mês anterior e ranking dentro do mês" — uma em SQL (window functions) e outra em Pandas (groupby + shift + rank). Compare os resultados linha a linha com pd.testing.assert_frame_equal.
Visualização que convence
IntermediárioGráfico não é decoração: é argumento. O objetivo é reduzir o tempo entre o olhar e a conclusão.
7.1 Escolhendo o gráfico pela pergunta
| Pergunta | Gráfico certo | Evite |
|---|---|---|
| Como evoluiu no tempo? | Linha | Barras para muitos períodos |
| Como se compara entre categorias? | Barras horizontais ordenadas | Pizza (humanos comparam mal ângulos) |
| Como se distribui? | Histograma, boxplot, ECDF | Só a média numa tabela |
| Há relação entre duas variáveis? | Dispersão (+ linha de tendência) | Duas linhas com eixos duplos |
| Qual a composição do total? | Barras empilhadas 100% (poucas categorias) | Pizza 3D (nunca) |
7.2 Matplotlib + Seaborn: o fluxo profissional
import matplotlib.pyplot as plt import seaborn as sns sns.set_theme(style="whitegrid", palette="deep") fig, ax = plt.subplots(figsize=(9, 5)) sns.lineplot(data=mensal, x="mes", y="receita", hue="canal", ax=ax) # O que separa um gráfico amador de um profissional: ax.set_title("Receita do canal app cresce 34% e ultrapassa o site em maio", loc="left", fontsize=13, fontweight="bold") # título = conclusão! ax.set_xlabel("") ax.set_ylabel("Receita (R$ mil)") ax.yaxis.set_major_formatter(lambda x, _: f"{x/1000:,.0f}") ax.spines[["top", "right"]].set_visible(False) ax.annotate("lançamento do cupom", xy=("2025-05", 480_000), xytext=(10, 15), textcoords="offset points", fontsize=9) fig.tight_layout() fig.savefig("receita_canal.png", dpi=150)
7.3 Os princípios que valem mais que qualquer biblioteca
- Data-ink ratio (Tufte): remova tudo que não é dado — bordas, grades pesadas, sombras, 3D.
- Eixo Y de barras começa em zero. Sempre. Linhas podem recortar, barras não.
- Cor com propósito: destaque 1 série na cor da marca, apague o resto em cinza. Um gráfico com 8 cores vibrantes não destaca nada.
- Rotule direto na linha em vez de legenda ao lado, quando possível.
- Uma mensagem por gráfico. Se precisa de dois insights, faça dois gráficos.
7.4 Interatividade e dashboards
# Plotly: exploração interativa e relatórios HTML import plotly.express as px fig = px.scatter(df, x="desconto", y="margem", color="categoria", size="receita", hover_data=["produto"], trendline="ols") fig.write_html("analise_margem.html") # Streamlit: de notebook a app interno em ~30 linhas # streamlit run app.py
Para crescer internamente, o combo vencedor costuma ser: Python para a análise profunda + a ferramenta de BI da casa (Power BI/Looker/Tableau) para o consumo recorrente. Automatize com Python o que alimenta o BI, e reserve o notebook para as perguntas novas. Analistas que fazem essa ponte viram referência do time.
Pegue um gráfico existente de um relatório seu e aplique o "makeover": título-conclusão, eixos limpos, uma cor de destaque + cinzas, anotação no ponto-chave. Mostre antes/depois para um colega e cronometre quanto tempo cada versão leva para ser entendida.
Pandas de alta performance
AvançadoQuando o dataset passa de milhões de linhas, saber como o Pandas funciona por baixo vira vantagem competitiva.
8.1 Memória: o primeiro gargalo
df.memory_usage(deep=True).sum() / 1e6 # MB reais # Downcast + category: reduções de 50–90% são comuns def otimizar(df, limite_cat=0.5): for col in df.select_dtypes("integer"): df[col] = pd.to_numeric(df[col], downcast="integer") for col in df.select_dtypes("float"): df[col] = pd.to_numeric(df[col], downcast="float") for col in df.select_dtypes("object"): if df[col].nunique() / len(df) < limite_cat: df[col] = df[col].astype("category") return df # Melhor ainda: nem carregar o que não precisa df = pd.read_parquet("vendas.parquet", columns=["data", "uf", "receita"], filters=[("ano", "=", 2025)])
8.2 Vetorização: a hierarquia de velocidade
| Abordagem | Velocidade relativa | Quando usar |
|---|---|---|
Operações vetorizadas / np.where / np.select | 1× (referência) | Sempre que possível |
.map() com dicionário | ~2–5× mais lento | Mapeamentos de/para |
.apply() em Series | ~30–100× mais lento | Lógica realmente complexa |
.apply(axis=1) em linhas | ~100–1000× mais lento | Último recurso |
for com iterrows() | pior caso | Praticamente nunca |
# ANTES (lento): lógica condicional com apply em linhas df["frete"] = df.apply(lambda r: 0 if r.receita > 200 else 15 if r.uf == "SP" else 25, axis=1) # DEPOIS (vetorizado): np.select com condições ordenadas cond = [df.receita > 200, df.uf.eq("SP")] df["frete"] = np.select(cond, [0, 15], default=25)
apply/linha: 4.31 s np.select : 0.019 s (≈ 226× mais rápido)
8.3 Além do Pandas: o ecossistema moderno
- PyArrow backend:
pd.read_csv(..., dtype_backend="pyarrow")— strings muito mais leves e I/O acelerado; é a direção oficial do Pandas. - Polars: engine em Rust, sintaxe expressiva, paralelismo nativo e execução lazy. Para transformações pesadas, ganhos de 5–30× são rotina. Vale ter no radar: cresce rápido em vagas.
- DuckDB: SQL analítico rodando em cima dos seus DataFrames/Parquet, sem servidor:
duckdb.sql("SELECT uf, SUM(receita) FROM df GROUP BY 1"). Espetacular para dados maiores que a RAM. - Parquet > CSV: colunar, comprimido, tipado. Leitura tipicamente 10–50× mais rápida e arquivos ~5× menores. CSV apenas como interface com humanos/sistemas legados.
import duckdb # 40 GB de parquet num laptop de 16 GB de RAM? DuckDB resolve: top = duckdb.sql(""" SELECT uf, date_trunc('month', data) AS mes, SUM(receita) AS receita FROM 'dados/vendas_*.parquet' GROUP BY 1, 2 ORDER BY receita DESC """).df() # .df() devolve um DataFrame Pandas
Saber dizer por que um job passou de 40 min para 40 s (Parquet + colunas seletivas + vetorização + DuckDB) é história de promoção. Performance traduzida em custo de infra e tempo de time é linguagem que gestor entende.
Gere um DataFrame sintético de 5 milhões de linhas. Meça com %timeit: (a) uma classificação condicional via apply(axis=1) vs np.select; (b) o mesmo groupby em Pandas vs DuckDB; (c) tamanho e tempo de leitura CSV vs Parquet. Monte uma tabelinha com os resultados — esse mini-benchmark é ótimo material de portfólio interno.
Estatística aplicada a decisões
AvançadoA diferença entre "parece que subiu" e "subiu, com 95% de confiança, entre 2,1 e 3,4 pontos".
9.1 Distribuições: conheça seus dados
- Normal: médias de amostras grandes (TCL). Base dos testes clássicos.
- Log-normal: receita, ticket, tempo de sessão — quase tudo em negócios é assimétrico à direita. Trabalhe com
np.log1pquando modelar. - Binomial/Bernoulli: conversões, churn, cliques — a matéria-prima de testes A/B.
- Poisson: contagens por intervalo (chamados/dia, defeitos/lote).
9.2 Intervalos de confiança: sempre reporte incerteza
from scipy import stats conv = df["converteu"] # série 0/1 n, p = len(conv), conv.mean() # IC 95% para proporção (aprox. normal) ep = np.sqrt(p * (1 - p) / n) ic = (p - 1.96 * ep, p + 1.96 * ep) print(f"Conversão: {p:.2%} (IC95%: {ic[0]:.2%} – {ic[1]:.2%})") # Bootstrap: funciona para QUALQUER métrica (mediana, p90, razões…) res = stats.bootstrap((df["ticket"].values,), np.median, n_resamples=5_000, confidence_level=0.95) print(f"Mediana do ticket: IC95% {res.confidence_interval}")
9.3 Testes de hipótese: o mapa de decisão
| Comparação | Teste | scipy/statsmodels |
|---|---|---|
| Média de 2 grupos | t de Welch | stats.ttest_ind(a, b, equal_var=False) |
| Mediana/dados assimétricos | Mann-Whitney U | stats.mannwhitneyu(a, b) |
| 2 proporções (conversão A vs B) | z de proporções | proportions_ztest([x1,x2],[n1,n2]) |
| Associação entre 2 categóricas | Qui-quadrado | stats.chi2_contingency(tabela) |
| 3+ grupos | ANOVA / Kruskal-Wallis | stats.f_oneway / stats.kruskal |
| Pareado (antes/depois no mesmo sujeito) | t pareado / Wilcoxon | stats.ttest_rel / stats.wilcoxon |
p-valor = probabilidade de observar um efeito tão extremo quanto o visto se não houvesse efeito real. Ele não é a probabilidade da hipótese ser verdadeira, e p < 0,05 não significa efeito grande ou importante. Com n gigante, diferenças irrelevantes ficam "significativas" — por isso, reporte sempre o tamanho do efeito (diferença absoluta, lift %, Cohen's d) junto com o IC.
9.4 Regressão: o cavalo de batalha analítico
import statsmodels.formula.api as smf # Regressão com interpretação de negócio (não é ML: é explicação) modelo = smf.ols( "np.log1p(receita) ~ desconto + C(canal) + C(uf) + dias_como_cliente", data=df ).fit(cov_type="HC3") # erros-padrão robustos print(modelo.summary()) # Leitura: coef de desconto = -0.021 → # cada ponto de desconto ↘ receita em ~2,1%, controlando canal, UF e tenure
Pontos que separam o uso amador do profissional: controlar variáveis de confusão (o C(canal) acima), usar log na resposta para dados monetários, checar multicolinearidade (VIF) e nunca ler coeficiente de modelo observacional como causal sem desenho adequado (Capítulo 11).
Com dados reais do trabalho: (a) calcule o IC95% via bootstrap de uma métrica que hoje você reporta como número seco; (b) rode um Mann-Whitney comparando dois segmentos; (c) monte uma OLS com 3–4 controles e escreva a interpretação de um coeficiente em uma frase que um gerente entenda.
Séries temporais
AvançadoQuase toda métrica de negócio vive no tempo. Dominar resample, janelas e decomposição é obrigatório para nível sênior.
10.1 Resample e janelas móveis
ts = df.set_index("data").sort_index() # Reamostragem: de eventos para grade regular diario = ts["receita"].resample("D").sum() mensal = ts["receita"].resample("MS").agg(["sum", "count", "mean"]) # Média móvel: suavizar sem mentir (window centrado p/ análise) diario_mm7 = diario.rolling(7).mean() diario_mm28 = diario.rolling(28, min_periods=20).mean() # Comparações que o negócio pede mom = mensal["sum"].pct_change() # vs mês anterior yoy = mensal["sum"].pct_change(12) # vs mesmo mês do ano anterior acum_12m = mensal["sum"].rolling(12).sum() # LTM (últimos 12 meses)
10.2 Decomposição: tendência × sazonalidade × ruído
from statsmodels.tsa.seasonal import STL stl = STL(diario, period=7, robust=True).fit() stl.plot() # Detecção de anomalia simples e eficaz: resíduo fora de 3 desvios resid = stl.resid anomalias = diario[np.abs(resid) > 3 * resid.std()]
A decomposição responde a pergunta que mais chega no Slack do analista: "a queda de ontem é problema ou é só segunda-feira?" Separe o efeito calendário (sazonalidade) da tendência real antes de acionar alarmes.
10.3 Previsão pragmática
- Baseline primeiro: naive sazonal (repetir o valor do mesmo dia da semana passada) é imbatível como referência. Nenhum modelo merece produção sem vencer o baseline.
- Suavização exponencial (ETS/Holt-Winters):
statsmodels.tsa.ExponentialSmoothing— robusto, interpretável, ótimo para horizonte curto. - SARIMA: quando há autocorrelação relevante além da sazonalidade simples.
- Prophet / gradient boosting com features de calendário: práticos para múltiplas séries com feriados (importantíssimo no varejo brasileiro).
# Validação correta de séries temporais: NUNCA embaralhe o tempo from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5, test_size=28) for treino_idx, teste_idx in tscv.split(diario): ... # treina no passado, testa no futuro, janela deslizante
Usar train_test_split aleatório em séries temporais deixa o modelo "ver o futuro" e infla métricas. É um dos erros mais comuns — e mais detectados em entrevistas técnicas de nível sênior.
Com uma métrica diária do seu trabalho: (a) decomponha com STL e identifique o dia mais anômalo do último trimestre; (b) construa a previsão naive sazonal para os próximos 14 dias; (c) tente vencê-la com Holt-Winters medindo MAE em validação temporal. Reporte se venceu — e por quanto.
Testes A/B e inferência causal
ExpertA habilidade que mais diferencia analistas sêniores: transformar "correlacionou" em "causou" com rigor.
11.1 Anatomia de um teste A/B sério
from statsmodels.stats.power import NormalIndPower from statsmodels.stats.proportion import proportion_effectsize, proportions_ztest # 1) Tamanho de amostra: base 3% de conversão, queremos detectar +0,5 p.p. efeito = proportion_effectsize(0.030, 0.035) n_por_grupo = NormalIndPower().solve_power(efeito, power=0.8, alpha=0.05) print(f"≈ {n_por_grupo:,.0f} usuários por grupo") # 2) Análise ao final stat, pval = proportions_ztest([conv_B, conv_A], [n_B, n_A]) lift = conv_B/n_B - conv_A/n_A print(f"Lift: {lift:+.2%} | p = {pval:.4f}")
≈ 21,216 usuários por grupo Lift: +0.58% | p = 0.0031
- Peeking: checar o p-valor todo dia e parar no primeiro p<0,05. Se precisa de monitoramento contínuo, use métodos sequenciais (mSPRT) ou correções apropriadas.
- Múltiplas comparações: 20 métricas × 5 segmentos = ~1 falso positivo garantido a 5%. Corrija (Bonferroni/FDR) ou pré-registre poucas hipóteses.
- SRM (Sample Ratio Mismatch): proporção observada ≠ planejada → bug na randomização. Teste com qui-quadrado antes de qualquer análise.
- Efeito novidade: lift da primeira semana que evapora. Rode tempo suficiente e olhe a curva do efeito.
11.2 Quando não dá para randomizar: quase-experimentos
A realidade corporativa: a feature já foi lançada, o preço já mudou, e agora perguntam "qual foi o impacto?". O kit causal do analista expert:
| Método | Cenário | Ideia central |
|---|---|---|
| Diferenças-em-diferenças | Mudança atingiu um grupo/região e outro não | Compara a variação dos tratados com a variação dos controles (exige tendências paralelas pré-evento) |
| Controle sintético / CausalImpact | Intervenção em uma unidade (1 cidade, 1 loja) | Constrói um "gêmeo" contrafactual a partir de séries não afetadas |
| Regressão descontínua | Regra de corte (score ≥ X ganha crédito) | Compara quem ficou logo acima vs. logo abaixo do corte |
| Matching / PSM | Adoção voluntária de um recurso | Pareia usuários tratados com controles estatisticamente similares |
# Diff-in-diff via regressão (o jeito profissional) did = smf.ols( "receita ~ tratado * pos_evento + C(uf) + C(mes)", data=painel ).fit(cov_type="cluster", cov_kwds={"groups": painel["uf"]}) # O coeficiente de tratado:pos_evento É o efeito causal estimado
"Medimos o impacto do lançamento com diff-in-diff, efeito de +R$ 340k/mês (IC95%: 210k–470k), robusto a placebo tests" é uma frase que muda o patamar da sua avaliação. Empresas pagam prêmio por quem responde perguntas causais com honestidade metodológica — inclusive quando a resposta é "não dá para afirmar impacto com esses dados".
(a) Dimensione um A/B real: com a conversão atual de uma métrica sua e um MDE que justifique o esforço, calcule n e duração; (b) identifique um lançamento passado da empresa e desenhe (no papel) como estimaria o impacto com diff-in-diff: quem seria controle, qual a janela, como testaria tendências paralelas.
Machine learning para analistas
ExpertNão para virar cientista de dados — para responder perguntas de negócio que estatística clássica não alcança: quem vai cancelar, quanto vale cada cliente, quais segmentos existem.
12.1 O fluxo scikit-learn correto
from sklearn.model_selection import train_test_split, cross_val_score from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler from sklearn.ensemble import HistGradientBoostingClassifier num = ["tenure_dias", "ticket_medio", "chamados_90d"] cat = ["plano", "canal_aquisicao", "uf"] prep = ColumnTransformer([ ("num", StandardScaler(), num), ("cat", OneHotEncoder(handle_unknown="ignore"), cat), ]) pipe = Pipeline([("prep", prep), ("modelo", HistGradientBoostingClassifier())]) X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42) pipe.fit(X_tr, y_tr)
12.2 Avaliação que reflete o negócio
from sklearn.metrics import (roc_auc_score, average_precision_score, classification_report) proba = pipe.predict_proba(X_te)[:, 1] print(f"ROC-AUC: {roc_auc_score(y_te, proba):.3f}") print(f"PR-AUC : {average_precision_score(y_te, proba):.3f}") # melhor p/ classes raras # A pergunta certa não é "qual a acurácia?" e sim: # "com capacidade de contatar 500 clientes/mês, quanto churn evitamos # se contatarmos os 500 de maior score?" → análise de lift por decil deciles = (pd.DataFrame({"proba": proba, "real": y_te}) .assign(decil=lambda d: pd.qcut(d.proba, 10, labels=False)) .groupby("decil")["real"].agg(["mean", "count"]))
Com 3% de churn, um modelo que prevê "ninguém cancela" tem 97% de acurácia e valor zero. Para eventos raros, olhe PR-AUC, recall no top-k e a curva de lift. E cuidado com o clássico vazamento: usar features geradas depois do evento (ex.: "dias desde o cancelamento") — AUC de 0,99 é quase sempre bug, não gênio.
12.3 Interpretabilidade: o que move a predição
import shap explainer = shap.TreeExplainer(pipe.named_steps["modelo"]) sv = explainer(prep.transform(X_te)) shap.plots.beeswarm(sv) # "chamados_90d ≥ 3 é o maior driver de churn; tenure protege" # → isso vira ação de negócio, não só um score
12.4 Segmentação não supervisionada
from sklearn.cluster import KMeans # RFM: a segmentação que todo negócio B2C entende rfm = (df.groupby("cliente_id") .agg(recencia=("data", lambda s: (hoje - s.max()).days), frequencia=("pedido_id", "nunique"), valor=("receita", "sum"))) X = StandardScaler().fit_transform(np.log1p(rfm)) rfm["cluster"] = KMeans(n_clusters=4, n_init="auto", random_state=42).fit_predict(X) rfm.groupby("cluster").median() # perfil de cada segmento → nomeie-os!
Monte um modelo de churn (ou propensão) ponta a ponta: pipeline com ColumnTransformer, validação estratificada, PR-AUC + lift por decil, beeswarm SHAP e — o mais importante — um parágrafo final: "se contatarmos o decil de maior risco, o retorno esperado é X". Esse projeto, documentado, é peça central de portfólio para promoção.
Engenharia e automação para analistas
ExpertO que transforma análises pontuais em ativos da empresa — e você em alguém difícil de substituir.
13.1 Do notebook ao código de produção
Notebook é ótimo para explorar e péssimo para manter. O padrão de maturidade:
2026-07-06_investigacao_churn_plano_basico.ipynb.src/limpeza.py, importada pelo notebook. Uma transformação = uma função testável.python relatorio.py --mes 2026-06. Roda sem intervenção humana, loga o que fez, falha ruidosamente.# Estrutura de projeto que times de dados respeitam projeto/ ├── data/ # raw/ e processed/ (fora do git!) ├── notebooks/ # exploração datada ├── src/ │ ├── extracao.py │ ├── limpeza.py # funções puras: df entra, df sai │ └── metricas.py ├── tests/ │ └── test_limpeza.py ├── requirements.txt └── README.md # como rodar, decisões, fontes
13.2 Testes: a rede de segurança das suas métricas
# tests/test_limpeza.py — rode com: pytest import pandas as pd from src.limpeza import classificar_faixa def test_faixas_cobrem_bordas(): entrada = pd.DataFrame({"ticket": [0, 50, 50.01, 150, 9999]}) saida = classificar_faixa(entrada) assert saida["faixa"].tolist() == ["baixo","baixo","médio","médio","alto"] def test_nao_perde_linhas(): entrada = pd.read_parquet("tests/fixtures/amostra.parquet") assert len(classificar_faixa(entrada)) == len(entrada)
13.3 Git: versionamento não é opcional
- O básico diário:
git add -p(revisar o que commita), commits pequenos com mensagem no formato "o que + por quê", branch por análise. - Nunca no repositório: dados brutos, credenciais, outputs pesados — use
.gitignoree variáveis de ambiente (python-dotenv). - Pull request de análise: mesmo sozinho, abrir PR do seu próprio código cria histórico auditável — e visibilidade com o time de engenharia.
13.4 Relatórios que se atualizam sozinhos
# O trio da automação leve: # 1. papermill: executa notebooks com parâmetros papermill relatorio.ipynb saida_2026-06.ipynb -p mes "2026-06" # 2. Quarto/nbconvert: notebook → HTML/PDF apresentável quarto render relatorio.ipynb --to html # 3. GitHub Actions (agenda semanal) envia por e-mail/Slack # on: schedule: - cron: "0 10 * * 1"
Conte as horas: se você gasta 4h/semana montando o mesmo relatório, automatizá-lo devolve ~200h/ano — quase 5 semanas de trabalho. Apresente essa conta ao seu gestor depois de automatizar. Poucas coisas constroem reputação mais rápido do que devolver tempo ao time com confiabilidade maior que o processo manual.
Escolha o relatório mais repetitivo da sua rotina: (a) extraia a lógica para funções em src/; (b) escreva 3 testes das regras de negócio críticas; (c) parametrize por período; (d) agende. Documente o tempo economizado por mês.
Crescendo no mercado
CarreiraTécnica te torna competente. O que promove é técnica × comunicação × impacto visível.
14.1 A escada de senioridade na prática
| Nível | O que entrega | Como é avaliado |
|---|---|---|
| Júnior | Executa análises pedidas, com supervisão | Corretude e prazo |
| Pleno | Resolve problemas ambíguos sozinho, questiona a pergunta | Autonomia e qualidade das recomendações |
| Sênior | Define métricas, desenha experimentos, influencia roadmap | Impacto em decisões e no time |
| Staff/Especialista | Padrões e infraestrutura analítica da empresa, mentoria | Multiplicação: quanto melhora o trabalho dos outros |
Note o padrão: a partir do pleno, ninguém é promovido por saber mais Pandas. É promovido por reduzir incerteza em decisões caras — Pandas é o meio.
14.2 Comunicação executiva: a pirâmide
Estruture toda apresentação de análise no formato resposta primeiro (Princípio de Minto):
Se você não consegue resumir a análise em duas frases faladas, a análise não terminou. "O que encontramos → o que fazer a respeito." Treine em voz alta antes de toda reunião importante.
14.3 Torne o impacto visível (sem arrogância)
- Diário de impacto: um doc pessoal onde toda sexta você anota: análise → decisão influenciada → resultado (R$, horas, risco evitado). No ciclo de avaliação, você terá evidências; quem não anota, esquece 80%.
- Quantifique em moeda ou tempo: "melhorei o dashboard" vale menos que "reduzi de 3 dias para 2h o tempo de fechamento, liberando o time para X".
- Compartilhe conhecimento: um snippet útil no canal do time, uma sessão de 20 min sobre window functions. Multiplicar é o comportamento avaliado para sênior+.
- Escolha problemas caros: pergunte ao seu gestor "qual decisão do trimestre tem mais dinheiro em jogo e menos dados?" — e vá lá.
14.4 Portfólio interno e preparação para entrevistas
- 3 projetos-âncora documentados: um de automação (Cap. 13), um de experimento/causal (Cap. 11), um de modelo acionável (Cap. 12). Cada um com README: problema, método, resultado em números.
- Entrevistas técnicas cobram, nesta ordem: SQL com window functions, manipulação Pandas (groupby/merge/reshape), um caso de métricas ("o DAU caiu 10%, o que você investiga?"), estatística de A/B, e comunicação de um projeto seu.
- Método para casos de métricas: clarifique a definição → segmente (plataforma, região, coorte, canal) → separe denominador vs. numerador → cheque sazonalidade e mudanças de tracking → só então hipóteses de negócio.
14.5 Roteiro de estudo sugerido (12 semanas)
| Semanas | Foco | Entregável |
|---|---|---|
| 1–2 | Caps. 4–5 a fundo + snippets pessoais | Função diagnostico() + template de EDA |
| 3–4 | Caps. 6–7 (SQL windows + viz) | Makeover de 3 gráficos reais do trabalho |
| 5–6 | Cap. 8 (performance) | Um job real otimizado + benchmark documentado |
| 7–8 | Caps. 9–10 (estatística + tempo) | Relatório com ICs e decomposição de uma métrica-chave |
| 9–10 | Cap. 11 (experimentação) | Desenho de A/B ou diff-in-diff de um caso real |
| 11–12 | Caps. 12–13 (ML + automação) | Modelo de propensão + 1 relatório automatizado |
O analista que cresce é o que responde três perguntas em toda entrega: "E daí?" (por que isso importa), "Quanto?" (tamanho do efeito, com incerteza) e "O que fazemos?" (recomendação acionável). A técnica desta apostila existe para que essas três respostas sejam verdadeiras e defensáveis.
Escreva hoje a primeira entrada do seu diário de impacto e escolha o projeto-âncora nº 1 entre os exercícios dos capítulos 11–13. Marque na agenda a conversa com seu gestor sobre "a decisão mais cara com menos dados" do trimestre.