Skip to content

Axes Method - plot_surface

The plot_surface method creates a 3D surface plot from grid data. It requires 2D arrays for X, Y coordinates and corresponding Z values.

Mental Model

plot_surface(X, Y, Z) renders a 3D terrain from three 2D arrays: X and Y define a coordinate grid (built with np.meshgrid), and Z holds the height at each grid point. Think of it as draping a colored sheet over a landscape. Use cmap to color the surface by height and view_init to rotate the camera for the best viewing angle.

Understanding meshgrid

Before using plot_surface, understand how np.meshgrid creates coordinate grids.

1. Basic meshgrid

```python import numpy as np

x = np.array([0, 1, 2]) # (3,) y = np.array([-2, -1, 0, 1, 2]) # (5,) X, Y = np.meshgrid(x, y) print(X) # (5, 3) print(Y) # (5, 3) ```

Output: ``` X: [[0 1 2] [0 1 2] [0 1 2] [0 1 2] [0 1 2]]

Y: [[-2 -2 -2] [-1 -1 -1] [ 0 0 0] [ 1 1 1] [ 2 2 2]] ```

2. meshgrid for Surface Plots

```python import numpy as np

n = 40 x = np.linspace(-3.0, 3.0, n) # (40,) y = np.linspace(-3.0, 3.0, n) # (40,) X, Y = np.meshgrid(x, y) # (40, 40), (40, 40) ```

Basic Usage

Create 3D surface plots with the projection='3d' subplot keyword.

1. Simple Surface

```python import matplotlib.pyplot as plt import numpy as np

def f(X, Y): return X2 + Y2

x = np.linspace(-2, 2, 30) y = np.linspace(-2, 2, 30) X, Y = np.meshgrid(x, y) Z = f(X, Y)

fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z) plt.show() ```

2. With Colormap

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis') plt.show()

3. With Labels

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis') ax.set_xlabel('X axis') ax.set_ylabel('Y axis') ax.set_zlabel('Z axis') plt.show()

Standard Normal PDF

Plot the standard bivariate normal probability density function.

1. Basic Normal PDF

```python import matplotlib.pyplot as plt import numpy as np

def f(X, Y): return np.exp(-X2 / 2 - Y2 / 2) / (2 * np.pi)

x = np.linspace(-4, 4, 100) y = np.linspace(-4, 4, 100) X, Y = np.meshgrid(x, y) Z = f(X, Y)

fig, ax = plt.subplots(figsize=(8, 6), subplot_kw={'projection': '3d'}) ax.set_title("Standard Normal PDF", fontsize=15) ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('f(x, y)') plt.show() ```

2. With Custom Z-ticks

python fig, ax = plt.subplots(figsize=(5 * 1.61803398875, 5), subplot_kw={'projection': '3d'}) ax.set_title("Standard Normal PDF", fontsize=15) ax.plot_surface( X, Y, Z, rstride=2, cstride=2, cmap=plt.cm.coolwarm, linewidth=0.5, antialiased=True ) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('f(x, y)') ax.set_zticks((0.05, 0.10, 0.15)) plt.show()

Multivariate Normal with Correlation

Plot bivariate normal distributions with different correlation coefficients.

1. Setup Function

```python import matplotlib.pyplot as plt import numpy as np from scipy import stats

def create_bivariate_normal(mu_1, mu_2, sigma_1, sigma_2, rho): return stats.multivariate_normal( [mu_1, mu_2], [[sigma_1, rho * sigma_1 * sigma_2], [rho * sigma_1 * sigma_2, sigma_2]] ) ```

2. Single Correlation

```python n = 40 mu_1, mu_2 = 0, 0 sigma_1, sigma_2 = 1, 0.5 rho = 0.0

x = np.linspace(-3.0, 3.0, n) y = np.linspace(-3.0, 3.0, n) X, Y = np.meshgrid(x, y) pos = np.empty(X.shape + (2,)) pos[:, :, 0] = X pos[:, :, 1] = Y

Z = create_bivariate_normal(mu_1, mu_2, sigma_1, sigma_2, rho)

fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z.pdf(pos), cmap='viridis', linewidth=0) ax.set_xlabel('X axis') ax.set_ylabel('Y axis') plt.show() ```

3. Comparing Correlations

