Python · Lesson 27

NumPy Random Distributions, Permutations, Seaborn Visualisation, and Universal Functions (ufuncs)

8 phases  ·  Build: Project Description

👋 Welcome to Lesson 27

Welcome to Lesson 27! In this lesson you will learn four powerful ideas that sit at the heart of modern data science and scientific computing in Python:

  1. Random Data Distributions · how to generate random numbers that follow real-world patterns (like weighted choices and probability rules).
  2. Random Permutations · how to shuffle arrays or create new rearrangements without changing the original data.
  3. Seaborn for Visualisation · how to draw beautiful distribution charts using a Python library called Seaborn, so you can see what your data looks like.
  4. Universal Functions (ufuncs) · NumPy's super-fast way of doing math operations on entire arrays at once without writing slow loops.

These four topics flow naturally into each other. First you will learn to create specially shaped random data, then to rearrange it, then to visualise it, and finally to process it efficiently with ufuncs. By the end of this lesson you will have everything you need for a real mini-project.

Prerequisites check: This lesson assumes you have already used NumPy and know how to create arrays with np.array(). If you have not, quickly review: import numpy as np and arr = np.array([1, 2, 3]) before continuing.


📚 8 phases🏗️ Project Description🐍 GitHub Repo
Phase 1 of 8
Lesson Introduction

Welcome to Lesson 27! In this lesson you will learn four powerful ideas that sit at the heart of modern data science and scientific computing in Python:

  1. Random Data Distributions · how to generate random numbers that follow real-world patterns (like weighted choices and probability rules).
  2. Random Permutations · how to shuffle arrays or create new rearrangements without changing the original data.
  3. Seaborn for Visualisation · how to draw beautiful distribution charts using a Python library called Seaborn, so you can see what your data looks like.
  4. Universal Functions (ufuncs) · NumPy's super-fast way of doing math operations on entire arrays at once without writing slow loops.

These four topics flow naturally into each other. First you will learn to create specially shaped random data, then to rearrange it, then to visualise it, and finally to process it efficiently with ufuncs. By the end of this lesson you will have everything you need for a real mini-project.

Prerequisites check: This lesson assumes you have already used NumPy and know how to create arrays with np.array(). If you have not, quickly review: import numpy as np and arr = np.array([1, 2, 3]) before continuing.


✏️ Your Task
Practise what you just learned about Lesson Introduction. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 2 of 8
Prerequisite Concepts

What is a Python Array (NumPy ndarray)?

Before we start, a very quick reminder. In NumPy, data is stored in an ndarray (n-dimensional array). Think of it as a super-powered list that can hold numbers very efficiently.

python
import numpy as np

arr = np.array([10, 20, 30, 40])
print(arr)

Expected Output:

code
[10 20 30 40]

What is a Python List?

A Python list is the built-in container: [1, 2, 3]. NumPy arrays and Python lists look similar but NumPy arrays are far faster for maths operations. You will see both used in this lesson.

What is a Library Import?

When you write from numpy import random, you are saying: "Go into the NumPy toolkit and bring out the random tool so I can use it directly." Instead of typing numpy.random.choice() every time, you just type random.choice().


✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 3 of 8
Part 1 · Random Data Distribution

What is a Data Distribution?

Imagine a bag of coloured marbles. You reach in and pull one out. A data distribution tells you the complete story of the bag: how many of each colour exist, and therefore how likely you are to pull each one.

In statistics and data science, a data distribution is a description of all the possible values a variable can take, and how often each value appears.

Real-world analogy: Think about a school canteen that sells only four snacks: biscuits, chips, juice, and water. If you ask 100 students what they bought, maybe 10 bought biscuits, 30 bought chips, 60 bought juice, and 0 bought water. That pattern · 10%, 30%, 60%, 0% · is the distribution of purchases.

What is Probability?

Probability is just a fancy word for "how likely is something to happen?" It is always a number between 0 and 1:

  • 0 means it will never happen.
  • 1 means it will always happen.
  • 0.5 means it happens roughly half the time.

The probabilities for all possible outcomes must always add up to exactly 1.0 (because something must happen).

