Mathew K Analytics

Lesson 21 · Probability and Statistics in python

Practical Applications of Simulation Techniques in Statistical Analysis and Decision-Making

Simulations help us understand probability and statistics using real or synthetic data. In this lesson, we will go step-by-step through: Core probability…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Welcome to Practical Simulation in Statistics!#

Simulations help us understand probability and statistics using real or synthetic data.

In this lesson, we will go step-by-step through:

  • Core probability concepts with simulations
  • Real-world datasets like Titanic and airline passengers
  • Discrete and continuous distributions
  • Descriptive statistics and handling missing data
  • Sampling, hypothesis testing, correlation, regression, time series, and more

Let us get started on our simulation journey!

import warnings
warnings.filterwarnings("ignore")

# Import all libraries needed throughout
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)

Probability Basics: Coin Flips and Dice#

Simulation means trying out what might happen by using code. Let us use Python to simulate coin flips and dice rollsclassic examples of probability.

Probability just means: how likely is an event to happen?

# Simulate 10 coin flips (0 = tails, 1 = heads)
coin_flips = np.random.choice([0, 1], size=10)
print("Coin flips:", coin_flips)

# Calculate the proportion of heads
prop_heads = np.mean(coin_flips)
print("Proportion of heads:", prop_heads)
Coin flips: [0 1 0 0 0 1 0 0 0 1]
Proportion of heads: 0.3
# Simulate 20 dice rolls (values 1 to 6)
dice_rolls = np.random.randint(1, 7, size=20)
print("Dice rolls:", dice_rolls)

# Probability of rolling a six
prob_six = np.mean(dice_rolls == 6)
print("Proportion of sixes:", prob_six)
Dice rolls: [3 3 3 5 4 3 6 5 2 4 6 6 2 4 5 1 4 2 6 5]
Proportion of sixes: 0.2

Descriptive Statistics: What Do Simulations Tell Us?#

After simulating, we want to describe our data. Common statistics: mean (average), median (middle), and standard deviation (spread).

These help us get a sense of the data's center and variability.

# Example: describe dice rolls
print("Mean:", np.mean(dice_rolls))
print("Median:", np.median(dice_rolls))
print("Standard Deviation:", np.std(dice_rolls))

# Visualize the dice rolls
plt.hist(dice_rolls, bins=np.arange(1,8)-0.5, rwidth=0.8)
plt.xlabel('Roll')
plt.ylabel('Frequency')
plt.title('Histogram of 20 Dice Rolls')
plt.show()
Mean: 3.95
Median: 4.0
Standard Deviation: 1.499166435056495
No description has been provided for this image
# Introduce missing values on purpose
dice_with_nan = dice_rolls.astype(float)
dice_with_nan[::5] = np.nan  # Every fifth value becomes missing

# Check for missing data
print("Has missing?", pd.isnull(dice_with_nan).any())
# Fill missing values with the median
filled_dice = pd.Series(dice_with_nan).fillna(np.nanmedian(dice_with_nan))
print("Filled dice rolls:", filled_dice.values)
Has missing? True
Filled dice rolls: [4. 3. 3. 5. 4. 4. 6. 5. 2. 4. 4. 6. 2. 4. 5. 4. 4. 2. 6. 5.]

Discrete Probability Distributions: Bernoulli, Binomial, Poisson#

Discrete distributions only allow whole-number outcomes. Bernoulli: one trial with two outcomes, like a coin flip. Binomial: multiple independent trials with two outcomes each. Poisson: counts events in a fixed period, such as emails per hour.

# Bernoulli: Simulate one coin flip (1 = heads, 0 = tails)
bernoulli_flips = np.random.binomial(1, 0.5, size=8)
print("Bernoulli results:", bernoulli_flips)

# Binomial: Simulate 10 trials of 5 coin flips each
binom_results = np.random.binomial(5, 0.5, size=10)
print("Binomial results (number of heads in 5 flips):", binom_results)

