Perturbation-space heatmaps
This notebook illustrates how to use demovuln outputs to visualize population reduction across slices of the perturbation space.
The example uses a simple three-stage model with one juvenile stage, one subadult stage, and one adult stage. The model is intentionally minimal, but the same workflow applies to empirical projection matrices.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from demovuln import MatrixPopulationModel, PerturbationGrid, run_grid
Define a three-stage projection matrix
The model contains one fecundity row and explicit stage definitions. Here, the adult source stage is the third column.
A = np.array([
[0.0, 0.0, 1.2],
[0.35, 0.55, 0.0],
[0.0, 0.20, 0.85],
])
model = MatrixPopulationModel(
A,
adult_stages=[2],
juvenile_stages=[0, 1],
name="three_stage_example",
)
print(f"Dominant eigenvalue: {model.lambda_:.3f}")
print("Stable stage distribution:", model.stable_distribution())
Helper function for heatmaps
The table returned by run_grid is transformed into a two-dimensional matrix using pandas.pivot_table. The plotting function below keeps each heatmap in its own figure.
def plot_heatmap(table, *, x, y, value="population_reduction", title=None):
pivot = table.pivot_table(index=y, columns=x, values=value, aggfunc="mean")
fig, ax = plt.subplots(figsize=(7, 4.5))
image = ax.imshow(
pivot.values,
origin="lower",
aspect="auto",
extent=[
pivot.columns.min(),
pivot.columns.max(),
pivot.index.min(),
pivot.index.max(),
],
)
ax.set_xlabel(x)
ax.set_ylabel(y)
if title is not None:
ax.set_title(title)
cbar = fig.colorbar(image, ax=ax)
cbar.set_label("Population reduction (%)")
fig.tight_layout()
plt.show()
Magnitude-duration slices at fixed period
We first fix the perturbation period and vary magnitude and duration. This is analogous to taking a two-dimensional slice through the full perturbation space.
fixed_period_grid = PerturbationGrid(
magnitudes=np.linspace(0, 0.8, 17),
durations=range(0, 7),
periods=[8],
)
fixed_period_results = {}
for target in ["adult_survival", "juvenile_survival", "fecundity"]:
fixed_period_results[target] = run_grid(
model,
target=target,
grid=fixed_period_grid,
t_max=48,
recovery_steps=12,
)
print(f"{target}: Φ = {fixed_period_results[target].vulnerability:.2f}%")
for target, result in fixed_period_results.items():
plot_heatmap(
result.table,
x="magnitude",
y="duration",
title=f"{target}: magnitude-duration slice at period = 8",
)
Duration-period slices at fixed magnitude
We can also fix perturbation magnitude and examine how population reduction depends on duration and recurrence.
fixed_magnitude = 0.25
duration_period_grid = PerturbationGrid(
magnitudes=[fixed_magnitude],
durations=range(0, 9),
periods=range(1, 13),
)
adult_duration_period = run_grid(
model,
target="adult_survival",
grid=duration_period_grid,
t_max=60,
recovery_steps=12,
skip_infeasible=True,
)
plot_heatmap(
adult_duration_period.table,
x="period",
y="duration",
title=f"Adult survival: duration-period slice at magnitude = {fixed_magnitude}",
)
Integrated vulnerability by demographic target
Finally, we compute integrated vulnerability for several demographic targets over the same perturbation grid.
full_grid = PerturbationGrid(
magnitudes=np.linspace(0, 0.8, 17),
durations=range(0, 7),
periods=range(1, 13),
)
target_summary = []
for target in ["adult_survival", "juvenile_survival", "fecundity", "all"]:
result = run_grid(
model,
target=target,
grid=full_grid,
t_max=60,
recovery_steps=12,
)
target_summary.append({"target": target, "vulnerability": result.vulnerability})
target_summary = pd.DataFrame(target_summary)
target_summary
fig, ax = plt.subplots(figsize=(7, 4))
ax.bar(target_summary["target"], target_summary["vulnerability"])
ax.set_xlabel("Perturbed demographic target")
ax.set_ylabel("Integrated vulnerability Φ (%)")
ax.set_title("Integrated vulnerability across perturbation targets")
ax.tick_params(axis="x", rotation=30)
fig.tight_layout()
plt.show()
Notes
These examples are deliberately small enough to run quickly. For empirical analyses, the same functions can be used with larger projection matrices and denser perturbation grids. The computational cost scales with the number of simulated perturbation regimes and the projection horizon.