Objetivo

Dominar o Pandas, a biblioteca padrão para manipulação de dados tabulares em Python. Aprender a criar, carregar, inspecionar e operar sobre Series e DataFrames, que são as estruturas de dados fundamentais para qualquer projeto de Machine Learning.


1. Por que Pandas é Essencial em IA?

Em todo projeto de ML, antes de qualquer modelo, você precisa lidar com dados. E dados reais vêm em formatos tabulares: CSVs, planilhas Excel, bancos de dados relacionais, resultados de APIs em JSON. O Pandas é a ferramenta que transforma esses dados brutos em algo que você pode analisar, limpar e usar para treinar modelos.

Na prática:

  • Carregar um CSV com 1 milhão de linhas: uma linha com Pandas
  • Calcular estatísticas por grupo em segundos: .groupby()
  • Tratar valores ausentes em todo o dataset de uma vez: .fillna()
  • Converter um DataFrame em arrays NumPy para passar ao modelo: .values
  • Inspecionar o dataset antes de treinar: .describe(), .info(), .head()

O fluxo padrão de um projeto de ML é: dados brutos → Pandas (limpeza e análise) → NumPy/PyTorch (modelagem).


2. Importando Pandas

A convenção universal é importar com o alias pd:

import pandas as pd
import numpy as np

print(pd.__version__)

3. Series

Uma Series é um array unidimensional com rótulos (índice). É como uma coluna de uma tabela, ou um dicionário ordenado.

3.1 Criando Series

# A partir de uma lista
notas = pd.Series([7.5, 8.0, 6.5, 9.0, 5.5])
print(notas)

Saída:

0    7.5
1    8.0
2    6.5
3    9.0
4    5.5
dtype: float64
# Com índice personalizado
notas = pd.Series(
    [7.5, 8.0, 6.5, 9.0, 5.5],
    index=["Ana", "Bruno", "Carla", "Diego", "Eva"]
)
print(notas)

Saída:

Ana      7.5
Bruno    8.0
Carla    6.5
Diego    9.0
Eva      5.5
dtype: float64
# A partir de um dicionário
populacao = pd.Series({
    "São Paulo": 12_325_232,
    "Rio de Janeiro": 6_747_815,
    "Brasília": 3_094_325,
    "Salvador": 2_886_698
})
print(populacao)

3.2 Acessando elementos

notas = pd.Series(
    [7.5, 8.0, 6.5, 9.0, 5.5],
    index=["Ana", "Bruno", "Carla", "Diego", "Eva"]
)

# Por rótulo (loc)
print(notas["Ana"])          # 7.5
print(notas.loc["Bruno"])    # 8.0

# Por posição (iloc)
print(notas.iloc[0])         # 7.5
print(notas.iloc[-1])        # 5.5
print(notas.iloc[1:3])       # Bruno e Carla

# Múltiplos rótulos
print(notas[["Ana", "Diego"]])

3.3 Operações com Series

notas = pd.Series([7.5, 8.0, 6.5, 9.0, 5.5])

# Operações escalares
print(notas + 1)
print(notas * 2)
print(notas > 7)

# Estatísticas
print(notas.mean())    # 7.3
print(notas.std())     # 1.3...
print(notas.min())     # 5.5
print(notas.max())     # 9.0
print(notas.median())  # 7.5
print(notas.sum())     # 36.5

# Describe: resumo estatístico
print(notas.describe())

Saída de describe():

count    5.000000
mean     7.300000
std      1.303840
min      5.500000
25%      6.500000
50%      7.500000
75%      8.000000
max      9.000000
dtype: float64

3.4 Valores ausentes em Series

s = pd.Series([1.0, None, 3.0, None, 5.0])
print(s)
print(s.isna())           # máscara booleana de NaN
print(s.isna().sum())     # 2 — quantos NaN
print(s.dropna())         # remove NaN
print(s.fillna(0))        # substitui NaN por 0
print(s.fillna(s.mean())) # substitui NaN pela média

4. DataFrames

Um DataFrame é uma tabela bidimensional com rótulos nas linhas (índice) e colunas. É a estrutura mais importante do Pandas — pense em uma planilha Excel programável.

