Derivatives, integrals, anti-derivatives and anti-integrals

python
import numpy as np
import matplotlib.pyplot as plt
import seaborn
python
seaborn.set_style('darkgrid')
seaborn.set(font_scale=1.2)
python
def draw_arrows(ax, top_from, top_to, top_text_xy, bottom_from, bottom_to, bottom_text_xy, diff_top=True):
    text_top = "Differentiation"
    text_bottom = "Integration"
    if not diff_top:
        text_top, text_bottom = text_bottom, text_top
        
    ax.annotate(
        "", top_from, top_to,
        xycoords="axes fraction",
        size="large", ha="left", va="center",
        arrowprops=dict(
            arrowstyle="->", color="k", lw=2,
            connectionstyle="angle3,angleA=50,angleB=-50",
        ),
    )
    
    ax.text(*top_text_xy, text_top, transform=ax.transAxes, ha="center")
    
    ax.annotate(
        "", bottom_from, bottom_to,
        xycoords="axes fraction",
        size="large", ha="left", va="center",
        arrowprops=dict(
            arrowstyle="->", color="k", lw=2,
            connectionstyle="angle3,angleA=50,angleB=-50",
        ),
    )
    ax.text(*bottom_text_xy, text_bottom, transform=ax.transAxes, ha="center")
    
def draw_deltas(ax, dx_from, dx_to, dx_width, dy_from, dy_to, dy_width, y_is_delta=True):
    ax.annotate(
        "$\Delta x$", dx_from, dx_to,
        size="large", ha="center", va="center",
        arrowprops=dict(arrowstyle=f"-[, widthB={dx_width}", color="k",),
    )
    ax.annotate(
        "$\Delta y$" if y_is_delta else "$y$", dy_from, dy_to,
        size="large", ha="left", va="center",
        arrowprops=dict(arrowstyle=f"-[, widthB={dy_width}", color="k"),
    )

There are two ways of describing a sequence of (\(x\), \(y\)) points:

  1. Enumerate all (\(x\), \(y\)) pairs
  2. Specify the first (\(x_0\), \(y_0\)) pair and the distance (\(\Delta x\), \(\Delta y\)) of each subsequent point

In a loose sense, differentiation goes from 1. to 2., and integration goes from 2. to 1.

python
f = lambda x: x ** 2
df = lambda x: 2 * x

dx = 0.2
x = np.arange(0, 4+dx, dx)
y = f(x)
python
focus = np.array([1, 5, 10, 15])

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
fig.subplots_adjust(wspace=0.3)

ax = axes[0]
ax.scatter(x, y, color="k")
ax.text(3.5-0.2, f(3.5), "$y = x^2$", va="bottom", ha="right")
ax.set_ylabel("y")

ax = axes[1]
ax.scatter(x, y, color="k")
ax.scatter(x[0], y[0], color="C2")
ax.scatter(x[focus], y[focus], color="C1")
ax.vlines(x[1:], y[:-1], y[1:], color="k")
ax.annotate(
    "$(x_0, y_0)$", (0.05, -0.05), (.1, 3.3),
    size="large", ha="left", va="center",
    arrowprops=dict(arrowstyle="->", color="k"),
)
ax.set_ylabel("y")

mid = (f(3-dx) + f(3)) / 2
draw_deltas(
    ax,
    (3 + dx/2, 7.5), (3 + dx/2, 6), 0.3,
    (3 + dx*2, mid), (3 + 0.6, mid), 0.4
)

ax = axes[2]
ax.scatter(x[1:], df(x)[1:] * dx, color="k")
ax.scatter(x[focus], df(x)[1:][focus-1] * dx, color="C1")
ax.vlines(x[1:], 0, df(x)[1:] * dx, color="k")
ax.text(3.5-0.1, df(3.5) * dx, "$\Delta y = 2x \Delta x$", va="bottom", ha="right")
ax.set_ylabel("$\Delta y$")

draw_arrows(
    ax, 
    (0.35, 0.85), (-0.65, 0.85), (-0.15, 1.2), 
    (-0.65, -0.1), (0.35, -0.1), (-0.15, -.3),
)

