2  Good coding practices

In this introductory chapter, we will discuss good coding practices that have to be applied when writing Python code for this course: efficient computations, no hard coding, don’t repeat yourself (DRY), single responsibility, and documentation. Overall, always try to keep your code simple and structured.

2.1 Efficient computations

Perhaps the most important topic of this course is that of efficient computation, i.e., to make efficient use of the mathematical functionality that Python has to offer, in particular the functionality of the NumPy package.

The way to think of this is as follows: Many of the mathematical tasks and exercises that we will see could, in theory, be solved using for- and/or while-loops, as well as if/else-statements. However, there are often more efficient functions programmed in the NumPy package in Python that can carry out these tasks in a quicker way using less code!

Let us look at an example. Suppose you are given the daily percentage returns of an investment, and are asked to compute the total number of days on which the investment made money. One way to solve this directly would be by using a for-loop and an if-statement to count the number of days with a positive return. We will illustrate this next for eight daily returns.

# Daily percentage returns
returns = [1.2, -0.5, 0.8, 0.0, -1.1, 2.3, 0.4, -0.7]

# Count days with a positive return
positive_days = 0
for daily_return in returns:
    if daily_return > 0:
        positive_days += 1

# Print total number of days with a positive return
print(positive_days)
4

Another way to do this is given below and avoids the use of the for-loop as well as the if-statement.

#Import Numpy package under the alias np
import numpy as np

# Define daily percentage returns as an array
returns = np.array([1.2, -0.5, 0.8, 0.0, -1.1, 2.3, 0.4, -0.7])

# Count days with a positive return
positive_days = np.sum(returns > 0)

# Print total number of days with a positive return
print(positive_days)
4

This is a much cleaner way to compute the total number of days with a positive return, but harder to understand than the for-loop approach. However, especially for large numbers of observations, the second piece of code that exploits the NumPy functionality will be much faster! We will learn how the second piece of code works in this course.

The Python code below (which we will not discuss) illustrates this comparison by timing how long both approaches require to count the positive returns for n = 30,000,000 (i.e., thirty million) simulated returns. Both approaches use the same data, generated before timing begins. If you run the code in the dropdown menu below on your own device, you might get different outputs, but, in general, there should be a significant (multiplicative) difference in execution time.

Code that times execution of the two approaches above
import numpy as np
import time

# Generate daily percentage returns for both approaches
n = 30000000
rng = np.random.default_rng(42)
returns = rng.normal(loc=0.05, scale=1.0, size=n)

## Counting days with a positive return with for-loop

# We determine the time it takes to execute the code between start and end
start = time.time()

# Count days with a positive return
positive_days = 0
for daily_return in returns:
    if daily_return > 0:
        positive_days += 1

end = time.time()

# Time difference
loop_time = end - start
print('%.10f seconds needed for counting positive returns' % (loop_time),
      'with for-loop')

## Counting days with a positive return with NumPy functions

# We determine the time it takes to execute the code between start and end
start = time.time()

# Count days with a positive return
positive_days = np.sum(returns > 0)

end = time.time()

# Time difference
numpy_time = end - start
print('%.10f seconds needed for counting positive returns' % (numpy_time),
      'with NumPy functions')

# Speedup of NumPy over the for-loop approach
if numpy_time != 0:
    print('NumPy is %.1f times faster than the for-loop approach.'
          % (loop_time / numpy_time))
12.8019752502 seconds needed for counting positive returns with for-loop
0.0954644680 seconds needed for counting positive returns with NumPy functions
NumPy is 134.1 times faster than the for-loop approach.

One important take-away of the above comparison is the following:

When performing mathematical tasks, avoid the use of for- and while-loops, as well as if/else-statements, by efficient use of Numpy functionality.

2.2 No hard coding

Suppose we are given the function f(x) = a \cdot x^2+ a\cdot b \cdot x - a \cdot b \cdot c and the goal is to compute f(10) for a = 3, b = 4 and c = -2. One way of doing this would be to plug in all the variables and return the resulting function value.

# Hard coding
print(3*(10**2) + 3*4*10 + 4*3*(-2))
396

However, this is inefficient for the following reason: If we wanted to change the number x = 10 to, e.g., x = 20, we would have to replace 10 by 20 twice. The same is true for the variable a, which appears in three places. In general, in large scripts, variables can appear in more than a hundred places. To overcome this inefficiency issue, it is better to separate the input data (the x,a,b and c in this case) from the function execution.

# No hard coding
def f(x, a, b, c):
    return a*x**2 + a*b*x + b*c*a

x = 10
a, b, c = 3, 4, -2
print(f(x, a, b, c))
396

The take-away of the above comparison is the following:

Separate input data from the execution of functions and commands.

2.3 Don’t repeat yourself (DRY)

If you have to carry out a piece of code for multiple sets of input data, always avoid copy-pasting the code. For example, suppose that for the upcoming week (Monday through Sunday), we have temperature predictions temperatures = [17, 20, 25, 21, 16, 19, 14], and we want to print the prediction per day in a nice message. We could do this by writing the message for Monday and then copying and pasting the code and replacing the day and temperature by that of the next day.