```python n = 40 mu_1, mu_2 = 0, 0 sigma_1, sigma_2 = 1, 0.5 rho1, rho2, rho3 = 0.0, -0.8, 0.8

x = np.linspace(-3.0, 3.0, n) y = np.linspace(-3.0, 3.0, n) X, Y = np.meshgrid(x, y) pos = np.empty(X.shape + (2,)) pos[:, :, 0] = X pos[:, :, 1] = Y

Z = lambda rho: stats.multivariate_normal( [mu_1, mu_2], [[sigma_1, rho * sigma_1 * sigma_2], [rho * sigma_1 * sigma_2, sigma_2]] )

fig, (ax0, ax1, ax2) = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

ax0.plot_surface(X, Y, Z(rho1).pdf(pos), cmap='Oranges', linewidth=0) ax0.set_xlabel('X axis') ax0.set_ylabel('Y axis') ax0.set_title(f'ρ = {rho1}')

ax1.plot_surface(X, Y, Z(rho2).pdf(pos), cmap='viridis', linewidth=0) ax1.set_xlabel('X axis') ax1.set_ylabel('Y axis') ax1.set_title(f'ρ = {rho2}')

ax2.plot_surface(X, Y, Z(rho3).pdf(pos), cmap='PuBuGn', linewidth=0) ax2.set_xlabel('X axis') ax2.set_ylabel('Y axis') ax2.set_title(f'ρ = {rho3}')

plt.tight_layout() plt.show() ```

Noisy Surface

Add random noise to surface data.

1. Noisy Bowl

```python import matplotlib.pyplot as plt import numpy as np

def f(X, Y, seed=0): np.random.seed(seed) return X2 + Y2 + 0.15 * np.random.normal(0., 1., X.shape)

x = np.linspace(-1.5, 1.5, 80) y = np.linspace(-1.5, 1.5, 80) X, Y = np.meshgrid(x, y) Z = f(X, Y)

fig, ax = plt.subplots(figsize=(9, 6), subplot_kw={'projection': '3d'}) ax.plot_surface( X, Y, Z, rstride=2, cstride=2, cmap=plt.cm.coolwarm, linewidth=0.5, antialiased=True ) ax.set_xlabel('x') ax.set_ylabel('y') ax.set_zlabel('f(x, y)') plt.show() ```

2. Noise Level Comparison

```python def f_noisy(X, Y, noise_level, seed=0): np.random.seed(seed) return X2 + Y2 + noise_level * np.random.normal(0., 1., X.shape)

x = np.linspace(-1.5, 1.5, 50) y = np.linspace(-1.5, 1.5, 50) X, Y = np.meshgrid(x, y)

fig, axes = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

noise_levels = [0.0, 0.15, 0.5] titles = ['No Noise', 'Low Noise', 'High Noise']

for ax, noise, title in zip(axes, noise_levels, titles): Z = f_noisy(X, Y, noise) ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, linewidth=0.5) ax.set_title(title) ax.set_xlabel('x') ax.set_ylabel('y')

plt.tight_layout() plt.show() ```

Stride Parameters

Control surface mesh density with rstride and cstride.

1. Default Stride

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis') plt.show()

2. Custom Stride

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, rstride=2, cstride=2, cmap='viridis') plt.show()

3. Stride Comparison

```python x = np.linspace(-2, 2, 60) y = np.linspace(-2, 2, 60) X, Y = np.meshgrid(x, y) Z = np.sin(np.sqrt(X2 + Y2))

fig, axes = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

strides = [1, 3, 6]

for ax, s in zip(axes, strides): ax.plot_surface(X, Y, Z, rstride=s, cstride=s, cmap='viridis') ax.set_title(f'rstride={s}, cstride={s}')

plt.tight_layout() plt.show() ```

Colormap Options

Different colormaps for surface visualization.

1. Sequential Colormaps

```python x = np.linspace(-3, 3, 50) y = np.linspace(-3, 3, 50) X, Y = np.meshgrid(x, y) Z = np.sin(X) * np.cos(Y)

fig, axes = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

cmaps = ['viridis', 'plasma', 'inferno']

for ax, cmap in zip(axes, cmaps): ax.plot_surface(X, Y, Z, cmap=cmap) ax.set_title(f"cmap='{cmap}'")

plt.tight_layout() plt.show() ```