What is a Probability Density Function (PDF)?

A Probability Density Function is a mathematical description of a continuous distribution · that is, a distribution where the values can be any decimal number, not just whole numbers. It describes the probability of all values in an array or dataset.

Simple explanation: Think of a PDF like a recipe that tells you the "shape" of your data. A bell-curve PDF means most values cluster in the middle. A flat PDF means all values are equally likely.


The random.choice() Method

NumPy's random module gives us a method called choice(). This method lets you:

  1. Give it a list of possible values.
  2. Tell it exactly how likely each value should be (using probabilities).
  3. Ask it to generate as many random picks as you want.

Syntax breakdown:

python
random.choice(values_list, p=probabilities_list, size=how_many_you_want)
  • values_list · the list of values to pick from, e.g. [3, 5, 7, 9]
  • p= · the probability for each value; must be the same length as the values list and must sum to exactly 1.0
  • size= · how many random values to generate; can be a single number like (100,) or a shape like (3, 5) for a 2D array

Important rule: The probabilities in the p list must sum to exactly 1.0. If they don't, Python will raise an error.


Simple Example 1 · Weighted Coin Flip

Let's start with the simplest possible example: a coin that is more likely to land on heads.

python
from numpy import random

# Our coin: 1 = heads, 0 = tails
# Probability of heads = 0.7, tails = 0.3
result = random.choice([0, 1], p=[0.3, 0.7], size=(10))

print(result)

Expected Output (your output will vary because it is random):

code
[1 1 0 1 1 1 0 1 1 1]

Line-by-line explanation:

LineWhat it does
from numpy import randomImports the random module from NumPy
random.choice([0, 1], ...)Tells Python to pick from the values 0 or 1
p=[0.3, 0.7]0 has a 30% chance; 1 has a 70% chance
size=(10)Generate 10 random picks
print(result)Show the result

Thinking prompt: What would happen if you changed p=[0.3, 0.7] to p=[0.0, 1.0]? Would you ever see a 0? Try it!


Simple Example 2 · Weighted Choice with Four Values

Now let's pick from four possible values with different probabilities, just like the canteen example.

python
from numpy import random

# Values: 3, 5, 7, 9
# 3 has 10% chance, 5 has 30% chance, 7 has 60% chance, 9 has 0% chance
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0], size=(100))

print(x)

Expected Output (yours will be different but follow the same pattern):

code
[7 7 5 7 7 3 7 5 7 7 7 5 7 7 7 7 7 5 7 7 7 5 7 7 7 7 7 5 7 7
 7 5 7 7 7 7 7 7 7 7 7 7 3 7 5 7 7 5 7 5 5 7 7 7 7 5 7 7 7 7
 5 7 7 7 5 5 7 7 7 5 7 7 5 7 7 7 7 7 7 5 7 7 3 5 7 7 5 7 7 7
 7 7 5 7 5 7 7 7 7 7]

Notice: The value 9 never appears because its probability is 0.0. The value 7 appears most often because it has the highest probability (0.6 = 60%).

Thinking prompt: Add up the probabilities: 0.1 + 0.3 + 0.6 + 0.0 = 1.0. They add up to exactly 1. This is always required.


Generating 2D Distribution Arrays

You can also generate the random values as a two-dimensional array (like a grid/table) by passing a tuple as the size parameter.

python
from numpy import random

# Same four values, same probabilities, but now get a 3×5 table
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0], size=(3, 5))

print(x)

Expected Output:

code
[[7 5 7 7 7]
 [5 7 7 3 7]
 [7 7 5 7 7]]

Line-by-line explanation:

Code PartMeaning
size=(3, 5)Create a 2D array with 3 rows and 5 columns
The resultA 3×5 table of randomly chosen values (total = 15 values)

Real-world use: In data science, you might model a weather system where rain (probability 0.4), cloud (0.35), sun (0.2), and storm (0.05) occur across a 10-day, 5-city forecast grid.


✏️ Your Task
Practise what you just learned about Random Data Distribution. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 4 of 8
Part 2 · Random Permutations

What is a Permutation?

A permutation is simply a rearrangement of the same elements in a different order.