# Poisson: Simulate number of emails received per hour, avg 4, for 12 hours
poisson_counts = np.random.poisson(4, size=12)
print("Poisson counts (emails per hour):", poisson_counts)
Bernoulli results: [0 0 1 0 0 0 0 1]
Binomial results (number of heads in 5 flips): [2 3 3 1 3 1 1 4 4 3]
Poisson counts (emails per hour): [3 2 5 7 1 4 3 3 0 5 4 6]
# Draw histograms for each distribution
fig, axs = plt.subplots(1, 3, figsize=(13,4))
axs[0].hist(bernoulli_flips, bins=3, rwidth=0.8, color='skyblue')
axs[0].set_title('Bernoulli')
axs[1].hist(binom_results, bins=range(0,7), rwidth=0.8, color='orange')
axs[1].set_title('Binomial')
axs[2].hist(poisson_counts, bins=range(min(poisson_counts), max(poisson_counts)+2), rwidth=0.8, color='green')
axs[2].set_title('Poisson')
plt.tight_layout()
plt.show()
No description has been provided for this image

Continuous Distributions: Normal and Exponential#

Continuous means values can take any number, not just whole numbers. Normal (or Gaussian) is the famous bell curve found in heights, IQ, and many natural processes. Exponential describes waiting times between random events, like the time until the next bus arrives.

# Simulate 100 values from normal and exponential distributions
normal_data = np.random.normal(loc=0, scale=1, size=100)
exp_data = np.random.exponential(scale=1, size=100)

# Plot both distributions
fig, axs = plt.subplots(1,2,figsize=(10,4))
axs[0].hist(normal_data, bins=15, color='purple', alpha=0.6)
axs[0].set_title('Normal Distribution')
axs[1].hist(exp_data, bins=15, color='red', alpha=0.6)
axs[1].set_title('Exponential Distribution')
plt.tight_layout()
plt.show()
No description has been provided for this image

Sampling and the Central Limit Theorem#

Sampling means taking a smaller part of data to estimate the whole. The Central Limit Theorem says: if we take lots of samples, their means form a bell curve, even if the data is not normal.

# Simulate population: waiting times (exponential, not normal)
population = np.random.exponential(2, 10000)

# Take 500 samples, each made of 30, and plot sample means
sample_means = [np.mean(np.random.choice(population, 30)) for _ in range(500)]
plt.hist(sample_means, bins=20, color='teal')
plt.title('Sample Means (CLT Demo)')
plt.xlabel('Sample Mean')
plt.ylabel('Frequency')
plt.show()
No description has been provided for this image
# Show actual population histogram for comparison
plt.hist(population, bins=20, color='grey', alpha=0.7)
plt.title('Original Population: Exponential Distribution')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.show()
No description has been provided for this image
# Data setup: Import the Titanic dataset using seaborn
titanic = sns.load_dataset("titanic")
print("Rows, columns:", titanic.shape)
print(titanic.head())
Rows, columns: (891, 15)
   survived  pclass     sex   age  sibsp  parch     fare embarked  class  \
0         0       3    male  22.0      1      0   7.2500        S  Third   
1         1       1  female  38.0      1      0  71.2833        C  First   
2         1       3  female  26.0      0      0   7.9250        S  Third   
3         1       1  female  35.0      1      0  53.1000        S  First   
4         0       3    male  35.0      0      0   8.0500        S  Third   

     who  adult_male deck  embark_town alive  alone  
0    man        True  NaN  Southampton    no  False  
1  woman       False    C    Cherbourg   yes  False  
2  woman       False  NaN  Southampton   yes   True  
3  woman       False    C  Southampton   yes  False  
4    man        True  NaN  Southampton    no   True  
# Simulate sampling survival on the Titanic
samples = [titanic['survived'].dropna().sample(30, replace=True, random_state=42+i).mean() for i in range(300)]
plt.hist(samples, bins=20, color='navy')
plt.title('Sample Mean Survival Rates (n=30)')
plt.xlabel('Sample Survival Rate')
plt.ylabel('Frequency')
plt.show()
No description has been provided for this image

Confidence Intervals: How Sure Are We?#

A confidence interval tells us a range for our estimatelike survival ratewith a chosen level of certainty, often 95%. Let us build an interval for Titanic survival using bootstrap (resampling).

