Differential Calculus and Optimization Basics › Multivariable Calculus
Quantitative FinanceAll chapters Differential Calculus and Optimization Basics › Multivariable CalculusOne section · Sections in this chapter3 / 11 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
The portfolio and option models used here depend on multiple inputs. A portfolio's return depends on the returns of its constituent assets, with their weights specified. An option's model value depends on price, time, volatility, and interest rates simultaneously. This requires extending calculus to functions of multiple variables.
In single-variable calculus, the input has one independent coordinate, which can increase or decrease. With two or more input dimensions, there are infinitely many unit directions we could move. Understanding how the function changes in each of these directions requires multivariable calculus.
Partial Derivatives
When a function depends on multiple variables, the partial derivative measures the rate of change with respect to one variable while holding all others constant.
The key conceptual insight is that partial derivatives treat other variables as if they were constants. If you want to know how portfolio return changes when you adjust the weight in one particular asset, you imagine all other weights frozen in place and observe how the return responds to that single change. This "one variable at a time" approach simplifies the analysis but also limits what we can learn. The true behavior of the function involves simultaneous changes in multiple variables, which we will address with gradients and directional derivatives.
For a function , the partial derivative with respect to is:
where:
- : the variable with respect to which we differentiate
- : a small increment approaching zero
- for : all other variables, held constant during differentiation
We treat all variables except as constants and differentiate normally.
The notation uses the curly "partial" symbol ∂ rather than the regular "d" used in single-variable calculus. This notational distinction is a reminder that we are holding other variables fixed. The computation itself follows the same rules as ordinary differentiation: we simply treat the other variables as constants and differentiate with respect to the variable of interest.
Consider a simple two-asset portfolio where total return depends on the returns and of each asset and their weights and :
The partial derivatives tell us how sensitive total return is to each input:
The first equation says that the sensitivity of portfolio return to the first weight equals the return of that asset. The second says that sensitivity to the first asset's return equals its weight in the portfolio. Both are intuitive, but partial derivatives make this reasoning precise.
These results have immediate practical interpretations. Holding fixed, increasing by 0.01 changes portfolio return by but also increases net exposure and therefore requires additional financing. In a fully invested two-asset portfolio, , so the relevant reallocation derivative is : shifting 0.01 from asset 2 to asset 1 changes return by . Separately, if asset 1's return changes by 0.01 while its weight is 0.40, portfolio return changes by 0.004, or 0.4 percentage points. The same local linearization idea also appears in factor-based risk decompositions.
For a linear factor-return model , is the vector of factor returns and is the vector of held-fixed exposures. A factor move changes return by exactly. For a differentiable nonlinear factor model, the corresponding local gradient gives a first-order approximation instead.
The Gradient Vector
Collecting all partial derivatives of a function into a single vector gives us the gradient, a fundamental object in optimization.
The gradient represents the multivariable generalization of the derivative. For a differentiable function, it describes first-order changes from simultaneous input moves. A nonzero gradient points toward local ascent in the chosen Euclidean coordinate scaling; its negative points toward local descent. Whether either move improves the objective depends on whether we are maximizing or minimizing and whether the direction is feasible.
The gradient of a function is the vector of all partial derivatives:
where:
- : the gradient operator (nabla) applied to , producing a vector
- : the partial derivative of with respect to the -th variable
- : the dimension of the input space
For a differentiable function, a nonzero points in the direction of steepest ascent among Euclidean unit directions in the chosen coordinate scaling. Existence of coordinate partial derivatives alone does not guarantee this directional interpretation.
Assume is differentiable at the point, so a small input vector satisfies : the remainder becomes small relative to the size of the input move. Among Euclidean unit directions, maximizes the directional derivative when the gradient is nonzero. Its magnitude is that maximum first-order rate of increase. Both the direction and norm depend on coordinate scaling, so inputs with different units need a specified scaling before those comparisons are meaningful.
To understand this result, consider the directional derivative for a Euclidean unit vector . The Cauchy-Schwarz inequality gives , with equality in the normalized gradient direction when the gradient is nonzero; the opposite direction gives steepest descent. If the gradient is zero, every first-order directional derivative is zero, so no unique steepest direction exists.
The magnitude measures the maximum first-order rate at that point under this differentiability and scaling convention. A small value does not imply small curvature or a uniformly flat surrounding region. A zero gradient identifies a stationary point with no preferred first-order direction; it may be a minimum, maximum, saddle, or degenerate stationary point requiring further analysis.
import numpy as np
def f(x, y):
"""A quadratic function of two variables"""
return x**2 + 2 * y**2 - 2 * x * y + 4 * x - 6 * y
def gradient_f(x, y):
"""Gradient of f computed analytically"""
df_dx = 2 * x - 2 * y + 4
df_dy = 4 * y - 2 * x - 6
return np.array([df_dx, df_dy])
# Evaluate gradient at a specific point
point = np.array([1.0, 2.0])
grad = gradient_f(point[0], point[1])Function value at (1, 2): -3.00 Gradient at (1, 2): [2.00, 0.00] Gradient magnitude: 2.00
The gradient at (1, 2) is [2, 0], so the positive direction has the largest first-order increase among Euclidean unit directions. The component is zero: a move has no first-order effect at this point, not no finite effect.
Holding gives the slice . It has a minimum at , and either signed move to raises the value by . The zero partial derivative is a stationary-slice condition, not constancy as varies. This is not the full two-variable optimum because the partial is still nonzero. For this quadratic, we find the candidate minimum by setting both partials to zero and then check its Hessian.

