W3
Beginner 3 sessions • 6 hours Python

Week 3: Data Manipulation with Pandas

.ipynb
Follow along in JupyterDownload the complete Week 3 notebook — every code example ready to run.
Download Notebook

Data Science Fundamentals Course

Week 3 Overview

Week 3 marks your transition from basic Python to data science libraries. NumPy is the foundation for numerical computing in Python, and pandas builds on NumPy to provide powerful data manipulation capabilities. These two libraries are essential for any data science work. This week you will learn:

  • NumPy arrays: the core data structure for scientific computing
  • How to perform efficient numerical operations
  • Broadcasting: a powerful feature for working with arrays of different shapes
  • Introduction to pandas: for handling and analyzing tabular data
  • Creating and manipulating DataFrames
  • Loading real data from files

By the end of Week 3, you will be able to:

  • Create and manipulate NumPy arrays efficiently
  • Use array indexing and slicing for data selection
  • Perform mathematical and statistical operations on arrays
  • Understand NumPy broadcasting
  • Create and manipulate pandas Series and DataFrames
  • Load data from CSV, Excel, and other formats
  • Perform basic data exploration

Week 3 is divided into three 2-hour sessions:

  • Session 1: NumPy Basics and Array Operations
  • Session 2: Advanced NumPy: Broadcasting and Functions
  • Session 3: Introduction to pandas and DataFrames

SESSION 1: NumPy Basics and Array Operations

Duration: 2 hours

1.1 What is NumPy?

NumPy (Numerical Python) is the fundamental package for numerical computing in Python. It provides:

  • Efficient array data structures
  • Mathematical and statistical functions
  • Linear algebra operations
  • Random number generation
  • Tools for integrating C/C++ and Fortran code

Why NumPy instead of lists?

  • Speed: NumPy operations are much faster than Python loops
  • Convenience: Intuitive mathematical operations on arrays
  • Memory efficiency: NumPy arrays use less memory than lists
  • Functionality: Extensive mathematical and statistical functions
  • Broadcasting: Elegant way to handle arrays of different shapes

Installing NumPy

If you installed Anaconda, NumPy is already included. Otherwise, install it:

pip install numpy

Verify installation:

import numpy as np
print(np.__version__)

1.2 Creating NumPy Arrays

import numpy as np

# Create array from Python list
arr1 = np.array([1, 2, 3, 4, 5])
print(arr1) # Output: [1 2 3 4 5]
print(type(arr1)) # Output: <class 'numpy.ndarray'>

# Create 2D array (matrix)
arr2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
print(arr2d)
# Output:
# [[1 2 3]
# [4 5 6]
# [7 8 9]]

# Create arrays with specific values
zeros = np.zeros(5) # Array of 5 zeros
print(zeros) # Output: [0. 0. 0. 0. 0.]

ones = np.ones((3, 4)) # 3x4 array of ones
print(ones)
# Output:
# [[1. 1. 1. 1.]
# [1. 1. 1. 1.]
# [1. 1. 1. 1.]]

# Array filled with specific value
full = np.full((2, 3), 7) # 2x3 array filled with 7
print(full)
# Output:
# [[7 7 7]
# [7 7 7]]

# Identity matrix (1s on diagonal, 0s elsewhere)
identity = np.eye(3)
print(identity)
# Output:
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]

# Create range of values
range_arr = np.arange(0, 10, 2) # start, stop, step
print(range_arr) # Output: [0 2 4 6 8]

# Evenly spaced values
linspace = np.linspace(0, 10, 5) # 5 values from 0 to 10
print(linspace) # Output: [ 0. 2.5 5. 7.5 10. ]

# Random values
random_arr = np.random.rand(3, 3) # 3x3 random array [0, 1)
print(random_arr)

# Random integers
random_int = np.random.randint(1, 10, size=(2, 3)) # Random ints 1-9
print(random_int)

Array Properties

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

# Shape: dimensions of array
print(arr.shape) # Output: (2, 3) - 2 rows, 3 columns

# Size: total number of elements
print(arr.size) # Output: 6

# Dtype: data type of elements
print(arr.dtype) # Output: int64

# Ndim: number of dimensions
print(arr.ndim) # Output: 2

