We will cover many topics in the first three fields.
A rough schedule of the class:
Week | Main Topics | Practical Problems | Instructor |
---|---|---|---|
1 | Introduction and Error Analysis | Error and floating point numbers | H. Cheng |
2-3 | Linear Algebra | Portfolio optimization, PCA, least square | H. Cheng |
4-5 | Rootfinding and interpolation | Curve building | H. Cheng |
6 | Derivatives and integration | Hedging, Risk Transformation | H. Cheng |
7-8 | Monte Carlo simulation | Exotic pricing | H. Cheng |
9-10 | Optimization | Model calibration | W. Lou |
11-13 | ODE/PDE | Pricing | W. Lou |
D Bindel and J Goodman, 2009
L Anderson and V. Piterbarg, 2010
P. Glasserman 2010
Available online at: http://yadongli.github.io/nyumath2048
Roberto Waltman: In the one and only true way. The object-oriented version of "Spaghetti code" is, of course, "Lasagna code".
Memory hierarchy
Storage | Bandwidth | Latency | Capacity |
---|---|---|---|
L-1 Cache | 98GB/s | 4 cycles | 32K/core |
L-2 Cache | 50GB/s | 10 cycles | 256K/core |
L-3 Cache | 30GB/s | 40 - 75 cycles | 8MB shared |
RAM | 15GB/s | 60-100 ns | ~TB |
SDD | up to 4GB/s | ~0.1ms | $\infty$ |
Network | up to 12GB/s | much longer | $\infty$ |
In practice, what often trumps everything else is:
These are good advice, but quite superficial.
by Paul Phillips "We're doing it all wrong":
(quoting George Orwell's 1984)
Sun Tzu (孙子): "the supreme art of quant modeling is to solve problems without coding"
There is no single right answer
Eric Raymond, "How to Become a Hacker":
"Lisp is worth learning for the profound enlightenment experience you will have when you finally get it; that experience will make you a better programmer for the rest of your days, even if you never actually use Lisp itself a lot."
Python
Python ecosystem
Python resources
Python version:
Python setup instructions:
James Gosling: "95% of the folks out there are completely clueless about floating-point"
If the true value $a$ is approximated by $\hat{a}$:
Both measure are useful in practice.
Imprecision in the theory or model itself
Truncation Error in numerical algorithm: introduced when exact mathematical formulae are represented by approximations.
Roundoff Errors: occur because computers have a limited ability to represent numbers
We cover the last category in this lecture.
First published in 1985, the IEEE 754 standard includes:
Default rule: round to the nearest, ties to even digit
IEEE 754 is a tremendously successful standard:
The IEEE 754 standard is widely adopted in modern hardware and software,
$$\underbrace{*}_{\text{sign bit}}\;\underbrace{********}_{\text{exponent}}\overbrace{\;1.\;}^{\text{implicit}}\;\underbrace{***********************}_{\text{fraction}} $$
$$(-1)^{\text{sign}}(\text{1 + fraction})2^{\text{exponent} - p}$$
Format | Sign | Fraction | Exponent | $p$ | (Exponent-p) range |
---|---|---|---|---|---|
IEEE 754 - 32 bit | 1 | 23 | 8 | 127 | -126 to 127 |
IEEE 754 - 64 bit | 1 | 52 | 11 | 1023 | -1022 to 1023 |
f2parts takes a floating number and its bit length (32 or 64) and shows the three parts.
f2parts(10.7, 32);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 0 | 10000010 | 01010110011001100110011 |
Decimal | 1.0 | 3.0 | 1.337499976158142 |
f2parts(-23445.25, 64)
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 1 | 10000001101 | 0110111001010101000000000000000000000000000000000000 |
Decimal | -1.0 | 14.0 | 1.4309844970703125 |
prec32 = 2**(-24)
prec64 = 2**(-53)
f32 = [parts2f(0, -126, 1), parts2f(0, 254-127, (2.-prec32*2)), prec32, -np.log10(prec32)]
f64 = [parts2f(0, -1022, 1), parts2f(0, 2046-1023, (2.-prec64*2)), prec64, -np.log10(prec64)]
fmt.displayDF(pd.DataFrame(np.array([f32, f64]), index=['32 bit', '64 bit'],
columns=["Min", "Max", "Machine Precision", "# of Significant Digits"]), "5g")
Min | Max | Machine Precision | # of Significant Digits | |
---|---|---|---|---|
32 bit | 1.1755e-38 | 3.4028e+38 | 5.9605e-08 | 7.2247 |
64 bit | 2.2251e-308 | 1.7977e+308 | 1.1102e-16 | 15.955 |
f2parts(-0., 32);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 1 | 00000000 | 00000000000000000000000 |
Decimal | -1.0 | -127.0 | 0.0 |
f2parts(np.NaN, 32);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 0 | 11111111 | 10000000000000000000000 |
Decimal | 1.0 | 128.0 | 1.5 |
f2parts(-np.Inf, 32);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 1 | 11111111 | 00000000000000000000000 |
Decimal | -1.0 | 128.0 | 1.0 |
f2parts(-1e-45, 32);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 1 | 00000000 | 00000000000000000000001 |
Decimal | -1.0 | -127.0 | 2.384185791015625e-07 |
a = 1./3
b = a + a + 1. - 1.
c = 2*a
print("b == c? {} !".format(b == c))
print("b - c = {:e}".format(b - c))
b == c? False ! b - c = -1.110223e-16
f2parts(b, 64);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 0 | 01111111110 | 0101010101010101010101010101010101010101010101010100 |
Decimal | 1.0 | -1.0 | 1.333333333333333 |
f2parts(c, 64);
Sign | Exp | Fraction | |
---|---|---|---|
Bits | 0 | 01111111110 | 0101010101010101010101010101010101010101010101010101 |
Decimal | 1.0 | -1.0 | 1.3333333333333333 |
x = 1e-308
print("inverse of {:e} is {:e}".format(x, 1/x))
inverse of 1.000000e-308 is 1.000000e+308
x = 1e-309
print("inverse of {:e} is {:e}".format(x, 1/x))
inverse of 1.000000e-309 is inf
print(1. - 1e-16 == 1.)
False
print(1. + 1e-16 == 1.)
True
largeint = 2**32 - 1
print("Integer arithmetic:")
a = math.factorial(30) # in Python's bignum
print("a = ", a)
print("a - {:d} == a ?".format(largeint), (a - largeint) == a)
print("\nFloating point arithmetic:")
b = float(math.factorial(30))
print("b = ", b)
print("b - {:d} == b ?".format(largeint), (b - largeint) == b)
Integer arithmetic: a = 265252859812191058636308480000000 a - 4294967295 == a ? False Floating point arithmetic: b = 2.6525285981219107e+32 b - 4294967295 == b ? True
The limited precision can cause subtle errors that are puzzling even for experienced programmers.
a = np.arange(0, 3. - 1., 1.)
print("a = ", a)
print("a/3 = ", a/3.)
a = [0. 1.] a/3 = [0. 0.33333333]
b = np.arange(0, 1 - 1./3., 1./3.)
print("b = ", b)
b = [0. 0.33333333 0.66666667]
Equality test in floating point number is always a bad idea:
It should become your instinct to:
numpy.allclose()
Dramatic loss of precision can happen when very similar numbers are subtracted
$$f(x) = \frac{1}{x^2}(1-\cos(x))$$
The straight forward implementation failed spectacularly for small $x$.
def f(x) :
return (1.-np.cos(x))/x**2
def g(x) :
return (np.sin(.5*x)**2*2)/x**2
x = np.arange(-4e-8, 4e-8, 1e-9)
df = pd.DataFrame(np.array([f(x), g(x)]).T, index=x, columns=['Numerical f(x)', 'True Value']);
df.plot();
x = 1.1e-8
print("cos({:.16e}) = {:.16f}".format(x, cos(x)))
print("1-cos({:.0e}) = {:.16e}".format(x, 1.-cos(x)))
print("true value of 1-cos({:.0e}) = {:.16e}".format(x, 2*sin(.5*x)**2))
cos(1.0999999999999999e-08) = 0.9999999999999999 1-cos(1e-08) = 1.1102230246251565e-16 true value of 1-cos(1e-08) = 6.0499999999999997e-17
The Cox-Ingersoll-Ross (CIR) process is widely used in quant finance, eg, in the short rate and Heston model:
$$ dr_t = \kappa(\mu - r) dt + \sigma \sqrt{r_t} dW_t $$
the variance of $r_t$ at $t > 0$:
$$\text{Var}[r_t | r_0] = \frac{r_0\sigma^2}{\kappa}(e^{-\kappa t} - e^{-2\kappa t}) + \frac{\mu\sigma^2}{2\kappa}(1-e^{-\kappa t})^2$$
how the catastrophic cancellation arises?
k = arange(-3e-8, 3e-8, 1e-9)*1e-7
t = 1
v = (1-exp(-k*t))/k
plot(k, v, '.-')
title(r'$\frac{1}{\kappa}(1-\exp(-\kappa))$')
xlabel(r'$\kappa$');
How to choose the $h$ so that the following finite difference approximation is the most accurate?
$$ f'(x) \approx \frac{1}{h}(f(x+h) - f(x)) $$
The best $h$ is when truncation error equals to the rounding error:
$$ \frac{1}{2} f''(x) h^2 = f(x) \epsilon_m $$
Thus the optimal $h^* = \sqrt{\frac{2 f(x)\epsilon_m}{f''(x)}} $.
It is however better off to simply use central difference:
$$ f'(x) \approx \frac{1}{2h}\left(f(x+h) - f(x-h)\right) $$
Consider the relative error of a function with multiple arguments: $f = f(x_1, ..., x_n)$:
$$ \small df = \sum_i \frac{\partial f}{\partial x_i} dx_i \iff \frac{df}{f} = \sum_i \frac{x_i}{f}\frac{\partial f}{\partial x_i} \frac{dx_i}{x_i} $$
Assuming input argument's relative errors are $\epsilon_i$ (could be negative):
$$ \small \left| \frac{\Delta f}{f} \right| = \left| \sum_i \frac{x_i}{f}\frac{\partial f}{\partial x_i} \epsilon_i \right| \le \sum_i \left| \frac{x_i}{f}\frac{\partial f}{\partial x_i} \right| \left|\epsilon_i\right| \le \sum_i \left| \frac{x_i}{f}\frac{\partial f}{\partial x_i} \right| \epsilon_m \equiv k(f) \epsilon_m $$
where $\epsilon_m$ is the maximum relative error of all inputs.
Condition number is the systematic approach to detect potential numerical problems:
$f(x_1, x_2) = x_1 - x_2$
$k(f) = \frac{|x_1| + |x_2|}{|x_1 - x_2|}$, ill conditioned when $x_1 \approx x_2$, catastrophic cancellation
$f(x_1, x_2) = x_1 x_2$
$k(f) = 2$, the multiplication is well conditioned
The problem is ill-posed if its condition number is large.
Consider the previous example of $f(x) = \frac{1}{x^2}(1-\cos(x))$:
$$ k(f) = \left| 2 + \frac{x \sin{\left (x \right )}}{\cos{\left (x \right )} - 1} \right| $$
The numerical algorithm can be unstable even if the problem itself is well-posed.
But an ill-posed problem is always numerically unstable by definition.
Ill-posed problem are generally not suitable for numerical solutions.
The Mandelbrot set is a famous example:
Required Reading:
Recommended Reading:
Homework