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.

Understanding meshgrid

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

1. Basic meshgrid

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

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

import matplotlib.pyplot as plt
import numpy as np

def f(X, Y):
    return X**2 + Y**2

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

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

3. With Labels

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

import matplotlib.pyplot as plt
import numpy as np

def f(X, Y):
    return np.exp(-X**2 / 2 - Y**2 / 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

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

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

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

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

import matplotlib.pyplot as plt
import numpy as np

def f(X, Y, seed=0):
    np.random.seed(seed)
    return X**2 + Y**2 + 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

def f_noisy(X, Y, noise_level, seed=0):
    np.random.seed(seed)
    return X**2 + Y**2 + 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

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

2. Custom Stride

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

x = np.linspace(-2, 2, 60)
y = np.linspace(-2, 2, 60)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

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

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

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

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

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

2. Visible Edge Lines

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

3. Antialiasing Comparison

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

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

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

3. Alpha Comparison

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

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

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

Z = X**2 - Y**2

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

Z = np.sin(np.sqrt(X**2 + Y**2))

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()
fig, axes = plt.subplots(2, 2, figsize=(12, 10), subplot_kw={'projection': '3d'})

# Paraboloid
axes[0, 0].plot_surface(X, Y, X**2 + Y**2, cmap='viridis')
axes[0, 0].set_title('Paraboloid')

# Saddle
axes[0, 1].plot_surface(X, Y, X**2 - Y**2, 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(-(X**2 + Y**2)), cmap='inferno')
axes[1, 1].set_title('Gaussian')

plt.tight_layout()
plt.show()

Full Customization

1. Complete Example

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(-(X**2 + Y**2) / 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

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()