# Bootstrap confidence interval for Titanic survival rate
boot_means = [titanic['survived'].dropna().sample(200, replace=True).mean() for _ in range(1000)]
ci_low = np.percentile(boot_means, 2.5)
ci_high = np.percentile(boot_means, 97.5)
print(f"95% confidence interval: [{ci_low:.2f}, {ci_high:.2f}]")
95% confidence interval: [0.32, 0.46]

Hypothesis Testing: Did Women Survive at Higher Rates?#

Hypothesis testing asks: 'Is there a real difference, or is this just luck?' Let us check if women were more likely to survive the Titanic using a simulation (permutation test).

# Permutation test for gender survival difference
women = titanic[titanic['sex']=='female']['survived'].dropna()
men = titanic[titanic['sex']=='male']['survived'].dropna()
actual_diff = women.mean() - men.mean()
combined = np.concatenate([women, men])
perms = [np.mean(np.random.permutation(combined)[:len(women)]) - np.mean(np.random.permutation(combined)[len(women):]) for _ in range(1000)]
p_value = np.mean(np.abs(perms) >= np.abs(actual_diff))
print(f"Actual difference: {actual_diff:.2f}")
print(f"Simulated p-value: {p_value:.3f}")
Actual difference: 0.55
Simulated p-value: 0.000

Correlation and Covariance: Are Two Things Related?#

Correlation measures how two variables move together, like height and weight. Covariance tells us the direction of the relationship but not the strength.

# Try with Titanic: Age and Fare
corr = titanic[['age', 'fare']].corr().iloc[0,1]
cov = titanic[['age', 'fare']].cov().iloc[0,1]
print(f"Correlation between age and fare: {corr:.2f}")
print(f"Covariance between age and fare: {cov:.2f}")
Correlation between age and fare: 0.10
Covariance between age and fare: 73.85

Simple Regression: Predicting Fares#

Regression finds patterns for predicting one variable from others. Let us predict the ticket fare using passenger age with linear regression.

from sklearn.linear_model import LinearRegression
# Prepare the data
reg = LinearRegression()
X = titanic[['age']].fillna(titanic['age'].mean())
y = titanic['fare']
reg.fit(X, y)
print(f"Slope: {reg.coef_[0]:.2f}")
print(f"Intercept: {reg.intercept_:.2f}")

# Plot
plt.scatter(X, y, alpha=0.2, label='data')
plt.plot(X, reg.predict(X), color='red', label='fit')
plt.xlabel('Age')
plt.ylabel('Fare')
plt.title('Linear Regression: Fare vs. Age')
plt.legend()
plt.show()
Slope: 0.35
Intercept: 21.81
No description has been provided for this image

Logistic Regression: Who Survived the Titanic?#

Logistic regression predicts the chance of an eventlike survivalusing characteristics such as age, class, and sex. We will predict survival using data from the Titanic.

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
# Prepare features: age, sex, class
features = titanic[['age','pclass','sex']].copy()
features['age'].fillna(features['age'].mean(), inplace=True)
features['sex'] = features['sex'].map({'male':0, 'female':1})
labels = titanic['survived']
X_train, X_test, y_train, y_test = train_test_split(features, labels, test_size=0.2, random_state=42)
lr = LogisticRegression()
lr.fit(X_train, y_train)

accuracy = lr.score(X_test, y_test)
print(f"Test accuracy: {accuracy:.2f}")
Test accuracy: 0.81

Time Series Basics: Airline Passengers Over Time#

Time series tracks changes in somethinglike daily temperatures or number of passengersover time. Let us work with real airline passenger data and visualize it.

# Data setup: Read air passengers data from URL
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/airline-passengers.csv"
air = pd.read_csv(url)
print(air.head())

# Plot passengers by month
plt.plot(air['Month'], air['Passengers'])
plt.xlabel('Month')
plt.ylabel('Number of Passengers')
plt.title('Monthly Airline Passengers')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
     Month  Passengers