for ax in axes:
    ax.set_xlabel("x")

png

Actually when we differentiate, we rather store the ratio of \(\frac{\Delta y}{\Delta x}\), which is also the slope of the straigth line that connects \((x_i, y_i)\) to \((x_{i+1}, y_{i+1})\)

python
fig, ax = plt.subplots(figsize=(4.5/1.2,6/1.2))
ax.scatter([0, 1], [0, 2], c="k")
ax.plot([-0.1, 1.1], [-0.2, 2.2], c="C1")

plt.text(
    0.25, 1.15, 
    "$\\frac{y_{i+1} - y_i}{x_{i+1} - x_i} = \\frac{\Delta y}{\Delta x} = 2$", 
    ha="center", va="center", fontsize=20,
    rotation=60.5,
)

ax.annotate(
    "$\Delta x$", (0.5, -0.5), (0.5, -0.75),
    size="large", ha="center", va="center",
    arrowprops=dict(arrowstyle="-[, widthB=2.7", color="k",),
)

ax.set_ylim([-1, 2.5])
ax.set_xlim([-0.5, 1.75])
ax.annotate(
    "$\Delta y$", (1.25, 1.0), (1.5, 1.0),
    size="large", ha="left", va="center",
    arrowprops=dict(arrowstyle="-[, widthB=4.6", color="k"),
)

ax.set_xticks([0, 0.5, 1, 1.5])
ax.set_yticks([0, 1, 2]);

png

This makes it so that the output of differentation is mostly the same regardless of the density of points in the original curve

python
dx1 = 0.1
x1 = np.arange(0, 4+dx1, dx1)

dx2 = 0.25
x2 = np.arange(0, 4+dx2, dx2)
python
fig, axes = plt.subplots(1, 3, figsize=(12, 4))

ax = axes[0]
ax.scatter(x1, f(x1), color="k", alpha=0.7, s=14, label=f"$\Delta x = {dx1}$")
ax.scatter(x2, f(x2), color="C1", alpha=0.7, s=14, label=f"$\Delta x = {dx2}$")
ax.legend()
ax.set_ylabel("y")
ax.set_xlabel("x")

ax = axes[1]
ax.scatter(x1[1:], np.diff(f(x1)), color="k", alpha=0.7, s=14)
ax.scatter(x2[1:], np.diff(f(x2)), color="C1", alpha=0.7, s=14)
ax.set_ylabel("$\Delta y$")
ax.set_xlabel("x")

ax = axes[2]
ax.scatter(x1[1:], np.diff(f(x1)) / dx1, color="k", alpha=0.7, s=14)
ax.scatter(x2[1:], np.diff(f(x2)) / dx2, color="C1", alpha=0.7, s=14)
ax.set_ylabel("$\\frac{\Delta y}{\Delta x}$")
ax.set_xlabel("x")

fig.tight_layout();

png

Integration as the inverse of differentation

Once we have a set of differences (or difference ratios), we can restore the original curve by adding those differences, starting from the initial point \((x0, y0)\)

\[y_i = y_0 + \sum_0^x{\Delta y_i} = y_0 + \sum_0^x{\frac{\Delta y_i}{\Delta x}}\Delta x\]

python
fig, axes = plt.subplots(1, 3, figsize=(12, 4))

ax = axes[0]
ax.scatter(x1[1:], np.diff(f(x1)) / dx1, color="k", alpha=0.7, s=14, label=f"$\Delta x = {dx1}$")
ax.scatter(x2[1:], np.diff(f(x2)) / dx2, color="C1", alpha=0.7, s=14, label=f"$\Delta x = {dx2}$")
ax.legend();
ax.set_ylabel("$\\frac{\Delta y}{\Delta x}$")

ax = axes[1]
ax.scatter(x1, np.cumsum(np.diff(f(x1), prepend=x1[0]) / dx1), color="k", alpha=0.7, s=14)
ax.scatter(x2, np.cumsum(np.diff(f(x2), prepend=x2[0]) / dx2), color="C1", alpha=0.7, s=14)