# Real-world: Check data dimensions
image = np.random.rand(1920, 1080, 3) # RGB image
print(f"Image shape: {image.shape}") # Output: Image shape: (1920, 1080, 3)
print(f"Total pixels: {image.size}") # Output: Total pixels: 6220800

1.3 Indexing and Slicing

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

# Access single element
print(arr[0]) # Output: 10
print(arr[2]) # Output: 30
print(arr[-1]) # Output: 50

# Slicing
print(arr[1:4]) # Output: [20 30 40]
print(arr[:3]) # Output: [10 20 30]
print(arr[2:]) # Output: [30 40 50]
print(arr[::2]) # Every 2nd element: [10 30 50]

# 2D array indexing
arr2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

# Access single element
print(arr2d[0, 0]) # Output: 1
print(arr2d[1, 2]) # Output: 6
print(arr2d[-1, -1]) # Output: 9

# Access entire row
print(arr2d[0]) # Output: [1 2 3]
print(arr2d[1]) # Output: [4 5 6]

# Access entire column
print(arr2d[:, 0]) # Output: [1 4 7]
print(arr2d[:, 2]) # Output: [3 6 9]

# 2D slicing
print(arr2d[0:2, 1:3]) # First 2 rows, columns 1-2
# Output:
# [[2 3]
# [5 6]]

# Modify elements
arr2d[0, 0] = 100
print(arr2d)
# Output:
# [[100 2 3]
# [ 4 5 6]
# [ 7 8 9]]

# Modifying slices affects original array!
arr = np.array([1, 2, 3, 4, 5])
arr[1:3] = [20, 30]
print(arr) # Output: [ 1 20 30 4 5]

Boolean Indexing

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# Create boolean mask
mask = arr > 5
print(mask) # Output: [False False False False False True True True True True]

# Use mask to filter
filtered = arr[mask]
print(filtered) # Output: [ 6 7 8 9 10]

# Combine conditions
mask2 = (arr > 3) & (arr < 8)
print(arr[mask2]) # Output: [4 5 6 7]

# Using conditions directly
print(arr[arr > 5]) # Output: [ 6 7 8 9 10]
print(arr[arr % 2 == 0]) # Even numbers: [ 2 4 6 8 10]

# Real-world: Filter data
scores = np.array([45, 52, 78, 95, 88, 62, 91, 55])
passing = scores[scores >= 60]
print(f"Passing scores: {passing}")
# Output: Passing scores: [78 95 88 62 91]

# Count elements meeting condition
count = np.sum(scores >= 70)
print(f"Students with score >= 70: {count}") # Output: Students with score >= 70: 4

SESSION 2: Advanced NumPy - Broadcasting and Functions

Duration: 2 hours

2.1 Arithmetic Operations and Broadcasting

import numpy as np

# Element-wise operations
a = np.array([1, 2, 3, 4, 5])
b = np.array([10, 20, 30, 40, 50])

print(a + b) # Output: [11 22 33 44 55]
print(a - b) # Output: [-9 -18 -27 -36 -45]
print(a * b) # Output: [10 40 90 160 250]
print(b / a) # Output: [10. 10. 10. 10. 10.]
print(a ** 2) # Output: [ 1 4 9 16 25]

# Operations with scalars
arr = np.array([1, 2, 3, 4, 5])
print(arr + 10) # Output: [11 12 13 14 15]
print(arr * 2) # Output: [ 2 4 6 8 10]
print(arr / 2) # Output: [0.5 1. 1.5 2. 2.5]

# 2D operations
arr2d = np.array([[1, 2, 3],
[4, 5, 6]])

print(arr2d * 2)
# Output:
# [[ 2 4 6]
# [ 8 10 12]]

# Comparison operations
scores = np.array([45, 78, 92, 55, 88])
print(scores > 70) # Output: [False True True False True]
print(scores == 88) # Output: [False False False False True]
print(scores != 70) # Output: [ True True True True True]

Broadcasting

Broadcasting is NumPy's mechanism for working with arrays of different shapes. It allows you to perform operations on arrays without explicitly replicating data.

