← Base: Python e matemática

Aula 3 de 6
  1. 1Python para quem vem de C# e TypeScript
  2. 2NumPy: pensar em arrays, não em laços
  3. 3Gráficos com matplotlib
  4. 4Álgebra linear: vetores, produto escalar e matrizes
  5. 5Cálculo: derivada, derivada parcial e gradiente
  6. 6Probabilidade e estatística: média, variância e distribuição normal

Gráficos com matplotlib

Objetivo: desenhar funções, dados e imagens.

Por que isso importa: em ML, o gráfico é o seu depurador. É olhando que você percebe que o modelo parou de aprender, que um dado tem um valor absurdo ou que dois grupos se misturam. Nas próximas aulas (álgebra linear, cálculo e estatística), todo conceito vai vir acompanhado de um gráfico.

1. Anatomia: Figure e Axes

O matplotlib confunde no começo porque tem dois jeitos de usar:

  • plt.plot(...) (jeito implícito): o matplotlib guarda uma “figura atual” numa variável global escondida e desenha nela. É prático, mas é mágica: você não vê onde está desenhando.
  • fig, ax = plt.subplots() (jeito explícito): você recebe os objetos e chama métodos neles. É o que vamos usar, e é o que você vai ver em código sério.

Você vai encontrar os dois jeitos em exemplos na internet. Eles fazem a mesma coisa.

┌─────────────── Figure (fig) ───────────────┐
│  ┌──── Axes (ax) ─────┐  ┌──── Axes ─────┐ │
│  │ título             │  │               │ │
│  │    ╱╲   ← linhas   │  │   ·  ·  ·     │ │
│  │   ╱  ╲             │  │  · ·   ·      │ │
│  │ eixo x, eixo y     │  │               │ │
│  └────────────────────┘  └───────────────┘ │
└────────────────────────────────────────────┘
  • fig é a tela inteira, o que vira a imagem final.
  • ax é uma área de desenho com seus eixos. É como o ctx = canvas.getContext("2d") do JS ou o objeto Graphics do C#. Uma figura pode ter vários.

Cuidado com o nome: Axes (a área do gráfico) é diferente de axis (um eixo, x ou y). Todo mundo tropeça nisso.

import numpy as np
import matplotlib.pyplot as plt

# Três cores que continuam distinguíveis para quem é daltônico (a seção 9 explica)
AZUL, LARANJA, VERDE = "#2a78d6", "#eb6834", "#1baf7a"

2. Primeiro gráfico: uma função

x = np.linspace(-3, 3, 100)    # 100 pontos de -3 a 3 (lembra: o linspace INCLUI o fim)
y = x ** 2                     # vetorizado: calcula y para todos os x de uma vez

fig, ax = plt.subplots(figsize=(6, 4))   # tamanho em polegadas: (largura, altura)
ax.plot(x, y, color=AZUL)
ax.set_title("f(x) = x²")
ax.set_xlabel("x")
ax.set_ylabel("f(x)")
ax.grid(alpha=0.3)             # grade clarinha, pra não brigar com a curva
plt.show()

Gráfico gerado pelo código acima

Sem mágica: o matplotlib não sabe o que é x². Ele recebe duas listas de números e liga os pontos com retas. Com poucos pontos, dá pra ver:

x5 = np.linspace(-3, 3, 5)
x100 = np.linspace(-3, 3, 100)

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x100, x100 ** 2, color=AZUL, label="100 pontos")
ax.plot(x5, x5 ** 2, color=LARANJA, marker="o", label="5 pontos")   # marker desenha cada ponto
ax.set_title("plot() só liga os pontos com retas")
ax.legend()                    # mostra os nomes dados em label=
ax.grid(alpha=0.3)
plt.show()

Gráfico gerado pelo código acima

Por isso o linspace vem com bastante ponto: 100 retas curtinhas parecem uma curva.

3. Várias curvas no mesmo gráfico

Cada chamada de ax.plot acrescenta uma curva. Com label= e ax.legend(), cada uma ganha um nome. Aqui estão três funções de ativação, que vão aparecer na disciplina de redes neurais:

  • sigmoide, 11+e−x\frac{1}{1 + e^{-x}}: espreme qualquer número pra dentro do intervalo de 0 a 1
  • tanh: parecida, só que entre -1 e 1
  • ReLU, max⁡(0,x)\max(0, x): zera os negativos e deixa passar os positivos. É a mais usada hoje