Everyday analogy: Imagine you have three books on your shelf: A, B, C. One arrangement is A-B-C. A permutation could be B-C-A or C-A-B. All three books are still there · they're just in a different order.

Examples of permutations:

  • [1, 2, 3] is a permutation of [3, 2, 1]
  • ["apple", "banana", "cherry"] is a permutation of ["banana", "cherry", "apple"]

NumPy's random module gives us two methods for working with permutations:

MethodWhat it doesChanges the original array?
shuffle()Rearranges the array in-placeYES · the original is changed
permutation()Returns a new rearranged arrayNO · the original stays the same

This is an important difference! Let's see both in action.


Method 1: shuffle() · Rearrange In-Place

"In-place" means the method directly modifies the original array. The array you started with gets a new order. Nothing new is created.

python
from numpy import random
import numpy as np

arr = np.array([1, 2, 3, 4, 5])

print("Before shuffle:", arr)

random.shuffle(arr)

print("After shuffle:", arr)

Expected Output (your shuffle order will vary):

code
Before shuffle: [1 2 3 4 5]
After shuffle:  [3 1 5 2 4]

Key point: After calling shuffle(arr), the original arr is permanently changed. The values [1, 2, 3, 4, 5] are now in a random order.

Common beginner mistake: Assigning the result to a new variable like this:

python
new_arr = random.shuffle(arr)  # WRONG! new_arr will be None

shuffle() does NOT return anything. It modifies arr directly and returns None. Simply call random.shuffle(arr) and then use arr.


Method 2: permutation() · Return a New Arrangement

permutation() is the safe option when you want a shuffled copy but need to keep your original data intact.

python
from numpy import random
import numpy as np

arr = np.array([1, 2, 3, 4, 5])

print("Original arr:", arr)

new_arr = random.permutation(arr)

print("New permutation:", new_arr)
print("Original arr still:", arr)

Expected Output:

code
Original arr: [1 2 3 4 5]
New permutation: [4 1 3 5 2]
Original arr still: [1 2 3 4 5]

Notice: arr is completely unchanged! The shuffled version is stored in new_arr.

Real-world use case: Imagine you have a list of 1000 student names for a quiz draw. You want to shuffle the order for fair selection, but you also want to keep the original alphabetical list intact for your records. Use permutation() · not shuffle().


Comparing shuffle() and permutation() Side by Side

python
from numpy import random
import numpy as np

original = np.array([10, 20, 30, 40, 50])

# --- Using shuffle ---
arr_for_shuffle = original.copy()  # Make a copy first to preserve original
random.shuffle(arr_for_shuffle)
print("After shuffle():     ", arr_for_shuffle)

# --- Using permutation ---
new_version = random.permutation(original)
print("After permutation(): ", new_version)
print("Original unchanged:  ", original)

Expected Output:

code
After shuffle():      [30 10 50 20 40]
After permutation():  [20 50 10 40 30]
Original unchanged:   [10 20 30 40 50]
Featureshuffle()permutation()
Modifies originalYESNO
Returns new arrayNO (returns None)YES
Safe for original dataOnly if you copy firstAlways safe

✏️ Your Task
Practise what you just learned about Random Permutations. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 5 of 8
Part 3 · Seaborn: Visualising Distributions

Why Do We Need to Visualise?

Numbers alone can be hard to understand. When you generate 1000 random numbers, looking at them as a list tells you very little. A chart instantly shows you the shape: Are most values clustered together? Are they spread evenly? Are there extreme values?

This is exactly why data scientists use visualisation libraries · tools that turn numbers into charts.

What is Seaborn?

Seaborn is a Python library specifically built for making statistical charts and visualising data distributions beautifully. It sits on top of another library called Matplotlib · think of Seaborn as the fancy user-friendly wrapper around Matplotlib's more technical engine.

Analogy: Matplotlib is like a professional artist's full studio · powerful but takes time to set up. Seaborn is like a smart automated art assistant that handles all the setup for you.

Installing Seaborn

Before using Seaborn, you need to install it. Open your terminal/command prompt and run:

bash
pip install seaborn

