HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMajor

Prettify math formula in code

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
prettifycodemathformula

Problem

I have a function to calculate the normal distribution in Python:

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


But I have to adhere to PEP8 and 80 char margin, hence the ugly \s. How else should I prettify my code?

In mathematical form,

$$\begin{align*}
\textrm{norm_cdf}(z) = 1 - 0.3989423 e^\frac{-z^2}{2} (&1.330274429 t^5 - 1.821255978 t^4 \\
&+ 1.781477937 t^3 - 0.356563782 t^2 + 0.319381530 t )
\end{align*}$$

where

$$ t = \frac{1}{1 + 0.2316419 z}$$

Solution

Let me quote the wonderful book Numerical Recipes in C++ (but also applicable):

We assume that you know enough never to evaluate a polynomial this way:

p=c[0]+c[1]*x+c[2]*x*x+c[3]*x*x*x+c[4]*x*x*x*x;


or (even worse!),

p=c[0]+c[1]*x+c[2]*pow(x,2.0)+c[3]*pow(x,3.0)+c[4]*pow(x,4.0);


Come the (computer) revolution, all persons found guilty of such criminal behavior will be summarily executed, and their programs won't be!

(You can find the page in your edition in the analytical index, under the entry "puns, particularly bad". I love this book.)

There are two reasons not to do that: accuracy and performance. The correct way to evaluate the polynomial is like this:

-t * (0.319381530  +  t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + 1.330274429 * t))))


And you can, of course, split at your convenience, as the newlines inside parenthesis are ignored. Remember the PEP: " The preferred place to break around a binary operator is after the operator, not before it."

-t * (0.319381530  +  t * (-0.356563782 +
    t * (1.781477937 + t * (-1.821255978 + 1.330274429 * t))))


Another alternative is to save the coefficients in a list:

coeff = [0, 0.319381530, -0.356563782, 1.781477937, -1.821255978, 1.330274429]
poly = coeff[-1]
for c in coeff[-2::-1]:
    poly *= x
    poly += c


I am doing operations in place to avoid allocating and deallocating memory, but this is only relevant if x is a NumPy array. If you are evaluating on a single number, you can just use instead the nicer expression:

poly = poly * x + coeff[i]


But I would stick with the first one because it is more general.

Of course, the result has to be multiplied by the prefactor:

return 1 - 0.3989423*math.exp(-z*z/2) * poly


Or, if you want to do it in place:

z2 = z * z # Be careful not to modify your input!
z2 *= 0.5  # Multiplication is faster than division.
np.exp(z2, out=z2)

probd = z2 * poly
probd *= -0.3989423
probd += 1
return probd


Bonus track:

If you are applying this function to large arrays (more than a thousand numbers), you may benefit from using the first technique in numexpr:

expr += '1 - 0.3989423* exp(-z * z / 2) * '
expr += '(-t * (0.319381530  +  t * (-0.356563782 +  t * '
expr += '(1.781477937 + t * (-1.821255978 + 1.330274429 * t)))))'
ne.evaluate(expr)


This will compile the expression for you and transparently use as many cores as you have.

Code Snippets

p=c[0]+c[1]*x+c[2]*x*x+c[3]*x*x*x+c[4]*x*x*x*x;
p=c[0]+c[1]*x+c[2]*pow(x,2.0)+c[3]*pow(x,3.0)+c[4]*pow(x,4.0);
-t * (0.319381530  +  t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + 1.330274429 * t))))
-t * (0.319381530  +  t * (-0.356563782 +
    t * (1.781477937 + t * (-1.821255978 + 1.330274429 * t))))
coeff = [0, 0.319381530, -0.356563782, 1.781477937, -1.821255978, 1.330274429]
poly = coeff[-1]
for c in coeff[-2::-1]:
    poly *= x
    poly += c

Context

StackExchange Code Review Q#59384, answer score: 43

Revisions (0)

No revisions yet.