Chapter 39, Ecosystem
NumPy and Matplotlib
A graph is the quickest way to find out that your data is not what you thought. Matplotlib takes arrays and draws, and returns the image, which in turn is also an array.
Code for this chapter: ecossistema/cap39_numpy_matplotlib.py
Draw without opening a window
In a script, on a server, or in a test, there is no screen. Matplotlib has a backend without a window, the Agg, which draws in memory and saves to a file. Choose it before importing the pyplot. In a notebook, the graphs appear automatically below the cell, and you don't need this line:
uv sync --group ecossistema
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 200)
fig, ax = plt.subplots(figsize=(4, 3), dpi=100)
ax.plot(x, np.sin(x), label="seno")
ax.plot(x, np.cos(x), label="cosseno")
ax.legend()
fig.savefig("ondas.png")
plt.close(fig)
I always use the object-oriented (fig, ax = plt.subplots() interface and then ax.plot(...)), and not the one that depends on a global "current graph" (plt.plot(...)): it becomes clear in which graph each command acts, and the code works in functions and in multiple graphs at the same time. And the plt.close(fig) frees memory, which matters when you generate hundreds of figures in a loop.
The saved graph is a tensor
Reading the image back returns an array of shape (altura, largura, 4), with the red, green, blue, and transparency channels. The size is that of the figure multiplied by the points per inch (4 × 100 for 3 × 100):
imagem = plt.imread("ondas.png")
print(imagem.shape, imagem.dtype)
(300, 400, 4) float32
It's the tensor from chapter 17 in action: an image is an array, and everything you learned (slicing, averaging a channel, comparing two images) works on it.
The `hist` uses `np.histogram`
Matplotlib does not reinvent the calculations. The ax.hist calls the np.histogram from chapter 22 and draws the result, and it also returns the counts for you to use:
dados = np.random.default_rng(0).normal(size=1000)
fig, ax = plt.subplots()
contagens, limites, _ = ax.hist(dados, bins=10)
esperado, _ = np.histogram(dados, bins=10)
print(np.array_equal(contagens, esperado))
plt.close(fig)
True
A 2D array as an image
The imshow draws a matrix as an image, with a color scale. Combined with the meshgrid from chapter 27, it shows a function of two variables. Here, a Gaussian "mountain", with the peak in the center:
X, Y = np.meshgrid(np.linspace(-2, 2, 100), np.linspace(-2, 2, 100))
Z = np.exp(-(X ** 2 + Y ** 2))
fig, ax = plt.subplots()
imagem_z = ax.imshow(Z, extent=(-2, 2, -2, 2), origin="lower")
fig.colorbar(imagem_z)
fig.savefig("montanha.png")
plt.close(fig)
print(Z.shape, round(float(Z.max()), 2))
(100, 100) 1.0
The parameter origin="lower" puts line 0 at the bottom, like in a graph, and not at the top, like in an image. This is a detail that confuses many: images index from top to bottom, and graphs from bottom to top.
Save a matrix directly as an image
The plt.imsave converts an array into an image file without creating a figure, with the color scale of your choice:
plt.imsave("cinza.png", Z, cmap="gray")
print(plt.imread("cinza.png").shape)
(100, 100, 4)
Cleanup
from pathlib import Path
for nome in ("ondas.png", "montanha.png", "cinza.png"):
Path(nome).unlink()
print(Path("ondas.png").exists())
False
| I want | Use |
|---|---|
| A series over time | ax.plot(x, y) |
| The distribution of values | ax.hist(dados, bins=...) |
| Two variables together | ax.scatter(x, y) |
| A matrix or image | ax.imshow(matriz) |
| A function of two variables | meshgrid + imshow or contourf |
| Save without window | matplotlib.use("Agg") + fig.savefig(...) |
Exercise 1
Save a graph and check
Write salvar_grafico(x, y, caminho) that draws the line, saves in caminho, closes the figure and returns the shape of the saved image. With the default figure (6.4 by 4.8 inches and 100 dots per inch), the shape should be (480, 640, 4).