# Broadcasting with scalar
arr = np.array([1, 2, 3, 4])
scalar = 5
print(arr + scalar) # Output: [6 7 8 9]

# Broadcasting 1D with 2D
arr2d = np.array([[1, 2, 3],
[4, 5, 6]])
arr1d = np.array([10, 20, 30])

result = arr2d + arr1d
print(result)
# Output:
# [[11 22 33]
# [14 25 36]]

# Broadcasting visualization:
# arr2d: arr1d: result:
# [[1 2 3] [10 20 30] [[11 22 33]
# [4 5 6]] + (broadcast) [14 25 36]]

# Column broadcasting
col = np.array([[10], [20]]) # Shape (2, 1)
arr2d = np.array([[1, 2, 3],
[4, 5, 6]]) # Shape (2, 3)

result = arr2d + col
print(result)
# Output:
# [[11 12 13]
# [24 25 26]]

# Real-world: Normalize data (subtract mean from each column)
data = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=float)

column_means = np.array([4., 5., 6.]) # Mean of each column
normalized = data - column_means
print(normalized)
# Output:
# [[-3. -3. -3.]
# [ 0. 0. 0.]
# [ 3. 3. 3.]]

2.2 Mathematical and Statistical Functions

import numpy as np

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

# Basic statistics
print(np.sum(arr)) # Output: 55
print(np.mean(arr)) # Output: 5.5
print(np.median(arr)) # Output: 5.5
print(np.std(arr)) # Standard deviation
print(np.var(arr)) # Variance

# Min and Max
print(np.min(arr)) # Output: 1
print(np.max(arr)) # Output: 10
print(np.argmin(arr)) # Index of minimum: 0
print(np.argmax(arr)) # Index of maximum: 9

# Mathematical functions
print(np.sqrt(arr)) # Square root
print(np.exp(arr)) # e^x
print(np.log(arr)) # Natural logarithm
print(np.abs(np.array([-1, -2, 3, -4]))) # Absolute value

# Rounding
decimals = np.array([1.234, 2.567, 3.891])
print(np.round(decimals, 1)) # Output: [1.2 2.6 3.9]
print(np.floor(decimals)) # Output: [1. 2. 3.]
print(np.ceil(decimals)) # Output: [2. 3. 4.]

# Operations along axis
arr2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])

print(np.sum(arr2d)) # Total sum: 45
print(np.sum(arr2d, axis=0)) # Sum of each column: [12 15 18]
print(np.sum(arr2d, axis=1)) # Sum of each row: [ 6 15 24]

print(np.mean(arr2d, axis=0)) # Mean of each column
# Output: [4. 5. 6.]

# Real-world: Calculate statistics on test scores
scores = np.array([45, 78, 92, 55, 88, 62, 91, 75])
print(f"Mean score: {np.mean(scores):.2f}")
print(f"Median score: {np.median(scores):.2f}")
print(f"Std dev: {np.std(scores):.2f}")
print(f"Best score: {np.max(scores)}")
print(f"Worst score: {np.min(scores)}")

Sorting and Unique Values

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

# Sorting
sorted_arr = np.sort(arr)
print(sorted_arr) # Output: [1 1 2 3 4 5 5 6 9]

# Get indices that would sort the array
indices = np.argsort(arr)
print(indices) # Output: [1 3 6 0 2 4 8 7 5]

# Reverse sort
reverse_sorted = np.sort(arr)[::-1]
print(reverse_sorted) # Output: [9 6 5 5 4 3 2 1 1]

# Unique values
unique = np.unique(arr)
print(unique) # Output: [1 2 3 4 5 6 9]

# Count occurrences
unique_vals, counts = np.unique(arr, return_counts=True)
for val, count in zip(unique_vals, counts):
print(f"Value {val} appears {count} times")

# Real-world: Find most common score
scores = np.array([78, 92, 78, 55, 88, 78, 91, 75])
unique_scores, counts = np.unique(scores, return_counts=True)
most_common_idx = np.argmax(counts)
most_common_score = unique_scores[most_common_idx]
print(f"Most common score: {most_common_score} (appears {counts[most_common_idx]} times)")

Reshaping and Combining Arrays

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