2. Diverging Colormaps

```python fig, axes = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

cmaps = ['coolwarm', 'RdBu', 'seismic']

for ax, cmap in zip(axes, cmaps): ax.plot_surface(X, Y, Z, cmap=cmap) ax.set_title(f"cmap='{cmap}'")

plt.tight_layout() plt.show() ```

3. Qualitative Colormaps

```python fig, axes = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

cmaps = ['Oranges', 'PuBuGn', 'YlOrRd']

for ax, cmap in zip(axes, cmaps): ax.plot_surface(X, Y, Z, cmap=cmap) ax.set_title(f"cmap='{cmap}'")

plt.tight_layout() plt.show() ```

Linewidth and Antialiasing

Control edge appearance.

1. No Edge Lines

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis', linewidth=0) plt.show()

2. Visible Edge Lines

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis', linewidth=0.5) plt.show()

3. Antialiasing Comparison

```python fig, axes = plt.subplots(1, 2, figsize=(12, 5), subplot_kw={'projection': '3d'})

axes[0].plot_surface(X, Y, Z, cmap='viridis', linewidth=0.5, antialiased=False) axes[0].set_title('antialiased=False')

axes[1].plot_surface(X, Y, Z, cmap='viridis', linewidth=0.5, antialiased=True) axes[1].set_title('antialiased=True')

plt.tight_layout() plt.show() ```

Alpha Transparency

Control surface transparency.

1. Opaque Surface

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis', alpha=1.0) plt.show()

2. Semi-transparent Surface

python fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.7) plt.show()

3. Alpha Comparison

```python fig, axes = plt.subplots(1, 3, figsize=(15, 5), subplot_kw={'projection': '3d'})

alphas = [1.0, 0.7, 0.4]

for ax, a in zip(axes, alphas): ax.plot_surface(X, Y, Z, cmap='viridis', alpha=a) ax.set_title(f'alpha={a}')

plt.tight_layout() plt.show() ```

Common Mathematical Surfaces

1. Paraboloid

```python x = np.linspace(-2, 2, 50) y = np.linspace(-2, 2, 50) X, Y = np.meshgrid(x, y) Z = X2 + Y2

fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='viridis') ax.set_title('Paraboloid: \(z = x^2 + y^2\)') plt.show() ```

2. Saddle Point

```python Z = X2 - Y2

fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='coolwarm') ax.set_title('Saddle: \(z = x^2 - y^2\)') plt.show() ```

3. Sinusoidal Surface

```python Z = np.sin(np.sqrt(X2 + Y2))

fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) ax.plot_surface(X, Y, Z, cmap='plasma') ax.set_title('Sinusoidal: \(z = sin(\\sqrt{x^2 + y^2})\)') plt.show() ```

```python fig, axes = plt.subplots(2, 2, figsize=(12, 10), subplot_kw={'projection': '3d'})

Paraboloid

axes[0, 0].plot_surface(X, Y, X2 + Y2, cmap='viridis') axes[0, 0].set_title('Paraboloid')

Saddle

axes[0, 1].plot_surface(X, Y, X2 - Y2, cmap='coolwarm') axes[0, 1].set_title('Saddle')

Sinusoidal

axes[1, 0].plot_surface(X, Y, np.sin(X) * np.cos(Y), cmap='plasma') axes[1, 0].set_title('sin(x)cos(y)')

Gaussian

axes[1, 1].plot_surface(X, Y, np.exp(-(X2 + Y2)), cmap='inferno') axes[1, 1].set_title('Gaussian')

plt.tight_layout() plt.show() ```

Full Customization

1. Complete Example

```python import matplotlib.pyplot as plt import numpy as np

x = np.linspace(-3, 3, 80) y = np.linspace(-3, 3, 80) X, Y = np.meshgrid(x, y) Z = np.exp(-(X2 + Y2) / 2) / (2 * np.pi)

fig, ax = plt.subplots(figsize=(10, 8), subplot_kw={'projection': '3d'}) surf = ax.plot_surface( X, Y, Z, rstride=2, cstride=2, cmap=plt.cm.coolwarm, linewidth=0.3, antialiased=True, alpha=0.9 ) ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') ax.set_title('Bivariate Normal Distribution', fontsize=14) fig.colorbar(surf, shrink=0.5, aspect=10) plt.show() ```

