Визуализация трёхмерных данных с использованием matplotlib#

Рассматриваются основы создания как трёхмерных графиков, так и двухмерных графиков для визуализации трёхмерных данных.

Создание трёхмерных осей#

Импортируем модуль pyplot:

import matplotlib.pyplot as plt

И создаём трёхмерные оси. Для этого сначала создаём объект окна fig класса plt.figure, а затем методом fig.add_subplot(...) добавляем к нему ось с ключевым параметром projection="3d":

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
../_images/869edef82fec793887ba710b6f37de63057e8519925eb5aca3edc99ac7acfc1a.png

Далее с объектом ax можем выполнять все те же операции, что рассматрены в Основы построения графиков и Особенности matplotlib.

Attention

Создавать трёхмерные графики имеет смысл тогда и только тогда, когда они будут использоваться в интерактивном режиме, т.е. тогда, когда имеется возможность “вертеть” эти графики, рассматривая их со всех сторон и масштабируя.

Не стоит вставлять изображения трёхмерных графиков в бумажные отчёты!

Построим график такой функции:

\[ f(x, y) = \sin{\sqrt{x^2 + y^2}} \]

в пределах \(x \in [-6, 6]\) и \(y \in [-6, 6]\).

Инициализация данных для визуализации#

Импортируем numpy и опишем функцию \(f(x, y)\):

import numpy as np

def f_sin_xy(x, y):
    return np.sin(np.sqrt(x**2 + y**2))

Расчётная сетка#

Чтобы построить трёхмерный график, необходимо определить каждую пару \((x_i; y_j)\) (комбинацию из двух элементов массивов x и y), где \(i = 1, 2, \dots, N_x\) и \(j = 1, 2, \dots, N_y\). Другими словами, нам нужны точки \((x_1; y_1)\), \((x_1; y_2)\), \(\dots\), \((x_1; y_{N_y})\), \((x_2; y_1)\), \((x_2; y_2)\), \(\dots\), \((x_{N_x}; y_{N_y})\).

Для начала инициализируем два соответствующих массива x и y в оговорённых пределах по 51 значению в каждом:

x = np.linspace(-6, 6, 51)  # N_x = 51
y = np.linspace(-6, 6, 51)  # N_y = 51

Чтобы их массивов x и y сформировать расчётную сетку \((x_i; y_j)\), применим функцию numpy.meshgrid(...):

x, y = np.meshgrid(x, y)

Мы получим расчётную сетку размером \(N_x \times N_y\). Убеждаемся:

x.shape
(51, 51)
y.shape
(51, 51)

Important

Массивы x и y с этого момента имеют одинаковую форму даже в случае, если бы \(N_x \neq N_y\).

Теперь мы можем вычислить массив z значений функции \(f(x, y)\):

z = f_sin_xy(x, y)
z.shape
(51, 51)

Как видим, массив z имеет такую же форму, как и массивы x и y - функция вычислена в каждой точке. Перейдём к способам визуализации.

Виды трёхмерных графиков#

Контурный график#

Есть три типа контурных графиков:

  • в виде линий уровня,

  • в виде заполненных цветом промежутков между линиями уровня и

  • их комбинация.