# Reshape to 2D
arr_2d = arr.reshape(2, 3)
print(arr_2d)
# Output:
# [[1 2 3]
# [4 5 6]]

# Reshape to 3D
arr_3d = arr.reshape(2, 3, 1)
print(arr_3d)

# Flatten: convert to 1D
flattened = arr_2d.flatten()
print(flattened) # Output: [1 2 3 4 5 6]

# Transpose (swap rows and columns)
transposed = arr_2d.T
print(transposed)
# Output:
# [[1 4]
# [2 5]
# [3 6]]

# Concatenate arrays
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
combined = np.concatenate([a, b])
print(combined) # Output: [1 2 3 4 5 6]

# Stack arrays vertically
arr1 = np.array([1, 2, 3])
arr2 = np.array([4, 5, 6])
stacked_v = np.vstack([arr1, arr2])
print(stacked_v)
# Output:
# [[1 2 3]
# [4 5 6]]

# Stack arrays horizontally
stacked_h = np.hstack([arr1, arr2])
print(stacked_h) # Output: [1 2 3 4 5 6]

# Split array
arr = np.array([1, 2, 3, 4, 5, 6])
split_result = np.split(arr, 3) # Split into 3 equal parts
print(split_result) # Output: [array([1, 2]), array([3, 4]), array([5, 6])]

SESSION 3: Introduction to pandas and DataFrames

Duration: 2 hours

3.1 What is pandas?

pandas is built on top of NumPy and provides high-level data structures and manipulation tools. The two main data structures are:

  • Series: 1-dimensional array with labels (like a dictionary)
  • DataFrame: 2-dimensional table with labeled rows and columns (like a spreadsheet)

Why pandas?

  • Handles missing data easily
  • Labeled axes (rows and columns have names, not just numbers)
  • Flexible data alignment and reshaping
  • Powerful grouping and aggregation
  • Easy data import from various formats
  • Integration with NumPy and other libraries

Installing pandas

pandas comes with Anaconda. Or install:

pip install pandas

3.2 pandas Series

import pandas as pd
import numpy as np

# Create Series from list
s1 = pd.Series([10, 20, 30, 40, 50])
print(s1)
# Output:
# 0 10
# 1 20
# 2 30
# 3 40
# 4 50
# dtype: int64

# Series with custom index
s2 = pd.Series([10, 20, 30, 40, 50],
index=['a', 'b', 'c', 'd', 'e'])
print(s2)
# Output:
# a 10
# b 20
# c 30
# d 40
# e 50
# dtype: int64

# Access elements by index label
print(s2['a']) # Output: 10
print(s2['c']) # Output: 30

# Create Series from dictionary
student_grades = {'Alice': 85, 'Bob': 92, 'Charlie': 78, 'Diana': 95}
s3 = pd.Series(student_grades)
print(s3)
# Output:
# Alice 85
# Bob 92
# Charlie 78
# Diana 95
# dtype: int64

# Series properties
print(s3.index) # Index(['Alice', 'Bob', 'Charlie', 'Diana'], dtype='object')
print(s3.values) # [85 92 78 95]
print(len(s3)) # Output: 4

# Series operations
print(s3 + 5) # Add 5 to all values
print(s3[s3 > 80]) # Values greater than 80
# Output:
# Alice 85
# Bob 92
# Diana 95
# dtype: int64

# Series methods
print(s3.mean()) # Output: 87.5
print(s3.min()) # Output: 78
print(s3.max()) # Output: 95
print(s3.sum()) # Output: 350

# Real-world: Track daily temperature
dates = pd.Series([20, 22, 18, 25, 23, 19, 21],
index=['Monday', 'Tuesday', 'Wednesday', 'Thursday',
'Friday', 'Saturday', 'Sunday'])
print(dates)
print(f"Average temperature: {dates.mean():.1f}")
print(f"Hottest day: {dates.idxmax()} ({dates.max()}°C)")

3.3 DataFrames: Tabular Data

A DataFrame is a 2D table with labeled rows and columns. Think of it as a spreadsheet or SQL table.

import pandas as pd

# Create DataFrame from dictionary of lists
data = {
'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'Age': [25, 30, 28, 32],
'City': ['Lagos', 'Accra', 'Nairobi', 'Johannesburg'],
'Salary': [60000, 75000, 65000, 80000]
}

