APOSTILA COMPLETA · PYTHON PARA DADOS

Análise de dados do básico ao expert

Um guia progressivo e orientado ao mercado: Pandas profundo, estatística que sustenta decisões, experimentação, ML aplicado e as habilidades que fazem um analista ser promovido.

Básico Intermediário Avançado Expert 14 capítulos · exercícios em todos
CAP 01

A mentalidade do analista de dados

Básico

Ferramenta 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:

Pergunta de negócio"Por que o churn subiu em maio?" — nunca "me mostra os dados de churn". Quem define bem a pergunta controla a análise.
HipótesesListe 3–5 causas plausíveis antes de abrir o notebook. Isso evita a "pescaria de padrões" que gera falsos positivos.
Coleta e preparação60–80% do tempo real de trabalho. Capítulos 4–6 desta apostila.
Exploração e modelagemEDA, estatística, modelos. Capítulos 7–12.
Comunicação e decisãoUma análise que não muda uma decisão tem valor zero para a empresa. Capítulo 14.
Visão de mercado

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

TipoExemplosO que pode fazerEstatísticas válidas
NominalUF, categoria de produtoContar, comparar igualdadeModa, frequência
OrdinalNPS (detrator/neutro/promotor), escolaridadeOrdenarMediana, percentis
IntervalarTemperatura em °C, datasSomar/subtrairMédia, desvio-padrão
RazãoReceita, quantidade, idadeTodas as operaçõesTodas + coef. de variação
Erro clássico

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.
Exercício 1

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.

CAP 02

Python essencial para dados

Básico

O 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
Alternativas modernas: uv (muito mais rápido) ou conda/mamba para ambientes científicos.

2.2 Estruturas que você usa todo dia

In [1]:
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}")
Out:
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.

In [2]:
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}")
Out:
[1990. 1350. 3125. 1078.8]
p90 da receita: 2784
Regra de ouro

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.

Exercício 2

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.

CAP 03

Pandas: fundamentos sólidos

Básico

Series, 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.

In [3]:
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

In [4]:
# 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'")
O erro nº 1: SettingWithCopyWarning

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

In [5]:
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)
)
Method chaining: cada passo transforma e devolve o DataFrame. Legível, testável e sem variáveis intermediárias df2, df3, df_final_v2...

3.4 Tipos de dados (dtypes) importam

dtypeUse paraObservação de mercado
int64 / float64NúmerosInt64 (maiúsculo) aceita nulos em inteiros
datetime64[ns]DatasSempre converta com pd.to_datetime na leitura
categoryTexto repetitivo (UF, canal, status)Economiza até 95% de memória e acelera groupby
stringTexto livrePrefira ao antigo object
boolFlagsTrue/False somam como 1/0: df.flag.mean() = proporção
Exercício 3

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.

CAP 04

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

In [6]:
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
Tenha essa função no seu snippet pessoal. Rodá-la em todo dataset novo evita surpresas na apresentação final.

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.
In [7]:
# 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

In [8]:
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]))
Visão de mercado

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

In [9]:
# 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)
Exercício 4

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.

CAP 05

GroupBy, merge e reshape

Intermediário

O trio que resolve 80% das demandas analíticas do dia a dia corporativo.

5.1 GroupBy: split → apply → combine

In [10]:
# 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.

In [11]:
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

In [12]:
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
validate= e indicator= são a diferença entre um join profissional e um bug silencioso que duplica receita.
how=MantémUso típico
leftTodas as linhas da esquerdaEnriquecer a tabela-fato (o mais comum)
innerSó correspondênciasQuando ambos os lados são obrigatórios
outerTudo dos dois ladosReconciliação entre sistemas
crossProduto cartesianoGrades de combinações (cuidado com o tamanho)
O bug que mais custa caro

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

In [13]:
# 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"
)
Princípio tidy data

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.

Exercício 5

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.

CAP 06

Datas, texto e SQL

Intermediário

Os três idiomas complementares do analista: tempo, strings e o SQL que nenhuma vaga dispensa.

6.1 Datas com o acessor .dt