x = np.linspace(-3, 3, 200)

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, 1 / (1 + np.exp(-x)), color=AZUL, label="sigmoide")
ax.plot(x, np.tanh(x), color=LARANJA, linestyle="--", label="tanh")
ax.plot(x, np.maximum(0, x), color=VERDE, linestyle=":", label="ReLU")   # np.maximum compara elemento a elemento
ax.axhline(0, color="gray", linewidth=0.8)   # linha horizontal em y = 0
ax.set_title("Funções de ativação")
ax.set_xlabel("x")
ax.legend()
ax.grid(alpha=0.3)
plt.show()

Gráfico gerado pelo código acima

Dois detalhes:

  • np.maximum(0, x), e não max(0, x): o max do Python compara dois valores; o np.maximum compara 0 com cada elemento de x (broadcasting, da aula passada).
  • linestyle diferente, além da cor: quem é daltônico, ou imprime em preto e branco, continua distinguindo as curvas.

4. Dispersão (scatter): olhando dados

O plot liga os pontos; o scatter desenha só os pontos. É o gráfico pra olhar dados, em que cada ponto é um exemplo.

rng = np.random.default_rng(seed=0)
area = rng.uniform(40, 150, size=50)                  # 50 casas entre 40 e 150 m²
preco = 3 * area + 50 + rng.normal(0, 40, size=50)    # preço ≈ 3 × área + 50, mais um "ruído"

fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(area, preco, color=AZUL, alpha=0.7)        # alpha = transparência (ajuda quando os pontos se sobrepõem)
ax.set_title("Casas: área × preço")
ax.set_xlabel("área (m²)")
ax.set_ylabel("preço (mil R$)")
ax.grid(alpha=0.3)
plt.show()

Gráfico gerado pelo código acima

Dá pra ver uma tendência: quanto maior a área, maior o preço. Regressão linear (disciplina 01) é achar a reta que passa o mais perto possível desses pontos.

Vamos chutar uma reta e desenhar o erro de cada casa:

def modelo(area):
    return 2.5 * area + 80        # um chute: preço = 2.5 × área + 80

previsto = modelo(area)
x_reta = np.linspace(40, 150, 2)  # pra desenhar uma reta, 2 pontos bastam
mse = np.mean((preco - previsto) ** 2)

fig, ax = plt.subplots(figsize=(6, 4))
ax.vlines(area, previsto, preco, color=LARANJA, alpha=0.6, label="erros")   # um tracinho por casa: do previsto até o real
ax.scatter(area, preco, color=AZUL, alpha=0.7, label="casas")
ax.plot(x_reta, modelo(x_reta), color="black", label="modelo (chute)")
ax.set_title(f"Erro quadrático médio do chute: {mse:.0f}")
ax.set_xlabel("área (m²)")
ax.set_ylabel("preço (mil R$)")
ax.legend()
ax.grid(alpha=0.3)
plt.show()

Gráfico gerado pelo código acima

O erro quadrático médio que você programou nas aulas passadas é a média dos quadrados desses tracinhos laranja. Treinar o modelo é ajustar a inclinação (2.5) e o deslocamento (80) da reta até esse número ficar o menor possível.

🧪 Experimente: mude o 2.5 e o 80 na função modelo e rode de novo. Quanto menor você consegue deixar o erro no título? Na disciplina 01, o computador vai fazer essa busca sozinho.

5. Grupos: um scatter por categoria

Muitas vezes cada ponto pertence a um grupo. Aqui temos 3 grupos de pontos em volta de 3 centros. Desenhamos um scatter por grupo, com cor e marcador diferentes.

rng = np.random.default_rng(seed=1)
centros = np.array([[0, 0], [4, 1], [1, 4]])
grupos = [c + rng.normal(0, 0.7, size=(40, 2)) for c in centros]   # 40 pontos em volta de cada centro

fig, ax = plt.subplots(figsize=(5, 5))
for pontos, cor, marcador, nome in zip(grupos, [AZUL, LARANJA, VERDE], ["o", "s", "^"], ["grupo A", "grupo B", "grupo C"]):
    ax.scatter(pontos[:, 0], pontos[:, 1], color=cor, marker=marcador, alpha=0.8, label=nome)