If you are using Jupyter Notebook, run this inside a cell:

python
!pip install seaborn

What is Matplotlib?

Matplotlib is the core plotting library for Python. Seaborn uses it behind the scenes. You need to import both.

python
import matplotlib.pyplot as plt   # For displaying charts
import seaborn as sns              # For creating distribution charts
  • matplotlib.pyplot is imported as plt · a short name by convention
  • seaborn is imported as sns · another short name by convention

What is a Displot?

Displot stands for distribution plot. It is Seaborn's function for visualising how data is distributed. It takes an array (a list of numbers) and draws a curve showing how the values are spread out.

Think of it like this: if you measured the heights of 1000 people and plotted them, the displot would show a curve peaking around the average height, with fewer and fewer people as you go towards very short or very tall heights.


Simple Example 1 · Basic Displot

python
import matplotlib.pyplot as plt
import seaborn as sns

# A small list of 6 numbers
sns.displot([0, 1, 2, 3, 4, 5])

plt.show()

What this produces: A distribution chart with:

  • A histogram (vertical bars showing how often each value appears)
  • A smooth curve over the bars showing the overall distribution shape

Line-by-line explanation:

LineMeaning
import matplotlib.pyplot as pltLoad the Matplotlib display engine, call it plt
import seaborn as snsLoad Seaborn charting library, call it sns
sns.displot([0, 1, 2, 3, 4, 5])Create a distribution chart from the list of 6 numbers
plt.show()Actually display the chart on screen

Why plt.show()? Seaborn creates the chart in memory, but plt.show() is the command that pushes it to your screen. Without it, nothing appears.


Simple Example 2 · Displot Without the Histogram (KDE Only)

Sometimes you want to see only the smooth curve (called a KDE · Kernel Density Estimate) without the histogram bars underneath. This is useful when you have a lot of data and just want to see the shape.

python
import matplotlib.pyplot as plt
import seaborn as sns

# Pass kind="kde" to show only the smooth curve
sns.displot([0, 1, 2, 3, 4, 5], kind="kde")

plt.show()

What this produces: Only the smooth density curve · no bars.

What is KDE (Kernel Density Estimate)? A KDE is a smooth curve that estimates the probability distribution of your data. Instead of counting exact values in bins (like a histogram), it draws a flowing curve that approximates the shape of the data.

Key note: For the rest of the NumPy tutorial (particularly when visualising random distributions like Normal, Binomial, Poisson, etc.), the standard approach is to use sns.displot(arr, kind="kde").


Practical Example · Visualising a Random Distribution

Now let's combine what we learned in Part 1 with Seaborn to actually see a distribution:

python
from numpy import random
import matplotlib.pyplot as plt
import seaborn as sns

# Generate 1000 random values from our weighted choice
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0], size=(1000))

# Visualise the distribution
sns.displot(x, kind="kde")
plt.title("Distribution of Weighted Random Choices")
plt.show()

What you will see: A curve with a large peak around 7 (since it has 60% probability), a smaller bump around 5 (30%), and a tiny presence near 3 (10%). The value 9 will not appear at all.

Thinking prompt: What would the chart look like if all four values had equal probability of 0.25? Try changing the p values and observe the shape.


✏️ Your Task
Practise what you just learned about Seaborn: Visualising Distributions. Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 6 of 8
Part 4 · NumPy Universal Functions (ufuncs)

What Are ufuncs?

ufuncs stands for Universal Functions. These are special NumPy functions that are designed to work on entire arrays at once · on every element simultaneously · instead of going through them one by one.

Analogy: Imagine you need to paint 100 fence posts. The slow way: you paint them one at a time with a small brush. The fast way: you use a spray painter that covers all 100 at once. ufuncs are NumPy's spray painter.

Why Use ufuncs?

The main reasons to use ufuncs are:

  1. Speed (Vectorization): They are dramatically faster than Python loops for large datasets.
  2. Simplicity: One line of code replaces a multi-line loop.
  3. Extra power: They support special options like where, dtype, and out for advanced control.

What is Vectorization?

Vectorization means converting an operation that would normally loop over elements one-by-one into a single operation that processes the entire array at once.