In [14]:
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)
Armadilha brasileira

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

In [15]:
# 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.

In [16]:
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)
CTEs (WITH) + window functions (OVER/PARTITION BY) são os dois tópicos de SQL mais cobrados em entrevistas de nível pleno/sênior.
Conceito SQLEquivalente Pandas
GROUP BY + HAVINGgroupby().agg() + .query()
ROW_NUMBER() OVER (PARTITION BY...)groupby().cumcount() / rank()
LAG / LEADgroupby().shift(1) / shift(-1)
SUM() OVER (ORDER BY...)groupby().cumsum()
LEFT JOIN ... ONmerge(how="left")
Exercício 6

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.

CAP 07

Visualização que convence

Intermediário

Grá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

PerguntaGráfico certoEvite
Como evoluiu no tempo?LinhaBarras para muitos períodos
Como se compara entre categorias?Barras horizontais ordenadasPizza (humanos comparam mal ângulos)
Como se distribui?Histograma, boxplot, ECDFSó 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

In [17]:
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)
Regra de ouro: o título carrega a conclusão ("X cresce 34%"), não a descrição ("Receita por canal").

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

In [18]:
# 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
Visão de mercado

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.

Exercício 7

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.

CAP 08

Pandas de alta performance

Avançado

Quando 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

In [19]:
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

AbordagemVelocidade relativaQuando usar
Operações vetorizadas / np.where / np.select1× (referência)Sempre que possível
.map() com dicionário~2–5× mais lentoMapeamentos de/para
.apply() em Series~30–100× mais lentoLógica realmente complexa
.apply(axis=1) em linhas~100–1000× mais lentoÚltimo recurso
for com iterrows()pior casoPraticamente nunca
In [20]:
# 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)
Out:
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.
In [21]:
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
Visão de mercado

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.

Exercício 8

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.

CAP 09

Estatística aplicada a decisões

Avançado

A 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.log1p quando 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

In [22]:
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}")
Bootstrap é o canivete suíço do analista sênior: sem fórmula fechada? Reamostre.

9.3 Testes de hipótese: o mapa de decisão

ComparaçãoTestescipy/statsmodels
Média de 2 grupost de Welchstats.ttest_ind(a, b, equal_var=False)
Mediana/dados assimétricosMann-Whitney Ustats.mannwhitneyu(a, b)
2 proporções (conversão A vs B)z de proporçõesproportions_ztest([x1,x2],[n1,n2])
Associação entre 2 categóricasQui-quadradostats.chi2_contingency(tabela)
3+ gruposANOVA / Kruskal-Wallisstats.f_oneway / stats.kruskal
Pareado (antes/depois no mesmo sujeito)t pareado / Wilcoxonstats.ttest_rel / stats.wilcoxon
Como interpretar p-valor sem errar

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

In [23]:
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).

Exercício 9

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.

CAP 10

Séries temporais

Avançado

Quase 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

In [24]:
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)
YoY neutraliza sazonalidade; MoM mostra momentum; LTM suaviza tudo. Reporte os três e você fala a língua de FP&A.

10.2 Decomposição: tendência × sazonalidade × ruído

In [25]:
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).
In [26]:
# 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
Vazamento temporal

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.

Exercício 10

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.

CAP 11

Testes A/B e inferência causal

Expert

A habilidade que mais diferencia analistas sêniores: transformar "correlacionou" em "causou" com rigor.

11.1 Anatomia de um teste A/B sério

Hipótese e métrica primáriaUMA métrica de decisão definida antes ("conversão do checkout"), métricas secundárias e de guarda-corpo (latência, cancelamentos) listadas.
Cálculo de amostra (poder estatístico)Antes de rodar: qual o efeito mínimo que interessa detectar (MDE)? Sem isso, o teste termina "inconclusivo" por design.
Randomização auditadaVerifique o balanceamento (SRM check): se 50/50 virou 52/48 com n grande, a atribuição está quebrada e o teste é inválido.
Duração fixa, sem espiarParar o teste "quando deu significativo" infla drasticamente falsos positivos (peeking). Rode ciclos completos de semana.
Análise e decisãoLift com IC, segmentações pré-registradas, recomendação clara.
In [27]:
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}")
Out:
≈ 21,216 usuários por grupo
Lift: +0.58% | p = 0.0031
As 4 armadilhas que invalidam testes
  • 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étodoCenárioIdeia central