ax.set_title("Três grupos")
ax.set_aspect("equal")    # mesma escala em x e y, senão as distâncias ficam distorcidas
ax.legend()
ax.grid(alpha=0.3)
plt.show()

Gráfico gerado pelo código acima

Repare no pontos[:, 0]: é o fatiamento 2D da aula passada (todas as linhas, coluna 0, ou seja, os x).

Aqui nós sabíamos de que grupo cada ponto veio e pintamos. O k-means (disciplina 01) faz o contrário: recebe os pontos todos da mesma cor e descobre os grupos sozinho.

6. Histograma: a forma de uma distribuição

Com milhares de valores, um scatter não ajuda. O histograma divide o intervalo em faixas (bins) e conta quantos valores caem em cada uma. A altura da barra é essa contagem.

rng = np.random.default_rng(seed=2)
alturas = rng.normal(170, 8, size=5000)    # 5000 alturas: média de 170 cm, desvio padrão de 8 cm

fig, ax = plt.subplots(figsize=(6, 4))
ax.hist(alturas, bins=40, color=AZUL, edgecolor="white")   # a borda branca separa as barras
ax.axvline(alturas.mean(), color=LARANJA, linestyle="--", label=f"média = {alturas.mean():.1f} cm")
ax.set_title("5000 alturas sorteadas")
ax.set_xlabel("altura (cm)")
ax.set_ylabel("quantidade")
ax.legend()
plt.show()

Gráfico gerado pelo código acima

Esse formato de sino é a distribuição normal, tema da aula 00.06.

🧪 Experimente bins=5 e bins=300. Com poucas faixas, a forma some; com faixas demais, vira ruído.

7. Vários gráficos lado a lado (subplots)

plt.subplots(linhas, colunas) devolve a figura e um array NumPy de Axes. Numa linha só, você acessa com axs[0], axs[1]…; numa grade, com axs[linha, coluna], a mesma sintaxe da aula passada.

x = np.linspace(-3, 3, 200)

fig, axs = plt.subplots(1, 3, figsize=(12, 3.5))
print(type(axs), axs.shape)

for ax, grau in zip(axs, [1, 2, 3]):
    ax.plot(x, x ** grau, color=AZUL)
    ax.set_title(f"$x^{grau}$")      # entre $...$ o texto vira fórmula matemática (é LaTeX)
    ax.grid(alpha=0.3)

fig.suptitle("Potências de x")       # título da figura inteira
fig.tight_layout()                   # ajusta os espaços pra nada se sobrepor
plt.show()
<class 'numpy.ndarray'> (3,)

Gráfico gerado pelo código acima

8. Imagens e mapas

Um array 2D vira imagem

O imshow desenha um array 2D como imagem: cada número vira um quadradinho colorido. Você já viu isso com a foto da aula 00.02. Agora, uma imagem feita à mão:

carinha = np.array([
    [0, 0, 1, 1, 1, 1, 0, 0],
    [0, 1, 0, 0, 0, 0, 1, 0],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 1, 1, 0, 0, 1],
    [0, 1, 0, 0, 0, 0, 1, 0],
    [0, 0, 1, 1, 1, 1, 0, 0],
])

fig, ax = plt.subplots(figsize=(3, 3))
ax.imshow(carinha, cmap="gray_r")  # cmap = "mapa de cores". "gray_r" = cinza invertido (r de reversed): 0 é branco e 1 é preto
ax.set_title("8×8 pixels")
ax.axis("off")
plt.show()

Gráfico gerado pelo código acima

É literalmente isso que uma imagem é para o computador. Cada dígito da base MNIST, que você vai usar na disciplina 02, é um array 28×28 como esse.

Funções de duas variáveis: curvas de nível

Em ML, a função de custo depende de vários parâmetros. Com dois parâmetros, f(x,y)f(x, y) vira um terreno, e dá pra desenhar como um mapa topográfico: cada cor é uma “altitude”.