Modern CPUs (the chips inside your computer) are specifically designed to perform these "do-it-to-everything-at-once" operations extremely fast. This is called SIMD (Single Instruction, Multiple Data) at the hardware level · but you don't need to worry about that. Just know that vectorization uses the computer's hardware more efficiently.


The Problem: Slow Python Loops

Let's first see the old slow way of adding two lists together:

python
# The slow Python way — using a loop
x = [1, 2, 3, 4]
y = [4, 5, 6, 7]
z = []   # Empty list to store results

for i, j in zip(x, y):      # zip() pairs up elements: (1,4), (2,5), (3,6), (4,7)
    z.append(i + j)          # Add each pair and store the result

print(z)

Expected Output:

code
[5, 7, 9, 11]

Line-by-line explanation:

LineMeaning
x = [1, 2, 3, 4]First list of numbers
y = [4, 5, 6, 7]Second list of numbers
z = []Start with an empty list to collect results
for i, j in zip(x, y):Loop through both lists together, pairing elements
z.append(i + j)Add the paired numbers and add them to the results list

This works! But imagine doing this with 1 million numbers · it would be slow.


The Fast Way: NumPy's np.add() ufunc

NumPy has a built-in ufunc called add() that does exactly the same thing in one line, but much faster:

python
import numpy as np

x = [1, 2, 3, 4]
y = [4, 5, 6, 7]

z = np.add(x, y)   # Add all elements at once — no loop needed!

print(z)

Expected Output:

code
[ 5  7  9 11]

Notice: The result is exactly the same as the loop version! But np.add() did it in a single operation instead of iterating step by step.

Thinking prompt: The output looks like [ 5 7 9 11] (with spaces, no commas) instead of [5, 7, 9, 11]. Why? Because NumPy arrays print differently from Python lists · no commas, and numbers are aligned.


What Happens Inside np.add()?

code
Input x:  [1,  2,  3,  4]
Input y:  [4,  5,  6,  7]
            ↓   ↓   ↓   ↓
Result z: [5,  7,  9, 11]

NumPy processes each matching pair (position 0 with position 0, position 1 with position 1, and so on) all at the same time · this is element-wise operation.


Additional ufunc Arguments

ufuncs support special optional arguments that give you more control:

ArgumentWhat it doesExample
whereA boolean condition · only apply the operation where Truenp.add(x, y, where=[True, False, True, True])
dtypeSpecify the data type of the outputnp.add(x, y, dtype=float)
outSpecify an output array to write results intonp.add(x, y, out=result_array)

You do not need to memorise all of these right now. The key idea is: ufuncs are flexible tools that go beyond basic loops.


The Performance Difference: Why It Matters

To understand why vectorisation matters, consider this: if you have a dataset with 1 million data points (common in real data science work), a Python loop might take several seconds. The equivalent NumPy ufunc operation runs in milliseconds.

For example, adding two arrays of 1,000,000 numbers:

  • Python loop: ~0.5 seconds
  • NumPy ufunc: ~0.005 seconds (100x faster)

This is why data scientists, machine learning engineers, and scientists use NumPy · it makes working with large datasets practical.


Simple ufunc Examples to Consolidate

Example A · Adding arrays:

python
import numpy as np

a = np.array([10, 20, 30])
b = np.array([1, 2, 3])

result = np.add(a, b)
print(result)

Output: [11 22 33]

Example B · Multiplying arrays:

python
import numpy as np

a = np.array([2, 4, 6])
b = np.array([3, 3, 3])

result = np.multiply(a, b)
print(result)

Output: [ 6 12 18]

Example C · Squaring all elements (using np.power):

python
import numpy as np

arr = np.array([1, 2, 3, 4, 5])

squared = np.power(arr, 2)   # Raise each element to the power of 2
print(squared)

Output: [ 1 4 9 16 25]

Thinking prompt: What would np.power(arr, 3) produce? Try to calculate the answer before running it.


✏️ Your Task
Practise what you just learned about NumPy Universal Functions (ufuncs). Open your editor, type the examples above by hand, modify them, and observe what changes.
Phase 7 of 8
Guided Practice Exercises
🎯 Your Challenge