4.1 Criando DataFrames

# A partir de um dicionário de listas
df = pd.DataFrame({
    "nome":   ["Ana", "Bruno", "Carla", "Diego", "Eva"],
    "idade":  [25, 30, 28, 35, 22],
    "nota":   [8.5, 7.0, 9.2, 6.5, 8.0],
    "cidade": ["SP", "RJ", "SP", "MG", "SP"]
})
print(df)

Saída:

    nome  idade  nota cidade
0    Ana     25   8.5     SP
1  Bruno     30   7.0     RJ
2  Carla     28   9.2     SP
3  Diego     35   6.5     MG
4    Eva     22   8.0     SP
# A partir de lista de dicionários
registros = [
    {"produto": "notebook", "preco": 3500, "estoque": 10},
    {"produto": "mouse",    "preco":   90, "estoque": 50},
    {"produto": "teclado",  "preco":  150, "estoque": 30},
]
df_produtos = pd.DataFrame(registros)

# A partir de array NumPy
arr = np.random.randn(4, 3)
df_numpy = pd.DataFrame(arr, columns=["feature_1", "feature_2", "feature_3"])
print(df_numpy)

4.2 Carregando dados de arquivos

# CSV — o mais comum
df = pd.read_csv("dados.csv")
df = pd.read_csv("dados.csv", sep=";")              # separador diferente
df = pd.read_csv("dados.csv", encoding="utf-8")     # encoding
df = pd.read_csv("dados.csv", index_col="id")       # definir coluna como índice
df = pd.read_csv("dados.csv", nrows=1000)           # apenas 1000 linhas
df = pd.read_csv("dados.csv", usecols=["a", "b"])   # apenas algumas colunas

# Excel
df = pd.read_excel("dados.xlsx", sheet_name="Plan1")

# JSON
df = pd.read_json("dados.json")

# Salvando
df.to_csv("saida.csv", index=False)   # index=False evita salvar o índice
df.to_excel("saida.xlsx", index=False)
df.to_json("saida.json", orient="records")

5. Inspeção Inicial do DataFrame

Estas são as primeiras coisas que você faz ao carregar qualquer dataset:

df = pd.DataFrame({
    "nome":   ["Ana", "Bruno", "Carla", "Diego", "Eva", "Fábio"],
    "idade":  [25, 30, 28, 35, 22, None],
    "nota":   [8.5, 7.0, 9.2, 6.5, 8.0, 7.5],
    "cidade": ["SP", "RJ", "SP", "MG", "SP", "RJ"],
    "aprovado": [True, True, True, False, True, True]
})

# Primeiras e últimas linhas
print(df.head())      # primeiras 5 linhas (padrão)
print(df.head(3))     # primeiras 3 linhas
print(df.tail())      # últimas 5 linhas

# Dimensões
print(df.shape)       # (6, 5) — linhas, colunas

# Informações gerais
print(df.info())

Saída de info():

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6 entries, 0 to 5
Data columns (total 5 columns):
 #   Column    Non-Null Count  Dtype
---  ------    --------------  -----
 0   nome      6 non-null      object
 1   idade     5 non-null      float64
 2   nota      6 non-null      float64
 3   cidade    6 non-null      object
 4   aprovado  6 non-null      bool
dtypes: bool(1), float64(2), object(2)
memory usage: 370.0+ bytes
# Estatísticas descritivas
print(df.describe())

# Incluindo colunas não numéricas
print(df.describe(include="all"))

# Tipos de dados
print(df.dtypes)

# Nomes das colunas
print(df.columns.tolist())

# Índice
print(df.index)

# Valores únicos por coluna
print(df["cidade"].unique())       # ['SP' 'RJ' 'MG']
print(df["cidade"].nunique())      # 3 — quantidade de únicos
print(df["cidade"].value_counts()) # frequência de cada valor

6. Selecionando Dados

6.1 Selecionando colunas

# Uma coluna → retorna Series
nomes = df["nome"]
print(type(nomes))   # <class 'pandas.core.series.Series'>

# Múltiplas colunas → retorna DataFrame
subdf = df[["nome", "nota"]]
print(type(subdf))   # <class 'pandas.core.frame.DataFrame'>

6.2 loc — seleção por rótulo

# Uma linha por índice
print(df.loc[0])

# Múltiplas linhas
print(df.loc[0:2])     # linhas 0, 1 e 2 (inclusive)

# Linha e colunas específicas
print(df.loc[0, "nome"])           # "Ana"
print(df.loc[0:2, ["nome", "nota"]])

# Todas as linhas, colunas específicas
print(df.loc[:, "nota"])

6.3 iloc — seleção por posição

# Por índice numérico
print(df.iloc[0])          # primeira linha
print(df.iloc[-1])         # última linha
print(df.iloc[0:3])        # primeiras 3 linhas
print(df.iloc[0, 1])       # linha 0, coluna 1
print(df.iloc[:, 2])       # toda a coluna 2
print(df.iloc[0:3, 0:2])   # submatriz

6.4 Filtragem booleana

# Filtro simples
aprovados = df[df["aprovado"] == True]
print(aprovados)

# Equivalente Pythonico
aprovados = df[df["aprovado"]]

# Notas altas
notas_altas = df[df["nota"] >= 8.0]
print(notas_altas)

# Múltiplas condições com & (e) e | (ou)
sp_aprovados = df[(df["cidade"] == "SP") & (df["aprovado"] == True)]
print(sp_aprovados)

jovens_ou_notas_altas = df[(df["idade"] < 25) | (df["nota"] > 9.0)]
print(jovens_ou_notas_altas)

# Usando query() — sintaxe mais legível
resultado = df.query("nota >= 8 and cidade == 'SP'")
print(resultado)

# isin — verificar pertencimento a uma lista
sudeste = df[df["cidade"].isin(["SP", "RJ", "MG"])]
print(sudeste)

7. Manipulando Colunas

7.1 Adicionando colunas

df = pd.DataFrame({
    "nome":  ["Ana", "Bruno", "Carla"],
    "nota1": [8.0, 7.0, 9.0],
    "nota2": [7.5, 8.5, 8.0],
})

# Nova coluna com operação
df["media"] = (df["nota1"] + df["nota2"]) / 2

# Nova coluna com condição
df["situacao"] = df["media"].apply(lambda x: "Aprovado" if x >= 7 else "Reprovado")

# Com np.where — mais eficiente para operações simples
df["situacao2"] = np.where(df["media"] >= 7, "Aprovado", "Reprovado")

print(df)

Saída:

    nome  nota1  nota2  media  situacao situacao2
0    Ana    8.0    7.5   7.75  Aprovado  Aprovado
1  Bruno    7.0    8.5   7.75  Aprovado  Aprovado
2  Carla    9.0    8.0   8.50  Aprovado  Aprovado

7.2 Removendo colunas e linhas

# Remover colunas
df_sem_nota2 = df.drop(columns=["nota2"])
df_sem_notas = df.drop(columns=["nota1", "nota2"])

# Remover linhas por índice
df_sem_primeira = df.drop(index=0)
df_sem_algumas = df.drop(index=[0, 2])

# inplace=True modifica o DataFrame original
df.drop(columns=["situacao2"], inplace=True)

7.3 Renomeando colunas

df = df.rename(columns={"nota1": "prova1", "nota2": "prova2"})
print(df.columns.tolist())

7.4 Alterando tipos

df["nota1"] = df["nota1"].astype(np.float32)
df["nome"]  = df["nome"].astype("category")   # eficiente para strings repetidas

8. Valores Ausentes

Lidar com valores ausentes é uma das tarefas mais comuns em ciência de dados.

df = pd.DataFrame({
    "nome":   ["Ana", "Bruno", "Carla", "Diego", "Eva"],
    "idade":  [25, None, 28, 35, None],
    "salario": [3000, 4500, None, 5000, 3500],
    "cidade": ["SP", "RJ", None, "MG", "SP"]
})

# Verificar ausentes
print(df.isna())
print(df.isna().sum())      # por coluna
print(df.isna().sum().sum()) # total

# Porcentagem de ausentes
print(df.isna().mean() * 100)

