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:
- Random Data Distributions · how to generate random numbers that follow real-world patterns (like weighted choices and probability rules).
- Random Permutations · how to shuffle arrays or create new rearrangements without changing the original data.
- Seaborn for Visualisation · how to draw beautiful distribution charts using a Python library called Seaborn, so you can see what your data looks like.
- 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 npandarr = np.array([1, 2, 3])before continuing.
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.
import numpy as np
arr = np.array([10, 20, 30, 40])
print(arr)Expected Output:
[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().
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:
- Give it a list of possible values.
- Tell it exactly how likely each value should be (using probabilities).
- Ask it to generate as many random picks as you want.
Syntax breakdown:
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.0size=· 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
plist 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.
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):
[1 1 0 1 1 1 0 1 1 1]Line-by-line explanation:
| Line | What it does |
|---|---|
from numpy import random | Imports 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]top=[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.
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):
[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.
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:
[[7 5 7 7 7]
[5 7 7 3 7]
[7 7 5 7 7]]Line-by-line explanation:
| Code Part | Meaning |
|---|---|
size=(3, 5) | Create a 2D array with 3 rows and 5 columns |
| The result | A 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.
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:
| Method | What it does | Changes the original array? |
|---|---|---|
shuffle() | Rearranges the array in-place | YES · the original is changed |
permutation() | Returns a new rearranged array | NO · 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.
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):
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:
new_arr = random.shuffle(arr) # WRONG! new_arr will be None
shuffle()does NOT return anything. It modifiesarrdirectly and returnsNone. Simply callrandom.shuffle(arr)and then usearr.
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.
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:
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()· notshuffle().
Comparing shuffle() and permutation() Side by Side
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:
After shuffle(): [30 10 50 20 40]
After permutation(): [20 50 10 40 30]
Original unchanged: [10 20 30 40 50]| Feature | shuffle() | permutation() |
|---|---|---|
| Modifies original | YES | NO |
| Returns new array | NO (returns None) | YES |
| Safe for original data | Only if you copy first | Always safe |
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:
pip install seabornIf you are using Jupyter Notebook, run this inside a cell:
!pip install seabornWhat is Matplotlib?
Matplotlib is the core plotting library for Python. Seaborn uses it behind the scenes. You need to import both.
import matplotlib.pyplot as plt # For displaying charts
import seaborn as sns # For creating distribution chartsmatplotlib.pyplotis imported asplt· a short name by conventionseabornis imported assns· 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
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:
| Line | Meaning |
|---|---|
import matplotlib.pyplot as plt | Load the Matplotlib display engine, call it plt |
import seaborn as sns | Load 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, butplt.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.
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:
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
pvalues and observe the shape.
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:
- Speed (Vectorization): They are dramatically faster than Python loops for large datasets.
- Simplicity: One line of code replaces a multi-line loop.
- Extra power: They support special options like
where,dtype, andoutfor 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:
# 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:
[5, 7, 9, 11]Line-by-line explanation:
| Line | Meaning |
|---|---|
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:
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:
[ 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()?
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:
| Argument | What it does | Example |
|---|---|---|
where | A boolean condition · only apply the operation where True | np.add(x, y, where=[True, False, True, True]) |
dtype | Specify the data type of the output | np.add(x, y, dtype=float) |
out | Specify an output array to write results into | np.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:
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:
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):
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.
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:
- Use
random.choice()to simulate 500 raffle draws. - Print the result.
- Count how many times each prize was won using
np.unique()(optional bonus).
Steps:
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):
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.
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):
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
studentsarray 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.
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:
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.
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.
Mistake 1 · Probabilities That Don't Sum to 1.0
# 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:
ValueError: probabilities do not sum to 1Correct version:
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()
# WRONG — shuffle() returns None, not the shuffled array
arr = np.array([1, 2, 3, 4])
shuffled = random.shuffle(arr) # shuffled = None!
print(shuffled) # Prints: NoneCorrect version:
arr = np.array([1, 2, 3, 4])
random.shuffle(arr) # Modifies arr directly
print(arr) # Prints the shuffled arrayMistake 3 · Wrong Number of Probabilities
# WRONG — 4 values but only 3 probabilities
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6])Error:
ValueError: 'a' and 'p' must have same sizeCorrect version:
x = random.choice([3, 5, 7, 9], p=[0.1, 0.3, 0.6, 0.0]) # One probability per valueMistake 4 · Forgetting plt.show() with Seaborn
import seaborn as sns
import matplotlib.pyplot as plt
sns.displot([1, 2, 3, 4, 5])
# Nothing appears! You forgot plt.show()Correct version:
sns.displot([1, 2, 3, 4, 5])
plt.show() # This actually displays the chartMistake 5 · Using a Python Loop Instead of a ufunc
# INEFFICIENT for large datasets
result = []
for val in big_array:
result.append(val * 2)Better version:
import numpy as np
result = np.multiply(big_array, 2) # Faster and cleanerMistake 6 · Expecting shuffle() Not to Change the Original
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:
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_copyProject Description
You work as a data analyst for a school. You need to:
- Simulate exam scores for 200 students with a realistic distribution.
- Randomly shuffle students to create fair seating groups.
- Apply performance bonuses (ufunc operations).
- Visualise the final distribution.
Stage 1 · Setup
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:
- ✅ Lesson Introduction
- ✅ Prerequisite Concepts
- ✅ Part 1 · Random Data Distribution
- ✅ Part 2 · Random Permutations
- ✅ Part 3 · Seaborn: Visualising Distributions
- ✅ Part 4 · NumPy Universal Functions (ufuncs)
- ✅ Guided Practice Exercises
- ✅ Common Beginner Mistakes