Exercise 1 · Weighted Lottery Simulation

Objective: Simulate a simple lottery where different prizes have different probabilities.

Scenario: A raffle has 4 possible prizes:

  • Prize A (₦5,000): 5% chance
  • Prize B (₦1,000): 15% chance
  • Prize C (₦200): 30% chance
  • No prize (₦0): 50% chance

Your Task:

  1. Use random.choice() to simulate 500 raffle draws.
  2. Print the result.
  3. Count how many times each prize was won using np.unique() (optional bonus).

Steps:

✏️ Task
Practise what you just learned about Guided Practice Exercises. Open your editor, type the examples above by hand, modify them, and observe what changes.
python
from numpy import random
import numpy as np

# Step 1: Define prizes and their probabilities
prizes = [5000, 1000, 200, 0]
probabilities = [0.05, 0.15, 0.30, 0.50]

# Step 2: Simulate 500 draws
draws = random.choice(prizes, p=probabilities, size=(500))

# Step 3: Print results
print("First 20 draws:", draws[:20])

# Bonus Step: Count how many times each prize appeared
values, counts = np.unique(draws, return_counts=True)
for val, cnt in zip(values, counts):
    print(f"Prize ₦{val}: appeared {cnt} times ({cnt/500*100:.1f}%)")

Expected Output (approximate · exact numbers vary):

code
First 20 draws: [   0    0  200    0    0    0  200    0 1000    0    0  200    0    0    0    0    0    0    0    0]
Prize ₦0:    appeared 251 times (50.2%)
Prize ₦200:  appeared 149 times (29.8%)
Prize ₦1000: appeared 76 times (15.2%)
Prize ₦5000: appeared 24 times (4.8%)

Self-check questions:

  • Do the counts roughly match the probabilities you set?
  • What happens if you increase the draws to 10,000? Do the percentages get closer to the exact probabilities?
  • What happens if all probabilities are equal (0.25 each)?

Exercise 2 · Shuffling a Student List

Objective: Practice the difference between shuffle() and permutation().

Scenario: You are a teacher with 8 students. You want to randomly assign them to two groups of 4.

python
from numpy import random
import numpy as np

students = np.array(["Alice", "Bob", "Carol", "David",
                     "Emma", "Frank", "Grace", "Henry"])

# --- Method 1: Using permutation (safe — keeps original intact) ---
shuffled = random.permutation(students)

print("Original list:", students)
print("Shuffled list:", shuffled)

# Split into two groups
group1 = shuffled[:4]
group2 = shuffled[4:]

print("\nGroup 1:", group1)
print("Group 2:", group2)

Expected Output (your shuffle will differ):

code
Original list: ['Alice' 'Bob' 'Carol' 'David' 'Emma' 'Frank' 'Grace' 'Henry']
Shuffled list: ['Grace' 'Bob' 'Emma' 'Alice' 'Henry' 'Frank' 'Carol' 'David']

Group 1: ['Grace' 'Bob' 'Emma' 'Alice']
Group 2: ['Henry' 'Frank' 'Carol' 'David']

Self-check questions:

  • Did the original students array change?
  • Could you use shuffle() here instead? What would you need to do differently?
  • What if you ran the code again · would you get the same groups?

Exercise 3 · Fast Array Arithmetic with ufuncs

Objective: Compare the loop approach vs ufunc approach.

Scenario: You have sales data for 5 shops. You need to apply a 10% bonus to each shop's monthly revenue.

python
import numpy as np

# Monthly revenue (in thousands of Naira)
revenue = np.array([120, 85, 200, 145, 310])

# --- The slow way (loop) ---
bonuses_loop = []
for r in revenue:
    bonuses_loop.append(r * 0.10)
print("Bonuses (loop method):", bonuses_loop)

# --- The fast way (ufunc / vectorized) ---
bonuses_ufunc = np.multiply(revenue, 0.10)
print("Bonuses (ufunc method):", bonuses_ufunc)