# Remover linhas com qualquer ausente
df_limpo = df.dropna()

# Remover linhas onde colunas específicas têm ausentes
df_sem_idade_nula = df.dropna(subset=["idade"])

# Preencher com valor fixo
df_filled = df.fillna(0)

# Preencher por coluna com estratégias diferentes
df["idade"]   = df["idade"].fillna(df["idade"].mean())
df["cidade"]  = df["cidade"].fillna("Desconhecido")
df["salario"] = df["salario"].fillna(df["salario"].median())

print(df)

9. Operações de String

Pandas tem um acessor .str para operações vetorizadas em colunas de texto:

df = pd.DataFrame({
    "nome": ["  ana silva  ", "BRUNO COSTA", "carla SANTOS"],
    "email": ["ana@email.com", "bruno@gmail.com", "carla@yahoo.com"]
})

# Limpeza e normalização
df["nome_limpo"] = (df["nome"]
                    .str.strip()        # remove espaços nas bordas
                    .str.title()        # capitaliza cada palavra
                    )

# Verificações
df["tem_gmail"] = df["email"].str.contains("gmail")
df["dominio"]   = df["email"].str.split("@").str[1]
df["nome_upper"] = df["nome"].str.upper()

# Substituição
df["email_limpo"] = df["email"].str.replace("@yahoo.com", "@email.com")

print(df[["nome_limpo", "dominio", "tem_gmail"]])

Saída:

      nome_limpo          dominio  tem_gmail
0      Ana Silva        email.com      False
1    Bruno Costa        gmail.com       True
2   Carla Santos        yahoo.com      False

10. Ordenação

df = pd.DataFrame({
    "nome":  ["Ana", "Bruno", "Carla", "Diego"],
    "nota":  [8.5, 7.0, 9.2, 6.5],
    "idade": [25, 30, 25, 35]
})

# Por uma coluna
print(df.sort_values("nota"))
print(df.sort_values("nota", ascending=False))

# Por múltiplas colunas
print(df.sort_values(["idade", "nota"], ascending=[True, False]))

# Por índice
print(df.sort_index())

11. Convertendo para NumPy

Em projetos de ML, o fluxo final é sempre converter o DataFrame em arrays NumPy para alimentar o modelo:

df = pd.DataFrame({
    "feature1": [1.0, 2.0, 3.0, 4.0],
    "feature2": [0.5, 1.5, 2.5, 3.5],
    "label":    [0, 1, 0, 1]
})

# Separar features e labels
X = df[["feature1", "feature2"]].values   # array NumPy (4, 2)
y = df["label"].values                     # array NumPy (4,)

print(type(X))    # <class 'numpy.ndarray'>
print(X.shape)    # (4, 2)
print(y.shape)    # (4,)
print(X.dtype)    # float64

# Garantir tipo float32 para PyTorch
X = X.astype(np.float32)
y = y.astype(np.int64)

12. Exemplo Completo: Análise de Dataset de ML

import pandas as pd
import numpy as np

np.random.seed(42)

# Simulando um dataset de clientes para modelo de churn
n = 200

df = pd.DataFrame({
    "cliente_id":    range(1, n + 1),
    "idade":         np.random.randint(18, 70, n),
    "tempo_cliente": np.random.randint(1, 120, n),   # meses
    "gasto_mensal":  np.random.uniform(50, 500, n).round(2),
    "num_produtos":  np.random.randint(1, 6, n),
    "satisfacao":    np.random.choice([1, 2, 3, 4, 5], n),
    "churn":         np.random.choice([0, 1], n, p=[0.75, 0.25])
})

# Inserir alguns valores ausentes artificialmente
idx_ausentes = np.random.choice(n, 15, replace=False)
df.loc[idx_ausentes[:8],  "gasto_mensal"] = np.nan
df.loc[idx_ausentes[8:],  "satisfacao"]   = np.nan

print("=" * 50)
print("1. INSPEÇÃO INICIAL")
print("=" * 50)
print(f"Shape: {df.shape}")
print(f"\nPrimeiras linhas:")
print(df.head())
print(f"\nTipos:")
print(df.dtypes)

