Data Science Fundamentals Course
Week 2 builds on your Python fundamentals by introducing data structures that are essential for organizing and manipulating data. You will learn how to work with collections of data using lists, tuples, dictionaries, and sets. Additionally, you will learn how to read and write data to files, which is crucial for data science work. Finally, we will cover exception handling to make your programs more robust. By the end of Week 2, you will be able to:
Week 2 is divided into three 2-hour sessions:
A list is an ordered, mutable collection of items. "Mutable" means you can change, add, or remove items after creating the list. Lists are one of the most commonly used data structures in Python and essential for data science. Key characteristics of lists:
# Empty list empty_list = [] print(empty_list) # Output: [] # List with initial values numbers = [1, 2, 3, 4, 5] print(numbers) # Output: [1, 2, 3, 4, 5] # List with different data types mixed_list = [1, "Alice", 3.14, True, None] print(mixed_list) # Output: [1, 'Alice', 3.14, True, None] # List with repeated values zeros = [0] * 5 print(zeros) # Output: [0, 0, 0, 0, 0] # Convert other types to list string_to_list = list("ABC") print(string_to_list) # Output: ['A', 'B', 'C'] # List of lists (nested) matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] print(matrix) # Output: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Lists use zero-based indexing, meaning the first element is at index 0.
fruits = ["apple", "banana", "cherry", "date", "elderberry"] # Access by positive index (from beginning) print(fruits[0]) # Output: apple print(fruits[1]) # Output: banana print(fruits[4]) # Output: elderberry # Access by negative index (from end) print(fruits[-1]) # Output: elderberry (last item) print(fruits[-2]) # Output: date (second to last) print(fruits[-5]) # Output: apple (first item) # Access nested list element matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] print(matrix[0]) # Output: [1, 2, 3] print(matrix[0][1]) # Output: 2 print(matrix[2][2]) # Output: 9 # Get list length print(len(fruits)) # Output: 5 print(len(matrix)) # Output: 3
Slicing extracts a portion of a list using the syntax [start:stop:step].
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # Basic slicing [start:stop] - stop is excluded print(numbers[2:5]) # Output: [2, 3, 4] print(numbers[0:3]) # Output: [0, 1, 2] # From beginning to index print(numbers[:4]) # Output: [0, 1, 2, 3] # From index to end print(numbers[7:]) # Output: [7, 8, 9] # With step print(numbers[::2]) # Every 2nd element: [0, 2, 4, 6, 8] print(numbers[1::2]) # Every 2nd element starting at index 1: [1, 3, 5, 7, 9] # Reverse a list print(numbers[::-1]) # Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] # Slice with negative indices print(numbers[-3:]) # Last 3 elements: [7, 8, 9] print(numbers[:-2]) # All except last 2: [0, 1, 2, 3, 4, 5, 6, 7] # Real-world: Get first 10 items from a long list data = list(range(1000)) # List of 0-999 first_10 = data[:10] print(first_10) # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Since lists are mutable, you can change, add, or remove elements.
numbers = [1, 2, 3, 4, 5] # Change a single element numbers[2] = 30 print(numbers) # Output: [1, 2, 30, 4, 5] # Change multiple elements with slicing numbers[1:3] = [20, 300] print(numbers) # Output: [1, 20, 300, 4, 5] # Add single element at end numbers.append(6) print(numbers) # Output: [1, 20, 300, 4, 5, 6] # Add multiple elements numbers.extend([7, 8, 9]) print(numbers) # Output: [1, 20, 300, 4, 5, 6, 7, 8, 9] # Insert at specific position numbers.insert(0, 0) # Insert 0 at beginning print(numbers) # Output: [0, 1, 20, 300, 4, 5, 6, 7, 8, 9] # Remove by value numbers.remove(20) print(numbers) # Output: [0, 1, 300, 4, 5, 6, 7, 8, 9] # Remove by index del numbers[2] print(numbers) # Output: [0, 1, 4, 5, 6, 7, 8, 9] # Pop removes and returns element at index last = numbers.pop() # Remove last element print(last) # Output: 9 print(numbers) # Output: [0, 1, 4, 5, 6, 7, 8] # Clear all elements numbers.clear() print(numbers) # Output: []
# index() and count() fruits = ["apple", "banana", "cherry", "banana"] print(fruits.index("banana")) # Output: 1 (first position) print(fruits.count("banana")) # Output: 2 # sort() numbers = [3, 1, 4, 1, 5, 9, 2, 6] numbers.sort() print(numbers) # Output: [1, 1, 2, 3, 4, 5, 6, 9] # reverse() numbers.reverse() print(numbers) # Output: [9, 6, 5, 4, 3, 2, 1, 1] # copy() - important for avoiding unintended changes original = [1, 2, 3] copy_list = original.copy() copy_list[0] = 99 print(original) # Output: [1, 2, 3] (unchanged) print(copy_list) # Output: [99, 2, 3]
A tuple is an ordered, immutable collection of items. "Immutable" means once created, you cannot change, add, or remove elements. Tuples are useful when you want to ensure data doesn't change accidentally. Key characteristics:
# Create tuples empty_tuple = () print(empty_tuple) # Output: () single_item = (1,) # Note the comma - important! print(single_item) # Output: (1,) coordinates = (10, 20, 30) print(coordinates) # Output: (10, 20, 30) mixed_tuple = (1, "Alice", 3.14, True) print(mixed_tuple) # Output: (1, 'Alice', 3.14, True) # Tuple unpacking x, y, z = coordinates print(x, y, z) # Output: 10 20 30 # Accessing elements (same as lists) print(coordinates[0]) # Output: 10 print(coordinates[-1]) # Output: 30 # Slicing (same as lists) print(coordinates[1:]) # Output: (20, 30) # Tuple methods numbers = (1, 2, 3, 2, 4, 2) print(numbers.count(2)) # Output: 3 print(numbers.index(3)) # Output: 2 # Converting between list and tuple list_data = [1, 2, 3] tuple_data = tuple(list_data) print(tuple_data) # Output: (1, 2, 3) # Back to list list_again = list(tuple_data) print(list_again) # Output: [1, 2, 3] # Real-world: Return multiple values as tuple def get_user_info(): name = "Alice" age = 25 city = "Lagos" return name, age, city # Returns tuple user = get_user_info() print(user) # Output: ('Alice', 25, 'Lagos') name, age, city = get_user_info() print(f"{name} is {age} and lives in {city}")
Use Lists when:
Use Tuples when:
A dictionary is a mutable, unordered collection of key-value pairs. Instead of accessing items by position like lists, dictionaries use keys to access values. Dictionaries are incredibly useful for organizing structured data. Key characteristics:
# Create dictionaries empty_dict = {} print(empty_dict) # Output: {} # Dictionary with initial values student = {"name": "Alice", "age": 25, "grade": "A"} print(student) # Output: {'name': 'Alice', 'age': 25, 'grade': 'A'} # Dictionary with mixed key types mixed = {1: "one", "two": 2, 3.0: "three"} print(mixed) # Output: {1: 'one', 'two': 2, 3.0: 'three'} # Accessing values by key print(student["name"]) # Output: Alice print(student["age"]) # Output: 25 # Using get() method (safer than direct access) print(student.get("name")) # Output: Alice print(student.get("email", "N/A")) # Output: N/A (key doesn't exist) # Adding new key-value pairs student["email"] = "alice@example.com" print(student) # Output: {'name': 'Alice', 'age': 25, 'grade': 'A', 'email': 'alice@example.com'} # Modifying existing values student["age"] = 26 print(student) # Output: {'name': 'Alice', 'age': 26, 'grade': 'A', 'email': 'alice@example.com'} # Removing key-value pairs del student["grade"] print(student) # pop() removes and returns value email = student.pop("email") print(email) # Output: alice@example.com # Clear all items student.clear() print(student) # Output: {}
person = {"name": "Bob", "age": 30, "city": "Lagos"}
# keys(), values(), items()
print(person.keys()) # Output: dict_keys(['name', 'age', 'city'])
print(person.values()) # Output: dict_values(['Bob', 30, 'Lagos'])
print(person.items()) # Output: dict_items([('name', 'Bob'), ('age', 30), ('city', 'Lagos')])
# Iterating through dictionary
for key in person:
print(f"{key}: {person[key]}")
for key, value in person.items():
print(f"{key}: {value}")
# update()
person.update({"age": 31, "email": "bob@example.com"})
print(person)
# Output: {'name': 'Bob', 'age': 31, 'city': 'Lagos', 'email': 'bob@example.com'}
# Real-world: Store student grades
grades = {
"Alice": 85,
"Bob": 92,
"Charlie": 78,
"Diana": 95
}
# Find student with highest grade
top_student = max(grades, key=grades.get)
print(f"Top student: {top_student} with grade {grades[top_student]}")
# Output: Top student: Diana with grade 95# Dictionary containing dictionaries company = { "employees": { "emp1": {"name": "Alice", "dept": "Data Science", "salary": 60000}, "emp2": {"name": "Bob", "dept": "Engineering", "salary": 70000}, "emp3": {"name": "Charlie", "dept": "Sales", "salary": 50000} }, "departments": ["Data Science", "Engineering", "Sales"], "founded": 2020 } # Accessing nested values print(company["employees"]["emp1"]["name"]) # Output: Alice print(company["employees"]["emp2"]["salary"]) # Output: 70000 # Iterating through nested structure for emp_id, emp_info in company["employees"].items(): print(f"{emp_id}: {emp_info['name']} - {emp_info['dept']}") # Real-world: Store customer data customers = { "cust001": { "name": "Alice Smith", "email": "alice@example.com", "purchases": [100, 200, 150] }, "cust002": { "name": "Bob Jones", "email": "bob@example.com", "purchases": [300, 250] } } # Calculate total spent by customer for cust_id, cust_data in customers.items(): total = sum(cust_data["purchases"]) print(f"{cust_data['name']} spent: {total}")
A set is an unordered collection of unique items. Sets are useful when you need to:
Key characteristics:
# Create sets empty_set = set() # Note: {} creates dict, not set print(empty_set) # Output: set() numbers = {1, 2, 3, 4, 5} print(numbers) # Output: {1, 2, 3, 4, 5} # Create from list duplicates = [1, 2, 2, 3, 3, 3, 4] unique = set(duplicates) print(unique) # Output: {1, 2, 3, 4} # Adding elements numbers.add(6) print(numbers) # Output: {1, 2, 3, 4, 5, 6} # Adding multiple elements numbers.update([7, 8, 9]) print(numbers) # Output: {1, 2, 3, 4, 5, 6, 7, 8, 9} # Removing elements numbers.remove(9) # Raises error if not found print(numbers) # discard() - doesn't raise error if not found numbers.discard(10) # No error even though 10 isn't in set numbers.discard(8) print(numbers) # Membership testing print(5 in numbers) # Output: True print(10 in numbers) # Output: False # Set operations set_a = {1, 2, 3, 4} set_b = {3, 4, 5, 6} # Union: all elements from both sets union = set_a | set_b print(union) # Output: {1, 2, 3, 4, 5, 6} # Intersection: elements in both sets intersection = set_a & set_b print(intersection) # Output: {3, 4} # Difference: elements in set_a but not in set_b difference = set_a - set_b print(difference) # Output: {1, 2} # Real-world: Find common programming languages between developers dev1_languages = {"Python", "JavaScript", "Java", "C++"} dev2_languages = {"Python", "Ruby", "Java", "Go"} common = dev1_languages & dev2_languages print(f"Languages they both know: {common}") # Output: Languages they both know: {'Java', 'Python'}
Strings are immutable sequences of characters. Understanding string operations is essential for data science, especially for cleaning and processing text data.
text = "Data Science" # Indexing print(text[0]) # Output: D print(text[5]) # Output: S print(text[-1]) # Output: e # Slicing print(text[0:4]) # Output: Data print(text[5:]) # Output: Science print(text[:4]) # Output: Data print(text[::2]) # Every 2nd character: DtSine print(text[::-1]) # Reverse: ecneicS ataD # String length print(len(text)) # Output: 12
text = " Hello, World! " # Case operations print(text.upper()) # Output: HELLO, WORLD! print(text.lower()) # Output: hello, world! print(text.capitalize()) # Output: hello, world! (weird!) print(text.title()) # Output: Hello, World! # strip() removes whitespace clean = text.strip() print(clean) # Output: Hello, World! # replace() message = "I like apples. Apples are great." new_message = message.replace("apples", "oranges") print(new_message) # Output: I like oranges. Apples are great. # split() and join() sentence = "Python is awesome" words = sentence.split() print(words) # Output: ['Python', 'is', 'awesome'] rejoined = " ".join(words) print(rejoined) # Output: Python is awesome # find() text = "Hello, World!" index = text.find("World") print(index) # Output: 7 # startswith() and endswith() print(sentence.startswith("Python")) # Output: True print(sentence.endswith("awesome")) # Output: True # Type checking print("123".isdigit()) # Output: True print("abc".isalpha()) # Output: True print("12a".isdigit()) # Output: False # Real-world: Process CSV-like data csv_line = "Alice, 25, Lagos, Data Science" fields = [field.strip() for field in csv_line.split(",")] print(fields) # Output: ['Alice', '25', 'Lagos', 'Data Science']
# f-strings (Python 3.6+) - Recommended name = "Alice" age = 25 city = "Lagos" print(f"Name: {name}, Age: {age}, City: {city}") # Output: Name: Alice, Age: 25, City: Lagos # With expressions in f-strings x = 10 y = 20 print(f"Sum of {x} and {y} is {x + y}") # Output: Sum of 10 and 20 is 30 # Formatting numbers pi = 3.14159 print(f"Pi rounded: {pi:.2f}") # Output: Pi rounded: 3.14 print(f"Percentage: {0.875:.1%}") # Output: Percentage: 87.5% # format() method template = "Hello {}, you are {} years old" message = template.format("Bob", 30) print(message) # Output: Hello Bob, you are 30 years old # Older % formatting (still seen in legacy code) message = "Hello %s, you are %d years old" % ("Charlie", 28) print(message) # Output: Hello Charlie, you are 28 years old # Real-world: Create formatted output for report data = {"name": "Diana", "score": 92.5, "percentile": 0.95} report = f""" Student Report: Name: {data['name']} Score: {data['score']:.1f} Percentile: {data['percentile']:.0%} """ print(report)
File handling is essential for data science. You need to read data from files and save results. Python provides simple methods to work with files.
# Basic file operations # Modes: 'r' (read), 'w' (write), 'a' (append), 'rb' (read binary), etc. # Method 1: Manual close (not recommended) file = open("data.txt", "r") content = file.read() file.close() # Method 2: with statement (RECOMMENDED) # Automatically closes file even if error occurs with open("data.txt", "r") as file: content = file.read() print(content) # After 'with' block, file is automatically closed
# Create a sample file first with open("sample.txt", "w") as f: f.write("Line 1: Hello\n") f.write("Line 2: World\n") f.write("Line 3: Python") # Read entire file as string with open("sample.txt", "r") as f: content = f.read() print(content) # Output: # Line 1: Hello # Line 2: World # Line 3: Python # Read one line at a time with open("sample.txt", "r") as f: line1 = f.readline() line2 = f.readline() print(line1) # Output: Line 1: Hello print(line2) # Output: Line 2: World # Read all lines as list with open("sample.txt", "r") as f: lines = f.readlines() print(lines) # Output: ['Line 1: Hello\n', 'Line 2: World\n', 'Line 3: Python'] # Iterate through lines (memory efficient for large files) with open("sample.txt", "r") as f: for line in f: print(line.strip()) # strip() removes newline character # Output: # Line 1: Hello # Line 2: World # Line 3: Python
# Write mode: overwrites existing file with open("output.txt", "w") as f: f.write("First line\n") f.write("Second line\n") # Append mode: adds to end of file with open("output.txt", "a") as f: f.write("Third line (appended)\n") # Write multiple lines at once lines = ["Data Science\n", "Machine Learning\n", "Artificial Intelligence\n"] with open("output.txt", "w") as f: f.writelines(lines) # Read back what we wrote with open("output.txt", "r") as f: content = f.read() print(content) # Output: # Data Science # Machine Learning # Artificial Intelligence # Real-world: Save analysis results results = [ "Analysis Results\n", "==================\n", "Total records: 1000\n", "Valid records: 950\n", "Invalid records: 50\n" ] with open("results.txt", "w") as f: f.writelines(results)
# Create sample CSV file csv_content = """name,age,city Alice,25,Lagos Bob,30,Accra Charlie,28,Nairobi Diana,32,Johannesburg""" with open("people.csv", "w") as f: f.write(csv_content) # Read CSV manually (without pandas) with open("people.csv", "r") as f: # Skip header header = f.readline().strip().split(",") print("Header:", header) # Output: Header: ['name', 'age', 'city'] # Read data rows for line in f: fields = line.strip().split(",") name, age, city = fields print(f"{name}: {age} years old, from {city}") # Output: # Alice: 25 years old, from Lagos # Bob: 30 years old, from Accra # Charlie: 28 years old, from Nairobi # Diana: 32 years old, from Johannesburg # Better: Use list comprehension to parse CSV def read_csv(filename): """Read CSV file and return list of dictionaries""" with open(filename, "r") as f: header = f.readline().strip().split(",") data = [] for line in f: values = line.strip().split(",") row = dict(zip(header, values)) data.append(row) return data people = read_csv("people.csv") for person in people: print(person) # Output: # {'name': 'Alice', 'age': '25', 'city': 'Lagos'} # {'name': 'Bob', 'age': '30', 'city': 'Accra'} # etc.
Exception handling allows your program to handle errors gracefully instead of crashing. This is essential for robust data science applications.
# Without exception handling - crashes # result = 10 / 0 # ZeroDivisionError! # With exception handling try: result = 10 / 0 except ZeroDivisionError: print("Error: Cannot divide by zero") # Output: Error: Cannot divide by zero # Catching generic exceptions try: x = int("not_a_number") except ValueError: print("Error: Could not convert to integer") # Output: Error: Could not convert to integer # Catch multiple specific exceptions try: # Some operation that might fail data = [1, 2, 3] print(data[10]) # IndexError except IndexError: print("Error: Index out of range") except KeyError: print("Error: Key not found") # Output: Error: Index out of range # Catch any exception try: # Unknown operation result = 10 / 0 except Exception as e: print(f"An error occurred: {e}") # Output: An error occurred: division by zero
# Complete try-except-else-finally structure try: # Code that might raise an exception num = int(input("Enter a number: ")) result = 10 / num except ValueError: print("Error: Invalid input, please enter a number") except ZeroDivisionError: print("Error: Cannot divide by zero") else: # Executed if no exception occurs print(f"Result: {result}") finally: # Always executed, whether exception or not print("Operation completed") # Real-world: Reading file safely def read_data_file(filename): try: with open(filename, "r") as f: data = f.read() return data except FileNotFoundError: print(f"Error: File '{filename}' not found") return None except IOError: print(f"Error: Could not read file '{filename}'") return None content = read_data_file("data.txt") if content: print("File contents:") print(content)
# Raise exceptions manually def validate_age(age): if age < 0: raise ValueError("Age cannot be negative") if age > 150: raise ValueError("Age seems unrealistic") return f"Age {age} is valid" # Using the function try: print(validate_age(-5)) except ValueError as e: print(f"Validation error: {e}") # Output: Validation error: Age cannot be negative # Real-world: Data validation def process_data(data): if not data: raise ValueError("Data cannot be empty") if not isinstance(data, list): raise TypeError("Data must be a list") if len(data) == 0: raise ValueError("Data list is empty") return sum(data) / len(data) # Average try: avg = process_data([10, 20, 30]) print(f"Average: {avg}") except (ValueError, TypeError) as e: print(f"Error: {e}")
By completing Week 2, you have learned:
Create a program that:
Write a program that:
Develop a text processing tool that: