Python Programming Laboratory Manual
UNIVERSITY OF ENGINEERING AND TECHNOLOGY
Department of Computer Science / Data Science
PYTHON PROGRAMMING
LABORATORY MANUAL & REPORT
Python Fundamentals • NumPy • Pandas & Data Analysis
Five Laboratory Sessions
Prepared from official course lecture materials
(Lecture_material_1_Basics, Lecture_material_1.1_numpy, Lecture_material_1.2_pandas)
June 2026
Page 1 of 89
Python Programming Laboratory Manual
Preface
This laboratory manual consolidates three sets of course lecture material — Python
fundamentals, NumPy, and Pandas — into five structured laboratory sessions. Each session
follows a consistent format used widely in university computing laboratories: stated learning
Objectives, a short Theory/Background introduction for every topic, numbered Steps containing
runnable Code, the actual Output produced by that code, a brief explanation of the result, and a
set of Post-Lab Tasks for independent practice.
The original lecture notebooks did not include any executed output (the code cells were left
blank, as is typical of an in-class teaching notebook). For this report, every code example was
actually executed in a standard Python data-science environment, and the genuine console
output, computed values, and charts are reproduced below — including a few interesting cases
where the original code contains an instructive bug or quirk (these are called out explicitly rather
than silently corrected, since recognising such mistakes is itself a valuable learning outcome).
Software versions used to generate the outputs in this report: Python 3.12, NumPy 2.4, pandas
3.0, Matplotlib 3.10, Seaborn 0.13, and SciPy 1.17. Minor differences in formatting (for
example, pandas' internal text-storage dtype) may appear if a different library version is used to
re-run the code, but the underlying logic and results will be identical.
Page 2 of 89
Python Programming Laboratory Manual
Table of Contents
TOC \h \o "1-3"
Page 3 of 89
Python Programming Laboratory Manual
Laboratory Session Overview
No. Session Title Source Material
Python Fundamentals —
1 Operators, Variables, and Data Lecture_material_1_Basics.pdf (Part 1)
Structures
Control Flow, Functions, and
2 Object-Oriented Programming Lecture_material_1_Basics.pdf (Part 2)
in Python
Introduction to NumPy —
3 Array Creation and Lecture_material_1.1_numpy.[Link] (Part 1)
Manipulation
NumPy for Statistics,
4 Probability, and Linear Lecture_material_1.1_numpy.[Link] (Part 2)
Algebra
Data Analysis with Pandas —
5 Cleaning, Exploring, and Lecture_material_1.2_pandas.pdf
Visualising the Titanic Dataset
Page 4 of 89
Python Programming Laboratory Manual
Lab Session 1: Python Fundamentals — Operators,
Variables, and Data Structures
Source material: Lecture_material_1_Basics.pdf (Part 1)
Objectives
• Write and run basic Python print statements and arithmetic expressions.
• Declare variables and apply arithmetic, comparison, and assignment operators.
• Construct and format strings using f-strings and the .format() method.
• Create and manipulate Python's core data structures: lists, tuples, dictionaries, and sets.
1.1 Your First Python Program and Arithmetic Operators
Every Python session begins with the simplest possible instruction: printing text to the screen.
Python operators behave like ordinary mathematics, but with a few extras (modulus % for
remainders, floor division // for whole-number division, and ** for powers) that are used
constantly in data science work.
Step 1: The print() Function
The built-in print() function writes text or values to the console. It is the most basic way to
produce visible output from a Python program.
Code:
print("Hello World")
print(2+3)
print("We are learning Python with Adeel")
Output:
Hello World
We are learning Python with Adeel
Step 2: Arithmetic Operators and Operator Precedence (PEMDAS)
Python supports the standard arithmetic operators. Notice that / always returns a float, //
performs floor (integer) division, % returns the remainder, and ** raises to a power. The last line
shows that Python respects the usual PEMDAS order of operations.
Code:
print(2+1)
Page 5 of 89
Python Programming Laboratory Manual
print(4-5)
print(5*3)
print(4/2)
print(17%3)
print(7//2)
print(3**4)
print(3**2/2*4/3+5-3)
Output:
3
-1
15
2.0
2
3
81
8.0
1.2 Variables and Assignment
A variable is simply a name bound to a value in memory. Python is dynamically typed, so the
same name can be reassigned to a different value (even a different type) at any time.
Step 3: Creating a String Variable
Assigning a string literal to a name creates a string variable. In a notebook, simply typing the
variable name on the last line of a cell auto-displays its value.
Code:
a= "Hello World"
Output:
Hello World
Step 4: Updating a Variable's Value
A variable can be reassigned based on its own previous value, which is exactly how counters and
accumulators work in loops.
Code:
age = 20
age = age + 1
print(age)
Output:
21
Step 5: Arithmetic Operators on Variables
The same arithmetic operators introduced earlier work identically when applied to variables
instead of literal numbers.
Page 6 of 89
Python Programming Laboratory Manual
Code:
a=11;b=3
print(a+b)
print(a-b)
print(a*b)
print(a/b)
print(a%b)
print(a**b)
print(a//b)
Output:
14
8
33
3.6666666666666665
2
1331
3
Step 6: Comparison Operators
Comparison operators always evaluate to a Boolean (True or False) and are the building blocks
of conditional logic.
Code:
a=11;b=11
print(a>b)
print(a<b)
print(a>=b)
print(a<=b)
print(a!=b)
print(a==b)
Output:
False
False
True
True
False
True
Step 7: Assignment (Compound) Operators
Operators such as *=, +=, and -= update a variable in place; a *= 2 is shorthand for a = a * 2.
Code:
a=2
print(a)
a*=2
print(a)
Output:
2
4
Page 7 of 89
Python Programming Laboratory Manual
Step 8: Another String Variable Example
Re-confirms that string assignment and auto-display behave consistently.
Code:
b = "We are learning Python with Adeel"
Output:
We are learning Python with Adeel
Step 9: int vs. float Data Types
Python automatically infers numeric types: whole numbers are int, and numbers with a decimal
point are float.
Code:
c= 20
d = 20.5
Output:
20.5
1.3 Strings and f-Strings
Strings are sequences of characters. Python 3.6 introduced f-strings — string literals prefixed
with f that allow variables and expressions to be embedded directly inside {curly braces}.
Forgetting the f prefix is one of the most common beginner mistakes, and the lecture material's
own slides demonstrate this slip, which is reproduced (and explained) below.
Step 10: Curly Braces Without an f-Prefix (a Common Pitfall)
Without the f prefix, Python treats {Adeel} and {Lahore} as literal text — it does NOT look
them up as variables, so the braces are printed exactly as written. This illustrates why the f prefix
is essential for variable substitution.
Code:
string="My name is {Adeel} and i am from {Lahore}"
print(string)
Output:
My name is {Adeel} and i am from {Lahore}
Step 11: The .format() Method
Before f-strings existed, the [Link]() method was the standard way to insert values into
placeholders {0}, {1}, etc., matched positionally to the arguments passed to format().
Code:
Page 8 of 89
Python Programming Laboratory Manual
string="My name is {0} and I am from {1}"
Name="Adeel"
City="Lahore"
print([Link](Name,City))
Output:
My name is Adeel and I am from Lahore
Step 12: f-Strings (the Modern, Preferred Approach)
Prefixing a string with f allows variable names to be evaluated directly inside the braces — this
is shorter and more readable than .format().
Code:
Name="Adeel"
City="Lahore"
string=(f"My name is {Name} and i am from {City}")
print(string)
Output:
My name is Adeel and i am from Lahore
Step 13: Number Formatting Inside f-Strings
The first snippet repeats the missing-f mistake from Step 10 (so the format spec {price:.2f} is
printed literally instead of being applied). The second snippet adds the f prefix correctly, so price
is rounded to 3 decimal places as requested by the :.3f format specifier.
Code:
# Without f prefix (bug: prints literally)
price=50.5235656
print("The price of the product is {price:.2f}")
# With f prefix (correct: formats the number)
price=50.5235656
print(f"The price of the product is {price:.3f}")
Output:
The price of the product is {price:.2f}
The price of the product is 50.524
1.4 Core Data Structures: Lists, Tuples, and Dictionaries
Python has four built-in collection types that store groups of values: list [] (ordered, mutable),
tuple () (ordered, immutable), dict {} (key-value pairs), and set {} (unordered, unique values).
Choosing the right structure is a foundational data-science skill.
Page 9 of 89
Python Programming Laboratory Manual
Step 14: Declaring a List, a Tuple, and a Dictionary
A list can mix data types (strings, floats, booleans, integers) and is enclosed in []. A tuple looks
similar but uses () and cannot be modified after creation. A dictionary stores key:value pairs
inside {}.
Code:
# List literal
e = ["apple","banana","orange",20.5,True,15]
# Tuple literal
f = ("apple","banana","orange",20.5,True,15)
# Dictionary literal
g = {"name":"Abdul Hannan","age":27,"weight":76.5,"height":167.5}
Output:
['apple', 'banana', 'orange', 20.5, True, 15]
('apple', 'banana', 'orange', 20.5, True, 15)
{'name': 'Abdul Hannan', 'age': 27, 'weight': 76.5, 'height': 167.5}
Step 15: String Indexing and Slicing
Strings are indexed from 0, and negative indices count from the end. The slice a[start:stop]
returns characters from start up to (but not including) stop. String methods like .upper()
and .replace() return new strings without changing the original.
Code:
a="Pakistan"
print(a[0])
print(a[4])
print(a[0:5])
x="zeshan"
print(x)
print([Link]())
print([Link]("a","e"))
a="Pakistan"
print(a[:7])
print(a[1:])
print(a[-4:])
print(a[:-3])
Output:
P
s
Pakis
zeshan
ZESHAN
Page 10 of 89
Python Programming Laboratory Manual
zeshen
Pakista
akistan
stan
Pakis
Step 16: List Methods — sort, count, len, repeat, append, insert, remove
Lists support a rich set of in-place methods. sort() arranges elements ascending (reverse=True for
descending); count() tallies occurrences of a value; list * n repeats the whole list n times;
append() adds to the end; insert(i, x) places x at index i; and remove(x) deletes the first matching
value.
Code:
# Original list
list1=[2,5,63,25,21,33,65,9,89,8,25]
# After .sort()
list1=[2,5,63,25,21,33,65,9,89,8,25]
[Link]()
# After .sort(reverse=True)
list1=[2,5,63,25,21,33,65,9,89,8,25]
[Link](reverse=True)
# [Link](25)
list1=[2,5,63,25,21,33,65,9,89,8,25]
[Link](reverse=True)
print([Link](25))
# len(list1)
list1=[2,5,63,25,21,33,65,9,89,8,25]
print(len(list1))
# list1 * 3
list1=[2,5,63,25,21,33,65,9,89,8,25]
# After .append('B')
list1=[2,5,63,25,21,33,65,9,89,8,25]
[Link]("B")
# After .insert(5,'B')
list1=[2,5,63,25,21,33,65,9,89,8,25]
[Link]("B")
[Link](5,"B")
# After .remove('B')
list1=[2,5,63,25,21,33,65,9,89,8,25]
[Link]("B")
Page 11 of 89
Python Programming Laboratory Manual
[Link](5,"B")
[Link]("B")
Output:
[2, 5, 63, 25, 21, 33, 65, 9, 89, 8, 25]
[2, 5, 8, 9, 21, 25, 25, 33, 63, 65, 89]
[89, 65, 63, 33, 25, 25, 21, 9, 8, 5, 2]
11
[2, 5, 63, 25, 21, 33, 65, 9, 89, 8, 25, 2, 5, 63, 25, 21, 33, 65, 9, 89, 8, 25, 2, 5, 63, 25, 21, 33, 65, 9, 89, 8, 25]
[2, 5, 63, 25, 21, 33, 65, 9, 89, 8, 25, 'B']
[2, 5, 63, 25, 21, 'B', 33, 65, 9, 89, 8, 25, 'B']
[2, 5, 63, 25, 21, 33, 65, 9, 89, 8, 25, 'B']
Step 17: Appending to a List and Concatenating Two Lists
append() adds one element to the end of a list, while the + operator concatenates two lists into a
new, combined list.
Code:
# [Link]('klm')
myList = ['abc', 'def', 'ghij']
[Link]('klm')
# myList2 + myList3
myList2 = [1,2,3]
myList3 = [4,5,6]
Output:
['abc', 'def', 'ghij', 'klm']
[1, 2, 3, 4, 5, 6]
Step 18: Dictionary Operations — pop, values, keys, update
.pop(key) removes and returns the value for that key; .values() and .keys() return view objects of
all values/keys; and .update() merges another dictionary's key-value pairs into the original
(overwriting any duplicate keys).
Code:
# Original dictionary
dic1={"a":25,"b":65,"c":98,"d":87}
# [Link]('a') return value
dic1={"a":25,"b":65,"c":98,"d":87}
# Dictionary after pop
dic1={"a":25,"b":65,"c":98,"d":87}
Page 12 of 89
Python Programming Laboratory Manual
[Link]("a")
# [Link]()
dic1={"a":25,"b":65,"c":98,"d":87}
# [Link]()
dic1={"a":25,"b":65,"c":98,"d":87}
# Adding a key and merging with update()
dic1={"a":25,"b":65,"c":98,"d":87}
dic1["e"]=10
print(dic1)
dict_2 = {"f": 120, "g":110,"h":70}
print(dict_2)
dict_2.update(dic1)
print(dict_2)
Output:
{'a': 25, 'b': 65, 'c': 98, 'd': 87}
25
{'b': 65, 'c': 98, 'd': 87}
dict_values([25, 65, 98, 87])
dict_keys(['a', 'b', 'c', 'd'])
{'a': 25, 'b': 65, 'c': 98, 'd': 87, 'e': 10}
{'f': 120, 'g': 110, 'h': 70}
{'f': 120, 'g': 110, 'h': 70, 'a': 25, 'b': 65, 'c': 98, 'd': 87, 'e': 10}
Step 19: A Practical Dictionary Example — Student Marks
Dictionaries are a natural fit for lookups such as 'name maps to a single value', e.g. a student
roster mapped to marks.
Code:
marks = {"Ali": 50, "Ahmed": 70, "Hassan": 90, "Khalid": 80, "Nasir": 60}
Output:
{'Ali': 50, 'Ahmed': 70, 'Hassan': 90, 'Khalid': 80, 'Nasir': 60}
Step 20: A Subtle Syntax Trap — 'Nested Dictionary' That Isn't
The intention here was to build one nested dictionary, but because the outer {} braces were left
out, Python's comma operator silently creates a tuple of four separate dictionaries instead of a
single nested one. type() confirms this: the result is a tuple, not a dict. This is a valuable lesson in
checking your data structures with type().
Code:
nested_dict = {"first":{"a":1},"second":{"b":2}}, {"third":{"c":3}}, {"fourth":{"d":4}}, {"fifth":{"e":5}}
Page 13 of 89
Python Programming Laboratory Manual
print(nested_dict)
print(type(nested_dict))
Output:
({'first': {'a': 1}, 'second': {'b': 2}}, {'third': {'c': 3}}, {'fourth': {'d': 4}}, {'fifth': {'e': 5}
})
<class 'tuple'>
Step 21: Tuple Operations — Concatenation, Repetition, max/min, Mixed Types
Like lists, tuples can be concatenated (+) and repeated (*), but they remain immutable. Built-in
max() and min() work directly on numeric tuples. A tuple can even hold a NumPy array as one
of its elements, as the last example shows.
Code:
# tup_1
tup_1=(2,"Pak",True,4.3)
# tup_2
tup_2=(4,"Pakistan",False,3.6)
# tup_1 + tup_2
tup_1=(2,"Pak",True,4.3)
tup_2=(4,"Pakistan",False,3.6)
# tup_1 * 2 + tup_2
tup_1=(2,"Pak",True,4.3)
tup_2=(4,"Pakistan",False,3.6)
# max() / min() of tup_3
tup_3=(40,33,45,76,87,98,34,65,89)
print(max(tup_3))
print(min(tup_3))
# A tuple containing a NumPy array, indexed
import numpy as np
myTuple = ('abc', [Link](0,3,0.2), 2.5)
Output:
(2, 'Pak', True, 4.3)
(4, 'Pakistan', False, 3.6)
(2, 'Pak', True, 4.3, 4, 'Pakistan', False, 3.6)
(2, 'Pak', True, 4.3, 2, 'Pak', True, 4.3, 4, 'Pakistan', False, 3.6)
98
33
2.5
Page 14 of 89
Python Programming Laboratory Manual
Step 22: Set Operations — add, union, intersection
Sets automatically discard duplicate values and have no fixed order. union() combines all unique
elements from two sets, while intersection() keeps only the elements common to both.
Code:
# Creating a set and adding an element
s1={1,2,5,"karachi", "lahore", "islamabad"}
print(s1)
[Link]("patoki")
print(s1)
# [Link](cities2)
cities1={"karachi","lahore","islamabad","patoki"}
cities2={"lahore","multan","hyderabad","naran"}
# [Link](cities2)
cities1={"karachi","lahore","islamabad","patoki"}
cities2={"lahore","multan","hyderabad","naran"}
Output:
{'lahore', 1, 2, 'karachi', 5, 'islamabad'}
{'lahore', 1, 2, 'patoki', 'karachi', 5, 'islamabad'}
{'hyderabad', 'karachi', 'islamabad', 'naran', 'multan', 'lahore', 'patoki'}
{'lahore'}
Post-Lab Tasks — Session 1
1. Write a Python program that swaps the values of two variables without using a third
variable.
2. Create an f-string that prints your name and age, formatting the age as a 3-digit zero-
padded number.
3. Given a list of 10 random integers, write code that sorts it, removes duplicates, and prints
the result as a tuple.
4. Build a dictionary mapping 5 fruit names to their prices, then use .update() to apply a
10% discount to every price.
5. Explain, in your own words, why the 'nested dictionary' example in Step 20 actually
produced a tuple, and rewrite it so it produces a true nested dictionary.
Page 15 of 89
Python Programming Laboratory Manual
Lab Session 2: Control Flow, Functions, and Object-
Oriented Programming in Python
Source material: Lecture_material_1_Basics.pdf (Part 2)
Objectives
• Control program flow using if / elif / else and nested conditionals.
• Write for loops and while loops, including nested loops and loop-control statements
(break, continue, pass).
• Use enumerate() to access both the index and value of items in an iterable.
• Define and call functions, including recursive functions and lambda expressions.
• Apply functions across collections using map().
• Define classes with attributes, methods, and constructors (__init__), and create multiple
independent objects from the same class.
2.1 Conditional Statements
Conditional statements let a program take different actions depending on whether a condition is
True or False. Python uses if, elif (else-if), and else, and relies strictly on indentation (not braces)
to mark which statements belong to which branch.
Step 1: A Simple if Statement
If the condition a < b evaluates to True, the indented line beneath it runs; otherwise it is skipped
entirely.
Code:
a=5
b=15
c=20.5
if a<b:
print("a is less than b")
Output:
a is less than b
Step 2: if...else
The else branch provides a fallback that runs whenever the if condition is False.
Code:
a=5
b=15
c=20.5
Page 16 of 89
Python Programming Laboratory Manual
if c<b:
print("c is less than b")
else:
print("c is greater than b")
Output:
c is greater than b
Step 3: if...elif...else Chains
Multiple elif branches let a program test several conditions in sequence, stopping at the first one
that is True. The compound condition age >= 6 and age <= 10 combines two comparisons with
the logical and operator.
Code:
age = 6
if age >= 18:
print("You are eligible to vote.")
elif age <= 5:
print("You are still a toddler.")
elif age >=6 and age <= 10:
print("You should enrol in a school with Form-B.")
else:
print("Abhi chotay ho ap.")
Output:
You should enrol in a school with Form-B.
Step 4: Branching on String Equality
Conditions are not limited to numbers — strings can be compared with == just as easily, which
is useful for menu-style branching logic.
Code:
occassion = "Eid-ul_Fitr"
if occassion == "Eid-ul_Fitr":
print("Let's prepare some Sheer Khurma.")
elif occassion == "Eid-ul_Adha":
print("Let's prepare some Kebab.")
else:
print("It's a normal day. let's have Aalo Gobhi.")
Output:
Let's prepare some Sheer Khurma.
Step 5: Nested if Statements
An if block can contain another if/else block inside it, allowing a program to check a second
condition only once the first condition has already been satisfied.
Code:
age = 18
Page 17 of 89
Python Programming Laboratory Manual
license = False
if age >= 18:
if license:
print("You are eligible to drive.")
else:
print("You are not eligible to drive, you need a license.")
else:
print("You are not eligible to drive.")
Output:
You are not eligible to drive, you need a license.
2.2 User Input and a Mini Project: BMI Calculator
The input() function pauses a program and waits for the user to type a response (always returned
as a string, so numeric input typically needs int() or float() conversion). Because this lab report is
generated non-interactively, the input() calls from the original notebook have been replaced with
fixed sample values so the resulting output can be shown; the underlying logic is identical to
what input() would produce.
Step 6: Comparing Two User-Entered Ages
Note that person_a_age and person_b_age are left as strings (as input() would return them), so
'25' < '30' is actually a lexicographic (alphabetical) string comparison, not a numeric one. For
single- and double-digit ages this happens to give the intuitively correct answer, but it is a subtle
bug worth being aware of — numeric input should generally be converted with int() before
comparison.
Code:
person_a_name = "Ali"
person_a_age = "25"
person_b_name = "Sara"
person_b_age = "30"
if person_a_age > person_b_age:
print(person_a_name,"is older than", person_b_name)
elif person_a_age < person_b_age:
print(person_a_name," is younger than",person_b_name)
else:
print(person_a_name,"is the same age as",person_b_name)
Output:
Ali is younger than Sara
Step 7: Body Mass Index (BMI) Calculator
BMI is computed as weight (kg) divided by height (m) squared. Height is entered in centimetres
and converted to metres by dividing by 100 before squaring.
Code:
Height = 170.0 # cm, simulated input
weight = 65.0 # kg, simulated input
Page 18 of 89
Python Programming Laboratory Manual
BMI = weight / (Height/100) ** 2
print(f"BMI = {BMI:.2f}")
Output:
BMI = 22.49
Step 8: A Multiplication Table Using a for Loop
A simple for loop combined with range() generates a multiplication table for any number entered
by the user. The original notebook loops from 1 to 99; only the first 10 lines are shown here for
brevity.
Code:
a = "5" # simulated input
for i in range(1, 11): # first 10 lines shown (original loops 1-99)
print(a, "x", i, "=", int(a) * i)
print("... (continues up to 99)")
Output:
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
5 x 6 = 30
5 x 7 = 35
5 x 8 = 40
5 x 9 = 45
5 x 10 = 50
... (continues up to 99)
Step 9: Raising a ValueError for Invalid Input
raise lets a program deliberately signal an error when input falls outside an expected range.
Wrapping the check in try/except here allows the error message to be captured and displayed
instead of stopping the whole program.
Code:
a = 15 # simulated input outside 1-10
try:
if a > 10 or a < 1:
raise ValueError("The number is not between 1 and 10")
except ValueError as e:
print("ValueError:", e)
Output:
ValueError: The number is not between 1 and 10
Page 19 of 89
Python Programming Laboratory Manual
2.3 Loops: for and while
A for loop iterates over a known sequence (a list, a range, a string, etc.), while a while loop
repeats as long as a condition stays True — useful when the number of iterations is not known in
advance.
Step 10: for Loop Over a List
Iterating directly over a list yields each element in turn, in order.
Code:
foods=['bacon','tuna','ham','sausages','beef','eggs','chicken']
for food in foods:
print(food)
Output:
bacon
tuna
ham
sausages
beef
eggs
chicken
Step 11: for Loop with range()
range(1, 11) produces the integers 1 through 10 (the stop value, 11, is excluded).
Code:
for x in range(1,11):
print(x)
Output:
1
2
3
4
5
6
7
8
9
10
Step 12: for Loop with a Step Value
A third argument to range(start, stop, step) skips elements — here, every second number.
Code:
for x in range(1,11,2):
print(x)
Output:
Page 20 of 89
Python Programming Laboratory Manual
1
3
5
7
9
Step 13: while Loop
The loop variable i is manually incremented inside the loop body; the loop ends as soon as the
condition i <= 20 becomes False.
Code:
i=1
while i<=20:
print(i)
i+=2
Output:
1
3
5
7
9
11
13
15
17
19
Step 14: for Loop Combined with if/else (Even/Odd Check)
The modulus operator % is the standard way to test whether a number is even (remainder 0 when
divided by 2) or odd.
Code:
for x in range(1,11):
if x%2==0:
print(x,"is even")
else:
print(x,"is odd")
Output:
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
10 is even
Page 21 of 89
Python Programming Laboratory Manual
Step 15: Nested for Loops
Placing one for loop inside another produces every combination of the outer and inner sequences
— here, every colour paired with every clothing item.
Code:
colors=["red","green","blue","yellow","orange","purple"]
items=["shirt","hat","socks","pants","jacket","shorts"]
for color in colors:
for item in items:
print(color,item)
Output:
red shirt
red hat
red socks
red pants
red jacket
red shorts
green shirt
green hat
green socks
green pants
green jacket
green shorts
blue shirt
blue hat
blue socks
blue pants
blue jacket
blue shorts
yellow shirt
yellow hat
yellow socks
yellow pants
yellow jacket
yellow shorts
orange shirt
orange hat
orange socks
orange pants
orange jacket
orange shorts
purple shirt
purple hat
purple socks
purple pants
purple jacket
purple shorts
Step 16: Nested Loops with Arithmetic
The inner loop's variable is recomputed for every iteration of the outer loop, so the total number
of printed values equals (outer iterations) x (inner iterations).
Code:
count = 0
for i in range(1,100,30):
Page 22 of 89
Python Programming Laboratory Manual
for j in range (200,300,20):
print(i*j)
count += 1
print(f"... total {count} values printed")
Output:
200
220
240
260
280
6200
6820
7440
8060
8680
12200
13420
14640
15860
17080
18200
20020
21840
23660
25480
... total 20 values printed
Step 17: A for Loop Nested Inside a while Loop
Loop types can be freely mixed: each pass of the outer while loop runs the complete inner for
loop from start to finish.
Code:
i = 1
while i < 10:
for j in range(1,10):
print(i, j)
i+=1
Output:
1 1
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
2 1
2 2
2 3
2 4
2 5
2 6
2 7
Page 23 of 89
Python Programming Laboratory Manual
2 8
2 9
3 1
3 2
3 3
3 4
3 5
3 6
3 7
3 8
3 9
4 1
4 2
4 3
4 4
4 5
4 6
4 7
4 8
4 9
5 1
5 2
5 3
5 4
5 5
5 6
5 7
5 8
5 9
6 1
6 2
6 3
6 4
6 5
6 6
6 7
6 8
6 9
7 1
7 2
7 3
7 4
7 5
7 6
7 7
7 8
7 9
8 1
8 2
8 3
8 4
8 5
8 6
8 7
8 8
8 9
9 1
9 2
9 3
9 4
9 5
9 6
Page 24 of 89
Python Programming Laboratory Manual
9 7
9 8
9 9
Step 18: A while Loop Nested Inside a for Loop
Here the roles are reversed — the inner while loop resets and runs to completion for every
iteration of the outer for loop.
Code:
for i in range(1,10):
j = 1
while j < 10:
print(i, j)
j+=1
Output:
1 1
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
2 1
2 2
2 3
2 4
2 5
2 6
2 7
2 8
2 9
3 1
3 2
3 3
3 4
3 5
3 6
3 7
3 8
3 9
4 1
4 2
4 3
4 4
4 5
4 6
4 7
4 8
4 9
5 1
5 2
5 3
5 4
5 5
5 6
Page 25 of 89
Python Programming Laboratory Manual
5 7
5 8
5 9
6 1
6 2
6 3
6 4
6 5
6 6
6 7
6 8
6 9
7 1
7 2
7 3
7 4
7 5
7 6
7 7
7 8
7 9
8 1
8 2
8 3
8 4
8 5
8 6
8 7
8 8
8 9
9 1
9 2
9 3
9 4
9 5
9 6
9 7
9 8
9 9
Step 19: while...else with break
The else clause of a loop only executes if the loop finished naturally (i.e. was NOT stopped by a
break). Here the loop is interrupted by break when i == 3, so the else branch is skipped — note
that '0' is never printed.
Code:
i = 0
while i < 5:
print(i)
i += 1
if i == 3:
break
else:
print(0)
Output:
0
Page 26 of 89
Python Programming Laboratory Manual
1
2
Step 20: continue, break, and pass
pass does nothing (a placeholder for syntax that requires a statement); continue skips the rest of
the current iteration and jumps to the next one (so iteration 5 prints nothing); and break exits the
loop entirely (so the loop stops the moment i == 8, and iterations 9 and 10 never run).
Code:
for i in range(1,11):
if i==1:
pass
if i==5:
continue
if i==8:
break
print ("This is iteration no. ", i)
Output:
This is iteration no. 1
This is iteration no. 2
This is iteration no. 3
This is iteration no. 4
This is iteration no. 6
This is iteration no. 7
2.4 The enumerate() Function
enumerate() pairs each element of an iterable with its index, removing the need to maintain a
manual counter variable when both the position and the value are needed inside a loop.
Step 21: Manually Tracking an Index
Before learning enumerate(), it is common to manage the index by hand with a separate counter
variable that is incremented on every iteration.
Code:
marks=[5,56,71,89,45,67,90,34,78,98]
index=0
for mark in marks:
if (index==5):
print("We are learning pythin ")
print(mark)
index +=1
Output:
5
56
71
89
45
We are learning pythin
Page 27 of 89
Python Programming Laboratory Manual
67
90
34
78
98
Step 22: The Same Logic Using enumerate()
enumerate(marks) automatically yields (index, value) pairs starting from 0, making the manual
counter from Step 21 unnecessary.
Code:
marks=[5,56,71,89,45,67,90,34,78,98]
for index, mark in enumerate (marks):
if (index==5):
print("We are learning pythin ")
print(mark)
Output:
5
56
71
89
45
We are learning pythin
67
90
34
78
98
Step 23: enumerate() with a Custom Start Value
Passing start=1 shifts the index sequence to begin at 1 instead of the default 0 — useful
whenever 1-based (human-friendly) numbering is wanted.
Code:
marks=[5,56,71,89,45,67,90,34,78,98]
for index, mark in enumerate (marks, start=1):
if (index==5):
print("We are learning pythin ")
print(mark)
Output:
5
56
71
89
We are learning pythin
45
67
90
34
78
98
Page 28 of 89
Python Programming Laboratory Manual
2.5 Functions, Lambda Expressions, and map()
A function (defined with def) is a named, reusable block of code. A lambda expression is a
compact, anonymous (unnamed) function limited to a single expression. The built-in map()
applies a function to every element of an iterable without writing an explicit loop.
Step 24: Defining and Calling Simple Functions
def introduces a function; return sends a value back to the caller. square() and add() are the most
basic possible examples.
Code:
def square(number):
return number*number
print(square(5))
def add(a,b):
return a+b
print(add(2,3))
Output:
25
5
Step 25: Factorial — The Manual (Non-Recursive) Way
Before introducing recursion, the lecture material first shows factorials computed by simply
writing out the multiplication by hand for a few small numbers.
Code:
print(7*6*5*4*3*2*1)
print(6*5*4*3*2*1)
print(5*4*3*2*1)
print(4*3*2*1)
Output:
5040
720
120
24
Step 26: Factorial — A Recursive Function
A recursive function calls itself with a smaller input until it reaches a base case (n == 0 or n ==
1, where the recursion stops and returns 1 directly).
Code:
def factorial (n):
if n==0 or n==1:
return 1
else:
return n*factorial(n-1)
Page 29 of 89
Python Programming Laboratory Manual
Output:
6
Step 27: Passing a Function as an Argument to Another Function
Functions are first-class objects in Python — apply(cube, 4) passes the function cube itself (not
its result) into apply(), which then calls fx(value) internally.
Code:
def cube (number):
return number**3
def apply(fx, value):
return 6+ fx(value)
print (apply(cube, 4))
Output:
70
Step 28: A Named Function vs. Its Lambda Equivalent
double() defined with def behaves identically whether written as a full function or rewritten as a
one-line lambda in the next step — lambda is simply a shorter way to write very simple
functions.
Code:
def double(x):
return x*2
print(double(5))
print(double(7))
Output:
10
14
Step 29: Assigning a Lambda to a Variable Name
lambda x: x*2 creates an anonymous function that is then bound to the name double, so it can be
called exactly like a regular function.
Code:
double=lambda x: x*2
print(double(5))
Output:
10
Step 30: A Lambda for Cubing a Number
Lambdas can contain any single valid Python expression, including exponentiation.
Page 30 of 89
Python Programming Laboratory Manual
Code:
cube=lambda x: x**3
print(cube(4))
Output:
64
Step 31: A Lambda with Two Arguments
Lambda expressions can take multiple parameters, separated by commas, just like a normal
function definition.
Code:
x = lambda a,b:a+b
print(x(2,3))
Output:
5
Step 32: A Lambda with One Argument and Multiplication
A further example confirming that lambda bodies can use any arithmetic operator.
Code:
x=lambda a: a*5
print(x(2))
Output:
10
Step 33: A Lambda with Three Arguments
Lambdas scale to any fixed number of parameters needed for the expression.
Code:
x=lambda a,b,c: a+b+c
print(x(2,3,4))
Output:
9
Step 34: Applying a Function to Every List Element Manually
Before introducing map(), the lecture material shows the 'manual' approach: looping over the list
and calling the function on each element individually.
Code:
def cube (x):
return x**3
Page 31 of 89
Python Programming Laboratory Manual
l=[1,3,4,5,8,15]
for i in l:
print(cube(i))
Output:
1
27
64
125
512
3375
Step 35: The map() Built-in Function
map(function, iterable) applies function to every element of iterable and returns a map object (a
lazy iterator), which is then converted to a concrete list with list().
Code:
def square(x):
return x**2
numbers = [2, 4, 6, 8, 10]
squared_numbers = map(square, numbers)
squared_list = list(squared_numbers)
print(squared_list)
Output:
[4, 16, 36, 64, 100]
Step 36: Building a New List by Appending Inside a Loop
An alternative, very common pattern: start with an empty list and .append() the transformed
value on every iteration — functionally equivalent to using map().
Code:
def cube (x):
return x**3
print(cube(5))
l=[1,3,4,5,8,15]
list_empty=[]
for i in l:
list_empty.append(cube(i))
print(list_empty)
Output:
125
[1, 27, 64, 125, 512, 3375]
2.6 Classes, Objects, and Constructors
A class is a blueprint for creating objects that bundle together data (attributes) and behaviour
(methods). Python's special __init__ method is the constructor — it runs automatically every
Page 32 of 89
Python Programming Laboratory Manual
time a new object is created, and is the standard way to give each object its own initial attribute
values.
Step 37: A Class with Class-Level Attributes
Here name, Occupation, and networth are defined directly on the class itself. Creating an object a
= person() and then assigning [Link] = '...' overrides the class-level default with an instance-
level value for that specific object only.
Code:
class person:
name="Adeel"
Occupation="Lecturer"
networth="1M PKR"
a= person()
[Link]="Adeel munir"
[Link]="Data Scientist"
print ([Link], [Link])
Output:
Adeel munir Data Scientist
Step 38: Adding a Method to a Class
self always refers to the specific object the method is being called on, which is how info() can
access that object's own name, Occupation, and networth.
Code:
class person:
name="Adeel"
Occupation="Lecturer"
networth="1M PKR"
def info(self):
return f"{[Link]} is a {[Link]} and his networth is {[Link]}"
a= person()
Output:
Adeel is a Lecturer and his networth is 1M PKR
Step 39: Two Independent Objects of the Same Class
Each object created from a class keeps its own independent copy of instance attributes —
changing b's attributes has no effect on a, even though both came from the same class.
Code:
class person:
name="Adeel"
Occupation="Lecturer"
networth="1M PKR"
def info(self):
return f"{[Link]} is a {[Link]} and his networth is {[Link]}"
a= person()
Page 33 of 89
Python Programming Laboratory Manual
b= person()
[Link]="Ali"
[Link]="HR"
[Link]="2M PKR"
Output:
Ali is a HR and his networth is 2M PKR
Step 40: A Proper Constructor with __init__
Defining __init__(self, brand, model, year) lets every new Car object be initialised with its own
values in a single line, rather than being assigned attribute-by-attribute after creation as in Steps
37-39.
Code:
class Car:
def __init__(self, brand, model, year):
[Link] = brand
[Link] = model
[Link] = year
def car_details(self):
print(f"Car: {[Link]} {[Link]} {[Link]}")
my_car = Car("Toyota", "Corolla", 2021)
my_car.car_details()
Output:
Car: 2021 Toyota Corolla
Post-Lab Tasks — Session 2
1. Write a function is_prime(n) that returns True or False, and use it inside a for loop to
print all prime numbers between 1 and 50.
2. Rewrite the BMI calculator (Step 7) as a function bmi(weight, height_cm) that also
returns a category string: 'Underweight', 'Normal', 'Overweight', or 'Obese'.
3. Using a lambda function and map(), convert a list of temperatures in Celsius to
Fahrenheit.
4. Define a class Student with attributes name and marks (a list of integers) and a method
average() that returns the mean mark. Create two Student objects and print both of their
averages.
5. Explain why the comparison in Step 6 (comparing ages as strings) could give an incorrect
result for ages like '9' and '10', and rewrite the code so the comparison is numerically
correct.
Page 34 of 89
Python Programming Laboratory Manual
Lab Session 3: Introduction to NumPy — Array Creation
and Manipulation
Source material: Lecture_material_1.1_numpy.[Link] (Part 1)
Objectives
• Explain what a NumPy ndarray is and how it differs from a Python list.
• Create arrays using [Link](), [Link](), [Link](), [Link](), and [Link]().
• Reshape, transpose, flatten, repeat, and tile arrays of various dimensions.
• Join arrays (concatenate, stack, vstack, hstack) and split them apart again.
• Index, slice, insert, append, and update elements within 1D, 2D, and 3D arrays.
• Apply element-wise mathematical and trigonometric functions across whole arrays.
3.1 Creating Arrays
NumPy's core object is the ndarray (n-dimensional array). Unlike a Python list, every element of
a NumPy array must share the same data type, which is exactly what makes NumPy's vectorised
operations so much faster than equivalent pure-Python loops.
Step 1: Creating a 1D Array
[Link]() converts a Python list into a NumPy array.
Code:
a = [Link]([1, 2, 3, 4, 5])
print(a)
Output:
[1 2 3 4 5]
Step 2: Creating a 2D Array and Checking Dimensions
A list of lists becomes a 2D array (a matrix). The .ndim attribute reports how many dimensions
(axes) the array has.
Code:
b = [Link]([[1, 2, 3], [4, 5, 6]])
print(b)
print([Link])
Output:
[[1 2 3]
[4 5 6]]
2
Page 35 of 89
Python Programming Laboratory Manual
Step 3: A 3x3 Array
The same pattern extends to any number of rows and columns, as long as every row has the same
length.
Code:
b1 = [Link]([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(b1)
b = [Link]([[1, 2, 3], [4, 5, 6]])
print([Link])
Output:
[[1 2 3]
[4 5 6]
[7 8 9]]
2
Step 4: Arrays of Zeros and Ones
[Link](shape) and [Link](shape) are convenient ways to pre-allocate an array of a known size,
commonly used to initialise weight matrices or accumulator arrays.
Code:
# [Link]((2,3))
c = [Link]((2, 3))
print(c)
# [Link]((2,3))
d = [Link]((2, 3))
print(d)
# Two more [Link]() examples of different shapes
print([Link]((4,5)))
print([Link]((6,7)))
Output:
[[0. 0. 0.]
[0. 0. 0.]]
[[1. 1. 1.]
[1. 1. 1.]]
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
[[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1. 1. 1.]]
Page 36 of 89
Python Programming Laboratory Manual
Step 5: An 'Empty' Array
[Link](shape) allocates memory for an array WITHOUT initialising the values — the contents
are whatever bytes happened to already be in that memory, so they are essentially random and
will differ on every run. It is faster than zeros()/ones() only when you intend to immediately
overwrite every element yourself.
Code:
e = [Link]((2, 6))
print(e)
Output:
[[3.78240792e-315 0.00000000e+000 1.70650942e-310 0.00000000e+000
0.00000000e+000 1.38260575e-316]
[2.36732142e-157 0.00000000e+000 0.00000000e+000 1.36380711e-315
9.06092203e-312 1.76125651e-312]]
Step 6: [Link]() — A Range as an Array
[Link](stop), [Link](start, stop), and [Link](start, stop, step) mirror Python's built-in
range(), but return a NumPy array instead of a range object. A dtype can be forced explicitly, and
step values do not have to be integers.
Code:
# [Link](10)
f = [Link](10)
print(f)
# [Link](1,10)
f1 = [Link](1, 10)
print(f1)
# [Link](2,10,2) + dtype
g = [Link](2, 10, 2)
print(g)
print([Link])
# [Link](2,10,2,dtype=float) + dtype
g1 = [Link](2, 10, 2, dtype=float)
print(g1)
print([Link])
# [Link](1,10,0.5) -- a float step
h = [Link](1, 10, 0.5)
print(h)
# dtype of the float-stepped array
h = [Link](1, 10, 0.5)
print([Link])
Page 37 of 89
Python Programming Laboratory Manual
Output:
[0 1 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]
[2 4 6 8]
int64
[2. 4. 6. 8.]
float64
[1. 1.5 2. 2.5 3. 3.5 4. 4.5 5. 5.5 6. 6.5 7. 7.5 8. 8.5 9. 9.5]
float64
Step 7: [Link]() — Evenly Spaced Values
Unlike arange (which is driven by a step size), linspace(start, stop, num) divides the range into
exactly num evenly spaced points, inclusive of both endpoints.
Code:
i = [Link](0, 12, 5)
print(i)
Output:
[ 0. 3. 6. 9. 12.]
3.2 Reshaping, Transposing, Flattening, and Repeating
Arrays can be reorganised into different shapes without copying or changing the underlying data,
which is essential when preparing data for machine-learning models that expect a specific input
shape.
Step 8: reshape() — Changing an Array's Shape
.reshape((rows, cols)) rearranges the same elements into a new shape; the total element count
must stay the same (8 elements -> 2x4, or 6 elements -> 2x3).
Code:
arr = [Link]([1, 2, 3, 4, 5, 6, 7, 8])
print(f"array before: {arr}")
arr = [Link]([1, 2, 3, 4, 5, 6, 7, 8])
reshaped_arr = [Link]((2, 4))
print(f"array after reshaping:\n{reshaped_arr}")
arr = [Link]([1, 2, 3, 4, 5, 6])
print(f"array before: {arr}")
print("------------------------")
reshaped_arr = [Link]((2, 3))
print(f"array after reshaping:\n{reshaped_arr}")
Output:
Page 38 of 89
Python Programming Laboratory Manual
array before: [1 2 3 4 5 6 7 8]
array after reshaping:
[[1 2 3 4]
[5 6 7 8]]
array before: [1 2 3 4 5 6]
------------------------
array after reshaping:
[[1 2 3]
[4 5 6]]
Step 9: transpose() — Swapping Rows and Columns
.transpose() flips a 2D array across its diagonal, turning rows into columns and vice versa.
Code:
arr = [Link]([[1, 2, 3], [4, 5, 6]])
print(f"array before:\n{arr}")
print("------------------------")
transposed_arr = [Link]()
print(f"array after transposing:\n{transposed_arr}")
Output:
array before:
[[1 2 3]
[4 5 6]]
------------------------
array after transposing:
[[1 4]
[2 5]
[3 6]]
Step 10: flatten() — Collapsing to 1D
.flatten() converts any multi-dimensional array into a single flat 1D array, reading row by row.
Code:
arr = [Link]([[1, 2, 3], [4, 5, 6]])
print(f"array before:\n{arr}")
print("------------------------")
flattened_arr = [Link]()
print(f"array after flattening:\n{flattened_arr}")
Output:
array before:
[[1 2 3]
[4 5 6]]
------------------------
array after flattening:
[1 2 3 4 5 6]
Page 39 of 89
Python Programming Laboratory Manual
Step 11: repeat() and tile() — Repeating Elements
[Link](arr, n) repeats EACH element n times in place (e.g. [1,1,1,2,2,2,3,3,3]), while
[Link](arr, n) repeats the WHOLE array n times in sequence (e.g. [1,2,3,1,2,3,1,2,3]).
Code:
# [Link](arr, 3)
arr = [Link]([1, 2, 3])
print(f"array before: {arr}")
print("------------------------")
repeated_arr = [Link](arr, 3)
print(f"array after repeating:\n{repeated_arr}")
# [Link](arr, 3)
arr = [Link]([1, 2, 3])
print(f"array before: {arr}")
tiled_arr = [Link](arr, 3)
print(f"tiled array:\n{tiled_arr}")
Output:
array before: [1 2 3]
------------------------
array after repeating:
[1 1 1 2 2 2 3 3 3]
array before: [1 2 3]
tiled array:
[1 2 3 1 2 3 1 2 3]
Step 12: Building and Inspecting a 3D Array
Reshaping a 1D range into (2,3,4) creates a 3D array (think of it as 2 'pages', each a 3x4 matrix).
The .shape attribute always reports (depth, rows, columns) for a 3D array.
Code:
c=[Link](24).reshape(2,3,4)
print(c)
print([Link])
print([Link])
print([Link])
Output:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
3
24
(2, 3, 4)
Page 40 of 89
Python Programming Laboratory Manual
Step 13: Manually Constructing a 3D Array
A nested list of lists of lists is automatically recognised as a 3-dimensional array by [Link]().
Code:
a=[Link]([[[1,2,5,6],
[5,7,8,11]],
[[9,5,2,3],
[8,7,4,5]],
[[5,4,3,2],
[5,6,4,2]]])
print(a)
print([Link])
print([Link])
print([Link])
Output:
[[[ 1 2 5 6]
[ 5 7 8 11]]
[[ 9 5 2 3]
[ 8 7 4 5]]
[[ 5 4 3 2]
[ 5 6 4 2]]]
3
24
(3, 2, 4)
3.3 Joining, Splitting, Indexing, and Slicing
NumPy provides several ways to combine separate arrays into one, break one array into several,
and extract specific elements or sub-ranges using indices and slices.
Step 14: Concatenating Arrays
[Link]() joins arrays end-to-end along an existing axis (for 1D arrays, this simply
appends one array after another).
Code:
arr1 = [Link]([1, 2, 3])
arr2 = [Link]([4, 5, 6])
print(f"array1: {arr1}")
print(f"array2: {arr2}")
print("------------------------")
concatenated_arr = [Link]((arr1, arr2))
print(f"concatenated array:\n{concatenated_arr}")
Output:
array1: [1 2 3]
array2: [4 5 6]
------------------------
concatenated array:
[1 2 3 4 5 6]
Page 41 of 89
Python Programming Laboratory Manual
Step 15: Stacking Arrays (stack, vstack, hstack)
[Link]() joins arrays along a brand-new axis, producing a 2D result from two 1D inputs.
[Link]() stacks them as new rows (vertically), while [Link]() places them side-by-side as
one long row (horizontally).
Code:
# [Link]()
arr1 = [Link]([1, 2, 3])
arr2 = [Link]([4, 5, 6])
stacked_arr = [Link]((arr1, arr2))
print(f"stacked array:\n{stacked_arr}")
# [Link]()
arr1 = [Link]([1, 2, 3])
arr2 = [Link]([4, 5, 6])
vstacked_arr = [Link]((arr1, arr2))
print(f"vertically stacked array:\n{vstacked_arr}")
# [Link]()
arr1 = [Link]([1, 2, 3])
arr2 = [Link]([4, 5, 6])
hstacked_arr = [Link]((arr1, arr2))
print(f"horizontally stacked array:\n{hstacked_arr}")
Output:
stacked array:
[[1 2 3]
[4 5 6]]
vertically stacked array:
[[1 2 3]
[4 5 6]]
horizontally stacked array:
[1 2 3 4 5 6]
Step 16: Splitting an Array
[Link](arr, n) divides an array into n equal-sized sub-arrays (returned as a list of arrays); the
original length must divide evenly by n.
Code:
arr = [Link]([1, 2, 3, 4, 5, 6])
split_arr = [Link](arr, 3)
print(f"split array:\n{split_arr}")
Output:
split array:
[array([1, 2]), array([3, 4]), array([5, 6])]
Page 42 of 89
Python Programming Laboratory Manual
Step 17: Deleting an Element by Index
[Link](arr, index) returns a new array with the element at that index removed.
Code:
arr = [Link]([1, 2, 3, 4, 5, 6])
deleted_arr = [Link](arr, 3)
print(f"array after deleting element at index 3: {deleted_arr}")
Output:
array after deleting element at index 3: [1 2 3 5 6]
Step 18: Indexing 1D and 2D Arrays
Single elements are accessed with arr[index] (1D) or arr[row, col] (2D). Negative indices count
backwards from the end of the array.
Code:
arr = [Link]([1, 2, 3, 4, 5, 6])
print(f"element at index 0: {arr[0]}")
print(f"element at index 1: {arr[1]}")
print(f"element at index -1: {arr[-1]}")
print(f"element at index -2: {arr[-2]}")
arr = [Link]([[1, 2, 3], [4, 5, 6]])
print(f"element at row 1, column 2: {arr[1, 2]}")
print(f"element at row 0, column 2: {arr[0, 2]}")
Output:
element at index 0: 1
element at index 1: 2
element at index -1: 6
element at index -2: 5
element at row 1, column 2: 6
element at row 0, column 2: 3
Step 19: Slicing 1D and 2D Arrays
A slice arr[start:stop:step] extracts a sub-range; for 2D arrays, row and column slices are
combined as arr[row_slice, col_slice].
Code:
arr = [Link]([1, 2, 3, 4, 5, 6])
print(f"elements from index 0 to 2: {arr[0:3]}")
print(f"elements from index 2 to 4: {arr[2:5]}")
print(f"elements from index 0 to 4 with step 2: {arr[0:5:2]}")
arr = [Link]([[1, 2, 3], [4, 5, 6]])
print(f"elements from row 0, column 0 to row 1, column 2:\n{arr[0:2, 0:3]}")
Output:
elements from index 0 to 2: [1 2 3]
elements from index 2 to 4: [3 4 5]
Page 43 of 89
Python Programming Laboratory Manual
elements from index 0 to 4 with step 2: [1 3 5]
elements from row 0, column 0 to row 1, column 2:
[[1 2 3]
[4 5 6]]
Step 20: Inserting, Appending, and Updating Elements
[Link](arr, index, value) inserts a new value at a position (shifting later elements along);
[Link](arr, value) adds a value at the end; and direct index assignment (arr[i] = value)
overwrites an existing element in place.
Code:
# [Link]()
arr = [Link]([1, 2, 3, 4, 5, 6])
inserted_arr = [Link](arr, 3, 10)
print(f"array after inserting 10 at index 3: {inserted_arr}")
# [Link]()
arr = [Link]([1, 2, 3, 4, 5, 6])
appended_arr = [Link](arr, 10)
print(f"array after appending 10 at the end: {appended_arr}")
# Direct index assignment
arr = [Link]([1, 2, 3, 4, 5, 6])
arr[3] = 10
print(f"array after updating element at index 3: {arr}")
Output:
array after inserting 10 at index 3: [ 1 2 3 10 4 5 6]
array after appending 10 at the end: [ 1 2 3 4 5 6 10]
array after updating element at index 3: [ 1 2 3 10 5 6]
3.4 Mathematical Operations
Because NumPy arrays support vectorised arithmetic, mathematical operators (+, -, *, /, **) and
functions (sqrt, exp, log, sin, cos, tan) apply element-by-element across an entire array in one
step — no explicit loop required.
Step 21: Element-wise Power
Squaring an array raises every individual element to the power of 2 simultaneously.
Code:
arr2 = [Link]([4, 5, 6])
arr2_squared = arr2 ** 2
print(f"array2 squared: {arr2_squared}")
Output:
Page 44 of 89
Python Programming Laboratory Manual
array2 squared: [16 25 36]
Step 22: Element-wise Addition, Subtraction, Multiplication, Division, Power
When two arrays of the same shape are combined with an arithmetic operator, the operation is
applied position-by-position (element-wise), not as matrix algebra.
Code:
arr1 = [Link]([1, 2, 3])
arr2 = [Link]([4, 5, 6])
sum_arr = arr1 + arr2
print(f"sum of arrays: {sum_arr}")
diff_arr = arr1 - arr2
print(f"difference of arrays: {diff_arr}")
prod_arr = arr1 * arr2
print(f"product of arrays: {prod_arr}")
quot_arr = arr1 / arr2
print(f"quotient of arrays: {quot_arr}")
pow_arr = arr1 ** 2
print(f"Raise the power: {pow_arr}")
Output:
sum of arrays: [5 7 9]
difference of arrays: [-3 -3 -3]
product of arrays: [ 4 10 18]
quotient of arrays: [0.25 0.4 0.5 ]
Raise the power: [1 4 9]
Step 23: Square Root
[Link]() computes the square root of every element.
Code:
arr = [Link]([1, 4, 9, 16, 25])
sqrt_arr = [Link](arr)
print(f"square root of the array: {sqrt_arr}")
Output:
square root of the array: [1. 2. 3. 4. 5.]
Step 24: Exponential (e^x)
[Link]() raises Euler's number e (approx. 2.71828) to the power of each element.
Code:
arr1 = [Link]([1, 2, 3])
exp_arr = [Link](arr1)
print(f"exponential of the array2: {exp_arr}")
Output:
exponential of the array2: [ 2.71828183 7.3890561 20.08553692]
Page 45 of 89
Python Programming Laboratory Manual
Step 25: Natural Log and Base-10 Log
[Link]() computes the natural logarithm (base e); np.log10() computes the base-10 logarithm of
each element.
Code:
arr1 = [Link]([250, 500, 1000000])
log_arr = [Link](arr1)
print(f"log of the array: {log_arr}")
log10_arr = np.log10(arr1)
print(f"log10 of the array: {log10_arr}")
Output:
log of the array: [ 5.52146092 6.2146081 13.81551056]
log10 of the array: [2.39794001 2.69897 6. ]
Step 26: Mathematical Constants
NumPy exposes the constants pi and e directly as [Link] and np.e for use in formulas.
Code:
print(f"pi: {[Link]}")
print(f"e: {np.e}")
Output:
pi: 3.141592653589793
e: 2.718281828459045
Step 27: Trigonometric Functions
[Link](), [Link](), and [Link]() operate element-wise in radians; values near zero in the output
(e.g. sin(pi)) reflect ordinary floating-point rounding error, not a calculation mistake.
Code:
arr = [Link]([0, [Link]/2, [Link]])
sin_arr = [Link](arr)
print(f"sin of the array: {sin_arr}")
cos_arr = [Link](arr)
print(f"cos of the array: {cos_arr}")
tan_arr = [Link](arr)
print(f"tan of the array: {tan_arr}")
Output:
sin of the array: [0.0000000e+00 1.0000000e+00 1.2246468e-16]
cos of the array: [ 1.000000e+00 6.123234e-17 -1.000000e+00]
tan of the array: [ 0.00000000e+00 1.63312394e+16 -1.22464680e-16]
Post-Lab Tasks — Session 3
1. Create a 4x4 array of random integers between 1 and 100 using [Link](), then
find its transpose and flattened form.
Page 46 of 89
Python Programming Laboratory Manual
2. Use [Link]() to generate 100 points between 0 and 2*pi, compute the sine of each,
and report the maximum and minimum values obtained.
3. Given two 1D arrays of length 6, demonstrate the difference between [Link]() and
[Link]() by printing both results and their .shape.
4. Write code that takes a 3x3 matrix and replaces every element greater than 5 with the
value 0, without using a Python for loop (hint: use boolean indexing).
5. [Link]() was shown to contain unpredictable values. Write a short paragraph (3-4
sentences) explaining when it would still be safe and useful to use [Link]() instead of
[Link]().
Page 47 of 89
Python Programming Laboratory Manual
Lab Session 4: NumPy for Statistics, Probability, and Linear
Algebra
Source material: Lecture_material_1.1_numpy.[Link] (Part 2)
Objectives
• Compute descriptive statistics (sum, mean, median, mode, standard deviation, variance,
percentiles) with NumPy and SciPy.
• Generate samples from normal, uniform, exponential, Poisson, and binomial
distributions, and visualise them as histograms.
• Apply the Shapiro-Wilk test to assess whether a sample is normally distributed.
• Use logical functions (where, any, all, isin) and set operations (union, intersection,
difference) on arrays.
• Perform core linear algebra operations: dot products, matrix multiplication, inversion,
determinants, and eigendecomposition.
• Sort arrays, persist them to disk, and clip values to a fixed range.
4.1 Statistical Operations
NumPy provides built-in aggregation functions for nearly every common descriptive statistic, all
of which work directly on arrays without needing any extra libraries for the basics.
Step 1: Sum (1D and 2D)
[Link]() adds every element in the array; for a 2D array it sums across all elements by default,
regardless of rows/columns.
Code:
# Sum of a 1D array
arr = [Link]([1, 2, 3, 4, 5])
sum_arr = [Link](arr)
print(f"sum of the array: {sum_arr}")
# Sum of a 2D array
arr = [Link]([[1, 2, 3], [4, 5, 6]])
sum_arr = [Link](arr)
print(f"sum of the array: {sum_arr}")
Output:
sum of the array: 15
sum of the array: 21
Page 48 of 89
Python Programming Laboratory Manual
Step 2: Mean and Median
[Link]() is the arithmetic average; [Link]() is the middle value when the data is sorted
(more resistant to outliers than the mean).
Code:
# [Link]()
arr = [Link](0, 500, 3)
mean_arr = [Link](arr)
print(f"mean of the array: {mean_arr}")
# [Link]()
arr = [Link](0, 500, 3)
median_arr = [Link](arr)
print(f"median of the array: {median_arr}")
Output:
mean of the array: 249.0
median of the array: 249.0
Step 3: Mode (via SciPy)
NumPy itself has no built-in mode function, so [Link]() is used instead to find the most
frequently occurring value.
Code:
arr = [Link]([1, 2, 3, 4, 5, 6, 2, 3, 4, 3, 4])
mode_arr = [Link](arr)
print(f"mode of the array: {mode_arr[0]}")
Output:
mode of the array: 3
Step 4: Standard Deviation and Variance
Variance measures the average squared distance from the mean; standard deviation is simply the
square root of variance, expressed in the same units as the original data.
Code:
# [Link]()
arr = [Link](0, 500, 3)
std_arr = [Link](arr)
print(f"standard deviation of the array: {std_arr}")
# [Link]()
arr = [Link](0, 500, 3)
var_arr = [Link](arr)
print(f"variance of the array: {var_arr}")
Page 49 of 89
Python Programming Laboratory Manual
Output:
standard deviation of the array: 144.6236495183274
variance of the array: 20916.0
Step 5: Standard Error of the Mean
Standard error = standard deviation / sqrt(n). It estimates how much a sample mean would be
expected to vary if the sampling were repeated.
Code:
arr = [Link](0, 500, 3)
std_err_arr = [Link](arr) / [Link](len(arr))
print(f"standard error of the array: {std_err_arr}")
Output:
standard error of the array: 11.191313997115618
Step 6: Minimum and Maximum
[Link]() and [Link]() return the smallest and largest values in the array.
Code:
arr = [Link]([1, 2, 3, 4, 5])
print([Link](arr))
print([Link](arr))
Output:
1
5
Step 7: Percentiles
The p-th percentile is the value below which p% of the data falls. The 50th percentile is always
identical to the median.
Code:
arr = [Link](0, 500, 3)
print(f"50th percentile of the array: {[Link](arr, 50)}")
print(f"75th percentile of the array: {[Link](arr, 75)}")
print(f"90th percentile of the array: {[Link](arr, 90)}")
Output:
50th percentile of the array: 249.0
75th percentile of the array: 373.5
90th percentile of the array: 448.20000000000005
Step 8: Cumulative Sum and Cumulative Product
[Link]() returns a running total at each position; [Link]() returns a running product.
Code:
Page 50 of 89
Python Programming Laboratory Manual
arr = [Link]([1, 2, 3, 4, 5])
print(f"cumulative sum of the array: {[Link](arr)}")
print(f"cumulative product of the array: {[Link](arr)}")
Output:
cumulative sum of the array: [ 1 3 6 10 15]
cumulative product of the array: [ 1 2 6 24 120]
4.2 Probability Distributions and Normality Testing
NumPy's [Link] module can generate samples from many standard probability distributions.
The Shapiro-Wilk test (from SciPy) is a formal statistical test for normality: a p-value greater
than 0.05 is conventionally taken as evidence that the sample is consistent with a normal
distribution.
Step 9: Normal (Gaussian) Distribution
[Link](n) draws n samples from the standard normal distribution (mean 0, standard
deviation 1). The Shapiro-Wilk test's high p-value (> 0.05) correctly confirms this sample looks
normally distributed, matching the classic bell-curve shape in the histogram below.
Code:
data = [Link](1000)
print(f"mean: {[Link](data):.4f}")
print(f"standard deviation: {[Link](data):.4f}")
print(f"variance: {[Link](data):.4f}")
print(f"standard error: {[Link](data) / [Link](len(data)):.4f}")
stat, p = shapiro(data)
print(f"statistic: {stat:.4f}, p-value: {p:.4f}")
if p > 0.05:
print("Data is normally distributed")
else:
print("Data is not normally distributed")
[Link](figsize=(5,3.5))
[Link](data, bins=30, density=True, color="#4C72B0", edgecolor="white")
[Link]("Histogram of Normally Distributed Data")
[Link]("Value"); [Link]("Density")
plt.tight_layout()
[Link]("images/fig_normal_dist.png", dpi=150)
[Link]()
Output:
mean: 0.0193
standard deviation: 0.9787
variance: 0.9579
standard error: 0.0310
statistic: 0.9986, p-value: 0.6273
Data is normally distributed
Page 51 of 89
Python Programming Laboratory Manual
Figure 4.1 - Histogram of 1,000 samples drawn from a standard normal distribution.
Step 10: Uniform Distribution
[Link](n) draws n samples uniformly between 0 and 1 — every value in that range is
equally likely, producing a roughly flat histogram rather than a bell curve, which is why the
Shapiro-Wilk test correctly rejects normality here.
Code:
data = [Link](1000)
stat, p = shapiro(data)
if p > 0.05:
print("Data is normally distributed")
else:
print("Data is not normally distributed")
[Link](figsize=(5,3.5))
[Link](data, bins=30, color="#DD8452", edgecolor="white")
[Link]("Histogram of Uniformly Distributed Data")
[Link]("Value"); [Link]("Frequency")
plt.tight_layout()
[Link]("images/fig_uniform_dist.png", dpi=150)
[Link]()
Output:
Data is not normally distributed
Page 52 of 89
Python Programming Laboratory Manual
Figure 4.2 - Histogram of 1,000 samples drawn from a uniform distribution on [0, 1).
Step 11: Exponential Distribution
[Link]() models the time between independent events occurring at a constant
average rate, producing a sharply right-skewed histogram.
Code:
data = [Link](1, 1000)
[Link](figsize=(5,3.5))
[Link](data, bins=30, color="#55A868", edgecolor="white")
[Link]("Histogram of Exponentially Distributed Data")
[Link]("Value"); [Link]("Frequency")
plt.tight_layout()
[Link]("images/fig_exponential_dist.png", dpi=150)
[Link]()
print("Exponential-distribution histogram generated (see Figure).")
Output:
Exponential-distribution histogram generated (see Figure).
Page 53 of 89
Python Programming Laboratory Manual
Figure 4.3 - Histogram of 1,000 samples drawn from an exponential distribution.
Step 12: Poisson Distribution
[Link]() models the count of events occurring in a fixed interval, given an average
rate (here, lambda = 1) — a discrete distribution concentrated at small whole numbers.
Code:
data = [Link](1, 1000)
[Link](figsize=(5,3.5))
[Link](data, bins=30, color="#C44E52", edgecolor="white")
[Link]("Histogram of Poisson-Distributed Data")
[Link]("Value"); [Link]("Frequency")
plt.tight_layout()
[Link]("images/fig_poisson_dist.png", dpi=150)
[Link]()
print("Poisson-distribution histogram generated (see Figure).")
Output:
Poisson-distribution histogram generated (see Figure).
Page 54 of 89
Python Programming Laboratory Manual
Figure 4.4 - Histogram of 1,000 samples drawn from a Poisson distribution (lambda=1).
Step 13: Binomial Distribution
[Link](n, p) models the number of successes in n independent trials, each with
success probability p — here, 10 coin-flip-like trials with a 50% success chance, repeated 1,000
times.
Code:
data = [Link](10, 0.5, 1000)
[Link](figsize=(5,3.5))
[Link](data, bins=30, color="#8172B2", edgecolor="white")
[Link]("Histogram of Binomially Distributed Data")
[Link]("Value"); [Link]("Frequency")
plt.tight_layout()
[Link]("images/fig_binomial_dist.png", dpi=150)
[Link]()
print("Binomial-distribution histogram generated (see Figure).")
Output:
Binomial-distribution histogram generated (see Figure).
Page 55 of 89
Python Programming Laboratory Manual
Figure 4.5 - Histogram of 1,000 samples drawn from a Binomial(n=10, p=0.5) distribution.
Step 14: A Bimodal Distribution
Concatenating two separate normal distributions with different means (0 and 5) produces a single
dataset with two distinct peaks — a reminder that real-world data is not always unimodal.
Code:
data1 = [Link](0, 1, 1000)
data2 = [Link](5, 1, 1000)
data = [Link]((data1, data2))
[Link](figsize=(5,3.5))
[Link](data, bins=30, color="#937860", edgecolor="white")
[Link]("Histogram of Bimodal Data")
[Link]("Value"); [Link]("Frequency")
plt.tight_layout()
[Link]("images/fig_bimodal_dist.png", dpi=150)
[Link]()
print("Bimodal-distribution histogram generated (see Figure).")
Output:
Bimodal-distribution histogram generated (see Figure).
Page 56 of 89
Python Programming Laboratory Manual
Figure 4.6 - Histogram of a bimodal dataset formed by combining two normal distributions.
4.3 Logical and Set Operations
NumPy supports vectorised Boolean logic and classic mathematical set operations directly on
arrays, both of which are heavily used for filtering and cleaning data.
Step 15: [Link]() — Conditional Element Selection
[Link](condition) returns the indices where the condition is True; those indices can then be
used to extract the matching elements themselves.
Code:
arr = [Link]([1, 2, 3, 4, 5])
where_arr = [Link](arr > 3)
print(f"elements greater than 3: {arr[where_arr]}")
Output:
elements greater than 3: [4 5]
Step 16: [Link]() and [Link]()
[Link]() returns True if AT LEAST ONE element satisfies the condition; [Link]() returns True
only if EVERY element satisfies it.
Code:
# [Link](arr > 3)
arr = [Link]([1, 2, 3, 4, 5])
Page 57 of 89
Python Programming Laboratory Manual
any_arr = [Link](arr > 3)
print(f"any element greater than 3: {any_arr}")
# [Link](arr > 3)
arr = [Link]([1, 2, 3, 4, 5])
all_arr = [Link](arr > 3)
print(f"all elements greater than 3: {all_arr}")
Output:
any element greater than 3: True
all elements greater than 3: False
Step 17: [Link]() — Membership Testing
[Link](arr1, arr2) checks, element-by-element, whether each value of arr1 also appears
anywhere in arr2, returning a Boolean array that can be used to filter arr1.
Code:
arr1 = [Link]([1, 2, 3, 4, 5])
arr2 = [Link]([4, 5, 6, 7, 8])
isin_arr = [Link](arr1, arr2)
print(f"elements in array1 that are also in array2: {arr1[isin_arr]}")
Output:
elements in array1 that are also in array2: [4 5]
Step 18: [Link]() — Removing Duplicates
[Link]() returns the sorted, distinct values in an array, with all duplicates removed.
Code:
arr = [Link]([1, 2, 3, 4, 5, 1, 2, 3, 4, 5])
unique_arr = [Link](arr)
print(f"unique elements in the array: {unique_arr}")
Output:
unique elements in the array: [1 2 3 4 5]
Step 19: Set Operations — Union, Intersection, Difference, Symmetric Difference
union1d combines all unique values from both arrays; intersect1d keeps only values present in
both; setdiff1d keeps values in the first array but NOT the second; and setxor1d keeps values that
appear in exactly one of the two arrays (but not both).
Code:
arr1 = [Link]([1, 2, 3, 4, 5])
arr2 = [Link]([4, 5, 6, 7, 8])
print(f"union of array1 and array2: {np.union1d(arr1, arr2)}")
print(f"intersection of array1 and array2: {np.intersect1d(arr1, arr2)}")
print(f"difference of array1 and array2: {np.setdiff1d(arr1, arr2)}")
Page 58 of 89
Python Programming Laboratory Manual
print(f"symmetric difference of array1 and array2: {np.setxor1d(arr1, arr2)}")
Output:
union of array1 and array2: [1 2 3 4 5 6 7 8]
intersection of array1 and array2: [4 5]
difference of array1 and array2: [1 2 3]
symmetric difference of array1 and array2: [1 2 3 6 7 8]
4.4 Linear Algebra and Matrices
The [Link] module implements the core operations of linear algebra — dot products,
matrix multiplication, inversion, determinants, and eigendecomposition — all of which underpin
machine learning algorithms such as linear regression and PCA.
Step 20: The Dot Product
For two 1D arrays, [Link]() computes the sum of the element-wise products: [1,2,3].[4,5,6] =
(1x4)+(2x5)+(3x6) = 32.
Code:
arr1 = [Link]([1, 2, 3])
arr2 = [Link]([4, 5, 6])
dot_arr = [Link](arr1, arr2)
print(f"dot product of array1 and array2: {dot_arr}")
Output:
dot product of array1 and array2: 32
Step 21: Slicing and Aggregating a Larger Matrix
Matrix b is built from a range reshaped into 4 rows by 5 columns. Row/column
slicing, .max(), .sum(), and .min(axis=0) (the minimum of each COLUMN) all behave
consistently with the 1D examples shown earlier.
Code:
b=[Link](1,100,5).reshape(4,5)
print(b)
print(b[0:2,1:4])
print([Link]())
print([Link]())
print([Link](axis=0))
Output:
[[ 1 6 11 16 21]
[26 31 36 41 46]
[51 56 61 66 71]
[76 81 86 91 96]]
[[ 6 11 16]
[31 36 41]]
96
970
[ 1 6 11 16 21]
Page 59 of 89
Python Programming Laboratory Manual
Step 22: Mixing a Tuple-of-Tuples with a NumPy Array
A plain Python tuple of tuples is automatically treated as array-like when combined with a real
NumPy array using * or +, so c*d and c+d both work element-wise exactly as if c had been an
ndarray all along.
Code:
c=((5,2),(5,4),(7,8))
print(c)
d=[Link](1,12,2).reshape(3,2)
print(d)
print(c*d)
print(c+d)
Output:
((5, 2), (5, 4), (7, 8))
[[ 1 3]
[ 5 7]
[ 9 11]]
[[ 5 6]
[25 28]
[63 88]]
[[ 6 5]
[10 11]
[16 19]]
Step 23: A 3D Array via arange().reshape()
The same reshape() pattern used for 2D matrices extends naturally to 3 (or more) dimensions.
Code:
z=[Link](24).reshape(2,3,4)
print(z)
Output:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
Step 24: Building 2D Matrices Two Ways
A matrix can be written out explicitly as nested lists, or generated more compactly by reshaping
a 1D [Link]() sequence — both approaches produce an identical ndarray.
Code:
matrix = [Link]([[1, 2, 3], [4, 5, 6]])
print(f"matrix:\n{matrix}")
matrix2 = [Link](1, 7).reshape(2, 3)
print(f"matrix:\n{matrix2}")
Page 60 of 89
Python Programming Laboratory Manual
Output:
matrix:
[[1 2 3]
[4 5 6]]
matrix:
[[1 2 3]
[4 5 6]]
Step 25: Matrix Multiplication
[Link]() performs true matrix multiplication (the dot product of rows and columns), which is
mathematically different from element-wise multiplication; it requires the number of columns in
the first matrix to equal the number of rows in the second.
Code:
matrix1 = [Link]([[1, 2, 3], [4, 5, 6]])
matrix2 = [Link]([[7, 8], [9, 10], [11, 12]])
matrix_mul = [Link](matrix1, matrix2)
print(f"matrix multiplication of matrix1 and matrix2:\n{matrix_mul}")
Output:
matrix multiplication of matrix1 and matrix2:
[[ 58 64]
[139 154]]
Step 26: Matrix Inverse
[Link]() computes A^-1, the matrix that satisfies A^-1 . A = the identity matrix. Inversion
is only defined for square matrices with a non-zero determinant.
Code:
matrix = [Link]([[1, 2], [3, 4]])
inv_matrix = [Link](matrix)
print(f"inverse of the matrix:\n{inv_matrix}")
Output:
inverse of the matrix:
[[-2. 1. ]
[ 1.5 -0.5]]
Step 27: Determinant
[Link]() computes the determinant; for a 2x2 matrix [[a,b],[c,d]] this is simply a*d - b*c.
Code:
matrix = [Link]([[1, 2], [3, 4]])
det_matrix = [Link](matrix)
print(f"determinant of the matrix: {det_matrix}")
Output:
determinant of the matrix: -2.0000000000000004
Page 61 of 89
Python Programming Laboratory Manual
Step 28: Eigenvalues and Eigenvectors
[Link]() returns the eigenvalues and corresponding eigenvectors of a square matrix — the
directions that the matrix only stretches or compresses, without rotating, used extensively in
PCA and other dimensionality-reduction techniques.
Code:
matrix = [Link]([[1, 2], [3, 4]])
eigenvalues, eigenvectors = [Link](matrix)
print(f"eigenvalues of the matrix: {eigenvalues}")
print(f"eigenvectors of the matrix:\n{eigenvectors}")
Output:
eigenvalues of the matrix: [-0.37228132 5.37228132]
eigenvectors of the matrix:
[[-0.82456484 -0.41597356]
[ 0.56576746 -0.90937671]]
4.5 Utility Functions
A handful of everyday utility functions round out the NumPy toolkit: sorting, persisting arrays to
disk, and clamping values to a fixed range.
Step 29: Sorting a 1D Array
[Link]() returns a new array with elements arranged in ascending order; the original array is left
unchanged.
Code:
arr = [Link]([3, 2, 1, 4, 5])
sorted_arr = [Link](arr)
print(f"sorted array: {sorted_arr}")
Output:
sorted array: [1 2 3 4 5]
Step 30: Sorting a 2D Array (Default, by Row, by Column)
By default, [Link]() on a 2D array sorts each ROW independently. Passing axis=0 sorts each
COLUMN independently instead, while axis=1 makes the row-wise behaviour explicit.
Code:
# Default sort (sorts each row)
matrix = [Link]([[3, 2, 1], [6, 5, 4]])
sorted_matrix = [Link](matrix)
print(f"sorted matrix:\n{sorted_matrix}")
# axis=0 (sorts each column)
matrix = [Link]([[6, 5, 4], [3, 2, 1], [9, 8, 7]])
sorted_matrix = [Link](matrix, axis=0)
Page 62 of 89
Python Programming Laboratory Manual
print(f"sorted matrix by column:\n{sorted_matrix}")
# axis=1 (sorts each row, explicit)
matrix = [Link]([[6, 5, 4], [3, 2, 1], [9, 8, 7]])
sorted_matrix = [Link](matrix, axis=1)
print(f"sorted matrix by row:\n{sorted_matrix}")
Output:
sorted matrix:
[[1 2 3]
[4 5 6]]
sorted matrix by column:
[[3 2 1]
[6 5 4]
[9 8 7]]
sorted matrix by row:
[[4 5 6]
[1 2 3]
[7 8 9]]
Step 31: Saving an Array to Disk
[Link]() writes an array to a binary .npy file, which can later be reloaded exactly (including
shape and dtype) with [Link]().
Code:
arr = [Link]([1, 2, 3, 4, 5])
[Link]("[Link]", arr)
print("array saved to [Link]")
Output:
array saved to [Link]
Step 32: Clipping Values to a Range
[Link](arr, min, max) caps every element so it falls between min and max — values below min
are raised to min, and values above max are lowered to max.
Code:
arr = [Link]([1, 2, 3, 4, 5])
clip_arr = [Link](arr, 2, 4)
print(f"clipped array: {clip_arr}")
Output:
clipped array: [2 2 3 4 4]
Page 63 of 89
Python Programming Laboratory Manual
Post-Lab Tasks — Session 4
1. Generate 500 samples from a normal distribution with mean 50 and standard deviation
10, then report the mean, median, and standard deviation you actually observe versus the
theoretical values.
2. Run the Shapiro-Wilk test on a sample drawn from an exponential distribution and
explain, in 2-3 sentences, why the test correctly rejects normality.
3. Given matrix A = [[2,0],[0,3]], compute its determinant and inverse by hand, then verify
your answer using [Link]() and [Link]().
4. Create two integer arrays of 10 random values each (range 1-20) and compute their
union, intersection, and symmetric difference.
5. Using [Link](), write code that takes an array of exam scores (some possibly above 100
or below 0 due to data-entry errors) and clamps every score into the valid 0-100 range.
Page 64 of 89
Python Programming Laboratory Manual
Lab Session 5: Data Analysis with Pandas — Cleaning,
Exploring, and Visualising the Titanic Dataset
Source material: Lecture_material_1.2_pandas.pdf
Objectives
• Read tabular data into pandas from dictionaries, delimited files, and built-in sample
datasets.
• Inspect a DataFrame's structure, data types, and summary statistics.
• Identify, quantify, and visualise missing data, then handle it by dropping or imputing
columns.
• Select data using column names, .loc (label-based), and .iloc (position-based) indexing.
• Explore relationships between variables using correlation matrices and pairplots.
• Engineer new features by binning a continuous variable into categories.
• Filter rows using single and combined Boolean conditions.
• Produce basic visualisations and grouped summary statistics directly from a DataFrame.
5.1 Introduction to Pandas and Reading Data
Pandas takes its name from 'Panel Data', an econometrics term for multi-dimensional structured
datasets; it was created by Wes McKinney in 2008 while he was working at a quantitative-
finance firm, to make tabular data manipulation in Python as convenient as in spreadsheets or R.
Its two core objects are the Series (a single labelled column) and the DataFrame (a full labelled
table). Pandas can read data from many sources: the clipboard, Excel workbooks, CSV files,
delimited text files, JSON, and SQL databases, among others.
Step 1: Exploring the Pandas Library and Checking the Version
dir(pd) lists every public name available in the pandas namespace; pd.__version__ reports
exactly which release is installed (results in this report were produced with the version shown).
Code:
# dir(pd) summary
print(f"pandas exposes {len(dir(pd))} top-level names, including DataFrame, Series, read_csv, read_excel, merge, concat, etc.")
# pd.__version__
import pandas as pd
Output:
pandas exposes 141 top-level names, including DataFrame, Series, read_csv, read_excel, merge, concat, etc.
3.0.2
Page 65 of 89
Python Programming Laboratory Manual
Step 2: Creating a DataFrame from a Python Dictionary
When a dictionary is passed to [Link](), each key becomes a column name and each
value list becomes that column's data — the most direct way to build a small table from scratch.
Code:
data = {'Name': ['Muhammad', 'Ali', 'Ahmad', 'Minahil'],
'Age': [28, 23, 25, 27],
'Weight': [70, 75, 80, 65]}
df = [Link](data)
Output:
Name Age Weight
0 Muhammad 28 70
1 Ali 23 75
2 Ahmad 25 80
3 Minahil 27 65
Step 3: Reading a Tab-Separated File with read_table()
The original notebook reads the classic 'chipotle' orders dataset directly from a GitHub URL
using pd.read_table(). The same file was downloaded locally for this report, and only the first
five rows are shown for brevity.
Code:
df_tabulated = pd.read_table('data/[Link]')
print(df_tabulated.head().to_string())
print(f"\nShape: {df_tabulated.shape}")
Output:
order_id quantity item_name choice_descrip
tion item_price
0 1 1 Chips and Fresh Tomato Salsa
NaN $2.39
1 1 1 Izze [Clement
ine] $3.39
2 1 1 Nantucket Nectar [Ap
ple] $3.39
3 1 1 Chips and Tomatillo-Green Chili Salsa
NaN $2.39
4 2 2 Chicken Bowl [Tomatillo-Red Chili Salsa (Hot), [Black Beans, Rice, Cheese, Sour Cre
am]] $16.98
Shape: (4622, 5)
5.2 Formatting Large Numbers
Financial and scientific datasets often contain very large or very small numbers that default to
ugly scientific notation when displayed. Pandas offers two complementary ways to control this: a
global display option, or per-column formatting with .apply() and an f-string.
Step 4: A DataFrame with Large Financial Values
By default, pandas may render very large or very precise floating-point numbers using scientific
notation, which can be hard to read at a glance.
Code:
Page 66 of 89
Python Programming Laboratory Manual
financial_data = {
'Company': ['Company A', 'Company B', 'Company C'],
'Revenue': [123456789, 987654321.34, 56473829100000000],
'Profit': [23456789, 876543211500000, 34738291.678]
}
fd = [Link](financial_data)
Output:
Company Revenue Profit
0 Company A 1.234568e+08 2.345679e+07
1 Company B 9.876543e+08 8.765432e+14
2 Company C 5.647383e+16 3.473829e+07
Step 5: Suppressing Scientific Notation Globally
Setting [Link].float_format applies a fixed formatting rule (here, comma thousands-
separators with 2 decimal places) to every float displayed afterwards, until the option is reset.
Code:
financial_data = {
'Company': ['Company A', 'Company B', 'Company C'],
'Revenue': [123456789, 987654321.34, 56473829100000000],
'Profit': [23456789, 876543211500000, 34738291.678]
}
fd = [Link](financial_data)
[Link].float_format = '{:,.2f}'.format
print(fd.to_string())
pd.reset_option('display.float_format')
Output:
Company Revenue Profit
0 Company A 123,456,789.00 23,456,789.00
1 Company B 987,654,321.34 876,543,211,500,000.00
2 Company C 56,473,829,100,000,000.00 34,738,291.68
Step 6: Formatting Specific Columns with apply() and a Lambda
Rather than changing the global display option, .apply(lambda x: f'{x:,.0f}') converts just the
Revenue and Profit columns into nicely formatted strings with comma separators and no decimal
places — note that after this, the column becomes text (dtype object/str), not a number, so further
arithmetic would require converting it back.
Code:
financial_data = {
'Company': ['Company A', 'Company B', 'Company C'],
'Revenue': [123456789, 987654321.34, 56473829100000000],
'Profit': [23456789, 876543211500000, 34738291.678]
}
fd = [Link](financial_data)
df_solution = [Link]()
df_solution['Revenue'] = df_solution['Revenue'].apply(lambda x: f'{x:,.0f}')
df_solution['Profit'] = df_solution['Profit'].apply(lambda x: f'{x:,.0f}')
print(df_solution.to_string())
Page 67 of 89
Python Programming Laboratory Manual
Output:
Company Revenue Profit
0 Company A 123,456,789 23,456,789
1 Company B 987,654,321 876,543,211,500,000
2 Company C 56,473,829,100,000,000 34,738,292
5.3 Loading and Inspecting the Titanic Dataset
The Titanic passenger dataset is one of the most widely used teaching datasets in data science. It
is bundled with Seaborn (sns.load_dataset('titanic')) and records 891 passengers with 15
attributes covering survival, class, demographics, and fare paid.
Step 7: Loading the Dataset and Previewing the First Rows
.head() displays the first five rows by default, giving a quick first look at the columns and data
types present.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print([Link]().to_string())
Output:
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alon
e
0 0 3 male 22.0 1 0 7.2500 S Third man True NaN Southampton no Fals
e
1 1 1 female 38.0 1 0 71.2833 C First woman False C Cherbourg yes Fals
e
2 1 3 female 26.0 0 0 7.9250 S Third woman False NaN Southampton yes Tru
e
3 1 1 female 35.0 1 0 53.1000 S First woman False C Southampton yes Fals
e
4 0 3 male 35.0 0 0 8.0500 S Third man True NaN Southampton no Tru
e
Step 8: Checking for Duplicate Rows
.drop_duplicates() removes any row that exactly matches another row across EVERY column.
Interestingly, 107 of the 891 rows turn out to be exact duplicates here — this does not mean 107
passengers were literally double-counted; rather, with only 15 mostly low-cardinality columns
(class, sex, a rounded age, fare, embarkation town, etc.), it is statistically common for two
different passengers to coincidentally share identical values across all of them. This is a good
reminder to inspect apparent duplicates carefully rather than dropping them automatically — a
unique passenger-ID column (which this dataset does not include) would be a far more reliable
basis for detecting true duplicates.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
before = len(df)
df2 = df.drop_duplicates()
print(f"Rows before: {before}, rows after drop_duplicates(): {len(df2)}")
Output:
Page 68 of 89
Python Programming Laboratory Manual
Rows before: 891, rows after drop_duplicates(): 784
Step 9: Grouping and Counting with groupby()
.groupby('class').size() counts how many passengers fall into each travel class — Third class
clearly carried the most passengers.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
Output:
class
First 216
Second 184
Third 491
Step 10: Grouping by Multiple Columns
groupby() accepts a list of columns to create a multi-level breakdown; note that 'class' was listed
twice in the original code, which pandas simply tolerates (it groups by the unique combination of
class and sex, exactly as if 'class' had only been listed once).
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
result = [Link](['class', 'sex', 'class']).size()
print(result.to_string())
Output:
class sex class
First female First 94
male First 122
Second female Second 76
male Second 108
Third female Third 144
male Third 347
Step 11: Renaming a Column
.rename(columns={...}, inplace=True) relabels 'sex' to 'gender' permanently in the existing
DataFrame (inplace=True avoids having to reassign the result).
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print([Link]().to_string())
Output:
survived pclass gender age sibsp parch fare embarked class who adult_male deck embark_town alive alon
e
0 0 3 male 22.0 1 0 7.2500 S Third man True NaN Southampton no Fals
Page 69 of 89
Python Programming Laboratory Manual
e
1 1 1 female 38.0 1 0 71.2833 C First woman False C Cherbourg yes Fals
e
2 1 3 female 26.0 0 0 7.9250 S Third woman False NaN Southampton yes Tru
e
3 1 1 female 35.0 1 0 53.1000 S First woman False C Southampton yes Fals
e
4 0 3 male 35.0 0 0 8.0500 S Third man True NaN Southampton no Tru
e
Step 12: Structural Overview with .info()
.info() reports the column names, non-null counts, and data types in one summary —
immediately revealing that 'age', 'embarked', 'embark_town', and especially 'deck' have missing
values (their non-null counts are below 891).
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
[Link]()
Output:
<class '[Link]'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 15 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 survived 891 non-null int64
1 pclass 891 non-null int64
2 gender 891 non-null str
3 age 714 non-null float64
4 sibsp 891 non-null int64
5 parch 891 non-null int64
6 fare 891 non-null float64
7 embarked 889 non-null str
8 class 891 non-null str
9 who 891 non-null str
10 adult_male 891 non-null bool
11 deck 203 non-null str
12 embark_town 889 non-null str
13 alive 891 non-null str
14 alone 891 non-null bool
dtypes: bool(2), float64(2), int64(4), str(7)
memory usage: 92.4 KB
Step 13: Column Data Types
.dtypes lists just the data type of every column, which is useful for quickly checking whether a
column is numeric, text, boolean, or categorical.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print([Link].to_string())
Output:
Page 70 of 89
Python Programming Laboratory Manual
survived int64
pclass int64
gender str
age float64
sibsp int64
parch int64
fare float64
embarked str
class str
who str
adult_male bool
deck str
embark_town str
alive str
alone bool
Step 14: Summary Statistics with .describe()
.describe() computes count, mean, standard deviation, min, max, and quartiles for every numeric
column in one call — a fast way to sanity-check a dataset's ranges and spread.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print([Link]().to_string())
Output:
survived pclass age sibsp parch fare
count 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200
Step 15: Counting Rows
len(df) returns the number of rows (passengers) in the DataFrame — 891, matching the row
count reported by .info() above.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(len(df))
Output:
891
Page 71 of 89
Python Programming Laboratory Manual
5.4 Exploring and Visualising Missing Data
Real-world datasets are rarely complete. Before any analysis, it is essential to quantify exactly
how much data is missing, and from which columns, since that shapes every later decision about
cleaning.
Step 16: Counting Missing Values per Column
.isnull() flags every cell as True (missing) or False (present); .sum() then totals the True values
down each column.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print([Link]().sum().to_string())
Output:
survived 0
pclass 0
gender 0
age 177
sibsp 0
parch 0
fare 0
embarked 2
class 0
who 0
adult_male 0
deck 688
embark_town 2
alive 0
alone 0
Step 17: Missing Values as a Percentage
Dividing the missing count by the total row count (and multiplying by 100) converts raw counts
into an easier-to-interpret percentage — 'deck' is missing for the large majority of passengers.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(([Link]().sum() / len(df) * 100).round(2).to_string())
Output:
survived 0.00
pclass 0.00
sex 0.00
age 19.87
sibsp 0.00
parch 0.00
fare 0.00
embarked 0.22
class 0.00
who 0.00
Page 72 of 89
Python Programming Laboratory Manual
adult_male 0.00
deck 77.22
embark_town 0.22
alive 0.00
alone 0.00
Step 18: Visualising Missingness with a Heatmap
[Link]([Link](), cbar=False) turns the True/False missing-value grid into a picture: each
coloured stripe marks a missing cell, making it immediately obvious that 'deck' (and to a lesser
extent 'age') are the columns most affected.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](figsize=(7,4))
[Link]([Link](), cbar=False)
[Link]("Missing-Value Heatmap (Titanic Dataset)")
plt.tight_layout()
[Link]("images/fig_missing_heatmap.png", dpi=150)
[Link]()
print("Heatmap of missing values generated (see Figure). Bright/coloured cells mark missing entries -- 'deck' and 'age' show the mos
t gaps.")
Output:
Heatmap of missing values generated (see Figure). Bright/coloured cells mark missing entries -- 'deck' and 'age' show the most gaps.
Figure 5.1 - Heatmap of missing values across all Titanic columns.
5.5 Selecting Columns and Summarising Categorical Data
Beyond numeric summaries, categorical (text-based) columns are explored with a different
toolkit: selecting by name, listing unique categories, and counting how often each category
occurs.
Page 73 of 89
Python Programming Laboratory Manual
Step 19: Selecting One or More Columns
df['col'] selects a single column as a Series; df[['col1','col2']] (note the double brackets) selects
multiple columns together as a DataFrame.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print("Single column (df['gender']):")
print(df['gender'].head().to_string())
print("\nTwo columns (df[['gender','age']]):")
print(df[['gender','age']].head().to_string())
Output:
Single column (df['gender']):
0 male
1 female
2 female
3 female
4 male
Two columns (df[['gender','age']]):
gender age
0 male 22.0
1 female 38.0
2 female 26.0
3 female 35.0
4 male 35.0
Step 20: Unique Values and Counting Them
.unique() lists every distinct value present in a column; .nunique() returns just the COUNT of
distinct values rather than the values themselves.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print(f"unique genders: {df['gender'].unique()}")
print(f"number of unique genders: {[Link]()}")
Output:
unique genders: <StringArray>
['male', 'female']
Length: 2, dtype: str
number of unique genders: 2
Step 21: Counting Unique Values Across All Columns at Once
Calling .nunique() on the whole DataFrame (rather than a single column) reports the distinct-
value count for every column in one shot — handy for quickly spotting which columns are
categorical (few unique values) versus continuous (many unique values).
Code:
Page 74 of 89
Python Programming Laboratory Manual
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print([Link]().to_string())
Output:
survived 2
pclass 3
gender 2
age 88
sibsp 7
parch 7
fare 248
embarked 3
class 3
who 3
adult_male 2
deck 7
embark_town 3
alive 2
alone 2
Step 22: Listing Column Names
.columns returns all column labels, useful for programmatically checking what is available
before writing further code.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print(list([Link]))
Output:
['survived', 'pclass', 'gender', 'age', 'sibsp', 'parch', 'fare', 'embarked', 'class', 'who', 'adult_male', 'deck', 'embark_town', '
alive', 'alone']
Step 23: Unique Embarkation Towns
Passengers embarked from one of three towns; .unique() confirms there are exactly three (plus a
missing/NaN value for the two passengers without an embarkation_town recorded).
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(df['embark_town'].unique())
Output:
<StringArray>
['Southampton', 'Cherbourg', 'Queenstown', nan]
Length: 4, dtype: str
Page 75 of 89
Python Programming Laboratory Manual
Step 24: Value Counts
.value_counts() tallies how many times each distinct value appears, automatically sorted from
most to least frequent — Southampton was clearly the dominant departure point.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(df['embark_town'].value_counts().to_string())
Output:
embark_town
Southampton 644
Cherbourg 168
Queenstown 77
5.6 Selecting Data with .loc and .iloc
.loc selects rows and columns by LABEL (column names, and index values), while .iloc selects
by integer POSITION, regardless of what the labels actually are. Both accept the same
row:column slicing syntax used elsewhere in pandas and NumPy.
Step 25: .loc with Label-Based Row and Column Ranges
[Link][0:5, 'gender':'fare'] selects rows 0 through 5 (inclusive of 5, unlike normal Python slicing)
and every column from 'gender' through 'fare' inclusive, based purely on their labels.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={'sex': 'gender'}, inplace=True)
print("[Link][0:5, 'gender':'fare']:")
print([Link][0:5, 'gender':'fare'].to_string())
Output:
[Link][0:5, 'gender':'fare']:
gender age sibsp parch fare
0 male 22.0 1 0 7.2500
1 female 38.0 1 0 71.2833
2 female 26.0 0 0 7.9250
3 female 35.0 1 0 53.1000
4 male 35.0 0 0 8.0500
5 male NaN 0 0 8.4583
Step 26: .loc with a Different Column Range
The column range can start and end anywhere; here, 'fare' through 'deck' selects a different slice
of the same DataFrame.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
Page 76 of 89
Python Programming Laboratory Manual
print("[Link][0:15, 'fare':'deck']:")
print([Link][0:15, 'fare':'deck'].to_string())
Output:
[Link][0:15, 'fare':'deck']:
fare embarked class who adult_male deck
0 7.2500 S Third man True NaN
1 71.2833 C First woman False C
2 7.9250 S Third woman False NaN
3 53.1000 S First woman False C
4 8.0500 S Third man True NaN
5 8.4583 Q Third man True NaN
6 51.8625 S First man True E
7 21.0750 S Third child False NaN
8 11.1333 S Third woman False NaN
9 30.0708 C Second child False NaN
10 16.7000 S Third child False G
11 26.5500 S First woman False C
12 8.0500 S Third man True NaN
13 31.2750 S Third man True NaN
14 7.8542 S Third child False NaN
15 16.0000 S Second woman False NaN
Step 27: .iloc with Integer Positions
[Link][0:5, 0:3] selects the first 5 rows and first 3 columns purely by their numeric position —
here the upper bound (5 and 3) is EXCLUSIVE, exactly like standard Python slicing, which is
the key difference from .loc.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print("[Link][0:5, 0:3]:")
print([Link][0:5, 0:3].to_string())
Output:
[Link][0:5, 0:3]:
survived pclass sex
0 0 3 male
1 1 1 female
2 1 3 female
3 1 1 female
4 0 3 male
Step 28: Grouping by Two Columns to Compare Survival
Grouping by both 'survived' and 'who' (man / woman / child) breaks down passenger counts into
a clear cross-tabulation, foreshadowing more detailed survival analysis.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print([Link](['survived', 'who']).size().to_string())
Output:
Page 77 of 89
Python Programming Laboratory Manual
survived who
0 child 34
man 449
woman 66
1 child 49
man 88
woman 205
5.7 Correlation Analysis and Visual Exploration
Before modelling, it is good practice to visually inspect how numeric variables relate to one
another. A correlation matrix quantifies linear relationships (from -1 to +1), while a pairplot
shows every pairwise scatter relationship and individual distribution at a glance.
Step 29: Computing a Correlation Matrix
.corr() computes the Pearson correlation coefficient between every pair of numeric columns;
values near +1 indicate a strong positive relationship, near -1 a strong negative one, and near 0
little to no linear relationship.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
correlation_df = df[['fare', 'age', 'sibsp', 'parch']].corr()
print(correlation_df.round(3).to_string())
[Link](figsize=(5.5,4.5))
[Link](correlation_df, annot=True, cmap="coolwarm", fmt=".2f")
[Link]("Correlation Matrix: fare, age, sibsp, parch")
plt.tight_layout()
[Link]("images/fig_correlation_heatmap.png", dpi=150)
[Link]()
print("\n(Correlation heatmap generated -- see Figure.)")
Output:
fare age sibsp parch
fare 1.000 0.096 0.160 0.216
age 0.096 1.000 -0.308 -0.189
sibsp 0.160 -0.308 1.000 0.415
parch 0.216 -0.189 0.415 1.000
(Correlation heatmap generated -- see Figure.)
Page 78 of 89
Python Programming Laboratory Manual
Figure 5.2 - Correlation heatmap for fare, age, sibsp, and parch.
Step 30: A Pairplot of the Numeric Columns
[Link]() draws a full grid of scatter plots for every pair of numeric variables (with
histograms along the diagonal), making it easy to spot patterns, clusters, and relationships across
several variables simultaneously.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
num_df = df[['survived','pclass','age','sibsp','parch','fare']].dropna()
g = [Link](num_df, height=1.3, plot_kws={"s":8, "alpha":0.5})
[Link]("images/fig_pairplot.png", dpi=130)
[Link]("all")
print("Pairplot of the numeric Titanic columns generated (see Figure).")
Output:
Pairplot of the numeric Titanic columns generated (see Figure).
Page 79 of 89
Python Programming Laboratory Manual
Figure 5.3 - Pairplot of survived, pclass, age, sibsp, parch, and fare.
5.8 Handling Missing Values
Once missing data has been identified (Section 5.4), it must be handled before further analysis:
either by dropping unusable columns, or by filling (imputing) gaps with a sensible value such as
the mean, median, or mode.
Step 31: Re-checking the Missing-Value Percentages
Re-running the missing-percentage calculation on a fresh copy of the dataset re-confirms exactly
which columns need attention before proceeding.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(([Link]().sum() / len(df) * 100).round(2).to_string())
Output:
Page 80 of 89
Python Programming Laboratory Manual
survived 0.00
pclass 0.00
sex 0.00
age 19.87
sibsp 0.00
parch 0.00
fare 0.00
embarked 0.22
class 0.00
who 0.00
adult_male 0.00
deck 77.22
embark_town 0.22
alive 0.00
alone 0.00
Step 32: Dropping a Column with Too Much Missing Data
As a rule of thumb, a column missing more than roughly 70% of its values is often dropped
entirely rather than imputed, since there is too little real information left to reliably fill in. 'deck'
(missing for over 77% of passengers) is dropped here with axis=1 (meaning 'operate on
columns').
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(f"Columns before drop: {list([Link])}")
[Link]('deck', axis=1, inplace=True)
print(f"Columns after dropping 'deck': {list([Link])}")
Output:
Columns before drop: ['survived', 'pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked', 'class', 'who', 'adult_male', 'deck'
, 'embark_town', 'alive', 'alone']
Columns after dropping 'deck': ['survived', 'pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked', 'class', 'who', 'adult_mal
e', 'embark_town', 'alive', 'alone']
Step 33: Mean, Median, and Mode of the 'age' Column
Before choosing how to fill missing ages, it is useful to compare the three central-tendency
measures: mean (sensitive to outliers), median (the middle value), and mode (the most frequent
value, which may not be unique).
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(f"mean age: {df['age'].mean():.4f}")
print(f"median age: {df['age'].median()}")
print(f"mode age: {df['age'].mode().tolist()}")
Output:
mean age: 29.6991
median age: 28.0
mode age: [24.0]
Page 81 of 89
Python Programming Laboratory Manual
Step 34: Filling Missing Ages with the Mean
.fillna(df['age'].mean()) replaces every missing age with the column's average, immediately
reducing the missing-age count to zero.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link]('deck', axis=1, inplace=True)
print(f"missing age values before fillna: {df['age'].isnull().sum()}")
df['age'] = df['age'].fillna(df['age'].mean())
print(f"missing age values after fillna(mean): {df['age'].isnull().sum()}")
print([Link]().sum().to_string())
Output:
missing age values before fillna: 177
missing age values after fillna(mean): 0
survived 0
pclass 0
sex 0
age 0
sibsp 0
parch 0
fare 0
embarked 2
class 0
who 0
adult_male 0
embark_town 2
alive 0
alone 0
Step 35: Filling Categorical Columns with the Mode
For categorical columns such as 'embarked' and 'embark_town', the mode (most common value)
— rather than a mean or median, which would not make sense for text — is the standard
imputation choice. After this step, every remaining column in the cleaned dataset is complete.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link]('deck', axis=1, inplace=True)
df['age'] = df['age'].fillna(df['age'].mean())
print(f"missing 'embarked' before: {df['embarked'].isnull().sum()}")
df['embarked'] = df['embarked'].fillna(df['embarked'].mode()[0])
print(f"missing 'embarked' after fillna(mode): {df['embarked'].isnull().sum()}")
df['embark_town'] = df['embark_town'].fillna(df['embark_town'].mode()[0])
print(f"missing 'embark_town' after fillna(mode): {df['embark_town'].isnull().sum()}")
print("\nFinal missing-value count per column:")
print([Link]().sum().to_string())
Output:
missing 'embarked' before: 2
missing 'embarked' after fillna(mode): 0
missing 'embark_town' after fillna(mode): 0
Final missing-value count per column:
Page 82 of 89
Python Programming Laboratory Manual
survived 0
pclass 0
sex 0
age 0
sibsp 0
parch 0
fare 0
embarked 0
class 0
who 0
adult_male 0
embark_town 0
alive 0
alone 0
Step 36: Minimum and Maximum Age
A final sanity check on the (now-imputed) age column confirms the data spans from infancy to
old age, with no impossible values such as negative ages.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
print(f"min age: {[Link]()}")
print(f"max age: {[Link]()}")
Output:
min age: 0.42
max age: 80.0
5.9 Feature Engineering: Binning a Continuous Variable
Feature engineering is the process of transforming raw columns into new, more useful ones. A
common technique is binning: converting a continuous numeric variable (like age) into a small
number of meaningful categories.
Step 37: Visualising the Age Distribution
A histogram of passenger ages shows the overall shape of the distribution before deciding where
to place bin boundaries.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](figsize=(5,3.5))
[Link](df['age'].dropna())
[Link]("Distribution of Passenger Age (Titanic)")
plt.tight_layout()
[Link]("images/fig_age_histogram.png", dpi=150)
[Link]()
print("Histogram of passenger age generated (see Figure). Most passengers are between 20 and 40 years old.")
Output:
Histogram of passenger age generated (see Figure). Most passengers are between 20 and 40 years old.
Page 83 of 89
Python Programming Laboratory Manual
Figure 5.4 - Distribution of passenger age.
Step 38: Defining Age Bins and Applying [Link]()
[Link](column, bins, labels) slices a continuous range into discrete labelled categories — here,
ages are grouped into 7 human-readable life stages from 'Infants' to 'Old'.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
binned = [Link](df['age'], bins=bins, labels=labels)
print([Link](10).to_string())
Output:
0 Youngs
1 Middle Aged
2 Youngs
3 Middle Aged
4 Middle Aged
5 NaN
6 Old
7 Toddlers
8 Youngs
9 Teens
Categories (7, str): ['Infants' < 'Toddlers' < 'Kids' < 'Teens' < 'Youngs' < 'Middle Aged' < 'Old']
Step 39: Adding the Binned Age as a New Column
Storing the result of [Link]() back into the DataFrame as df['binned_age'] creates a brand new
categorical feature that downstream analysis (and machine-learning models) can use directly.
Code:
Page 84 of 89
Python Programming Laboratory Manual
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
print(df['binned_age'].value_counts().to_string())
Output:
binned_age
Youngs 270
Middle Aged 241
Teens 70
Old 64
Toddlers 30
Kids 25
Infants 14
5.10 Filtering Data with Boolean Conditions
Pandas supports SQL-like row filtering directly with Boolean conditions inside square brackets,
including combining multiple conditions with & (AND) and | (OR).
Step 40: Building a Smaller Working Subset
df_01 keeps only the four columns relevant to the upcoming filtering examples (survived,
binned_age, fare, class), which also confirms 'binned_age' is now a proper categorical column.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
df_01 = df[['survived','binned_age','fare','class']]
df_01.info()
Output:
<class '[Link]'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 survived 891 non-null int64
1 binned_age 714 non-null category
2 fare 891 non-null float64
3 class 891 non-null str
dtypes: category(1), float64(1), int64(1), str(1)
memory usage: 22.2 KB
Step 41: Filtering Rows by a Single Category
df_01[df_01['class'] == 'First'] keeps only the rows where the class column equals 'First' —
exactly mirroring a SQL WHERE clause.
Code:
Page 85 of 89
Python Programming Laboratory Manual
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
df_01 = df[['survived','binned_age','fare','class']]
print("df['class'].value_counts():")
print(df['class'].value_counts().to_string())
df_first = df_01[df_01['class'] == 'First']
print(f"\nRows where class == 'First': {len(df_first)}")
print(df_first.head().to_string())
Output:
df['class'].value_counts():
class
Third 491
First 216
Second 184
Rows where class == 'First': 216
survived binned_age fare class
1 1 Middle Aged 71.2833 First
3 1 Middle Aged 53.1000 First
6 0 Old 51.8625 First
11 1 Old 26.5500 First
23 1 Youngs 35.5000 First
Step 42: Filtering Rows by a Numeric Threshold
df_01[df_01['fare'] > 200] keeps only the higher-fare passengers; cross-tabulating this subset
against 'class' (next step) confirms these were overwhelmingly First-class travellers.
Code:
# Passengers with fare > 200
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
df_01 = df[['survived','binned_age','fare','class']]
df_200 = df_01[df_01['fare'] > 200]
print(f"Rows with fare > 200: {len(df_200)}")
print(df_200.head(10).to_string())
# Class breakdown of that high-fare subset
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
df_01 = df[['survived','binned_age','fare','class']]
df_200 = df_01[df_01['fare'] > 200]
print(df_200['class'].value_counts().to_string())
Output:
Rows with fare > 200: 20
survived binned_age fare class
27 0 Youngs 263.0000 First
88 1 Youngs 263.0000 First
Page 86 of 89
Python Programming Laboratory Manual
118 0 Youngs 247.5208 First
258 1 Middle Aged 512.3292 First
299 1 Middle Aged 247.5208 First
311 1 Teens 262.3750 First
341 1 Youngs 263.0000 First
377 0 Youngs 211.5000 First
380 1 Middle Aged 227.5250 First
438 0 Old 263.0000 First
class
First 20
Step 43: Combining Conditions with AND (&)
Both conditions must be True simultaneously for a row to be kept; parentheses around each
individual condition are required due to Python's operator precedence rules.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
df_01 = df[['survived','binned_age','fare','class']]
res = df_01[(df_01['fare'] > 70) & (df_01['class']=='First')].sort_values(by='fare')
print(f"Rows matching fare>70 AND class=='First': {len(res)}")
print([Link](10).to_string())
Output:
Rows matching fare>70 AND class=='First': 100
survived binned_age fare class
540 1 Middle Aged 71.0000 First
745 0 Old 71.0000 First
1 1 Middle Aged 71.2833 First
366 1 Old 75.2500 First
218 1 Middle Aged 76.2917 First
52 1 Middle Aged 76.7292 First
681 1 Youngs 76.7292 First
645 1 Middle Aged 76.7292 First
124 0 Old 77.2875 First
102 0 Youngs 77.2875 First
Step 44: Combining Conditions with OR (|)
At least one of the two conditions must be True for a row to be kept, which is why this filter
returns more rows than the AND version in Step 43.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
bins = [0,1,5,12,18,30,50,80]
labels = ['Infants', 'Toddlers', 'Kids', 'Teens', 'Youngs', 'Middle Aged', 'Old']
df['binned_age'] = [Link](df['age'], bins=bins, labels=labels)
df_01 = df[['survived','binned_age','fare','class']]
res = df_01[(df_01['fare'] > 70) | (df_01['class']=='First')].sort_values(by='fare')
print(f"Rows matching fare>70 OR class=='First': {len(res)}")
print([Link](10).to_string())
Output:
Page 87 of 89
Python Programming Laboratory Manual
Rows matching fare>70 OR class=='First': 221
survived binned_age fare class
263 0 Middle Aged 0.0000 First
822 0 Middle Aged 0.0000 First
806 0 Middle Aged 0.0000 First
633 0 NaN 0.0000 First
815 0 NaN 0.0000 First
872 0 Middle Aged 5.0000 First
662 0 Middle Aged 25.5875 First
168 0 NaN 25.9250 First
862 1 Middle Aged 25.9292 First
796 1 Middle Aged 25.9292 First
5.11 Basic Plotting and Grouped Aggregation
Pandas DataFrames have a built-in .plot() method (a thin wrapper around Matplotlib) for quick,
no-fuss visualisation directly from a DataFrame, and groupby() can be combined with an
aggregation function such as .mean() to summarise one column by the categories of another.
Step 45: Scatter Plots with Different Colour Options
kind='scatter' plots one numeric column against another; the color argument accepts either a
named colour (like 'red') or a hex code (like '#32a852'), giving full control over the plot's
appearance.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={"sex": "gender"}, inplace=True)
fig, axes = [Link](1, 3, figsize=(11,3.3))
[Link](kind='scatter', x='age', y='fare', ax=axes[0], title="default color")
[Link](kind='scatter', x='age', y='fare', color='#32a852', ax=axes[1], title="hex color #32a852")
[Link](kind='scatter', x='age', y='fare', color='red', ax=axes[2], title="named color red")
plt.tight_layout()
[Link]("images/fig_scatter_age_fare.png", dpi=150)
[Link]()
print("Three age-vs-fare scatter plots generated side by side (default, hex-code green, and named red) -- see Figure.")
Output:
Three age-vs-fare scatter plots generated side by side (default, hex-code green, and named red) -- see Figure.
Figure 5.5 - Age vs. fare scatter plots using the default colour, a custom hex colour, and a named colour.
Step 46: Average Fare by Gender
groupby('gender')['fare'].mean() answers a natural business question directly: on average, female
passengers paid a noticeably higher fare than male passengers, which (combined with the class
Page 88 of 89
Python Programming Laboratory Manual
breakdown in earlier steps) hints at a relationship between gender, class, and ticket price worth
exploring further.
Code:
titanic = pd.read_csv('data/[Link]')
df = [Link]()
[Link](columns={"sex": "gender"}, inplace=True)
group = [Link]('gender')['fare'].mean()
print(group.to_string())
Output:
gender
female 44.479818
male 25.523893
Post-Lab Tasks — Session 5
1. Load the Titanic dataset fresh and report what percentage of passengers survived overall,
and separately for each travel class.
2. Create a new 'fare_category' column that bins the 'fare' column into 'Low', 'Medium', and
'High' using your own chosen thresholds.
3. Using .loc, select all First-class female passengers who survived, and report how many
there are.
4. Produce a correlation heatmap that additionally includes the 'survived' column, and
identify which numeric variable correlates most strongly with survival.
5. Write a short paragraph (4-6 sentences) summarising what this lab's exploration revealed
about who was more likely to survive the Titanic disaster, citing at least three specific
pieces of evidence from the steps above.
Page 89 of 89