Линии уровня#

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.contour3D(x, y, z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/e68a444e7b070492de8ba2494c9188c2c13281d7c6b3932f8e98d92d8bbe58b4.png

Линий уровня на рисунке выше маловато. Увеличим их число до 30:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.contour3D(x, y, z, levels=30)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/c672abbc003399bd24f69673e11eb1f78ef12390a90317c271e782f8eae6b853.png

Note

Цвет в данном случае определяется значением координаты z.

Стоит отметить, что общий стиль графиков также можно изменять с помощью стилевого контекста:

with plt.style.context("bmh"):
    fig = plt.figure()
    ax = fig.add_subplot(projection="3d")

    ax.contour3D(x, y, z, levels=30)

    ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$")
../_images/1a95eb044adc49dbbc30a236062ca8877651776947dafcc853cfbfd8cb27a0ba.png

Заполненные контуры#

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.contourf3D(x, y, z, levels=30)  # Буква "f" добавилась к названию "contour"

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/aa2015b7a3c8705adbf56fe9aa7bf3c595b6e55758f61e404bde4f92f4afe249.png

Attention

Не стоит применять такой график.

Каркас#

Представляет из себя набор прямых, соединяющих ближайшие соседние точки:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.plot_wireframe(x, y, z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/eb269878a0c15d6cbf8bf5e52916547944eff5a1fab8b488d589b07cabd8b0b9.png

Поверхностный график#

Представляет собой заполненные цветом плоскости, образованные ближайшими соседними точками, т.е. области, ограниченные рёбрами каркасного графика:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.plot_surface(x, y, z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/07530299537059c5bfc1e7e3b2ebed20c2084b616a5dcc876abd044ffca62663.png

Можно задать конкретный цвет графика через параметр color или c:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.plot_surface(x, y, z, color="green")

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/0c3d705713d2bba9cfa78db21736449916b716f85d815fbe9496425ca010c757.png

Или задать цветовую схему cmap (color map), например viridis. Тогда цвет будет показывать относительную величину z-координаты.

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.plot_surface(x, y, z, cmap="viridis")

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/e26ef323f22306898d84bf769b76a575c8b49e0a7e99e0aaea17343c327d214e.png

Точечный график#

Изобразим трёхмерные точки:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.scatter3D(x, y, z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/2427b944d9416cc6100647ebbce84eadc860943f1655adc2ceb5a49da74eadf8.png

Можно поставить цвет точки в зависимость от значений той или иной координаты графика. Это делается через параметр цвета c, которому присваивается желаемый массив, например, z:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.scatter3D(x, y, z, c=z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/a8d30f9786763895f0d85a4990a67ae32fef4c3b287b05a566e98bad20bfb5e8.png

Или свяжем цвет с координатой x:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.scatter3D(x, y, z, c=x)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/67dfa18085234e827ea11110f199e463d139c8bc729024c9711f246e14454c39.png

Или, и вовсе, сделаем выражением \((x - y)^2\):

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.scatter3D(x, y, z, c=(x-y)**2)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/8ef91bd331ccb16ba6bb6c88fac556031c584782adf84847996cf1db93d0a48f.png

Attention

У данного типа графика (scatter3D) нет параметра color, только c.

Триангулированная поверхность#

Данный тип графиков несколько особенный. Это объясняется его предназначением - таким способом строят сложные поверхности с неравномерной сеткой. Примером такой поверхности может быть сфера, тор и т.п. То есть точки для данного графика могут быть расположены в пространстве произвольным образом и на вход должны подаваться в виде одномерных массивов x, y, z.

Переиницилизируем массивы x и y, причём заполним их случайными значениями, и пересчитаем массив z:

x = np.random.uniform(-6, 6, 1001)
y = np.random.uniform(-6, 6, 1001)
z = f_sin_xy(x, y)
x.shape, y.shape, z.shape
((1001,), (1001,), (1001,))

Убедимся, что эти точки расположены в пространстве хаотично, построив точечный график:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.scatter3D(x, y, z, c=z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/10f1374f006ef4a1b43c061fde6788ae6f1bb93e0d47c82f6d022e204eb56572.png

Очевидно, что точки действительно расположены в плоскости \(Oxy\) случайным образом.

Теперь построим триангулированную поверхность:

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

ax.plot_trisurf(x, y, z)

ax.set(xlabel="$x$", ylabel="$y$", zlabel="$z$");
../_images/cc9a921c9d3ca6df4314c9375fb2e0652b36fb6d253c8c78f69f5ce251e9928c.png

Триангулированные поверхности строятся как выпуклые оболочки, этим и достигается возможность однозначного связывания точек.

Note

Очевидно, что поверхность будет тем качественнее, чем большее количество точек задано.

Important

К настоящему моменту после просмотра ряда неинтерактивных трёхмерных графиков должно сформироваться понимание их малой информативности и ценности при использовании в бумажных (плоских) отчётах. Поэтому далее рассмотрим двумерные способы визуализации трёхмерных данных.

Стоит отметить, что возможно строить и трёхмерные кривые (и прямые), а также плоские кривые и точки, лежащие в одной плоскости, в трёхмерных осях.

Двухмерные способы визуализации трёхмерных данных#

Будем визуализировать всё ту же функцию \(f(x, y) = \sqrt{x^2 + y^2}\).

Актуализируем массивы x, y, z:

x, y = np.meshgrid(
    np.linspace(-6, 6, 51),
    np.linspace(-6, 6, 51)
)
z = f_sin_xy(x, y)

Как видите, массивы инициализированы абсолютно так же, как и для построения трёхмерных графиков. В общем и целом, методы построения графиков двухмерных визуализаций 3D-данных те же, что описаны выше.

Линии уровня#

Метод plt.axis.contour(...) построит нам линии уровня, как если бы мы смотрели на 3D-график сверху:

fig, ax = plt.subplots()

ax.contour(x, y, z, levels=30)

ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/a5ffa13496c1b5f1ed75a1c116baac68c79655a8bfa6913e0787e6e6d87a79ee.png

Однако по сравнению с 3D-графиками у нас явно остутствует третья ось - z. Это приводит к тому, что не ясны значения цветов - видно лишь качественное поведение графика, но не видны абсолютные значения z, стоящие за ним. Исправить ситуацию поможет цветовая шкала (colorbar):

fig, ax = plt.subplots()

# Сохраняем изображение оси:
# именно на этом этапе цвета cmap связываются с данными z
image = ax.contour(x, y, z, levels=30)
# Используем это изображение для цветовой шкалы.
# Заодно присвоем шкале метку недостающей оси z
fig.colorbar(image, ax=ax, label="$z$")

ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/b1f23106f1730fe41f8edb7d116ac29ebf29c1e15b563195b6fd02c9e2d178cc.png

Вместо цветовой шкалы можно применить и другой подход - показывать абсолютные значения z прямо на линиях уровня (только число линий уровня нужно сделать небольшим):

fig, ax = plt.subplots()

# Сохраняем связь контуров с цветовой схемой
contours = ax.contour(x, y, z)
# и используем для инициализации меток в этих контурах
ax.clabel(contours, inline=True)

ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/f74b9c9a44514953150686ee0c1833395c40e0da66681e2dbb52b022130c935d.png

Заполненные контуры#

Аналогично contourf3D(...) функция plt.axis.contourf(...) строит линии уровня, линейно заполняя цветом пространство между ними. Для разнообразия используем стиль dark_background:

fig, ax = plt.subplots()
ax.contourf(x, y, z, levels=30)
ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/d819b97cda11fd405d2ce3e9a7b4e5435089d26dd4b0a4b8bdc46965630b4e3d.png

В данном случае так же не хватает значений третьей оси. Добавим цветовую шкалу:

fig, ax = plt.subplots()
contours = ax.contourf(x, y, z, levels=30)
fig.colorbar(contours, ax=ax, label="$z$")
ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/d7aac98ae9552c9e0bafaa89e61c5f7c94be6eac2465442d4261775f42b824b0.png

Заполненный контур с линиями уровня#

Для этого достаточно построить заполненный контур, а затем построить линии уровня. Главное, чтобы линии уровня хорошо выделялись на фоне цветов заполненного контура. Для этого сделаем линии уровня красными (cmap="Reds"):

fig, ax = plt.subplots()
# Строим заполненный контур
contours = ax.contourf(x, y, z, levels=30)
# и связанную с ним цветовую шкалу.
fig.colorbar(contours, ax=ax, label="$z$")
# Строим красные линии уровня
contours = ax.contour(x, y, z, levels=5, cmap="Reds")
# с метками значений на линии.
ax.clabel(contours, inline=True)
ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/fbb68521cba8adef0be06ad7a806260b1600d87354e9a9c3eec708aa205693f2.png

График разброса (точечный)#

Аналогичную функции plt.axis.scatter3D(...) работу выполняет метод plt.axis.scatter(...):

fig, ax = plt.subplots()
# Обратите внимание, что z не передаётся как координата,
# вместо этого z имеет смысл для инициализации цвета
points = ax.scatter(x, y, c=z, marker=".")
fig.colorbar(points, ax=ax, label="$z$")
ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/1334dd5af94b9d6b98bd21710746ab031b0ae90a3d8aa9a092fc977a468ab6f7.png

Создадим случайные массивы x и y и пересчитаем z:

x = np.random.uniform(-6, 6, 1001)
y = np.random.uniform(-6, 6, 1001)
z = f_sin_xy(x, y)

Снова построим график разброса:

fig, ax = plt.subplots()
points = ax.scatter(x, y, c=z, marker=".")
fig.colorbar(points, ax=ax, label="$z$")
ax.set(xlabel="$x$", ylabel="$y$", aspect="equal");
../_images/98ca4fad35adaa1a2ff4c2d319e421b1e36ca2faf627713fae1f5af1b3fbb291.png

Удобство и информативность двумерных графиков трёхмерных данных очевидна.

Important

Используйте двумерные способы визуализации трёхмерных данных при оформлении бумажных отчётов по своим работам. Трёхмерную визуализацию оставьте для интерактивных способов публикации своих результатов.