whereas:
a*x**3 + b*x**2 + c*x = ((a*x + b)*x + c)*x
h/t @ Emily L. for "Horner" reference:
https://en.wikipedia.org/wiki/Horner%27s_method
and h/t @ Davidmh for noting improvements in computational speed / precision of this method
gale-church cited it like this in 1990:
import math
def pnorm(z):
t = 1 / (1 + 0.2316419 * z)
pd = (1 - 0.3989423 *
math.exp(-z * z / 2) *
((((1.330274429 * t - 1.821255978) * t
+ 1.781477937) * t - 0.356563782) * t + 0.319381530) * t)
return pd
This method conveniently avoids the t^n issue.
citation:


source:
http://www.aclweb.org/anthology/J93-1004
page 21 of 28 in the pdf
page 95 of the journal Computational Linguistics Volume 19, Number 1
I might "prettify" to:
def pnorm(z):
t = 1 / (1 + 0.2316419 * z)
pd = (1 - 0.3989423 * math.exp(-z * z / 2) *
((((1.330274429 * t -
1.821255978) * t +
1.781477937) * t -
0.356563782) * t +
0.319381530) * t )
return pd
if you check the
Abromowitz and Stegun, Handbook of Mathematical Functions
page 932 equation 26.2.17
citation:
http://people.math.sfu.ca/~cbm/aands/page_932.htm
you'll find this:

from which we can create a table giving us:
def pnorm(z):
p = 0.2316419
b1 = 0.319381530
b2 = -0.356563782
b3 = 1.781477937
b4 = -1.821255978
b5 = 1.330274429
t = 1 / (1 + p * z)
pd = (1 - 0.3989423 * math.exp(-z * z / 2) *
((((b5 * t + b4) * t + b3) * t + b2) * t + b1) * t )
return pd
Then from the previous page; 931 you will find:

Zx = (1/√(2* π))*e(-z*z/2)
in python:
Zx = (1/math.sqrt(2* math.pi))*math.exp(-z*z/2)
and we find that (1/√(2* π)) = 0.3989423
also, I think I like this:
t * (b1 + t * (b2 + t * (b3 + t * (b4 + t * b5))))
better than:
(((b5 * t + b4) * t + b3) * t + b2) * t + b1) * t
so then, finally:
import math
def pnorm(z):
p = 0.2316419
b1 = 0.319381530
b2 = -0.356563782
b3 = 1.781477937
b4 = -1.821255978
b5 = 1.330274429
t = 1 / (1 + p * z)
Zx = (1 / math.sqrt(2 * math.pi)) * math.exp(-z * z / 2)
pd = Zx * t * (b1 + t * (b2 - t * (b3 + t * (b4 - t * b5))))
return (1 - pd)
checking my work against the op's
import matplotlib.pyplot as plt
import numpy as np
import math
def norm_cdf(z):
""" Use the norm distribution functions as of Gale-Church (1993) srcfile. """
# Equation 26.2.17 from Abramowitz and Stegun (1964:p.932)
t = 1.0 / (1+0.2316419*z) # t = 1/(1+pz) , p=0.2316419
probdist = 1 - 0.3989423*math.exp(-z*z/2) * ((0.319381530 * t)+ \
(-0.356563782* math.pow(t,2))+ \
(1.781477937 * math.pow(t,3)) + \
(-1.821255978* math.pow(t,4)) + \
(1.330274429 * math.pow(t,5)))
return probdist
for z in np.arange (-3,3,0.01):
zf = pnorm(z)
plt.plot(z,zf, c='red', marker = '.', ms=1)
for z in np.arange (-3,3,0.01):
zf = norm_cdf(z)+0.1 #offset 0.1
plt.plot(z,zf, c='blue', marker = '.', ms=1)
plt.show()
plt.pause(0.1)

I was expecting the Horner method to be faster, so I ran a time test, substituting:
#Zx = (1.0 / math.sqrt(2.0 * math.pi)) * math.exp(-z * z / 2.0)
Zx = 0.3989423* math.exp(-z * z / 2.0)
to make it fair and upping the np.arrange resolution to 0.0001:
t0 = time.time()
for z in np.arange (-3,3,0.0001):
zf = pnorm(z)
t1 = time.time()
for z in np.arange (-3,3,0.0001):
zf = norm_cdf(z)
t2 = time.time()
print ('pnorm time : %s' % (t1-t0))
print ('norm_cdf time : %s' % (t2-t1))
and the results, spinning my quad core AMD 7950 FM2+ w/ 16G ram pretty hard (albeit with several other apps running)... defied my expectations:
>>>
pnorm time : 81.4725670815
norm_cdf time : 80.7865998745
The Horner method was not faster
typedef
for your mathematical constants? It would make the code a hell of a lot more readable, imo. Something liketypdef 0.319381530 NORMALITY
. But you know, pick better names and stuff. For Python, you could just have all your constants at the top of the method (for readability), something likeNORMALITY = 0.319381530
. That's my preferred method of organizing mathematical functions anyway. \$\endgroup\$typedef
in Python, which is why I suggested just defining a "constant" likeNORMALITY = 0.319381530
. Personally, I don't like the list approach as suggested in @Davidmh's answer:coeff = [1, 0.319381530, -0.356563782, 1.781477937, -1.821255978, 1.330274429]
If there are a lot of constants, usingcoeff[0]
,coeff[1]
etc. starts to become unreadable in a long function, especially functions that use the constants multiple times. Again, just my opinion. \$\endgroup\$