0  1949-01         112
1  1949-02         118
2  1949-03         132
3  1949-04         129
4  1949-05         121
No description has been provided for this image

Bayesian Introduction: Updating Beliefs#

Bayesian methods use probabilities to update what we believe as we see new data. We can simulate how learning changes what we think about an unknown eventlike the chance of rain tomorrow.

# Beta-Binomial example: coin with unknown chance of heads
prior_alpha, prior_beta = 2, 2  # Initial guess: any value is possible
data = [1, 0, 1, 1, 0, 1]  # Observed flips: 1=head, 0=tail
post_alpha = prior_alpha + sum(data)
post_beta = prior_beta + len(data) - sum(data)
x = np.linspace(0,1,100)
prior_pdf = stats.beta.pdf(x, prior_alpha, prior_beta)
post_pdf = stats.beta.pdf(x, post_alpha, post_beta)
plt.plot(x, prior_pdf, label='Prior')
plt.plot(x, post_pdf, label='Posterior', linestyle='--')
plt.xlabel('Probability of Heads')
plt.ylabel('Density')
plt.legend()
plt.title('Prior and Posterior Beliefs about Fairness of Coin')
plt.show()
No description has been provided for this image

Mini-Project Part 1: Simulate a Probability Experiment#

Imagine you toss two coins 1000 times. Simulate the number of times both land heads and calculate the empirical probability.

This is a hands-on chance to model a real event using simulation!

# Simulate two coin tosses 1000 times
results = np.random.choice([0,1], size=(1000,2))
both_heads = np.sum(np.all(results==1, axis=1))
empirical_prob = both_heads / 1000
print(f"Number of both heads: {both_heads}")
print(f"Empirical probability: {empirical_prob:.2f}")
Number of both heads: 242
Empirical probability: 0.24

Mini-Project Part 2: Analyze a Real Dataset#

Let us explore the Titanic data by simulating survival rates for each passenger class. We will use simulation to build confidence intervals for each group.

# Simulate survival rate CIs for Titanic classes
for cls in [1,2,3]:
    surv = titanic[titanic['pclass']==cls]['survived'].dropna()
    boot = [surv.sample(80, replace=True).mean() for _ in range(500)]
    lo, hi = np.percentile(boot, [2.5,97.5])
    print(f"Class {cls}: 95% CI [{lo:.2f}, {hi:.2f}]")
    
Class 1: 95% CI [0.52, 0.74]
Class 2: 95% CI [0.37, 0.59]
Class 3: 95% CI [0.16, 0.35]
# Practice prompt: Try changing passenger class, or simulate class differences using passenger sex.
print("Practice: Repeat the above simulation but group by sex instead of class. What do you find?")
Practice: Repeat the above simulation but group by sex instead of class. What do you find?

Best Practices and Troubleshooting#

  • Always set a random seed to make simulations reproducible.
  • Visualize your simulated results to spot unexpected outcomes early.
  • Use larger sample sizes for smoother results, but smaller ones to see more randomness.
  • Double-check your simulation logic against known probabilities.

If something looks strange, go step-by-step and print results to debug.

Extra Tips#

  • Try simulating a process in your own field (sports, weather, sales)
  • Explore built-in datasets from seaborn for fast practice
  • Write a function for repeated simulation steps to make your code reusable

Challenge Exercises#

  1. Simulate rolling two dice 5000 times. What percentage shows both sixes?
  2. Use the airline passenger data and plot a moving average to see trends.
  3. Try running a hypothesis test on tips dataset (group by smoker vs. non-smoker).

Feel free to share your code or results with others!

Recap: What We Learned with Simulations#

  • Simulations let us model chance and uncertainty in code
  • We saw probability basics, discrete and continuous distributions, sampling, and confidence intervals
  • We practiced with the Titanic and airline data
  • Simulation shines when theory is too hard, or we want to test an idea fast

Keep exploringyour understanding grows with every experiment!

Thank You! Subscribe for More Python Stats#

If you found this hands-on guide helpful, please:

  • Like this video
  • Subscribe for more Python and data science tutorials

Drop your questions and experiences in the comments. Happy simulating!

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.