O problema: pra desenhar o terreno, é preciso calcular f(x,y)f(x, y) para todas as combinações de x e y. Com xs = [0, 1, 2] e ys = [10, 20], são 2 × 3 = 6 pontos. Dá pra organizar como uma tabela, com os x nas colunas e os y nas linhas:

           x=0       x=1       x=2
  y=10   (0, 10)   (1, 10)   (2, 10)
  y=20   (0, 20)   (1, 20)   (2, 20)

Em C#, você preencheria essa tabela com dois for aninhados:

for (int i = 0; i < ys.Length; i++)       // cada linha (um y)
    for (int j = 0; j < xs.Length; j++)   // cada coluna (um x)
        Z[i, j] = f(xs[j], ys[i]);

O meshgrid pega essa tabela de pares e separa em duas tabelas do mesmo formato, uma só com o x de cada célula (X) e outra só com o y (Y):

xs = np.array([0, 1, 2])
ys = np.array([10, 20])

X, Y = np.meshgrid(xs, ys)
print(X)
print(Y)
[[0 1 2]
 [0 1 2]]
[[10 10 10]
 [20 20 20]]
  • No X, cada linha repete os xs, porque o x muda de coluna em coluna.
  • No Y, cada coluna repete os ys, porque o y muda de linha em linha.

A mesma posição nas duas tabelas dá um ponto. X[1, 2] é 2 e Y[1, 2] é 20, ou seja, a célula da linha 1, coluna 2 é o ponto (2, 20).

Pra que separar: com o x e o y de cada ponto em arrays do mesmo formato, a conta vira elemento a elemento, que é a vetorização da aula 00.02. Os dois for aninhados viram uma linha:

print(X[1, 2], Y[1, 2])   # o ponto da linha 1, coluna 2

Z = X + Y                 # f(x, y) = x + y, calculada na tabela inteira de uma vez
print(Z)
2 20
[[10 11 12]
 [20 21 22]]

Cada célula de Z é o x mais o y daquele ponto. Abaixo, com 200 valores de x e 200 de y, o meshgrid monta uma grade de 40.000 pontos, e Z = X ** 2 + Y ** 2 calcula a “altitude” de todos eles de uma vez. O contourf só pinta essa tabela.

x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = X ** 2 + Y ** 2                     # uma "tigela": o ponto mais baixo é o (0, 0)

fig, ax = plt.subplots(figsize=(5.5, 4.5))
mapa = ax.contourf(X, Y, Z, levels=20, cmap="Blues")    # contourf = curvas de nível preenchidas
fig.colorbar(mapa, ax=ax, label="f(x, y)")              # a legenda das cores
ax.plot(0, 0, marker="*", markersize=15, color=LARANJA, label="mínimo")
ax.set_title("f(x, y) = x² + y²")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_aspect("equal")
ax.legend()
plt.show()

Gráfico gerado pelo código acima

Guarde esta imagem: o gradiente descendente (disciplina 01) é uma bolinha que começa em algum ponto desse mapa e vai descendo até a estrela.

9. Boas práticas (e como salvar)

  • Sempre ponha título e nome nos eixos, com a unidade. Um gráfico sem rótulo vira uma charada.
  • Com duas ou mais séries, use legenda. E diferencie também por marcador ou tipo de linha, não só pela cor.
  • Use uma escala de y por gráfico. Se duas grandezas têm escalas muito diferentes (preço em milhares e quartos de 1 a 5), faça dois gráficos lado a lado, em vez de um eixo y de cada lado.
  • Para intensidade (mapas e curvas de nível), use um colormap de uma cor só, do claro ao escuro ("Blues", "Greys"). O arco-íris ("jet") cria “fronteiras” que não existem nos dados.
  • Deixe a grade clarinha (alpha=0.3): ela ajuda a ler, mas não pode chamar mais atenção que os dados.
  • Escolha cores pensando em daltonismo. As três cores desta aula foram validadas para continuar distinguíveis. Com mais de 3 grupos num scatter, prefira juntar os menores em “outros” ou separar em subplots.

Para salvar um gráfico em arquivo: fig.savefig("grafico.png", dpi=150, bbox_inches="tight"). O bbox_inches="tight" corta as bordas brancas sobrando.


Exercícios

Cada exercício tem uma função que recebe o ax (ou os axs) e desenha nele. A célula já cria a figura, chama a sua função e confere o resultado. A imagem “Objetivo” mostra aonde você precisa chegar.