2. Professional Presentation

```python from scipy import stats

n = 50 x = np.linspace(-3, 3, n) y = np.linspace(-3, 3, n) X, Y = np.meshgrid(x, y) pos = np.dstack((X, Y))

rv = stats.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]]) Z = rv.pdf(pos)

fig, ax = plt.subplots(figsize=(10, 8), subplot_kw={'projection': '3d'}) surf = ax.plot_surface( X, Y, Z, cmap='viridis', linewidth=0, antialiased=True ) ax.set_xlabel('X', fontsize=12) ax.set_ylabel('Y', fontsize=12) ax.set_zlabel('Density', fontsize=12) ax.set_title('Bivariate Normal (ρ = 0.5)', fontsize=14, fontweight='bold') fig.colorbar(surf, shrink=0.6, aspect=15, label='Probability Density') plt.tight_layout() plt.show() ```

In Machine Learning

Surface plots visualize loss landscapes — plot the loss function over two weight dimensions to see minima, saddle points, and the terrain that gradient descent navigates. This is a key tool for understanding optimization behavior, especially when comparing different architectures or learning rates.

Choosing the Right 3D Representation

Data type Method When to use
1D parametric: x(t), y(t), z(t) ax.plot(x, y, z) Trajectories, helices, orbits
2D scalar field: z = f(x, y) ax.plot_surface(X, Y, Z) Landscapes, PDFs, potential fields
Complex function: f(t) = u(t) + iv(t) ax.plot(t, Re, Im) Characteristic functions, Euler's formula
Contour projection ax.contour(X, Y, Z) Isolines, level sets

Use parametric curves when data flows along a single parameter; use surfaces when data covers a 2D domain.

Surface vs contour: use plot_surface when absolute height matters (seeing peaks and valleys). Use contour/contourf when structure matters more than height (seeing level sets, decision boundaries, gradients). Often the clearest approach is a 2D contour plot rather than a 3D surface — especially in static figures.


Exercises

Exercise 1. Create a 3D surface plot of the function \(z = \cos(x) \cdot \sin(y)\) over the domain \([-\pi, \pi] \times [-\pi, \pi]\) using meshgrid. Apply the coolwarm colormap, add a colorbar with label "Value", and set axis labels.

Solution to Exercise 1
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-np.pi, np.pi, 100)
y = np.linspace(-np.pi, np.pi, 100)
X, Y = np.meshgrid(x, y)
Z = np.cos(X) * np.sin(Y)

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='coolwarm')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title(r'$z = \cos(x) \cdot \sin(y)$')
fig.colorbar(surf, shrink=0.6, label='Value')
plt.show()

Exercise 2. Plot two surfaces on the same 3D axes: \(z_1 = x^2 + y^2\) (a paraboloid) and \(z_2 = 8 - x^2 - y^2\) (an inverted paraboloid) over \([-2, 2] \times [-2, 2]\). Use different colormaps for each surface and set alpha=0.7 so both are visible. Add a title describing the intersection.

Solution to Exercise 2
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-2, 2, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z1 = X**2 + Y**2
Z2 = 8 - X**2 - Y**2

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X, Y, Z1, cmap='viridis', alpha=0.7)
ax.plot_surface(X, Y, Z2, cmap='plasma', alpha=0.7)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title('Paraboloid and Inverted Paraboloid')
plt.show()

Exercise 3. Create a surface plot of the 2D Gaussian \(z = e^{-(x^2 + y^2)}\) over \([-3, 3] \times [-3, 3]\). Customize the plot with rstride=2, cstride=2, edgecolor='black', linewidth=0.3, and the viridis colormap. Add a colorbar and adjust the view angle to elev=35, azim=225.

Solution to Exercise 3
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X**2 + Y**2))

fig = plt.figure(figsize=(10, 7))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='viridis', rstride=2, cstride=2,
                       edgecolor='black', linewidth=0.3)
ax.view_init(elev=35, azim=225)
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')
ax.set_title(r'$z = e^{-(x^2 + y^2)}$')
fig.colorbar(surf, shrink=0.6)
plt.show()