print("\n" + "=" * 50)
print("2. QUALIDADE DOS DADOS")
print("=" * 50)
ausentes = df.isna().sum()
ausentes = ausentes[ausentes > 0]
print(f"Valores ausentes:\n{ausentes}")
print(f"\nPorcentagem ausente:")
print((ausentes / len(df) * 100).round(2))

print("\n" + "=" * 50)
print("3. ESTATÍSTICAS DESCRITIVAS")
print("=" * 50)
print(df.describe().round(2))

print("\n" + "=" * 50)
print("4. DISTRIBUIÇÃO DO TARGET")
print("=" * 50)
contagem = df["churn"].value_counts()
pct = df["churn"].value_counts(normalize=True) * 100
print(pd.DataFrame({"contagem": contagem, "porcentagem": pct.round(1)}))

print("\n" + "=" * 50)
print("5. ANÁLISE POR GRUPO")
print("=" * 50)
analise = df.groupby("churn")[["idade", "gasto_mensal",
                                "tempo_cliente", "satisfacao"]].mean().round(2)
print(analise)

print("\n" + "=" * 50)
print("6. TRATAMENTO DE AUSENTES")
print("=" * 50)
df["gasto_mensal"] = df["gasto_mensal"].fillna(df["gasto_mensal"].median())
df["satisfacao"]   = df["satisfacao"].fillna(df["satisfacao"].mode()[0])
print(f"Ausentes após tratamento: {df.isna().sum().sum()}")

print("\n" + "=" * 50)
print("7. PREPARANDO PARA O MODELO")
print("=" * 50)
features = ["idade", "tempo_cliente", "gasto_mensal",
            "num_produtos", "satisfacao"]

X = df[features].values.astype(np.float32)
y = df["churn"].values.astype(np.int64)

print(f"X shape: {X.shape}")
print(f"y shape: {y.shape}")
print(f"X dtype: {X.dtype}")
print(f"y dtype: {y.dtype}")

# Divisão treino/teste
corte = int(len(df) * 0.8)
X_train, X_test = X[:corte], X[corte:]
y_train, y_test = y[:corte], y[corte:]

print(f"\nTreino: {X_train.shape[0]} exemplos")
print(f"Teste:  {X_test.shape[0]} exemplos")

Saída resumida:

==================================================
1. INSPEÇÃO INICIAL
==================================================
Shape: (200, 8)

Primeiras linhas:
   cliente_id  idade  tempo_cliente  gasto_mensal  ...  churn
0           1     57             65        446.22  ...      0
1           2     37             64        146.04  ...      0
...

==================================================
4. DISTRIBUIÇÃO DO TARGET
==================================================
       contagem  porcentagem
churn
0           151         75.5
1            49         24.5

==================================================
5. ANÁLISE POR GRUPO
==================================================
       idade  gasto_mensal  tempo_cliente  satisfacao
churn
0      43.12        275.43          59.82        3.11
1      44.02        271.89          57.94        2.94

==================================================
7. PREPARANDO PARA O MODELO
==================================================
X shape: (200, 5)
y shape: (200,)
X dtype: float32
y dtype: int64

Treino: 160 exemplos
Teste:  40 exemplos

Resumo da Aula

  • Series é um array 1D com índice: pense em uma coluna de tabela
  • DataFrame é uma tabela 2D com índice e colunas: pense em uma planilha
  • read_csv(), read_excel(), read_json() carregam dados de arquivos
  • head(), info(), describe() são os primeiros comandos ao explorar um dataset
  • loc seleciona por rótulo, iloc seleciona por posição numérica
  • Filtragem booleana: df[df["coluna"] > valor] — use & e | para múltiplas condições
  • isna(), fillna(), dropna() tratam valores ausentes
  • .str permite operações vetorizadas em colunas de texto
  • groupby() agrega dados por grupo (próxima aula)
  • .values converte DataFrame/Series para array NumPy
  • O fluxo final sempre é: DataFrame limpo → NumPy → modelo