As cores e os detalhes de estilo ficam por sua conta. A conferência olha só o essencial: a curva certa, os títulos e a legenda.

Código de apoio dos exercícios
from matplotlib.contour import ContourSet


def _tem_curva(ax, f, min_pontos=2):
    # existe alguma linha cujos pontos seguem y = f(x)?
    for linha in ax.lines:
        x = np.asarray(linha.get_xdata(), dtype=float)
        y = np.asarray(linha.get_ydata(), dtype=float)
        if len(x) >= min_pontos and np.allclose(y, f(x), atol=1e-6):
            return True
    return False


def _tem_horizontal(ax, y0, tracejada=False):
    for linha in ax.lines:
        y = np.asarray(linha.get_ydata(), dtype=float)
        if len(y) and np.allclose(y, y0) and (not tracejada or linha.get_linestyle() not in ("-", "None")):
            return True
    return False


def _tem_ponto(ax, x0, y0):
    # vale tanto ax.plot(x0, y0, marker=...) quanto ax.scatter
    for linha in ax.lines:
        x, y = np.asarray(linha.get_xdata(), dtype=float), np.asarray(linha.get_ydata(), dtype=float)
        if len(x) == 1 and np.isclose(x[0], x0) and np.isclose(y[0], y0):
            return True
    for colecao in ax.collections:
        offsets = np.asarray(colecao.get_offsets())
        if offsets.size and np.any(np.all(np.isclose(offsets, [x0, y0]), axis=1)):
            return True
    return False


def confere_grafico(ax, itens):
    if not (ax.lines or ax.collections or ax.patches or ax.images):
        print("⏳ ainda não implementado")
        return
    for descricao, ok in itens.items():
        print(("✅ " if ok else "❌ falta: ") + descricao)


def _basicos(ax, rotulos=True):
    itens = {"título": ax.get_title() != ""}
    if rotulos:
        itens["nome dos dois eixos"] = ax.get_xlabel() != "" and ax.get_ylabel() != ""
    return itens


def confere_ex1(ax):
    confere_grafico(ax, {
        **_basicos(ax),
        "curva da sigmoide de -6 a 6": _tem_curva(ax, lambda x: 1 / (1 + np.exp(-x)), min_pontos=10),
        "linha horizontal tracejada em y = 0.5": _tem_horizontal(ax, 0.5, tracejada=True),
    })


def confere_ex2(ax):
    confere_grafico(ax, {
        **_basicos(ax),
        "pontos dos alunos (scatter)": len(ax.collections) >= 1,
        "reta do modelo: nota = 0.8 × horas + 2": _tem_curva(ax, lambda x: 0.8 * x + 2),
        "legenda": ax.get_legend() is not None,
    })


def confere_ex3(ax):
    transparentes = len(ax.patches) > 0 and all((p.get_alpha() or 1) < 1 for p in ax.patches)
    legenda = ax.get_legend()
    confere_grafico(ax, {
        **_basicos(ax),
        "barras semitransparentes": transparentes,
        "legenda com as duas turmas": legenda is not None and len(legenda.get_texts()) >= 2,
    })


def confere_ex4(axs):
    print("Gráfico da esquerda:")
    confere_grafico(axs[0], {
        **_basicos(axs[0], rotulos=False),
        "curva de f(x) = (x - 2)²": _tem_curva(axs[0], lambda x: (x - 2) ** 2, min_pontos=10),
    })
    print("Gráfico da direita:")
    confere_grafico(axs[1], {
        **_basicos(axs[1], rotulos=False),
        "curva de f'(x) = 2(x - 2)": _tem_curva(axs[1], lambda x: 2 * (x - 2), min_pontos=10),
        "linha horizontal em y = 0": _tem_horizontal(axs[1], 0),
    })


def confere_ex5(fig, ax):
    confere_grafico(ax, {
        **_basicos(ax),
        "curvas de nível (contourf)": any(isinstance(c, ContourSet) for c in ax.collections),
        "barra de cores (colorbar)": len(fig.axes) >= 2,
        "mínimo marcado em (1, -1)": _tem_ponto(ax, 1, -1),
    })

Exercício 1 · Sigmoide