# --- New totals ---
new_revenue = np.add(revenue, bonuses_ufunc)
print("Revenue after bonus:", new_revenue)

Expected Output:

code
Bonuses (loop method): [12.0, 8.5, 20.0, 14.5, 31.0]
Bonuses (ufunc method): [12.  8.5 20.  14.5 31. ]
Revenue after bonus: [132.   93.5 220.  159.5 341. ]

Self-check questions:

  • Are both methods producing the same result?
  • Which method would you prefer for a dataset with 1 million entries?
  • Can you modify the code to apply a 15% bonus only to shops earning more than 150?

Exercise 4 · Visualising a Weighted Distribution

Objective: Use Seaborn to visualise a weighted distribution and observe its shape.

python
from numpy import random
import matplotlib.pyplot as plt
import seaborn as sns

# Simulate exam grade categories (A, B, C, D, F)
# Most students get B or C, few get A or F
grades_numeric = random.choice([90, 75, 60, 45, 30],
                                p=[0.10, 0.35, 0.40, 0.10, 0.05],
                                size=(500))

# Visualise
sns.displot(grades_numeric, kind="kde")
plt.title("Simulated Exam Grade Distribution")
plt.xlabel("Score")
plt.ylabel("Density")
plt.show()

What to observe: The curve should peak around 60-75 (since C and B have the highest probabilities), with smaller peaks toward 90 and declining toward 30.


Phase 8 of 8
Common Beginner Mistakes

Mistake 1 · Probabilities That Don't Sum to 1.0

python
# WRONG — probabilities sum to 0.9, not 1.0
x = random.choice([1, 2, 3], p=[0.2, 0.3, 0.4])  # Missing 0.1!

Error you will see:

code
ValueError: probabilities do not sum to 1

Correct version:

python
x = random.choice([1, 2, 3], p=[0.2, 0.3, 0.5])  # 0.2+0.3+0.5 = 1.0 ✓

Mistake 2 · Assigning the Return Value of shuffle()

python
# WRONG — shuffle() returns None, not the shuffled array
arr = np.array([1, 2, 3, 4])
shuffled = random.shuffle(arr)   # shuffled = None!
print(shuffled)  # Prints: None

Correct version:

python
arr = np.array([1, 2, 3, 4])
random.shuffle(arr)   # Modifies arr directly
print(arr)            # Prints the shuffled array

Mistake 3 · Wrong Number of Probabilities

python
# WRONG — 4 values but only 3 probabilities
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6])

Error:

code
ValueError: 'a' and 'p' must have same size

Correct version:

python
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0])  # One probability per value

Mistake 4 · Forgetting plt.show() with Seaborn

python
import seaborn as sns
import matplotlib.pyplot as plt

sns.displot([1, 2, 3, 4, 5])
# Nothing appears! You forgot plt.show()

Correct version:

python
sns.displot([1, 2, 3, 4, 5])
plt.show()  # This actually displays the chart

Mistake 5 · Using a Python Loop Instead of a ufunc

python
# INEFFICIENT for large datasets
result = []
for val in big_array:
    result.append(val * 2)

Better version:

python
import numpy as np
result = np.multiply(big_array, 2)  # Faster and cleaner

Mistake 6 · Expecting shuffle() Not to Change the Original

python
arr = np.array([1, 2, 3, 4, 5])
random.shuffle(arr)
# arr is NOW changed — you cannot get the original back!

Safe version if you need the original:

python
arr = np.array([1, 2, 3, 4, 5])
arr_copy = arr.copy()    # Save a copy FIRST
random.shuffle(arr)       # Now it's safe — original is in arr_copy

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes. Open your editor, type the examples above by hand, modify them, and observe what changes.
🏗️ Build It — Mini Project
Project Description

Project Description

You work as a data analyst for a school. You need to:

  1. Simulate exam scores for 200 students with a realistic distribution.
  2. Randomly shuffle students to create fair seating groups.
  3. Apply performance bonuses (ufunc operations).
  4. Visualise the final distribution.

Stage 1 · Setup

starter.py
from numpy import random
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

print("=== Student Performance Analysis System ===\n")

Lesson 27 complete! 🎉

You covered: