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
typedeffor 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\$typedefin 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\$