Exercícios

  1. Crie um DataFrame com dados de 5 funcionários contendo: nome, departamento, salário e anos de empresa. Em seguida: exiba apenas os funcionários do departamento "TI", ordene por salário decrescente e adicione uma coluna "bonus" que seja 10% do salário para quem tem mais de 3 anos de empresa e 5% para os demais.

    ✓ Resposta:
    df = pd.DataFrame({
        "nome":        ["Ana", "Bruno", "Carla", "Diego", "Eva"],
        "departamento": ["TI", "RH", "TI", "TI", "Financeiro"],
        "salario":     [8000, 5000, 9500, 7000, 6000],
        "anos":        [5, 2, 8, 1, 4]
    })
    
    # Filtrar TI
    ti = df[df["departamento"] == "TI"].copy()
    
    # Ordenar por salário
    ti = ti.sort_values("salario", ascending=False)
    
    # Adicionar bônus
    ti["bonus"] = ti.apply(
        lambda row: row["salario"] * 0.10 if row["anos"] > 3 else row["salario"] * 0.05,
        axis=1
    )
    
    print(ti)
    

    Saída:

        nome departamento  salario  anos   bonus
    2  Carla           TI     9500     8   950.0
    0    Ana           TI     8000     5   800.0
    3  Diego           TI     7000     1   350.0
    
  2. Dado o DataFrame abaixo com valores ausentes, escreva o código para: identificar quantos ausentes há em cada coluna, preencher os ausentes numéricos com a mediana da coluna e os ausentes de texto com "Desconhecido".

    df = pd.DataFrame({
        "produto":  ["A", "B", None, "D", "E"],
        "preco":    [10.0, None, 30.0, None, 50.0],
        "estoque":  [100, 200, None, 400, 500],
        "categoria": ["X", "Y", "X", None, "Y"]
    })
    

    ✓ Resposta:
    df = pd.DataFrame({
        "produto":   ["A", "B", None, "D", "E"],
        "preco":     [10.0, None, 30.0, None, 50.0],
        "estoque":   [100, 200, None, 400, 500],
        "categoria": ["X", "Y", "X", None, "Y"]
    })
    
    # Identificar ausentes
    print("Ausentes por coluna:")
    print(df.isna().sum())
    
    # Colunas numéricas — preencher com mediana
    colunas_numericas = df.select_dtypes(include=[np.number]).columns
    for col in colunas_numericas:
        df[col] = df[col].fillna(df[col].median())
    
    # Colunas de texto — preencher com "Desconhecido"
    colunas_texto = df.select_dtypes(include=["object"]).columns
    for col in colunas_texto:
        df[col] = df[col].fillna("Desconhecido")
    
    print("\nApós tratamento:")
    print(df)
    print(f"\nAusentes restantes: {df.isna().sum().sum()}")
    
  3. Explique a diferença entre df.loc e df.iloc. Quando o uso de um pode gerar resultados inesperados se você usar o outro por engano? Dê um exemplo concreto.

    ✓ Resposta:

    df.loc seleciona por rótulo do índice. df.iloc seleciona por posição inteira (0, 1, 2...).

    Quando o índice do DataFrame é o padrão RangeIndex (0, 1, 2...), os dois parecem equivalentes, mas têm uma diferença sutil: loc com slicing é inclusivo no final (df.loc[0:2] retorna linhas 0, 1 e 2), enquanto iloc é exclusivo no final (df.iloc[0:2] retorna apenas linhas 0 e 1).

    O problema inesperado surge quando o índice não é sequencial. Por exemplo, após filtrar um DataFrame, o índice original é preservado:

    df = pd.DataFrame({
        "nome": ["Ana", "Bruno", "Carla", "Diego"],
        "nota": [8.5, 7.0, 9.2, 6.5]
    })
    
    # Filtrar apenas aprovados (nota >= 7)
    aprovados = df[df["nota"] >= 7.0]
    print(aprovados.index.tolist())  # [0, 1, 2] — índice 3 foi removido
    
    # iloc[1] retorna a segunda linha DO RESULTADO FILTRADO (Bruno)
    print(aprovados.iloc[1])   # Bruno, nota 7.0
    
    # loc[1] retorna a linha com RÓTULO 1 (também Bruno nesse caso)
    print(aprovados.loc[1])    # Bruno, nota 7.0
    
    # Mas se o índice fosse [0, 2, 5]:
    # loc[1] → KeyError (não existe rótulo 1)
    # iloc[1] → segunda posição do resultado (funciona normalmente)
    

    Regra prática: após filtrar um DataFrame, use .reset_index(drop=True) para redefinir o índice se for usar loc com números.

  4. Dado um DataFrame de notas com colunas ["aluno", "matematica", "portugues", "ciencias"], escreva em uma linha com Pandas: calcule a média das três disciplinas para cada aluno e adicione como coluna "media", depois filtre apenas os alunos com média acima de 7.0.

    ✓ Resposta:
    df = pd.DataFrame({
        "aluno":       ["Ana", "Bruno", "Carla", "Diego", "Eva"],
        "matematica":  [8.0, 6.5, 9.0, 5.5, 7.5],
        "portugues":   [7.5, 7.0, 8.5, 6.0, 8.0],
        "ciencias":    [9.0, 5.5, 9.5, 7.0, 6.5]
    })
    
    # Tudo em uma expressão encadeada
    resultado = (df.assign(media=df[["matematica", "portugues", "ciencias"]].mean(axis=1))
                   .query("media > 7.0"))
    
    print(resultado)
    

    Saída:

       aluno  matematica  portugues  ciencias   media
    0    Ana         8.0        7.5       9.0  8.1667
    2  Carla         9.0        8.5       9.5  9.0000
    4    Eva         7.5        8.0       6.5  7.3333
    
  5. Explique o que value_counts() e value_counts(normalize=True) fazem. Em que situação de ML essa função é especialmente útil?

    ✓ Resposta:

    value_counts() conta quantas vezes cada valor único aparece em uma Series, retornando em ordem decrescente de frequência.

    value_counts(normalize=True) faz o mesmo, mas retorna a proporção (fração) em vez de contagem absoluta — os valores somam 1.0.

    s = pd.Series(["gato", "cachorro", "gato", "peixe", "gato", "cachorro"])
    print(s.value_counts())
    # gato       3
    # cachorro   2
    # peixe      1
    
    print(s.value_counts(normalize=True))
    # gato       0.500
    # cachorro   0.333
    # peixe      0.167
    

    Em ML essa função é especialmente útil para verificar o balanceamento de classes no dataset antes de treinar um modelo de classificação. Se uma classe aparece em 95% dos exemplos e a outra em apenas 5%, o modelo pode aprender a sempre prever a classe majoritária e ainda assim ter 95% de acurácia — o que é enganoso. Detectar esse desbalanceamento com value_counts() é o primeiro passo para decidir se você precisa de oversampling, undersampling ou pesos de classe no modelo.

  6. Por que o método .values é necessário ao preparar dados para um modelo de ML com PyTorch ou Scikit-learn? O que aconteceria se você passasse um DataFrame diretamente para um modelo sem convertê-lo?

    ✓ Resposta:

    .values é necessário porque Scikit-learn e PyTorch esperam arrays NumPy ou tensores como entrada, não DataFrames do Pandas.

    Um DataFrame carrega informações extras além dos dados em si: o índice, os nomes das colunas, os tipos por coluna, metadados. Embora muitas versões recentes do Scikit-learn aceitem DataFrames diretamente, o PyTorch não aceita — ele precisa de tensores, que são construídos a partir de arrays NumPy.

    Além disso, mesmo quando o Scikit-learn aceita DataFrames, é boa prática converter explicitamente para garantir que:

    • O tipo de dado é o esperado (float32 para PyTorch, float64 para Scikit-learn)
    • Não há índices estranhos que possam causar comportamentos inesperados
    • O código é explícito sobre o que está sendo passado ao modelo

    Se você passasse um DataFrame diretamente ao PyTorch:

    import torch
    
    df_features = pd.DataFrame({"a": [1.0, 2.0], "b": [3.0, 4.0]})
    
    # Isso vai dar erro
    tensor = torch.tensor(df_features)   # TypeError
    
    # Isso funciona
    tensor = torch.tensor(df_features.values)
    # Ou mais explicitamente:
    tensor = torch.tensor(df_features.values.astype(np.float32))
    

Referências