Desenhe a sigmoide σ(x)=11+e−x\sigma(x) = \frac{1}{1 + e^{-x}} para x de -6 a 6, com título, nome nos eixos, grade e uma linha horizontal tracejada em y = 0.5.

Por que 0.5? Na regressão logística, a sigmoide dá a “probabilidade” de algo ser verdade, e 0.5 é a fronteira da decisão: acima dela, o modelo responde “sim”.

Objetivo:

Figura

def desenhar_sigmoide(ax):
    ...  # seu código aqui


fig, ax = plt.subplots(figsize=(6, 4))
desenhar_sigmoide(ax)
plt.show()
confere_ex1(ax)
Dica

np.exp(-x) calcula e−xe^{-x} para cada elemento. A linha horizontal sai com ax.axhline(0.5, linestyle="--").

Gabarito
def desenhar_sigmoide(ax):
    x = np.linspace(-6, 6, 200)
    ax.plot(x, 1 / (1 + np.exp(-x)), color=AZUL)
    ax.axhline(0.5, color="gray", linestyle="--")
    ax.set_title("Sigmoide")
    ax.set_xlabel("x")
    ax.set_ylabel("σ(x)")
    ax.grid(alpha=0.3)

Exercício 2 · Dados e modelo

Desenhe os dados de 30 alunos (horas de estudo × nota) como pontos, com a legenda “alunos”, e a reta do modelo nota = 0.8 × horas + 2, com a legenda “modelo”. Não esqueça título, nome nos eixos e legenda.

Objetivo:

Figura

rng = np.random.default_rng(seed=3)
horas = rng.uniform(0, 10, size=30)                     # horas de estudo de 30 alunos
nota = 0.8 * horas + 2 + rng.normal(0, 0.8, size=30)   # nota na prova


def desenhar_estudo(ax, horas, nota):
    ...  # seu código aqui


fig, ax = plt.subplots(figsize=(6, 4))
desenhar_estudo(ax, horas, nota)
plt.show()
confere_ex2(ax)
Dica

É a mesma receita da seção 4: ax.scatter para os pontos e ax.plot para a reta. Pra desenhar uma reta, dois pontos bastam: np.linspace(0, 10, 2).

Gabarito
def desenhar_estudo(ax, horas, nota):
    ax.scatter(horas, nota, color=AZUL, alpha=0.7, label="alunos")
    x = np.linspace(0, 10, 2)
    ax.plot(x, 0.8 * x + 2, color=LARANJA, label="modelo")
    ax.set_title("Horas de estudo × nota")
    ax.set_xlabel("horas de estudo")
    ax.set_ylabel("nota")
    ax.legend()
    ax.grid(alpha=0.3)

Exercício 3 · Comparando duas turmas

Desenhe no mesmo gráfico os histogramas das notas das turmas A e B. Use barras semitransparentes, pra uma não esconder a outra, e ponha uma legenda com o nome de cada turma.

Objetivo:

Figura

rng = np.random.default_rng(seed=4)
turma_a = rng.normal(6.0, 1.5, size=500)   # notas da turma A
turma_b = rng.normal(7.5, 1.0, size=500)   # notas da turma B


def desenhar_turmas(ax, turma_a, turma_b):
    ...  # seu código aqui


fig, ax = plt.subplots(figsize=(6, 4))
desenhar_turmas(ax, turma_a, turma_b)
plt.show()
confere_ex3(ax)
Dica

Chame ax.hist duas vezes, com alpha=0.5 e label=. Truque: passe as mesmas faixas para as duas turmas, com bins=np.linspace(0, 11, 34), que as barras ficam alinhadas.

Gabarito
def desenhar_turmas(ax, turma_a, turma_b):
    faixas = np.linspace(0, 11, 34)   # as mesmas faixas pras duas turmas, pras barras alinharem
    ax.hist(turma_a, bins=faixas, color=AZUL, alpha=0.5, label="turma A")
    ax.hist(turma_b, bins=faixas, color=LARANJA, alpha=0.5, label="turma B")
    ax.set_title("Notas das turmas")
    ax.set_xlabel("nota")
    ax.set_ylabel("quantidade de alunos")
    ax.legend()

Exercício 4 · Uma função e a derivada dela, lado a lado