df = pd.DataFrame(data)
print(df)
# Output:
# Name Age City Salary
# 0 Alice 25 Lagos 60000
# 1 Bob 30 Accra 75000
# 2 Charlie 28 Nairobi 65000
# 3 Diana 32 Johannesburg 80000

# DataFrame properties
print(df.shape) # Output: (4, 4) - 4 rows, 4 columns
print(df.columns) # Index(['Name', 'Age', 'City', 'Salary'], dtype='object')
print(df.index) # RangeIndex(start=0, stop=4, step=1)
print(df.dtypes) # Data types of each column
print(df.info()) # Summary information

# View first/last rows
print(df.head()) # First 5 rows (default)
print(df.head(2)) # First 2 rows
print(df.tail()) # Last 5 rows

# Access columns
print(df['Name']) # Get 'Name' column as Series
print(df['Age']) # Get 'Age' column

# Access multiple columns
print(df[['Name', 'City']]) # Get multiple columns as DataFrame

# Access rows by index
print(df.loc[0]) # First row by label
print(df.loc[2]) # Row with index 2

# Access by position
print(df.iloc[0]) # First row by position
print(df.iloc[1, 2]) # Row 1, column 2 (Row 1, 'City')

# Basic statistics
print(df.describe()) # Summary statistics for numeric columns
print(df['Salary'].mean()) # Average salary
print(df['Age'].max()) # Maximum age

Modifying DataFrames

import pandas as pd

df = pd.DataFrame({
'Name': ['Alice', 'Bob', 'Charlie'],
'Age': [25, 30, 28],
'Score': [85, 92, 78]
})

# Add new column
df['Grade'] = ['B', 'A', 'C']
print(df)

# Calculate new column from existing
df['Age_in_months'] = df['Age'] * 12
print(df)

# Modify existing column
df['Score'] = df['Score'] + 5 # Add 5 to all scores
print(df)

# Drop column
df = df.drop('Age_in_months', axis=1) # axis=1 for column
print(df)

# Drop row
df = df.drop(1, axis=0) # axis=0 for row
print(df)

# Rename columns
df = df.rename(columns={'Score': 'Test_Score', 'Grade': 'Letter_Grade'})
print(df)

# Sort by column
df = df.sort_values('Test_Score', ascending=False)
print(df)

# Filter rows
high_scores = df[df['Test_Score'] > 85]
print(high_scores)

3.4 Loading Data from Files

import pandas as pd

# Read CSV file
df = pd.read_csv('data.csv')

# Read with options
df = pd.read_csv('data.csv',
sep=',', # Delimiter
encoding='utf-8', # File encoding
nrows=100) # Read only first 100 rows

# Read Excel file
df = pd.read_excel('data.xlsx', sheet_name=0) # First sheet

# Read from multiple sheets
xls = pd.ExcelFile('data.xlsx')
print(xls.sheet_names) # List all sheets
df_sheet1 = pd.read_excel('data.xlsx', sheet_name='Sheet1')
df_sheet2 = pd.read_excel('data.xlsx', sheet_name=1)

# Read from other formats
df = pd.read_json('data.json')
df = pd.read_html('webpage.html') # Read tables from HTML

# Example: Create and save CSV
df = pd.DataFrame({
'Product': ['Apple', 'Banana', 'Orange'],
'Price': [0.50, 0.30, 0.60],
'Quantity': [100, 150, 80]
})

# Save to CSV
df.to_csv('products.csv', index=False)

# Save to Excel
df.to_excel('products.xlsx', index=False)

# Save to JSON
df.to_json('products.json')

# Real-world: Load and explore data
df = pd.read_csv('student_data.csv')
print(f"Dataset shape: {df.shape}")
print(f"Columns: {list(df.columns)}")
print(df.head(10))
print(f"\nData types:\n{df.dtypes}")
print(f"\nMissing values:\n{df.isnull().sum()}")
print(f"\nBasic statistics:\n{df.describe()}")

Practical Example: Data Exploration

import pandas as pd
import numpy as np

