Differential Calculus and Optimization Basics › Multivariable Calculus

Michael BrenndoerferOctober 24, 202512 min read
Quantitative FinanceAll chapters
Part 1 of 15Mathematical Foundations
Chapter 6Differential Calculus and Optimization Basics › Multivariable Calculus
Reading section 3 of 11 · Multivariable Calculus · 12 minSection 3 of 11 · 12 min

Book contents

Section 3 of 11 · 12 min read

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.

Partial Derivative

For a function f(x1,x2,…,xn)f(x_1, x_2, \ldots, x_n), the partial derivative with respect to xix_i is:

∂f∂xi=lim⁡h→0f(x1,…,xi+h,…,xn)−f(x1,…,xi,…,xn)h\frac{\partial f}{\partial x_i} = \lim_{h \to 0} \frac{f(x_1, \ldots, x_i + h, \ldots, x_n) - f(x_1, \ldots, x_i, \ldots, x_n)}{h}

where:

  • xix_i: the variable with respect to which we differentiate
  • hh: a small increment approaching zero
  • xjx_j for j≠ij\ne i: all other variables, held constant during differentiation

We treat all variables except xix_i as constants and differentiate normally.

The notation ∂f∂xi\frac{\partial f}{\partial x_i} 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 RR depends on the returns r1r_1 and r2r_2 of each asset and their weights w1w_1 and w2w_2:

R(w1,w2,r1,r2)=w1r1+w2r2R(w_1, w_2, r_1, r_2) = w_1 r_1 + w_2 r_2

The partial derivatives tell us how sensitive total return is to each input:

∂R∂w1=r1,∂R∂r1=w1\frac{\partial R}{\partial w_1} = r_1, \quad \frac{\partial R}{\partial r_1} = w_1

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 w2w_2 fixed, increasing w1w_1 by 0.01 changes portfolio return by 0.01r10.01r_1 but also increases net exposure and therefore requires additional financing. In a fully invested two-asset portfolio, w2=1−w1w_2=1-w_1, so the relevant reallocation derivative is dR/dw1=r1−r2dR/dw_1=r_1-r_2: shifting 0.01 from asset 2 to asset 1 changes return by 0.01(r1−r2)0.01(r_1-r_2). 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 R=βTFR=\beta^\mathsf{T}F, FF is the vector of factor returns and β\beta is the vector of held-fixed exposures. A factor move ΔF\Delta F changes return by βTΔF\beta^\mathsf{T}\Delta F 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.

Gradient

The gradient of a function f:Rn→Rf: \mathbb{R}^n \to \mathbb{R} is the vector of all partial derivatives:

∇f=(∂f∂x1∂f∂x2⋮∂f∂xn)\nabla f = \begin{pmatrix} \frac{\partial f}{\partial x_1} \\ \frac{\partial f}{\partial x_2} \\ \vdots \\ \frac{\partial f}{\partial x_n} \end{pmatrix}

where:

  • ∇f\nabla f: the gradient operator (nabla) applied to ff, producing a vector
  • ∂f∂xi\frac{\partial f}{\partial x_i}: the partial derivative of ff with respect to the ii-th variable
  • nn: the dimension of the input space

For a differentiable function, a nonzero ∇f\nabla f 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 ff is differentiable at the point, so a small input vector dd satisfies f(x+d)=f(x)+∇f(x)Td+o(∥d∥)f(x+d)=f(x)+\nabla f(x)^\mathsf{T}d+o(\|d\|): the remainder becomes small relative to the size of the input move. Among Euclidean unit directions, ∇f/∥∇f∥\nabla f/\|\nabla f\| 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 Duf=∇fTuD_u f=\nabla f^\mathsf{T}u for a Euclidean unit vector uu. The Cauchy-Schwarz inequality gives Duf≤∥∇f∥D_u f\leq\|\nabla f\|, 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 ∥∇f∥=∑i(∂f/∂xi)2\|\nabla f\| = \sqrt{\sum_i (\partial f / \partial x_i)^2} 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.

In[8]:
Code
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])
Out[9]:
Console
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 xx direction has the largest first-order increase among Euclidean unit directions. The yy component is zero: a yy move has no first-order effect at this point, not no finite effect.

Holding x=1x=1 gives the slice f(1,y)=2(y−2)2−3f(1,y)=2(y-2)^2-3. It has a minimum at y=2y=2, and either signed move to 2+h2+h raises the value by 2h22h^2. The zero partial derivative is a stationary-slice condition, not constancy as yy varies. This is not the full two-variable optimum because the xx partial is still nonzero. For this quadratic, we find the candidate minimum by setting both partials to zero and then check its Hessian.

Out[10]:
Visualization
Contour plot with arrows showing gradient directions pointing away from the minimum.
Gradient directions over the quadratic's contours. Arrows use data-coordinate directions and soft-normalized lengths for readability; the colorbar gives the unnormalized magnitude. At regular contour points, the gradient is perpendicular to the contour in these Euclidean coordinates.

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:

f′(x)≈f(x+h)−f(x)hf'(x) \approx \frac{f(x + h) - f(x)}{h}

For sufficiently smooth functions, the central difference formula has higher truncation order:

f′(x)≈f(x+h)−f(x−h)2hf'(x) \approx \frac{f(x + h) - f(x - h)}{2h}

where:

  • f′(x)f'(x): the derivative we are approximating
  • hh: a small step size (often around 10−610^{-6} to 10−510^{-5} for central differences in double precision, depending on scaling)
  • f(x+h)f(x + h) and f(x−h)f(x - h): function evaluations at points symmetric around xx

For smooth functions with the required derivatives, the central difference has O(h2)O(h^2) truncation error compared to O(h)O(h) for the forward difference. This compares truncation orders, not total numerical errors: choosing hh 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 f(x+h)f(x+h) and f(x−h)f(x-h), the first-order terms have opposite signs, so subtraction produces 2hf′(x)2h f'(x). The even-order terms have the same signs and cancel. In particular,

f(x+h)−f(x−h)2h=f′(x)+h26f′′′(x)+O(h4),\frac{f(x+h)-f(x-h)}{2h}=f'(x)+\frac{h^2}{6}f'''(x)+O(h^4),

when the required derivatives exist near xx. Thus the central quotient has O(h2)O(h^2) truncation error, whereas the forward difference generally has O(h)O(h) truncation error.

The choice of step size hh balances truncation error against evaluation error and cancellation. The indicative 10−610^{-6} to 10−510^{-5} 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 hh, and both evaluation points must remain in the function's domain. A finite quotient alone does not establish that a derivative exists.

In[11]:
Code
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])
Out[12]:
Console
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 2.8×10−102.8\times10^{-10} 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.