Em dois gráficos lado a lado, para x de -1 a 5, desenhe:

  • à esquerda: f(x)=(x−2)2f(x) = (x - 2)^2
  • à direita: a derivada dela, f′(x)=2(x−2)f'(x) = 2(x - 2), com uma linha horizontal em y = 0

Dê um título a cada gráfico. Se quiser, marque x = 2 nos dois com uma linha vertical tracejada (ax.axvline).

Objetivo:

Figura

def desenhar_funcao_e_derivada(axs):
    ...  # seu código aqui


fig, axs = plt.subplots(1, 2, figsize=(10, 3.5))
desenhar_funcao_e_derivada(axs)
fig.tight_layout()
plt.show()
confere_ex4(axs)

Repare no resultado: onde a função tem o ponto mais baixo (x = 2), a derivada vale zero. À esquerda do mínimo a derivada é negativa (a função desce) e à direita é positiva (a função sobe). Essa é a ideia central do gradiente descendente, e é o assunto da aula 00.05.

Dica

axs[0] e axs[1] são dois ax normais. Use o mesmo x para os dois.

Gabarito
def desenhar_funcao_e_derivada(axs):
    x = np.linspace(-1, 5, 200)
    axs[0].plot(x, (x - 2) ** 2, color=AZUL)
    axs[0].set_title("f(x) = (x - 2)²")
    axs[1].plot(x, 2 * (x - 2), color=LARANJA)
    axs[1].axhline(0, color="gray", linewidth=0.8)
    axs[1].set_title("f'(x) = 2(x - 2)")
    for ax in axs:
        ax.axvline(2, color="gray", linestyle="--")
        ax.set_xlabel("x")
        ax.grid(alpha=0.3)

Exercício 5 (desafio) · O terreno de uma função de custo

Desenhe as curvas de nível de f(x,y)=(x−1)2+2(y+1)2f(x, y) = (x - 1)^2 + 2(y + 1)^2 para x e y de -3 a 3, com a barra de cores e o ponto mínimo, (1, -1), marcado.

Repare que o “2” na frente do (y+1)2(y + 1)^2 deixa a tigela mais estreita numa direção. Quando a gente chegar no gradiente descendente, você vai ver que esse tipo de terreno “achatado” deixa o treino mais difícil.

Objetivo:

Figura

def desenhar_terreno(fig, ax):
    ...  # seu código aqui


fig, ax = plt.subplots(figsize=(5.5, 4.5))
desenhar_terreno(fig, ax)
plt.show()
confere_ex5(fig, ax)
Dica

Siga a receita da seção 8: linspace → meshgrid → calcular Z → contourf → fig.colorbar. Para marcar o mínimo: ax.plot(1, -1, marker="*", markersize=15).

Gabarito
def desenhar_terreno(fig, ax):
    x = np.linspace(-3, 3, 200)
    y = np.linspace(-3, 3, 200)
    X, Y = np.meshgrid(x, y)
    Z = (X - 1) ** 2 + 2 * (Y + 1) ** 2
    mapa = ax.contourf(X, Y, Z, levels=20, cmap="Blues")
    fig.colorbar(mapa, ax=ax, label="f(x, y)")
    ax.plot(1, -1, marker="*", markersize=15, color=LARANJA, label="mínimo")
    ax.set_title("f(x, y) = (x - 1)² + 2(y + 1)²")
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_aspect("equal")
    ax.legend()

Resumo

Quero… Código
criar uma figura fig, ax = plt.subplots(figsize=(largura, altura))
vários gráficos fig, axs = plt.subplots(linhas, colunas)
curva / função ax.plot(x, y, color=..., linestyle="--", label="...")
pontos (dados) ax.scatter(x, y, alpha=0.7, marker="o")
distribuição ax.hist(valores, bins=30)
imagem / matriz ax.imshow(array, cmap="gray")
função de 2 variáveis X, Y = np.meshgrid(x, y) e depois ax.contourf(X, Y, Z, cmap="Blues")
linha de referência ax.axhline(y), ax.axvline(x)
textos ax.set_title(...), ax.set_xlabel(...), ax.set_ylabel(...), ax.legend()
mostrar / salvar plt.show(), fig.savefig("arquivo.png", dpi=150)