ax = axes[1]
ax.scatter(x1, np.cumsum(np.diff(f(x1), prepend=x1[0]) / dx1), color="k", alpha=0.7, s=14)
ax.scatter(x2, np.cumsum(np.diff(f(x2), prepend=x2[0]) / dx2), color="C1", alpha=0.7, s=14)
ax.set_ylabel("$y_0 + \sum_0^x{\\frac{\Delta y}{\Delta x}}$")

ax = axes[2]
ax.scatter(x1, np.cumsum(np.diff(f(x1), prepend=x1[0]) / dx1) * dx1, color="k", alpha=0.7, s=14)
ax.scatter(x2, np.cumsum(np.diff(f(x2), prepend=x2[0]) / dx2) * dx2, color="C1", alpha=0.7, s=14)
ax.set_ylabel("$y_0 + \sum_0^x{\\frac{\Delta y}{\Delta x}\Delta x} = y_0 + \sum_0^x{\Delta y}$")

for ax in axes:
    ax.set_xlabel("x")

fig.tight_layout();

png

In that sense integration is the inverse of differentation in the same sense that addition and multiplication are the inverses of subtraction and division.

If we add up (integrate) all the differences from a starting point, we get back the original series.

But this process of integration has another interesting meaning…

Differentiation as the inverse of integration

Let’s grab the leftmost plot from above and forget that it describes \(\frac{\Delta y}{\Delta x}\).

Let’s say we have just a vanilla \(y = 2x\) series.

What are we doing when we add up all the \(y\) up to \(x\) and multiply by \(\Delta x\)?

\[s_i = \sum_0^x{y \Delta x}\]

python
g = lambda x : 2*x
sg = lambda x : x ** 2

dx = 0.25
x = np.arange(0, 3.5+dx, dx)
y = g(x)
python
focus = 9

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
fig.subplots_adjust(wspace=0.3)

ax = axes[0]
ax.scatter(x, y, color="k")
ax.text(3.2-0.2, g(3.2), "$y = 2x$", va="bottom", ha="right")
ax.set_ylabel("y")

ax = axes[1]
ax.scatter(x[focus:], y[focus:], color="k")
ax.scatter(x[:focus], y[:focus], color="C1")
ax.scatter(x[focus], y[focus], color="C0")
ax.vlines(x[:focus], 0, y[:focus], color="C1")
ax.fill_between(x[:focus+1], 0, y[:focus+1], step="post", color="C1", alpha=0.7)
ax.bar(x[focus]+dx/2, y[focus], width=dx, color="C0", alpha=0.7, ec="C0")
ax.set_ylabel("y")
ax.set_ylim([-.5, None])
mid = y[focus] / 2
draw_deltas(
    ax,
    (x[focus] + dx/2, y[focus] + 1.0), (x[focus] + dx/2, y[focus] + 1.8), 0.4,
    (x[focus] + dx + 0.15, mid), (x[focus] + dx + 0.4, mid), 3.58,
    y_is_delta=False,
)

ax = axes[2]
ax.scatter(x, sg(x), color="k")
ax.scatter(x[focus-1], sg(x[focus-1]), color="C1")
ax.scatter(x[focus], sg(x[focus]), color="C0")
ax.vlines(x[focus], sg(x[focus-1]), sg(x[focus]), color="C0")
ax.text(3.1 - 0.2, sg(3.1), "$s \\approx x^2$", ha="right")
ax.set_ylabel("$s = \sum_0^x{\ y \Delta x}$", labelpad=-5)

draw_arrows(
    ax,
    (0.35-0.2, 0.85), (-0.65-0.2, 0.85), (-0.15, 1.2),
    (-0.65, -0.1), (0.35, -0.1), (-0.15, -.3),
    diff_top=False,
)

for ax in axes:
    ax.set_xlabel("x")

png

We are adding up the areas of the rectangles below \(y\) up to \(x\)! So our function \(s\) is an area function!

Now, the difference between successive points in that area function \(\Delta s_i\) gives us the difference in areas up to \(x_{i}\) and up to \(x_{i-1}\), which must intuitively be \(y_i\Delta x\) and the ratio \(\frac{y_i \Delta x}{\Delta x} = y_i\)

