Skip to content

hide: - toc


Random Numbers

Random number generation is central to simulation. In finance, random values are frequently used in Monte Carlo simulation, stress testing, bootstrapping, option pricing, and modelling uncertain future outcomes, essentially, any situation where you want to explore a range of possible scenarios rather than a single fixed calculation.

NumPy can generate random numbers from several distributions, including uniform distributions, random integers, and the normal (Gaussian) distribution. Importantly, setting a random seed allows these "random" results to be reproduced exactly, which matters enormously in finance and research, where results need to be checked, audited, or repeated by someone else.

Python Code Examples: Random Number Generation

import numpy as np
import matplotlib.pyplot as plt

# np.random.rand() draws from a uniform distribution between 0 and 1
print(np.random.rand(3, 4))       # a 3x4 array of uniformly distributed random values

# np.random.randn() draws from the standard normal distribution (mean 0, standard deviation 1)
print(np.random.randn(3, 4))

Visualising the difference between distributions

It's often easier to understand the difference between rand() and randn() by plotting them side by side:

plt.hist(np.random.rand(100000), density=True, bins=100,
         histtype="step", color="blue", label="rand")
plt.hist(np.random.randn(100000), density=True, bins=100,
         histtype="step", color="red", label="randn")

plt.axis([-2.5, 2.5, 0, 1.1])
plt.legend(loc="upper left")
plt.title("Random distributions")
plt.xlabel("Value")
plt.ylabel("Density")
plt.show()

Running this, you'll see rand produces a flat, "boxy" distribution (every value between 0 and 1 is equally likely), while randn produces the familiar bell-shaped curve, clustered around 0 and tapering off symmetrically. Recognising which distribution a simulation calls for is an important first step in any modelling task.

Generating and Converting Random Arrays

# Uniform random values in a 2x3 array
a = np.random.rand(2, 3)
print(a)

# Uniform random values in a 3x2 array
rand = np.random.rand(3, 2)
print(rand)

# .tolist() converts a NumPy array into an ordinary nested Python list
rand_list = rand.tolist()
print(rand_list)

# np.random.randint() generates random whole numbers within a specified range
# Here: values from 2 (inclusive) up to 10 (exclusive), in a 2x3 array
rand_int = np.random.randint(2, 10, size=(2, 3))
print(rand_int)

# np.random.normal() draws from a normal distribution with a chosen mean and standard deviation
# Here: mean 0, standard deviation 1, arranged as a 10x10 array
rand_norm = np.random.normal(0, 1, size=(10, 10))
print(rand_norm)

# np.random.standard_normal() is a shorthand specifically for mean 0, standard deviation 1
rand_std_norm = np.random.standard_normal(size=(10, 10))
print(rand_std_norm)

Reproducibility with Random Seeds

Setting a seed before generating random numbers ensures you get the exact same "random" results every time you rerun the code. This is essential for reproducible research and for debugging, since it means a colleague (or your future self) can rerun your code and get identical output.

np.random.seed(1)
rand_1 = np.random.normal(0, 1, size=(50, 10))   # a 50 x 10 matrix of standard normal values

Mean_rand_1 = rand_1.mean()
Std_rand_1 = rand_1.std()
print(Mean_rand_1, Std_rand_1)
# As the sample size grows toward infinity, the mean tends toward 0 and the
# standard deviation tends toward 1, since that's how the distribution was defined

Transforming a standard normal distribution

A standard normal distribution (mean 0, standard deviation 1) can be rescaled into a normal distribution with any mean and standard deviation you like, using simple arithmetic. This is a common technique in financial simulation, for example, simulating returns with a specific expected value and volatility:

a = 1.5   # desired mean
b = 3.5   # desired standard deviation

Y = a + b * rand_1   # rescales rand_1 to have mean 'a' and standard deviation 'b'
print(Y)

Combining Random Arrays: Concatenation and Stacking

