A primer on numerical differentiation

In order to numerically evaluate a derivative $y'(x)=dy/dx$ at point $x_0$, we approximate is by using finite differences: Therefore we find: $$\begin{eqnarray} && dx \approx \Delta x &=&x_1-x_0, \\ && dy \approx \Delta y &=&y_1-y_0 = y(x_1)-y(x_0) = y(x_0+\Delta_x)-y(x_0),\end{eqnarray}$$

Then we re-write the derivative in terms of discrete differences as: $$\frac{dy}{dx} \approx \frac{\Delta y}{\Delta x}$$

Example

Let's look at the accuracy of this approximation in terms of the interval $\Delta x$. In our first example we will evaluate the derivative of $y=x^2$ at $x=1$.


In [124]:
delta = 1.
while(delta > 1.e-10):
    x = 1.
    d = (x+delta)*(x+delta)-x*x
    d = d / delta
    print("%6.0e %20.16f %20.16f" % (delta, d, d-2.))
    delta = delta / 10.


 1e+00   3.0000000000000000   1.0000000000000000
 1e-01   2.1000000000000019   0.1000000000000019
 1e-02   2.0100000000000007   0.0100000000000007
 1e-03   2.0009999999996975   0.0009999999996975
 1e-04   2.0000999999991720   0.0000999999991720
 1e-05   2.0000100000139298   0.0000100000139298
 1e-06   2.0000009999243669   0.0000009999243669
 1e-07   2.0000001010878061   0.0000001010878061
 1e-08   1.9999999878450576  -0.0000000121549424
 1e-09   2.0000001654807416   0.0000001654807416
 1e-10   2.0000001654807416   0.0000001654807416

Why is it that the sequence does not converge? This is due to the round-off errors in the representation of the floating point numbers. To see this, we can simply type:


In [123]:
((1.+0.0001)*(1+0.0001)-1)


Out[123]:
0.0002000099999999172

In addition, one could consider the midpoint difference, defined as: $$ dy \approx \Delta y = y(x_0+\frac{\Delta_x}{2})-y(x_0-\frac{\Delta_x}{2}).$$

For a more complex function we need to import it from math. For instance, let's calculate the derivative of $sin(x)$ at $x=\pi/4$, including both the forward and midpoint differences.


In [125]:
from math import sin, sqrt
delta = 1.
while(delta > 1.e-10):
    x = 3.14159265358979323846264338327950288419716939937510/4.;
    d1 = sin(x+delta) - sin(x); #forward
    d2 = sin(x+delta*0.5) - sin(x-delta*0.5); # midpoint
    d1 = d1 / delta;
    d2 = d2 / delta;
    print("%6.0e %20.16f %20.16f %20.16f %20.16f" % (delta, d1, d1-sqrt(2.)/2., d2, d2-sqrt(2.)/2.) )
    delta = delta / 10.


 1e+00   0.2699544827129282  -0.4371522984736194   0.6780100988420897  -0.0290966823444578
 1e-01   0.6706029729039897  -0.0365038082825578   0.7068121901873392  -0.0002945909992084
 1e-02   0.7035594916892096  -0.0035472894973380   0.7071038349119707  -0.0000029462745769
 1e-03   0.7067531099742563  -0.0003536712122912   0.7071067517236962  -0.0000000294628514
 1e-04   0.7070714246681931  -0.0000353565183545   0.7071067808916975  -0.0000000002948500
 1e-05   0.7071032456451575  -0.0000035355413901   0.7071067811947883   0.0000000000082407
 1e-06   0.7071064276331639  -0.0000003535533837   0.7071067811281749  -0.0000000000583726
 1e-07   0.7071067453789935  -0.0000000358075540   0.7071067809061303  -0.0000000002804172
 1e-08   0.7071067731345692  -0.0000000080519784   0.7071067731345692  -0.0000000080519784
 1e-09   0.7071068175434900   0.0000000363569425   0.7071068175434900   0.0000000363569425
 1e-10   0.7071065954988851  -0.0000001856876625   0.7071054852758605  -0.0000012959106871

A more in-depth discussion about round-off erros in numerical differentiation can be found here

Special functions in numpy

numpy provides a simple method diff() to calculate the numerical derivatives of a dataset stored in an array by forward differences. The function gradient() will calculate the derivatives by midpoint (or central) difference, that provides a more accurate result.


In [110]:
%matplotlib inline
import numpy as np
from matplotlib import pyplot

y = lambda x: x*x

x1 = np.arange(0,10,1)
x2 = np.arange(0,10,0.1)

pyplot.plot(x1,np.gradient(y(x1),1.),'r--o');
pyplot.plot(x1[:x1.size-1],np.diff(y(x1))/np.diff(x1),'b--x');


Notice above that gradient() uses forward and backward differences at the two ends.


In [39]:
pyplot.plot(x2,np.gradient(y(x2),0.1),'b--o');


More discussion about numerical differenciation, including higher order methods with error extrapolation can be found here.

The module scipy also includes methods to accurately calculate derivatives:


In [91]:
from scipy.misc import derivative
from math import *

f = lambda x: x**2

delta = 1.
while(delta > 1.e-10):
    x = 1.
    d = derivative(f, x, delta, n=1, order=3)
    print("%6.0e %20.16f %20.16f" % (delta, d, d-2.))
    delta = delta / 10.


 1e+00 2.000000000000000000 0.000000000000000000
 1e-01 2.000000000000000444 0.000000000000000444
 1e-02 2.000000000000001776 0.000000000000001776
 1e-03 1.999999999999835243 -0.000000000000164757
 1e-04 1.999999999999224620 -0.000000000000775380
 1e-05 2.000000000002000178 0.000000000002000178
 1e-06 2.000000000001999734 0.000000000001999734
 1e-07 2.000000000057510885 0.000000000057510885
 1e-08 1.999999993396172737 -0.000000006603827263
 1e-09 2.000000054458439092 0.000000054458439092
 1e-10 2.000000165480741554 0.000000165480741554

One way to improve the roundoff errors is by simply using the decimal package


In [126]:
from decimal import Decimal

delta = Decimal("1.")
while(delta >= Decimal("1.e-10")):
    x = Decimal("1.")
    d = (x+delta)*(x+delta)-x*x
    d = d / delta
    print("%6.0e %20.16f %20.16f" % (delta, d, d-Decimal("2.")))
    delta = delta / Decimal("10.")


 1e+00   3.0000000000000000   1.0000000000000000
 1e-01   2.1000000000000001   0.1000000000000000
 1e-02   2.0099999999999998   0.0100000000000000
 1e-03   2.0009999999999999   0.0010000000000000
 1e-04   2.0001000000000002   0.0001000000000000
 1e-05   2.0000100000000001   0.0000100000000000
 1e-06   2.0000010000000001   0.0000010000000000
 1e-07   2.0000000999999998   0.0000001000000000
 1e-08   2.0000000099999999   0.0000000100000000
 1e-09   2.0000000010000001   0.0000000010000000
 1e-10   2.0000000001000000   0.0000000001000000