This is just another way of saying that differentation is the inverse of integration.

Bonus: Illustrating the sin function

python
f = lambda x: np.sin(x)
df = lambda x: np.cos(x)

dx = 0.3
x = np.arange(0, np.pi*2+dx, dx)
y = f(x)
python
focus = np.array([1, 6, 11, 16])

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
fig.subplots_adjust(wspace=0.3)

ax = axes[0]
ax.scatter(x, y, color="k")
ax.text(6, 0.5, "$y = sin(x)$", va="bottom", ha="right")
ax.set_ylabel("y")

ax = axes[1]
ax.scatter(x, y, color="k")
ax.scatter(x[0], y[0], color="C2")
ax.scatter(x[focus], y[focus], color="C1")
ax.vlines(x[1:], y[:-1], y[1:], color="k")
ax.annotate(
    "$(x_0, y_0)$", (0.05, -0.05), (.1, -.3),
    size="large", ha="left", va="center",
    arrowprops=dict(arrowstyle="->", color="k"),
)
draw_deltas(
    ax,
    (3.2, 0.52), (3.2, 0.75), .25,
    (3.7, 0.27), (4.05, 0.27), .85,
)
ax.set_ylabel("y")

ax = axes[2]
ax.scatter(x[1:], np.diff(y), color="k")
ax.scatter(x[focus], np.diff(y)[focus-1], color="C1")
ax.vlines(x[1:], 0, np.diff(y), color="k")
ax.text(5.25, 0.2, "$\Delta y \\approx cos(x) \Delta x$", va="bottom", ha="right")
ax.set_ylabel("$\Delta y$")

draw_arrows(
    ax,
    (0.35, 0.85), (-0.65, 0.85), (-0.15, 1.2),
    (-1.0, 0.22), (0.15, 0.22), (-0.35, -.21),
)

for ax in axes:
    ax.set_xlabel("x");

png

python
g = lambda x : np.cos(x)
sg = lambda x : np.sin(x)

dx = 0.25
x = np.arange(0, np.pi*2+dx, dx)
y = g(x)
python
focus = 9

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
fig.subplots_adjust(wspace=0.35)

ax = axes[0]
ax.scatter(x, y, color="k")
ax.text(5.3, 0.75, "$y = cos(x)$", va="bottom", ha="right")
ax.set_ylabel("y")

ax = axes[1]
ax.scatter(x[focus:], y[focus:], color="k")
ax.scatter(x[:focus], y[:focus], color="C1")
ax.scatter(x[focus], y[focus], color="C0")
ax.vlines(x[:focus], 0, y[:focus], color="C1")
ax.fill_between(x[:focus+1], 0, y[:focus+1], step="post", color="C1", alpha=0.7)
ax.bar(x[focus]+dx/2, y[focus], width=dx, color="C0", alpha=0.7, ec="C0")
ax.set_ylabel("y")
# ax.set_ylim([-.5, None])
mid = y[focus] / 2
draw_deltas(
    ax,
    (x[focus] + dx/2, 0.1), (x[focus] + dx/2, .3), 0.3,
    (x[focus] + dx + 0.3, mid), (x[focus] + dx + 0.65, mid), 1.78,
    y_is_delta=False,
)

ax = axes[2]
ax.scatter(x, sg(x), color="k")
ax.scatter(x[10], sg(x[10]), color="C1")
ax.scatter(x[11], sg(x[11]), color="C0")
ax.vlines(x[11], sg(x[10]), sg(x[11]), color="C0")
ax.text(6.5 - 0.1, sg(6.5)+ 0.3, "$s \\approx sin(x)$", ha="right")
ax.set_ylabel("$s = \sum_0^x{\ y \Delta x}$", labelpad=-5)

draw_arrows(
    ax,
    (0.35-0.25, 0.85), (-0.65-0.25, 0.85), (-0.35, 1.2),
    (-0.65, .1), (0.35, 0.1), (-0.15, -.3),
    diff_top=False,
)

for ax in axes:
    ax.set_xlabel("x")

png

python