# Create sample dataset
np.random.seed(42)
df = pd.DataFrame({
'Student': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
'Math': [85, 92, 78, 88, 95],
'English': [90, 85, 88, 92, 89],
'Science': [88, 88, 92, 85, 93]
})

print("Dataset:")
print(df)
print()

# Calculate average score for each student
df['Average'] = df[['Math', 'English', 'Science']].mean(axis=1)
print(df)
print()

# Find best subject
print("Best scores in each subject:")
print(df[['Math', 'English', 'Science']].max())
print()

# Find students with average > 85
excellent = df[df['Average'] > 85]
print("Students with average > 85:")
print(excellent)
print()

# Summary statistics
print("Summary statistics:")
print(df[['Math', 'English', 'Science']].describe())
print()

# Top performer
top_student_idx = df['Average'].idxmax()
top_student = df.loc[top_student_idx, 'Student']
print(f"Top performer: {top_student}")

Week 3 Summary

By completing Week 3, you have learned:

  • NumPy arrays: the foundation of numerical computing
  • Creating arrays: from lists, ranges, zeros, ones, random values
  • Array properties: shape, size, dtype, ndim
  • Indexing and slicing 1D and 2D arrays
  • Boolean indexing for filtering data
  • Arithmetic operations on arrays (element-wise)
  • Broadcasting: working with arrays of different shapes
  • Mathematical functions: sum, mean, median, std, sqrt, exp, log
  • Statistical operations: min, max, argmin, argmax
  • Sorting, unique values, and value counting
  • Reshaping and combining arrays
  • pandas Series: 1D labeled data
  • pandas DataFrames: 2D tabular data
  • Loading data from CSV, Excel, and other formats
  • Basic DataFrame exploration and manipulation

Week 3 Assignments

Assignment 1: NumPy Operations

Create a Python script that:

  • Generates a 10x10 random matrix (values 0-100)
  • Calculates row-wise mean, column-wise mean, and overall mean
  • Finds the maximum and minimum values and their positions
  • Normalizes the matrix (subtract mean, divide by std dev)
  • Uses boolean indexing to find all values > 75
  • Reshapes a 1D array into a 2D array and performs calculations

Assignment 2: Data Analysis with pandas

Create a dataset and analyze it:

  • Create a DataFrame with at least 50 rows and 5 columns (you can use random data or a public dataset)
  • Use head(), tail(), info(), describe() to explore the data
  • Calculate summary statistics by groups
  • Filter data based on multiple conditions
  • Create new columns based on existing data
  • Identify and report patterns or trends
  • Save your analysis results

Assignment 3: Real Dataset Exploration

Download a real dataset (from Kaggle, UCI, or government sources) and:

  • Load the data into a DataFrame
  • Perform complete exploratory analysis
  • Create a summary report with insights
  • Save clean data for later analysis
  • Document your findings in a text file
  • Create functions for common NumPy operations (normalize, standardize, outlier detection)
  • Practice array slicing with different combinations of indices
  • Implement mathematical operations without using built-in functions
  • Create a data cleaning pipeline (handle missing values, duplicates)
  • Practice merging DataFrames with different join types
  • Create visualizations of your data (using pandas .plot() method)
  • Load multiple datasets and combine them
  • Practice filtering and sorting operations on real datasets

Additional Resources

Books

  • Chapter 4: NumPy Basics - "Python for Data Analysis" by Wes McKinney
  • Chapter 5: Getting Started with pandas - "Python for Data Analysis" by Wes McKinney
  • Data Science from Scratch by Joel Grus - NumPy chapter

Online Documentation

  • NumPy Documentation: https://numpy.org/doc/
  • pandas Documentation: https://pandas.pydata.org/docs/
  • NumPy Tutorial: https://numpy.org/doc/stable/user/basics.broadcasting.html
  • pandas Getting Started: https://pandas.pydata.org/docs/getting_started/index.html

Practice Datasets

  • Kaggle: https://www.kaggle.com/ - Thousands of free datasets
  • UCI Machine Learning Repository: https://archive.ics.uci.edu/ml/
  • Google Dataset Search: https://datasetsearch.research.google.com/
  • Seaborn Datasets: Pre-built datasets in seaborn library