When running simulations, it's common to generate several random series and then combine them into a single dataset, for example, simulating several assets' returns together.

np.random.seed(1)
rand_1 = np.random.normal(0, 1, size=(50, 1))
rand_2 = np.random.normal(0, 1, size=(50, 1))
rand_3 = np.random.normal(0, 1, size=(50, 1))

# np.concatenate() joins arrays together along a chosen axis
# axis=0 (the default) stacks them on top of each other, i.e. as extra rows
concat_column = np.concatenate((rand_1, rand_2, rand_3))
print(concat_column)          # shape (150, 1): all three columns stacked into one long column

# axis=1 joins them side by side instead, i.e. as extra columns
concat_row = np.concatenate((rand_1, rand_2, rand_3), axis=1)
print(concat_row)             # shape (50, 3): three separate columns, side by side

# np.vstack() and np.hstack() are convenient shortcuts for the same two operations
V_Stack = np.vstack((rand_1, rand_2, rand_3))     # equivalent to concatenate with axis=0
print(V_Stack)

H_Stack = np.hstack((rand_1, rand_2, rand_3))     # equivalent to concatenate with axis=1
print(H_Stack)

A useful way to remember the difference: vstack stacks arrays vertically (adding rows), while hstack stacks them horizontally (adding columns).

Repeating, Inserting, and Replacing Values

np.random.seed(1)
rand = np.random.normal(0, 1, size=(50, 10))

# np.repeat() duplicates values along a chosen axis
repeat_rand_row = np.repeat(rand, 2, axis=0)      # each row is repeated twice, doubling the number of rows
print(repeat_rand_row)

repeat_rand_column = np.repeat(rand, 2, axis=1)   # each column is repeated twice, doubling the number of columns
print(repeat_rand_column)

# np.insert() adds a new row or column of a specified value at a given position
rand_insert_row = np.insert(rand, 1, 5, axis=0)      # inserts a new row of 5s at index 1
print(rand_insert_row)

rand_insert_column = np.insert(rand, 1, 5, axis=1)   # inserts a new column of 5s at index 1
print(rand_insert_column)

Replacing an entire row or column

Sometimes you need to overwrite a whole row or column with new values, for instance, to simulate a shock to a particular time period or variable:

# Replacing a row: note the shape must match, here a 1x10 matrix for a single row
New_Values_Row = np.random.normal(0, 1, size=(1, 10)) * 50
rand[0] = New_Values_Row
print(rand)

# Replacing a column: note the shape here is a plain 1D array of length 50, matching the column
New_Values_Column = np.random.normal(0, 1, size=50) * 50
rand[:, 0] = New_Values_Column
print(rand)

Pay close attention to the shapes involved here, replacing a row expects a shape matching the row's width, while replacing a column expects a shape matching the column's length. Mismatched shapes are one of the most common sources of errors when manipulating arrays like this.

Padding and Deleting Rows or Columns

np.random.seed(1)
rand = np.random.normal(0, 1, size=(50, 10))

# np.pad() adds extra rows and columns around the edges of an array
# 'constant' mode fills the new border with a fixed value (here, 0)
Zero_Pad = np.pad(rand, (1, 1), 'constant', constant_values=0)
print(Zero_Pad)

# 'mean' mode instead fills the new border with the average value of the array
Mean_Pad = np.pad(rand, (1, 1), 'mean')
print(Mean_Pad)

# np.delete() removes a specified row or column
Rand_del_row = np.delete(rand, 1, axis=0)       # removes row at index 1
print(Rand_del_row)

Rand_del_column = np.delete(rand, 1, axis=1)    # removes column at index 1
print(Rand_del_column)

Why this matters for financial data preparation: padding, inserting, and deleting rows or columns are exactly the kinds of operations you'll rely on when cleaning real datasets, for example, adding a placeholder row for a missing observation, or removing a column of data that turned out to be unreliable, before passing the data on to further analysis.