Numerical Differentiation
While analytical derivatives are preferred when available, we often need to compute derivatives numerically, especially for complex models or when working with black-box functions.
Sometimes we have access to a function only through evaluations: we can supply inputs and observe outputs, but cannot differentiate an accessible expression. This can arise with simulation-based models, evaluation-only software interfaces, or nested pricing calculations. Numerical differentiation approximates derivatives using function evaluations. Nested calculations alone do not rule out automatic differentiation when the underlying code is accessible.
The simplest approach uses the finite difference approximation:
For sufficiently smooth functions, the central difference formula has higher truncation order:
where:
- : the derivative we are approximating
- : a small step size (often around to for central differences in double precision, depending on scaling)
- and : function evaluations at points symmetric around
For smooth functions with the required derivatives, the central difference has truncation error compared to for the forward difference. This compares truncation orders, not total numerical errors: choosing involves a trade-off between truncation error, evaluation error, and floating-point cancellation.
The Taylor expansions show where the higher truncation order comes from. In the expansions of and , the first-order terms have opposite signs, so subtraction produces . The even-order terms have the same signs and cancel. In particular,
when the required derivatives exist near . Thus the central quotient has truncation error, whereas the forward difference generally has truncation error.
The choice of step size balances truncation error against evaluation error and cancellation. The indicative to range assumes well-scaled, smooth functions and accurate deterministic double-precision evaluations; different coordinates can need different steps. Simulation noise is amplified by division by , and both evaluation points must remain in the function's domain. A finite quotient alone does not establish that a derivative exists.
def numerical_gradient(f, x, h=1e-6):
"""
Compute gradient numerically using central differences.
Parameters:
f: function that takes array x and returns scalar
x: finite real one-dimensional point; copied to float64
h: finite positive step; both perturbed coordinates must be representable
Returns:
gradient vector (numpy array)
"""
if np.iscomplexobj(x):
raise ValueError("x must be real")
x = np.asarray(x, dtype=float)
h = float(h)
if x.ndim != 1 or x.size == 0 or not np.all(np.isfinite(x)):
raise ValueError("x must be a nonempty finite one-dimensional point")
if not np.isfinite(h) or h <= 0:
raise ValueError("h must be finite and positive")
n = len(x)
grad = np.zeros(n)
for i in range(n):
x_plus = x.copy()
x_minus = x.copy()
x_plus[i] += h
x_minus[i] -= h
if (
not np.isfinite(x_plus[i])
or not np.isfinite(x_minus[i])
or x_plus[i] == x[i]
or x_minus[i] == x[i]
):
raise ValueError("Choose a representable step for this coordinate scale")
value_plus, value_minus = f(x_plus), f(x_minus)
if (
np.ndim(value_plus) != 0
or np.ndim(value_minus) != 0
or np.iscomplexobj(value_plus)
or np.iscomplexobj(value_minus)
or not np.isfinite(value_plus)
or not np.isfinite(value_minus)
):
raise ValueError("f must return finite real scalars at both points")
grad[i] = (float(value_plus) - float(value_minus)) / (2 * h)
if not np.all(np.isfinite(grad)):
raise ValueError("Finite-difference calculation overflowed")
return grad
# Define gradient_f for use in this block
def gradient_f(x, y):
"""Gradient of f computed analytically"""
df_dx = 2 * x - 2 * y + 4
df_dy = 4 * y - 2 * x - 6
return np.array([df_dx, df_dy])
# Test numerical vs analytical gradient
def f_array(x):
"""Wrapper function that takes array input for numerical gradient."""
return x[0] ** 2 + 2 * x[1] ** 2 - 2 * x[0] * x[1] + 4 * x[0] - 6 * x[1]
point = np.array([1.0, 2.0])
numerical_grad = numerical_gradient(f_array, point)
analytical_grad = gradient_f(point[0], point[1])Analytical gradient: [2. 0.] Numerical gradient: [2. 0.] Difference: [2.79555934e-10 0.00000000e+00]
For this function, point, and step, the numerical gradient differs from the analytic result by about in absolute value. That is close agreement for this finite-difference example, not machine-epsilon accuracy or validation of every input, scale, and step. The routine above uses floating copies and rejects invalid or unrepresentable perturbations; cancellation, noise, domain boundaries, and nonsmoothness still require separate checks.