print("The predicted temperature for Monday is 17 degrees Celsius.")
print("The predicted temperature for Tuesday is 20 degrees Celsius.")
print("The predicted temperature for Wednesday is 25 degrees Celsius.")
print("The predicted temperature for Thursday is 21 degrees Celsius.")
print("The predicted temperature for Friday is 16 degrees Celsius.")
print("The predicted temperature for Saturday is 19 degrees Celsius.")
print("The predicted temperature for Sunday is 14 degrees Celsius.")
The predicted temperature for Monday is 17 degrees Celsius.
The predicted temperature for Tuesday is 20 degrees Celsius.
The predicted temperature for Wednesday is 25 degrees Celsius.
The predicted temperature for Thursday is 21 degrees Celsius.
The predicted temperature for Friday is 16 degrees Celsius.
The predicted temperature for Saturday is 19 degrees Celsius.
The predicted temperature for Sunday is 14 degrees Celsius.

This is quite tedious and very inefficient, for example, if you want to print longer forecasts. Instead we can use a for-loop that only requires two lines of code, and generalizes to longer forecasts.

temperatures = [17, 20, 25, 21, 16, 19, 14]
weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", 
                                "Friday", "Saturday", "Sunday"]

for day, temp in zip(weekdays, temperatures):
    print(f"The predicted temperature for {day} is {temp} degrees Celsius.")
The predicted temperature for Monday is 17 degrees Celsius.
The predicted temperature for Tuesday is 20 degrees Celsius.
The predicted temperature for Wednesday is 25 degrees Celsius.
The predicted temperature for Thursday is 21 degrees Celsius.
The predicted temperature for Friday is 16 degrees Celsius.
The predicted temperature for Saturday is 19 degrees Celsius.
The predicted temperature for Sunday is 14 degrees Celsius.

The take-away of the above comparison is the following:

Don’t carry out the same task on different inputs twice by copy-pasting code, but use, e.g., a for-loop in which you iterate over the inputs.

2.4 Single responsibility

When you write larger pieces of code, it is often useful to split them up in smaller parts that all serve their own purpose. Suppose we want to implement Newton’s method for finding a root x of a function f : \mathbb{R} \rightarrow \mathbb{R}, i.e., a point x that satisfies f(x) = 0.

Newton’s method starts with a (suitably chosen) initial guess x_0 \in \mathbb{R} and repeatedly computes better approximations using the recursive formula x_{i+1} = x_i - \frac{f(x_i)}{f'(x_i)} One iteration of Newton’s method is illustrated in the figure below.

The goal is to implement this formula for the function f(x) = (x-1)^2 - 1, whose derivative is f'(x) = 2(x-1). The roots of this function are x = 0 and x = 2.

# We implement the function y = x - f(x)/f'(x)
def y(x):
    return x - ((x-1)**2 - 1)/(2*(x-1))

x0 = 3
x1 = y(x0) 

print(x1)
2.25

A clearer way to implement the formula x_{i+1} = x_i - f(x_i)/f'(x_i) is to define separate functions for the evaluation of the functions f and f', and then combine these to create the recursive formula. In this way, we get three functions that are each responsible for one aspect of Newton’s formula, namely implementing the function f, implementing the function f' and computing the recursive formula. This also makes it easier for a user of the code to understand what is happening.

# Single responsibility version

# Define f
def f(x):
    return (x-1)**2 - 1

# Define f'
def fprime(x):
    return 2*(x-1)

# Implement function y = x - f(x)/f'(x)
def y(x):
    return x - f(x)/fprime(x)

x0 = 3
x1 = y(x0) 

print(x1)
2.25

The take-away of the above comparison is the following:

If you are implementing a complex mathematical task involving multiple objects, try to separate these aspects in different functions.

2.5 Documentation

The final good coding practice that we discuss is documentation. Ideally, you should always explain inside a function what the inputs and outputs are, and for larger scripts it is good to indicate what every part of the script does. For smaller functions and scripts this is not always necessary. We will next give an example for Newton’s method as in the previous section.

## Function that implements Newton's method
def newton(f,fprime,x0,iters):
    """
    This function implements Newton's iterative method 
    x_{i+1} = x_i - f(x_i)/f'(x_i) for finding a root of f.
    
    Input parameters
    ----------
    f : Function f
    fprime : Derivative of the function f
    x0 : Initial estimate for root x of f.
    iters : Number of iterations that we run Newton's method.
    
    Returns
    -------
    Approximation to a root x satisfying f(x) = 0.
    """
    
    # Initial guess
    x = x0 
    
    # Repeatedly compute the recursive formula 
    # by overwriting x for 'iters' iterations
    for i in range(iters):
        x_new = x - f(x)/fprime(x) 
        x = x_new
    return x

Next we run the function newton() on some inputs.

## An example of Newton's method as implemented above

# Define f
def f(x):
    return (x-1)**2 - 1

# Define f'
def fprime(x):
    return 2*(x-1)
    
# Define initial guess and number of iterations
x0 = 10
iters = 6   

# Run Newton's method
root = newton(f,fprime,x0,iters)

# Print output and explain what has been computed
print('Running Newton\'s method for %.i iterations with initial' % iters,
       'estimate %.2f' % x0,'\n','gives (estimated) root x = %.7f' % root, 
       'with f(x) = %.7f' % f(root))
Running Newton's method for 6 iterations with initial estimate 10.00 
 gives (estimated) root x = 2.0000013 with f(x) = 0.0000025

The following is a set of guidelines regarding how to add documentation to a function. For the exam you do not have to add documentation to your functions, but you should do this for the assignment of this module.

Try to adhere to the following documentation rules when writing complex functions:
1. Place function documentation between triple double-quote characters.
2. Clearly describe what a function does and what its input and output arguments are.
3. Choose descriptive variable names, lines not longer than 80 characters.
4. Don’t add comments for every line. Add comments for main ideas and complex parts.