Diferenças-em-diferençasMudança atingiu um grupo/região e outro nãoCompara a variação dos tratados com a variação dos controles (exige tendências paralelas pré-evento)
Controle sintético / CausalImpactIntervençã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ínuaRegra de corte (score ≥ X ganha crédito)Compara quem ficou logo acima vs. logo abaixo do corte
Matching / PSMAdoção voluntária de um recursoPareia usuários tratados com controles estatisticamente similares
In [28]:
# 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
Visão de mercado

"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".

Exercício 11

(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.

CAP 12

Machine learning para analistas

Expert

Nã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

In [29]:
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)
Pipeline + ColumnTransformer garantem que TODO o pré-processamento aprende só no treino — a defesa estrutural contra vazamento de dados.

12.2 Avaliação que reflete o negócio

In [30]:
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"]))
Acurácia engana

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

In [31]:
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

In [32]:
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!
Cluster sem nome de negócio ("VIPs em risco", "novos promissores") é só matemática. O nome é o entregável.
Exercício 12

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.

CAP 13

Engenharia e automação para analistas

Expert

O 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:

Notebook exploratórioLivre, rápido, descartável. Nomeie com data e objetivo: 2026-07-06_investigacao_churn_plano_basico.ipynb.
Funções extraídasLógica repetida vira função pura em src/limpeza.py, importada pelo notebook. Uma transformação = uma função testável.
Script parametrizadopython relatorio.py --mes 2026-06. Roda sem intervenção humana, loga o que fez, falha ruidosamente.
Job agendadoCron, GitHub Actions ou o orquestrador da casa (Airflow/Dagster). O relatório chega sozinho toda segunda 7h.
In [33]:
# 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

In [34]:
# 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)
Teste as bordas das regras de negócio. É onde as métricas quebram silenciosamente quando alguém "só ajustou um filtro".

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 .gitignore e 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

In [35]:
# 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"
Visão de mercado

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.

Exercício 13

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.

CAP 14

Crescendo no mercado

Carreira

Técnica te torna competente. O que promove é técnica × comunicação × impacto visível.

14.1 A escada de senioridade na prática

NívelO que entregaComo é avaliado
JúniorExecuta análises pedidas, com supervisãoCorretude e prazo
PlenoResolve problemas ambíguos sozinho, questiona a perguntaAutonomia e qualidade das recomendações
SêniorDefine métricas, desenha experimentos, influencia roadmapImpacto em decisões e no time
Staff/EspecialistaPadrões e infraestrutura analítica da empresa, mentoriaMultiplicaçã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):

Recomendação (30 segundos)"Recomendo pausar o cupom X: ele canibaliza margem sem elevar retenção."
Três evidências-chaveCada uma com um gráfico de título-conclusão. Não dez gráficos: três.
Metodologia e ressalvas (apêndice)Disponível para quem perguntar. Antecipe as duas objeções mais prováveis.
Teste do elevador

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)

SemanasFocoEntregável
1–2Caps. 4–5 a fundo + snippets pessoaisFunção diagnostico() + template de EDA
3–4Caps. 6–7 (SQL windows + viz)Makeover de 3 gráficos reais do trabalho
5–6Cap. 8 (performance)Um job real otimizado + benchmark documentado
7–8Caps. 9–10 (estatística + tempo)Relatório com ICs e decomposição de uma métrica-chave
9–10Cap. 11 (experimentação)Desenho de A/B ou diff-in-diff de um caso real
11–12Caps. 12–13 (ML + automação)Modelo de propensão + 1 relatório automatizado
A síntese de tudo

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.

Exercício final

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.

Apostila de Análise de Dados com Python · gerada com Claude · Estude com dados reais sempre que possível — é o que fixa.