Back
In print settings: Save as PDF, turn headers and footers off, turn background graphics on.

Python for Scientific Computing

A comprehensive journey through Python, focusing on data science, symbolic math, machine learning, and high-end scientific visualization.

Official Documentation

July 2026

Contents

Python Basics

  • The Python Ecosystem and Philosophy

NumPy Deep Dive

  • NumPy: The Foundation of Numerical Computing
  • Advanced NumPy: Indexing, Slicing, and Reshaping
  • The Rules of NumPy Broadcasting

SymPy Deep Dive

  • SymPy: Symbolic Mathematics in Python
  • Advanced SymPy: Calculus, Differential Equations, and Optimization
  • SymPy: Matrices, Eigenvalues, and Linear Algebra

Pandas Deep Dive

  • Pandas: High-Performance Data Structures
  • Pandas: Data Cleaning and Wrangling
  • Pandas: Time Series Analysis

Scikit-Learn Deep Dive

  • Scikit-Learn: Machine Learning Principles
  • Scikit-Learn: Supervised Learning Deep Dive
  • Scikit-Learn: Unsupervised Learning

Advanced Scientific Stack

  • SciPy: Essential Scientific Algorithms

Data Visualization Deep Dive

  • Matplotlib: The Figure and Axes Architecture
  • Matplotlib: Styling and Customization
  • Seaborn: Statistical Data Visualization
  • Scientific Storytelling: The Art of the Visual

Advanced Scientific Stack

  • Capstone: Integrating the Full Scientific Stack

Python Basics

Section Detail

The Python Ecosystem and Philosophy

The Philosophy of Python

Python is a high-level, interpreted programming language known for its readability and versatility. Often described as “executable pseudocode,” Python’s syntax allows developers to express concepts in fewer lines of code than might be possible in languages such as C++ or Java.

The Zen of Python

Python’s design is guided by a set of principles known as The Zen of Python (PEP 20). These principles emphasize simplicity, clarity, and beauty.

  1. Beautiful is better than ugly.
  2. Explicit is better than implicit.
  3. Simple is better than complex.
  4. Readability counts.

In the context of scientific computing, these principles are invaluable. Scientists and researchers need a language that stays out of their way, allowing them to focus on algorithms and data rather than memory management or boilerplate syntax.

Why Python for Science?

Python has become the de facto standard for data science, machine learning, and scientific research for several key reasons:

1. The “Glue Language” Property

Python is exceptionally good at interfaced with other languages. Most performance-critical scientific libraries (like NumPy, SciPy, and TensorFlow) are actually written in C, C++, or Fortran for speed, with Python providing a high-level “glue” interface that is easy to use.

2. Rich Library Ecosystem

Instead of reinventing the wheel, Python users leverage a massive ecosystem of specialized libraries:

  • NumPy: The foundation for numerical computing.
  • Pandas: Essential for data manipulation and analysis.
  • Matplotlib/Seaborn: For data visualization.
  • SciPy: For advanced scientific calculations (integration, optimization).
  • SymPy: For symbolic mathematics.
  • Scikit-Learn: The standard for classical machine learning.

3. Community and Documentation

The scientific Python community (SciPy stack) is one of the most robust in the world, ensuring that libraries are well-maintained and that help is always available.

The Execution Model

Python uses a bytecode-interpreted execution model. While slower than compiled languages for raw loop execution, its efficient C-based library backends often make scientific Python code nearly as fast as hand-coded C for vectorizable operations.

Code
skinparam componentStyle rectangle

package "Development" {
component "Python Source (.py)" as SRC
}

node "Execution Environment" {
component "CPython Interpreter" as INT
component "Bytecode Compiler" as COMP
component "PVM (Python Virtual Machine)" as PVM
}

node "Native Code" {
component "NumPy Core (C/Fortran)" as NP
component "System Libs" as SYS
}

SRC --> COMP
COMP --> INT : "Bytecode (.pyc)"
INT --> PVM
PVM <-> NP : "Vectorized Operations"
PVM <-> SYS
DevelopmentExecution EnvironmentNative CodePython Source (.py)CPython InterpreterBytecode CompilerPVM (Python Virtual Machine)NumPy Core (C/Fortran)System LibsBytecode (.pyc)Vectorized Operations

Getting Started

Let’s look at a simple Python script that demonstrates its clean syntax.

python
1 
2import math
3 
4def calculate_circle_area(radius):
5 """Simple function to calculate circle area."""
6 return math.pi * (radius ** 2)
7 
8# Calculate areas for a range of radii
9radii = [1, 2, 3, 4, 5]
10areas = [calculate_circle_area(r) for r in radii]
11 
12for r, a in zip(radii, areas):
13 print(f"Radius: {r}, Area: {a:.2f}")
14 

In the following modules, we will dive deep into how to leverage this simplicity for complex scientific tasks.

NumPy Deep Dive

Section Detail

NumPy: The Foundation of Numerical Computing

The Importance of NumPy

NumPy (Numerical Python) is the fundamental package for scientific computing in Python. It provides a high-performance multidimensional array object and tools for working with these arrays. If you are doing any form of data science or scientific computing in Python, NumPy is the engine under the hood.

Why not use Python Lists?

Python lists are incredibly flexible—they can hold elements of different types, and they grow dynamically. However, this flexibility comes at a significant performance cost.

The Problem with Python Lists:

  1. Memory Overhead: Each element in a Python list is a full-fledged object. A list of integers doesn’t just store the numbers; it stores pointers to integer objects, which each contain type information and reference counts.
  2. Lack of Locality: Because list elements are pointers, they can be scattered across memory. This prevents hardware-level optimizations like CPU caching and pre-fetching.
  3. Looping Speed: Iterating over a Python list in a loop is slow because the interpreter must check the type and perform dispatching for every single operation.

The NumPy Solution: ndarray

The core of NumPy is the ndarray (n-dimensional array) object.

  • Contiguous Memory: ndarrays store data in a single, contiguous block of memory.
  • Homogeneous Types: Every element in a NumPy array must be of the same type (e.g., all 64-bit floats).
  • Vectorized Operations: Operations on NumPy arrays are performed by compiled C/Fortran code, which can process entire arrays at once without Python loop overhead.

Creating Arrays

NumPy provides multiple ways to initialize arrays.

python
1 
2import numpy as np
3 
4# From a list
5a = np.array([1, 2, 3, 4, 5])
6print("From list:", a)
7 
8# Filled with zeros or ones
9zeros = np.zeros((3, 3))
10ones = np.ones((2, 4))
11print("Zeros:\n", zeros)
12 
13# Ranges
14arange = np.arange(0, 10, 2)
15linspace = np.linspace(0, 1, 5) # 5 points between 0 and 1
16print("Arange:", arange)
17print("Linspace:", linspace)
18 
19# Random numbers
20rand = np.random.rand(2, 2)
21print("Random:\n", rand)
22 

Array Attributes

Every ndarray has properties that describe its shape, size, and data type.

  • shape: A tuple indicating the size of each dimension (e.g., (rows, cols)).
  • ndim: The number of dimensions (axes).
  • size: The total number of elements.
  • dtype: The data type of the elements (e.g., int32, float64).
python
1 
2import numpy as np
3 
4arr = np.array([[1, 2, 3], [4, 5, 6]])
5 
6print(f"Shape: {arr.shape}")
7print(f"Dimensions: {arr.ndim}")
8print(f"Size: {arr.size}")
9print(f"Data Type: {arr.dtype}")
10 

Vectorization: The Secret Sauce

Vectorization is the process of replacing explicit Python loops with array expressions. This is the primary way to achieve high performance in NumPy.

Consider adding two large vectors:

python
1 
2import numpy as np
3import time
4 
5size = 1000000
6a = list(range(size))
7b = list(range(size))
8 
9# Python Loop approach
10start = time.time()
11c = [a[i] + b[i] for i in range(size)]
12end = time.time()
13print(f"Python list addition time: {end - start:.4f}s")
14 
15# NumPy approach
16na = np.arange(size)
17nb = np.arange(size)
18start = time.time()
19nc = na + nb
20end = time.time()
21print(f"NumPy vector addition time: {end - start:.4f}s")
22 

As you can see, the NumPy version is orders of magnitude faster. It’s not just “shorter code”—it’s fundamentally different execution.

Memory Layout and Slicing

NumPy slicing is unique because it creates views of the data rather than copies. This is extremely efficient for large datasets but requires caution: changing a slice changes the original array.

python
1 
2import numpy as np
3 
4arr = np.array([0, 1, 2, 3, 4, 5])
5s = arr[1:4]
6s[0] = 99
7 
8print("Original array after modifying slice:", arr)
9 

In the next module, we will explore advanced indexing and multi-dimensional array manipulations.

Section Detail

Advanced NumPy: Indexing, Slicing, and Reshaping

Multi-Dimensional Indexing

In the previous module, we touched on basic slicing. However, NumPy’s true power comes from its ability to manipulate multi-dimensional arrays with surgical precision.

Comma-Separated Indexing

Unlike Python lists, where you access nested elements using list[i][j], NumPy allows you to use a single set of brackets with comma-separated indices: arr[i, j]. This is not just syntactic sugar; it is more efficient and allows for complex expressions.

python
1 
2import numpy as np
3 
4arr = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]])
5 
6print("Element at [1, 2]:", arr[1, 2]) # Row 1, Col 2
7print("All rows, first column:", arr[:, 0])
8print("First two rows, last two columns:\n", arr[:2, 1:])
9 

Integer Array Indexing (Fancy Indexing)

Fancy indexing is a term for passing arrays of indices to access multiple array elements at once.

python
1 
2import numpy as np
3 
4a = np.arange(12).reshape((3, 4))
5print("Array:\n", a)
6 
7# Select elements at [0,0], [1,1], [2,0]
8rows = np.array([0, 1, 2])
9cols = np.array([0, 1, 0])
10print("\nSelected elements:", a[rows, cols])
11 

Boolean Indexing (Masking)

This is perhaps the most useful feature for data processing. You can index an array using another array of booleans of the same shape.

python
1 
2import numpy as np
3 
4arr = np.array([1, 2, 3, 4, 5, 6])
5 
6# Create a mask where elements are greater than 3
7mask = arr > 3
8print("Mask:", mask)
9 
10# Apply the mask
11print("Filtered Array:", arr[mask])
12 
13# Complex conditions
14mask_complex = (arr > 2) & (arr < 6)
15print("Filtered (2 < x < 6):", arr[mask_complex])
16 

Shape Manipulation

Frequently, data arrives in a shape that doesn’t match the input requirements of your algorithm (e.g., deep learning models often require a batch dimension).

Reshape

reshape() gives a new shape to an array without changing its data.

python
1 
2import numpy as np
3 
4# 1D array of 0-11
5a = np.arange(12)
6print("1D array:", a)
7 
8# Reshape to 3x4
9a_2d = a.reshape((3, 4))
10print("\n3x4 array:\n", a_2d)
11 
12# Flatten back to 1D
13print("\nFlattened:", a_2d.flatten())
14 

Stacking and Splitting

Combining multiple arrays into one or splitting one into many.

python
1 
2import numpy as np
3 
4x = np.array([1, 2, 3])
5y = np.array([4, 5, 6])
6 
7# Vertical Stack
8print("V-Stack:\n", np.vstack((x, y)))
9 
10# Horizontal Stack
11print("H-Stack:", np.hstack((x, y)))
12 

Broadcasting: A Preview

If you try to add a scalar to an array, NumPy implicitly expands the scalar to match the array’s shape. This is called broadcasting.

python
1 
2import numpy as np
3 
4arr = np.array([[1, 2, 3], [4, 5, 6]])
5print("Array + 10:\n", arr + 10)
6 

In the next module, we will explore the formal rules of broadcasting and how it allows for high-performance operations without unnecessary memory replication.

Section Detail

The Rules of NumPy Broadcasting

Efficient Operations with Broadcasting

Broadcasting is a powerful mechanism that allows NumPy to perform arithmetic operations on arrays of different shapes. Usually, for element-wise operations, the arrays must be the same size. Broadcasting allows “stretching” the smaller array across the larger one to make them compatible.

The Mental Model

Imagine you have a 3×33 \times 3 matrix and you want to add a 1×31 \times 3 vector to every row. Using a loop is slow. Instead, NumPy “broadcasts” the vector.

Code
skinparam componentStyle rectangle

component "Row 1" as M1
component "Row 2" as M2
component "Row 3" as M3

component "Vector (1x3)" as V1

component "Result Row 1" as R1
component "Result Row 2" as R2
component "Result Row 3" as R3

M1 --> R1 : "+ V1"
M2 --> R2 : "+ V1"
M3 --> R3 : "+ V1"

note right of V1: "Vector values: [10, 20, 30]\nBroadcast across each matrix row"
note left of R1: "[11, 22, 33]"
note left of R2: "[14, 25, 36]"
note left of R3: "[17, 28, 39]"
Row 1Row 2Row 3Vector (1x3)Result Row 1Result Row 2Result Row 3"Vector values: [10, 20, 30]Broadcast across each matrixrow""[11, 22, 33]""[14, 25, 36]""[17, 28, 39]"V1V1V1

The Four Rules of Broadcasting

When operating on two arrays, NumPy compares their shapes element-wise. It starts with the trailing (rightmost) dimensions and works its way left. Two dimensions are compatible when:

  1. They are equal.
  2. One of them is 1.

Example 1: Scalar and Array

A scalar has effectively an infinite number of dimensions of size 1.

  • Array A: (3, 3)
  • Array B: (1, ) (Scalar) Result: (3, 3)

Example 2: Vector and Matrix

  • Array A: (3, 3)
  • Array B: (3, ) -> broadcast becomes (1, 3) Comparing trailing dimensions: 3 and 3 (Equal). Comparing next: 3 and 1 (One of them is 1). Result: (3, 3)

Example 3: Incompatible Shapes

  • Array A: (3, 3)
  • Array B: (2, ) Trailing dimensions: 3 and 2 (Not equal, neither is 1). Result: ValueError!

Broadcasting in Action

Let’s test these rules with code.

python
1 
2import numpy as np
3 
4# Rule 1: Matrix + Scalar
5m = np.ones((3, 3))
6print("Matrix + 5:\n", m + 5)
7 
8# Rule 2: Matrix + Row Vector
9v_row = np.array([1, 2, 3])
10print("\nMatrix + Row Vector (v.shape=(3,)):\n", m + v_row)
11 
12# Rule 3: Matrix + Column Vector
13v_col = np.array([[1], [2], [3]]) # Shape (3, 1)
14print("\nMatrix + Column Vector (v.shape=(3, 1)):\n", m + v_col)
15 

Centering an Array

Broadcasting is vital for data preprocessing. For instance, to “center” a dataset (subtract the mean of each feature), you can use broadcasting.

python
1 
2import numpy as np
3 
4# Random data: 10 samples, 3 features
5data = np.random.randn(10, 3)
6 
7# Calculate mean along the columns (feature mean)
8# Shape will be (3,)
9mean = data.mean(axis=0)
10 
11# Subtract mean from data
12# (10, 3) - (3,) -> (3,) is broadcast to (10, 3)
13centered_data = data - mean
14 
15print("Original shape:", data.shape)
16print("Mean shape:", mean.shape)
17print("Centered mean (should be ~0):", centered_data.mean(axis=0))
18 

Performance Benefits

Broadcasting is computationally efficient because it does not actually replicate the data in memory. The “stretching” is conceptual and handled at the C-level, minimizing memory bandwidth usage.

In the next module, we will apply these techniques to Linear Algebra operations like matrix multiplication and decompositions.

SymPy Deep Dive

Section Detail

SymPy: Symbolic Mathematics in Python

Introduction to Symbolic Computation

Most numerical libraries (like NumPy) work with floating-point numbers. While fast, floating-point numbers are always approximations. For example, 2\sqrt{2} in NumPy is represented as 1.4142135623730951. In symbolic mathematics, we want to keep it as 2\sqrt{2} to maintain exactness throughout our derivations.

SymPy is a Python library for symbolic mathematics. It aims to become a full-featured Computer Algebra System (CAS) while keeping the code as simple as possible in order to be comprehensible and easily extensible.

Symbols and Expressions

In SymPy, we must explicitly define variables as symbolic objects.

python
1 
2import sympy as sp
3 
4# Define symbols
5x, y = sp.symbols('x y')
6 
7# Create an expression
8expr = x**2 + 2*x + 1
9 
10print("Expression:", expr)
11print("Substituted (x=2):", expr.subs(x, 2))
12 

Structural Manipulations

One of the most powerful features of SymPy is the ability to simplify, expand, and factor expressions automatically.

Expansion and Factoring

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5poly = (x + 1)**3
6expanded = sp.expand(poly)
7factored = sp.factor(expanded)
8 
9print(f"Original: {poly}")
10print(f"Expanded: {expanded}")
11print(f"Factored: {factored}")
12 

Simplification

SymPy has a general-purpose simplify() function that attempts to find the most compact form of an expression.

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5expr = (x**2 + x) / x
6simplified = sp.simplify(expr)
7 
8print(f"Expression: {expr}")
9print(f"Simplified: {simplified}")
10 
11# Trigonometric simplification
12trig_expr = sp.sin(x)**2 + sp.cos(x)**2
13print(f"sin^2 + cos^2 = {sp.simplify(trig_expr)}")
14 

Calculus with SymPy

SymPy can perform various calculus operations exactly.

Differentiation

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5f = sp.sin(x) * sp.exp(x)
6derivative = sp.diff(f, x)
7 
8print(f"f(x) = {f}")
9print(f"f'(x) = {derivative}")
10 

Integration

SymPy can handle both definite and indefinite integrals.

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5# Indefinite integral
6indef = sp.integrate(sp.cos(x), x)
7print(f"Integral of cos(x): {indef}")
8 
9# Definite integral from 0 to pi/2
10def_int = sp.integrate(sp.exp(-x), (x, 0, sp.oo)) # oo is infinity
11print(f"Integral of e^-x from 0 to infinity: {def_int}")
12 

Limits

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5limit_val = sp.limit(sp.sin(x)/x, x, 0)
6print(f"Limit of sin(x)/x as x -> 0: {limit_val}")
7 

Equation Solving

SymPy can solve equations and systems of equations symbolically.

python
1 
2import sympy as sp
3x, y = sp.symbols('x y')
4 
5# Solve x^2 - 1 = 0
6solutions = sp.solve(x**2 - 1, x)
7print(f"Solutions for x^2 - 1 = 0: {solutions}")
8 
9# Solve system of linear equations
10# x + y = 5
11# x - y = 1
12sol_system = sp.solve([x + y - 5, x - y - 1], [x, y])
13print(f"System solution: {sol_system}")
14 

Pretty Printing and LaTeX

SymPy can output expressions in various formats, including LaTeX, which is beautiful for documentation and papers.

python
1 
2import sympy as sp
3x = sp.symbols('x')
4expr = sp.sqrt(x**2 + 1) / sp.sin(x)
5 
6print("Standard print:", expr)
7print("LaTeX:", sp.latex(expr))
8 

Bridging SymPy and NumPy: lambdify

Often you derive an expression symbolically in SymPy but need to evaluate it numerically for thousands of points using NumPy. The lambdify function creates a fast numerical function from a SymPy expression.

python
1 
2import sympy as sp
3import numpy as np
4 
5x = sp.symbols('x')
6expr = sp.sin(x)**2
7 
8# Convert to a numpy-friendly function
9f = sp.lambdify(x, expr, 'numpy')
10 
11# Now evaluate on a numpy array
12data = np.linspace(0, 10, 5)
13results = f(data)
14print("Numerical results from symbolic expr:", results)
15 

In the next modules, we will dive deeper into Matrix algebra and structural mechanics applications.

Section Detail

Advanced SymPy: Calculus, Differential Equations, and Optimization

Beyond Simple Algebra

While simplifying polynomials is useful, SymPy’s real power for scientists lies in its ability to solve complex calculus problems and differential equations—tasks that usually require expensive software like Mathematica or Maple.

Taylor Series Expansions

In physics and engineering, we often approximate complex functions using Taylor series. SymPy can generate these to any arbitrary order.

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5# Taylor series of sin(x) around 0 up to 10th order
6series = sp.series(sp.sin(x), x, 0, 10)
7print(f"Taylor series of sin(x):\n{series}")
8 
9# Remove the 'O' term for numerical evaluation
10func_approx = series.removeO()
11print(f"\nPolynomial version: {func_approx}")
12 

Solving Ordinary Differential Equations (ODEs)

SymPy can solve many classes of ODEs analytically. This is extremely useful for verifying numerical solvers or finding exact solutions for simple physical models.

Example: The Harmonic Oscillator

The equation is f(x)+ω2f(x)=0f''(x) + \omega^2 f(x) = 0.

python
1 
2import sympy as sp
3x = sp.symbols('x')
4f = sp.Function('f')
5omega = sp.symbols('omega', positive=True)
6 
7# Define the ODE
8diffeq = sp.Eq(f(x).diff(x, x) + omega**2 * f(x), 0)
9print(f"ODE: {diffeq}")
10 
11# Solve the ODE
12solution = sp.dsolve(diffeq, f(x))
13print(f"General Solution: {solution}")
14 

Multivariable Calculus

SymPy handles gradients, Jacobians, and Hessians with ease.

python
1 
2import sympy as sp
3x, y, z = sp.symbols('x y z')
4 
5f = x**2 + y**2 + z**2
6 
7# Partial derivatives
8df_dx = sp.diff(f, x)
9print(f"df/dx = {df_dx}")
10 
11# Gradient vector
12grad = [sp.diff(f, var) for var in (x, y, z)]
13print(f"Gradient: {grad}")
14 

Symbolic Linear Algebra

Calculating the eigenvalues or inverse of a matrix with symbols is a common requirement in theoretical research.

python
1 
2import sympy as sp
3a, b, c, d = sp.symbols('a b c d')
4 
5M = sp.Matrix([[a, b], [c, d]])
6print("Matrix M:\n", M)
7 
8# Determinant
9print("\nDeterminant:", M.det())
10 
11# Inverse
12print("\nInverse:\n", M.inv())
13 
14# Eigenvalues (returns a dict of value: multiplicity)
15print("\nEigenvalues:", M.eigenvals())
16 

Global Optimization (Symbolic)

If a function is simple enough, we can find its global minimum by solving where the derivative is zero.

python
1 
2import sympy as sp
3x = sp.symbols('x')
4 
5f = x**4 - 4*x**2 + 3
6 
7# Find critical points
8critical_points = sp.solve(sp.diff(f, x), x)
9print(f"Critical points: {critical_points}")
10 
11# Determine which is a minimum using second derivative
12for point in critical_points:
13 second_deriv = sp.diff(f, x, 2).subs(x, point)
14 nature = "minimum" if second_deriv > 0 else "maximum"
15 print(f"x = {point} is a {nature} (f''={second_deriv})")
16 

In the next section, we will integrate these symbolic results into numerical workflows using the SciPy stack.

Section Detail

SymPy: Matrices, Eigenvalues, and Linear Algebra

Symbolic Linear Algebra

While NumPy is the king of numerical linear algebra, SymPy allows us to perform linear algebra with variables. This is essential for deriving formulas, checking proofs, and solving systems where some parameters are not yet known.

Creating Symbolic Matrices

In SymPy, matrices are created using the Matrix class. Unlike NumPy arrays, these matrices are mutable by default and can contain any SymPy expression.

python
1 
2import sympy as sp
3x, y = sp.symbols('x y')
4 
5# Create a 2x2 matrix
6M = sp.Matrix([[x, y], [y, x]])
7print("Symbolic Matrix M:\n")
8sp.pprint(M)
9 
10# Basic operations
11print("\nMatrix Squared:\n")
12sp.pprint(M**2)
13 

Determinants and Inversion

Calculating a determinant symbolically is a common task in stability analysis and geometry.

python
1 
2import sympy as sp
3a, b, c, d = sp.symbols('a b c d')
4 
5# The general 2x2 matrix
6A = sp.Matrix([[a, b], [c, d]])
7 
8det_A = A.det()
9print(f"Determinant of A: {det_A}")
10 
11inv_A = A.inv()
12print("\nInverse of A:")
13sp.pprint(inv_A)
14 

Eigenvalues and Eigenvectors

One of the most powerful features of SymPy is calculating exact eigenvalues. Numerical methods might lose precision for nearly singular matrices or complex clusters; SymPy finds the exact roots of the characteristic polynomial.

python
1 
2import sympy as sp
3lam = sp.symbols('lambda')
4M = sp.Matrix([[3, -2], [4, -1]])
5 
6# Characteristic polynomial
7poly = M.charpoly(lam)
8print(f"Characteristic Polynomial: {poly.as_expr()}")
9 
10# Eigenvalues
11eigenvals = M.eigenvals()
12print(f"Eigenvalues: {eigenvals}") # Returns {value: multiplicity}
13 
14# Eigenvectors
15eigenvects = M.eigenvects()
16print("\nEigenvectors (Value, Multiplicity, Basis):")
17for v in eigenvects:
18 print(v)
19 

Matrix Decompositions

SymPy supports several decompositions, including LU, QR, and Diagonalization.

python
1 
2import sympy as sp
3M = sp.Matrix([[1, 2], [2, 1]])
4 
5# Diagonalization: M = P * D * P^-1
6P, D = M.diagonalize()
7 
8print("Diagonal Matrix D:")
9sp.pprint(D)
10print("\nTransformation Matrix P:")
11sp.pprint(P)
12 
13# Verify
14assert P * D * P.inv() == M
15print("\nVerification successful: P * D * P^-1 == M")
16 

Solving Linear Systems (Ax=bAx = b)

You can solve a system of linear equations by passing a matrix and a column vector to the LUsolve method or by using solve_linear_system.

python
1 
2import sympy as sp
3x1, x2 = sp.symbols('x1 x2')
4 
5# System:
6# x1 + 2*x2 = 5
7# 3*x1 + 4*x2 = 11
8 
9A = sp.Matrix([[1, 2], [3, 4]])
10b = sp.Matrix([5, 11])
11 
12sol = A.LUsolve(b)
13print(f"Solution vector: {sol}")
14 

Applications in Engineering

Symbolic matrices are often used to define Stiffness Matrices in structural analysis or Jacobian Matrices in robot kinematics. Because the variables are preserved, we can calculate the Jacobian once and then substitute specific joint angles thousands of times during a simulation.

python
1 
2import sympy as sp
3theta = sp.symbols('theta')
4 
5# Rotation matrix around Z-axis
6R = sp.Matrix([
7 [sp.cos(theta), -sp.sin(theta), 0],
8 [sp.sin(theta), sp.cos(theta), 0],
9 [0, 0, 1]
10])
11 
12# Derivative of rotation matrix with respect to theta
13dR_dtheta = R.diff(theta)
14 
15print("Rotation Matrix Derivative:")
16sp.pprint(dR_dtheta)
17 

In the next module, we will conclude our deep dive into the scientific stack by looking at how to bridge these symbolic results back into the numerical world of SciPy.

Pandas Deep Dive

Section Detail

Pandas: High-Performance Data Structures

Why Pandas?

While NumPy provides the computational horsepower for numerical arrays, it lacks the high-level features needed for real-world data analysis, such as:

  • Handling missing data (NaN).
  • Working with labeled axes (instead of just integer indices).
  • Merging and joining datasets (SQL-like operations).
  • Time-series functionality.

Pandas builds on top of NumPy to provide these features. It is the primary tool for data “munging” or “wrangling”—the process of cleaning and transforming raw data into a format suitable for analysis.

Core Data Structures: Series and DataFrame

There are two primary data structures in Pandas:

  1. Series: A 1D array-like object with an associated index.
  2. DataFrame: A 2D table-like structure with labeled rows and columns.

The Series Object

Think of a Series as a cross between a NumPy array and a Python dictionary.

python
1 
2import pandas as pd
3import numpy as np
4 
5# Creating a Series from a list
6data = pd.Series([0.25, 0.5, 0.75, 1.0], index=['a', 'b', 'c', 'd'])
7print("Series:\n", data)
8 
9# Accessing by label
10print("\nValue at index 'b':", data['b'])
11 
12# Series behaves like a numpy array
13print("\nMean of Series:", data.mean())
14 

The DataFrame Object

The DataFrame is the most important structure in Pandas. It represents a table of data, similar to a spreadsheet or a SQL table. Each column in a DataFrame is itself a Series.

python
1 
2import pandas as pd
3 
4data = {
5 'Name': ['Alice', 'Bob', 'Charlie', 'David'],
6 'Age': [25, 30, 35, 40],
7 'City': ['New York', 'London', 'Paris', 'Tokyo']
8}
9 
10df = pd.DataFrame(data)
11print("DataFrame:\n", df)
12 
13# Check basic info
14print("\nColumns:", df.columns)
15print("Index:", df.index)
16 

Data Selection and Indexing

Selection in Pandas can be confusing because there are multiple ways to do it.

1. loc: Label-based selection

loc is used to select data using the labels of rows and columns.

2. iloc: Integer-based selection

iloc is used to select data using the 0-based integer position.

python
1 
2import pandas as pd
3 
4df = pd.DataFrame({
5 'A': [1, 2, 3],
6 'B': [4, 5, 6]
7}, index=['row1', 'row2', 'row3'])
8 
9print("Selection with loc (labels):\n", df.loc['row2', 'A'])
10print("\nSelection with iloc (positions):\n", df.iloc[1, 0])
11 

Importing Data

In practice, you rarely create DataFrames by hand. You load them from files (CSV, Excel, JSON, SQL).

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

# Writing to an Excel file
df.to_excel('output.xlsx')

Handling Missing Data

Real-world data is messy. Pandas uses NaN (Not a Number) to represent missing values and provides robust tools to handle them.

python
1 
2import pandas as pd
3import numpy as np
4 
5df = pd.DataFrame({
6 'A': [1, 2, np.nan, 4],
7 'B': [5, np.nan, np.nan, 8],
8 'C': [1, 2, 3, 4]
9})
10 
11print("Original DF with NaNs:\n", df)
12 
13# Fill NaNs with a value
14print("\nFilled NaNs:\n", df.fillna(value=0))
15 
16# Drop rows with any NaN
17print("\nDropped NaNs:\n", df.dropna())
18 

GroupBy and Aggregation

Similar to GROUP BY in SQL, Pandas allows you to split data into groups and apply functions to each group separately.

python
1 
2import pandas as pd
3 
4df = pd.DataFrame({
5 'Company': ['GOOG', 'GOOG', 'MSFT', 'MSFT', 'FB', 'FB'],
6 'Person': ['Sam', 'Charlie', 'Amy', 'Vanessa', 'Carl', 'Sarah'],
7 'Sales': [200, 120, 340, 124, 243, 350]
8})
9 
10by_comp = df.groupby("Company")
11print("Mean sales per company:\n", by_comp['Sales'].mean())
12 

In the following modules, we will explore advanced joining techniques and time-series analysis.

Section Detail

Pandas: Data Cleaning and Wrangling

The Reality of Data

In textbooks, data is clean. In reality, data is missing, duplicated, inconsistent, and scattered across multiple files. Data scientists spend roughly 80% of their time cleaning and wrangling data. Pandas is designed to make this “grunt work” efficient.

Duplicates and Noise

Common cleaning tasks include removing duplicate rows and inconsistent strings.

python
1 
2import pandas as pd
3 
4df = pd.DataFrame({
5 'k1': ['one', 'two'] * 3 + ['two'],
6 'k2': [1, 1, 2, 3, 3, 4, 4]
7})
8 
9print("Original DF:\n", df)
10 
11# Identify duplicates
12print("\nIs Duplicate:\n", df.duplicated())
13 
14# Remove duplicates
15print("\nCleaned DF:\n", df.drop_duplicates())
16 

Transforming Data: map and apply

Sometimes you need to apply a custom function to every element or row in a DataFrame.

  • map(): Used on a Series to substitute values based on a dictionary or function.
  • apply(): Used on a DataFrame to apply a function along an axis (rows or columns).
python
1 
2import pandas as pd
3 
4df = pd.DataFrame({
5 'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami', 'corned beef'],
6 'ounces': [4, 3, 12, 6, 7.5]
7})
8 
9# Mapping food to animal source
10meat_to_animal = {
11 'bacon': 'pig',
12 'pulled pork': 'pig',
13 'pastrami': 'cow',
14 'corned beef': 'cow'
15}
16 
17# Normalize string and map
18df['animal'] = df['food'].str.lower().map(meat_to_animal)
19print("Transformation results:\n", df)
20 

Combining Datasets: Merge, Join, and Concatenate

In a relational database model, data is split across tables. Pandas allows you to bring them together.

Merge (SQL Join)

Merge connects rows in DataFrames based on one or more keys.

python
1 
2import pandas as pd
3 
4df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'], 'data1': range(7)})
5df2 = pd.DataFrame({'key': ['a', 'b', 'd'], 'data2': range(3)})
6 
7# Inner Join (default)
8merged = pd.merge(df1, df2, on='key')
9print("Merged Data (Inner):\n", merged)
10 
11# Outer Join
12outer = pd.merge(df1, df2, on='key', how='outer')
13print("\nMerged Data (Outer):\n", outer)
14 

Concatenate

Concatenate “stacks” DataFrames on top of each other or side-by-side.

python
1 
2import pandas as pd
3import numpy as np
4 
5s1 = pd.Series([0, 1], index=['a', 'b'])
6s2 = pd.Series([2, 3, 4], index=['c', 'd', 'e'])
7 
8print("Concatenated Series:\n", pd.concat([s1, s2]))
9 

Reshaping and Pivoting

Changing the layout of a DataFrame (Long to Wide format or vice versa) is essential for visualization and certain machine learning models.

python
1 
2import pandas as pd
3 
4df = pd.DataFrame({
5 'date': ['2021-01-01', '2021-01-01', '2021-01-02'],
6 'variable': ['temp', 'humidity', 'temp'],
7 'value': [22.5, 60, 23.0]
8})
9 
10# Pivot to wide format
11pivoted = df.pivot(index='date', columns='variable', values='value')
12print("Pivoted (Wide) Data:\n", pivoted)
13 

Advanced String Manipulation

Pandas provides a .str accessor for performing vectorized string operations.

python
1 
2import pandas as pd
3 
4names = pd.Series([' Alice ', 'bOB', ' Charlie ', 'd_avid'])
5 
6# Strip whitespace and capitalize
7clean_names = names.str.strip().str.capitalize()
8print("Clean names:\n", clean_names)
9 
10# Regex operations
11print("\nContains 'a'?", clean_names.str.contains('a'))
12 

In the next module, we’ll dive into Time-Series analysis, one of Pandas’ strongest features.

Section Detail

Pandas: Time Series Analysis

Why Time Series in Pandas?

Pandas was originally developed in a financial context (at AQR Capital Management), which explains why it has world-class support for time series. Temporal data often requires specialized operations like resampling (converting frequency), time zone conversion, and rolling windows.

The DatetimeIndex

The key to time series in Pandas is having a DatetimeIndex.

python
1 
2import pandas as pd
3import numpy as np
4 
5# Create a range of dates
6dates = pd.date_range('2023-01-01', periods=6, freq='D')
7print("Dates Index:\n", dates)
8 
9# Create a Series with the date index
10ts = pd.Series(np.random.randn(6), index=dates)
11print("\nTime Series:\n", ts)
12 

Resampling: Upsampling and Downsampling

Resampling is the process of changing the frequency of your time series observations.

  • Downsampling: Aggregating data (e.g., daily to monthly).
  • Upsampling: Increasing frequency (e.g., monthly to daily), often requiring interpolation.
python
1 
2import pandas as pd
3import numpy as np
4 
5# Hourly data for 3 days
6rng = pd.date_range('1/1/2023', periods=72, freq='H')
7ts = pd.Series(np.random.randn(len(rng)), index=rng)
8 
9# Downsample to Daily frequency and get the mean
10daily_summary = ts.resample('D').mean()
11print("Daily Mean:\n", daily_summary)
12 

Moving Windows (Rolling Operations)

Rolling operations allow you to calculate statistics over a sliding window of time. This is common for smoothing noisy data or calculating moving averages in finance.

python
1 
2import pandas as pd
3import numpy as np
4 
5# Create 100 days of data
6ts = pd.Series(np.random.randn(100), index=pd.date_range('1/1/2023', periods=100))
7 
8# Calculate 7-day rolling mean
9rolling_mean = ts.rolling(window=7).mean()
10 
11print("Original Data (first 10):\n", ts.head(10))
12print("\n7-day Moving Average (first 10):\n", rolling_mean.head(10))
13 

Handling Time Zones

Global data often requires reconciling different time zones.

python
1 
2import pandas as pd
3 
4ts = pd.Series([1, 2, 3], index=pd.date_range('2023-01-01', periods=3, freq='D'))
5 
6# Localize to UTC
7ts_utc = ts.tz_localize('UTC')
8print("UTC Series:\n", ts_utc)
9 
10# Convert to US Eastern time
11ts_eastern = ts_utc.tz_convert('US/Eastern')
12print("\nEastern Time Series:\n", ts_eastern)
13 

Shifting and Lagging

In time series modeling, you often want to shift data forward or backward in time (e.g., to calculate percentage changes).

python
1 
2import pandas as pd
3 
4ts = pd.Series([10, 20, 30, 40], index=pd.date_range('2023-01-01', periods=4))
5 
6# Shift forward by 1
7shifted = ts.shift(1)
8print("Shifted (Lag 1):\n", shifted)
9 
10# Calculate percentage change
11pct_change = (ts - ts.shift(1)) / ts.shift(1)
12print("\nPercent Change:\n", pct_change)
13 

Period Containers

While Timestamp represents a point in time, Period represents a duration (a day, a month, a year).

python
1 
2import pandas as pd
3 
4p = pd.Period('2023-01', freq='M')
5print(f"Period: {p}")
6print(f"Next month: {p + 1}")
7 
8# Converting from Timestamp to Period
9ts = pd.Timestamp('2023-01-15')
10period = ts.to_period('M')
11print(f"Timestamp to Monthly Period: {period}")
12 

In the next section, we will integrate these data manipulation skills with Scikit-Learn to build predictive models.

Scikit-Learn Deep Dive

Section Detail

Scikit-Learn: Machine Learning Principles

The Scikit-Learn Ecosystem

Scikit-Learn (frequently abbreviated as sklearn) is the primary library for classical machine learning in Python. Built on top of NumPy, SciPy, and Matplotlib, it provides simple and efficient tools for predictive data analysis.

Key Concepts and the Estimator API

The brilliance of Scikit-Learn lies in its consistent API. Whether you are performing linear regression, support vector machines, or random forests, the workflow is almost identical.

Code
skinparam componentStyle rectangle

package "Data Preparation" {
[Features (X)]
[Target (y)]
}

node "Scikit-Learn Workflow" {
component "Instantiate Model" as MK
component "fit(X, y)" as FIT
component "predict(X_new)" as PRED
}

[Features (X)] --> MK
[Target (y)] --> MK
MK --> FIT : "Training"
FIT --> PRED : "Inference"
Data PreparationScikit-Learn WorkflowFeatures (X)Target (y)Instantiate Modelfit(X, y)predict(X_new)TrainingInference

The Three Main Steps:

  1. Instantiate: Choose your model class and set hyperparameters.
  2. Fit: Train the model on your data using the fit() method.
  3. Predict: Apply the trained model to new data using predict() or transform().

Data Representation in Scikit-Learn

Scikit-Learn expects data in a specific format:

  • X (Feature Matrix): A 2D array or DataFrame of shape [n_samples, n_features].
  • y (Target Vector): A 1D array or Series of length n_samples.

A Simple Example: Linear Regression

Let’s see how we can predict a continuous value using Scikit-Learn.

python
1 
2from sklearn.linear_model import LinearRegression
3import numpy as np
4 
5# 1. Create Data
6X = np.array([[1], [2], [3], [4]]) # Feature matrix
7y = np.array([2, 4, 6, 8]) # Target vector (y = 2*x)
8 
9# 2. Instantiate Model
10model = LinearRegression()
11 
12# 3. Fit Model
13model.fit(X, y)
14 
15# 4. Predict
16X_new = np.array([[5], [10]])
17predictions = model.predict(X_new)
18 
19print(f"Predictions for 5 and 10: {predictions}")
20print(f"Coefficient (Slope): {model.coef_[0]}")
21print(f"Intercept: {model.intercept_}")
22 

The Machine Learning Workflow

A real-world project involves more than just fitting a model. It requires rigorous evaluation.

Train/Test Splitting

We must never evaluate a model on the same data it was trained on. Scikit-Learn provides train_test_split to handle this.

python
1 
2from sklearn.model_selection import train_test_split
3from sklearn.datasets import load_iris
4 
5# Load sample data
6iris = load_iris()
7X, y = iris.data, iris.target
8 
9# Split into 80% training, 20% testing
10X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
11 
12print(f"Training set size: {X_train.shape[0]}")
13print(f"Testing set size: {X_test.shape[0]}")
14 

Preprocessing: Scaling and Encoding

Machine learning models are sensitive to the scale of features. For example, a model might give more weight to a feature ranging from 0 to 1000 than to one ranging from 0 to 1.

python
1 
2from sklearn.preprocessing import StandardScaler
3import numpy as np
4 
5data = np.array([[10, 0.001], [20, 0.002], [30, 0.003]])
6scaler = StandardScaler()
7 
8scaled_data = scaler.fit_transform(data)
9print("Original Data:\n", data)
10print("\nScaled Data (mean=0, std=1):\n", scaled_data)
11 

Evaluation Metrics

How do we know if our model is any good? Scikit-Learn offers a suite of metrics.

  • For Regression: Mean Squared Error (MSE), R-squared.
  • For Classification: Accuracy, Precision, Recall, F1-Score.
python
1 
2from sklearn.metrics import accuracy_score
3 
4# Simulated ground truth and predictions
5y_true = [0, 1, 2, 0, 1]
6y_pred = [0, 2, 1, 0, 1]
7 
8acc = accuracy_score(y_true, y_pred)
9print(f"Model Accuracy: {acc * 100:.1f}%")
10 

In the next module, we will explore supervised learning models like Decision Trees and Random Forests in much greater detail.

Section Detail

Scikit-Learn: Supervised Learning Deep Dive

Supervised Learning Categories

Supervised learning is divided into two main tasks:

  1. Regression: Predicting a continuous numerical value (e.g., house prices).
  2. Classification: Predicting a discrete category or label (e.g., spam vs. not spam).

Classification: Beyond the Basics

We already saw the Estimator API. Let’s look at more complex classifiers.

Support Vector Machines (SVM)

SVMs are powerful models that attempt to find the hyperplane that best separates classes with the maximum margin.

python
1 
2from sklearn.svm import SVC
3from sklearn.datasets import make_classification
4from sklearn.model_selection import train_test_split
5 
6# Generate synthetic data
7X, y = make_classification(n_samples=100, n_features=2, n_redundant=0, random_state=42)
8X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
9 
10# Use Radial Basis Function (RBF) kernel
11model = SVC(kernel='rbf', C=1.0)
12model.fit(X_train, y_train)
13 
14print(f"SVM Test Accuracy: {model.score(X_test, y_test):.2f}")
15 

Decision Trees and Random Forests

Decision Trees mimic human decision-making by splitting data based on feature thresholds. Random Forests are “ensembles” of many decision trees, which reduces overfitting.

python
1 
2from sklearn.ensemble import RandomForestClassifier
3from sklearn.datasets import load_digits
4 
5digits = load_digits()
6X, y = digits.data, digits.target
7 
8model = RandomForestClassifier(n_estimators=100)
9model.fit(X, y)
10 
11print(f"Random Forest Accuracy on Digits: {model.score(X, y):.2f}")
12 

Regression: Complexity and Regularization

Simple linear regression often underfits complex data. We can use techniques like Ridge and Lasso regression to prevent overfitting by penalizing large coefficients.

python
1 
2from sklearn.linear_model import Ridge, Lasso
3import numpy as np
4 
5# Synthetic noisy data
6X = np.random.rand(100, 10)
7y = 2*X[:, 0] + 3*X[:, 1] + np.random.randn(100) * 0.1
8 
9ridge = Ridge(alpha=1.0)
10ridge.fit(X, y)
11 
12lasso = Lasso(alpha=0.1)
13lasso.fit(X, y)
14 
15print(f"Ridge Coefficients: {ridge.coef_[:2]}")
16print(f"Lasso Coefficients: {lasso.coef_[:2]}")
17 

Hyperparameter Tuning

How do we choose the best alpha for Ridge or the best n_estimators for a Random Forest? We use Grid Search.

python
1 
2from sklearn.model_selection import GridSearchCV
3from sklearn.ensemble import RandomForestClassifier
4 
5param_grid = {
6 'n_estimators': [10, 50, 100],
7 'max_depth': [None, 5, 10]
8}
9 
10grid = GridSearchCV(RandomForestClassifier(), param_grid, cv=5)
11# grid.fit(X, y) # Uncomment to run (takes time)
12# print(f"Best params: {grid.best_params_}")
13print("GridSearchCV defined with cross-validation.")
14 

Pipelines: Chaining Transformations

A pipeline combines a series of preprocessing steps and a final estimator into one object. This prevents data leakage during cross-validation.

python
1 
2from sklearn.pipeline import Pipeline
3from sklearn.preprocessing import StandardScaler
4from sklearn.svm import SVC
5 
6pipeline = Pipeline([
7 ('scaler', StandardScaler()),
8 ('svc', SVC())
9])
10 
11# Fit and predict just like a single estimator
12# pipeline.fit(X_train, y_train)
13print("Pipeline created: [Scaler -> SVC]")
14 

In the next module, we’ll explore Unsupervised Learning techniques like Clustering and Dimensionality Reduction.

Section Detail

Scikit-Learn: Unsupervised Learning

Learning Without Labels

Unsupervised learning is used when we have features (XX) but no target (yy). The goal is to find inherent patterns or structures within the data.

1. Clustering: Grouping Similar Samples

Clustering algorithms attempt to partition the data into groups (clusters) where samples in the same group are more similar to each other than to those in other groups.

K-Means Clustering

The most popular clustering algorithm. it partitions data into KK clusters by minimizing the distance between points and their cluster centroids.

python
1 
2from sklearn.cluster import KMeans
3from sklearn.datasets import make_blobs
4import matplotlib.pyplot as plt
5 
6# Create 3 distinct blobs of data
7X, _ = make_blobs(n_samples=300, centers=3, cluster_std=0.60, random_state=0)
8 
9# Instantiate and fit
10kmeans = KMeans(n_clusters=3)
11kmeans.fit(X)
12 
13# Predicted labels
14y_kmeans = kmeans.predict(X)
15print(f"Centroids:\n{kmeans.cluster_centers_}")
16 

2. Dimensionality Reduction

High-dimensional data (hundreds or thousands of features) is difficult to visualize and can lead to the “curse of dimensionality.” Dimensionality reduction seeks to represent data in a lower-dimensional space while preserving as much information as possible.

Principal Component Analysis (PCA)

PCA finds the “principal components”—the orthogonal axes along which the data varies the most.

python
1 
2from sklearn.decomposition import PCA
3from sklearn.datasets import load_iris
4 
5iris = load_iris()
6X = iris.data # 4 features
7 
8# Reduce from 4D to 2D
9pca = PCA(n_components=2)
10X_reduced = pca.fit_transform(X)
11 
12print(f"Original shape: {X.shape}")
13print(f"Reduced shape: {X_reduced.shape}")
14print(f"Explained variance ratio: {pca.explained_variance_ratio_}")
15 

3. Anomaly Detection

Identifying outliers or unusual patterns that do not conform to expected behavior.

Isolation Forest

Isolation Forest works by isolating anomalies using trees. Anomalies are easier to isolate and thus have shorter path lengths in the trees.

python
1 
2from sklearn.ensemble import IsolationForest
3import numpy as np
4 
5# Generate normal data
6X_train = 0.3 * np.random.randn(100, 2)
7X_train = np.r_[X_train + 2, X_train - 2]
8 
9# Generate some abnormal novel observations
10X_outliers = np.random.uniform(low=-4, high=4, size=(20, 2))
11 
12clf = IsolationForest(contamination=0.1)
13clf.fit(X_train)
14 
15y_pred_outliers = clf.predict(X_outliers)
16print(f"Predictions for outliers (-1 is anomaly):\n{y_pred_outliers}")
17 

Summary of Unsupervised Techniques

Code
skinparam componentStyle rectangle

package "Unsupervised Tasks" {
component "Clustering" as CLUST
component "Dimensionality Reduction" as DIM
component "Density Estimation" as DENS
}

node "Algorithms" {
component "K-Means / DBSCAN / Agglomerative" as CALG
component "PCA / t-SNE / UMAP" as DALG
component "GMM / Kernel Density" as DEALG
}

CLUST --> CALG
DIM --> DALG
DENS --> DEALG
Unsupervised TasksAlgorithmsClusteringDimensionality ReductionDensity EstimationK-Means / DBSCAN /AgglomerativePCA / t-SNE / UMAPGMM / Kernel Density

In the next section, we will explore specialized scientific libraries like SciPy and Matplotlib to round out our scientific Python toolkit.

Advanced Scientific Stack

Section Detail

SciPy: Essential Scientific Algorithms

Introduction to SciPy

The SciPy library (pronounced “Sigh Pie”) is built on NumPy and provides many user-friendly and efficient numerical routines, such as routines for numerical integration, interpolation, optimization, linear algebra, and statistics.

While NumPy provides the data structure (arrays) and basic operations, SciPy provides the algorithms.

Optimization and Root Finding

The scipy.optimize subpackage provides several commonly used optimization algorithms.

Minimizing a Scalar Function

Let’s find the minimum of the Rosenbrock function, a common test problem for optimization algorithms.

python
1 
2from scipy.optimize import minimize
3import numpy as np
4 
5def rosen(x):
6 """The Rosenbrock function"""
7 return sum(100.0*(x[1:]-x[:-1]**2.0)**2.0 + (1.0-x[:-1])**2.0)
8 
9x0 = np.array([1.3, 0.7, 0.8, 1.9, 1.2])
10res = minimize(rosen, x0, method='nelder-mead', options={'xatol': 1e-8, 'disp': True})
11 
12print(f"Minimum found at: {res.x}")
13 

Numerical Integration

The scipy.integrate subpackage provides several integration techniques, including an ordinary differential equation (ODE) integrator.

General Integration (quad)

python
1 
2from scipy.integrate import quad
3import numpy as np
4 
5# Integrate exp(-x^2) from 0 to infinity (The Gaussian integral)
6func = lambda x: np.exp(-x**2)
7result, error = quad(func, 0, np.inf)
8 
9print(f"Result: {result}")
10print(f"Error Estimate: {error}")
11 

Interpolation

scipy.interpolate is useful for fitting a function to a set of data points and then evaluating that function at new points.

python
1 
2from scipy.interpolate import interp1d
3import numpy as np
4 
5x = np.linspace(0, 10, num=11, endpoint=True)
6y = np.cos(-x**2/9.0)
7 
8# Create linear and cubic interpolation functions
9f_linear = interp1d(x, y)
10f_cubic = interp1d(x, y, kind='cubic')
11 
12xnew = np.linspace(0, 10, num=41, endpoint=True)
13# Evaluate at new points
14print(f"Linear interp at 5.5: {f_linear(5.5)}")
15print(f"Cubic interp at 5.5: {f_cubic(5.5)}")
16 

Signal Processing

scipy.signal contains tools for filtering, spectral analysis, and LTI (linear time-invariant) system analysis.

python
1 
2from scipy import signal
3import numpy as np
4 
5# Create a noisy signal
6t = np.linspace(0, 1, 100)
7sig = np.sin(2*np.pi*10*t) + np.random.randn(100)*0.1
8 
9# Apply a Butterworth filter
10b, a = signal.butter(3, 0.05)
11filtered_sig = signal.filtfilt(b, a, sig)
12 
13print("Signal filtered using Butterworth filter.")
14 

Statistics in SciPy

scipy.stats contains a huge number of probability distributions and statistical functions.

python
1 
2from scipy import stats
3import numpy as np
4 
5# Normal distribution
6data = np.random.normal(0, 1, 1000)
7 
8# Calculate parameters
9mean, std = stats.norm.fit(data)
10print(f"Fitted Mean: {mean:.2f}, Std: {std:.2f}")
11 
12# Perform a T-test
13t_stat, p_val = stats.ttest_1samp(data, 0.0)
14print(f"T-statistic: {t_stat:.2f}, P-value: {p_val:.2f}")
15 

In the final module, we will explore how to visualize our findings using Matplotlib and Seaborn.

Data Visualization Deep Dive

Section Detail

Matplotlib: The Figure and Axes Architecture

Beyond “Simple” Plotting

Many beginners start with plt.plot() and quickly get frustrated when they can’t control the details of their figure. To master Matplotlib, you must understand its Object-Oriented (OO) Architecture.

The Hierarchy of a Plot

A Matplotlib plot is not just a bunch of lines; it’s a hierarchy of objects called Artists.

Code
skinparam componentStyle rectangle

package "Figure" as Fig {
component "Axes" as AX1
}

component "Axis" as Axis
component "Title" as Title
component "Legend" as Legend
component "Ticks" as Ticks
component "Grid" as Grid
component "Lines" as Lines

AX1 *-- Axis
AX1 *-- Title
AX1 *-- Legend
AX1 *-- Ticks
AX1 *-- Grid
AX1 *-- Lines

note bottom of AX1: "A Figure can contain\nmultiple Axes (subplots)."
FigureAxesAxisTitleLegendTicksGridLines"A Figure can containmultiple Axes (subplots)."

The Figure vs. The Axes

  1. Figure: The top-level container. Think of it as the physical page or empty window.
  2. Axes: A “subplot.” It is the area where the data is actually plotted. Most operations (setting titles, labels, grids) happen on an Axes object.

Creating Subplots the Pro Way

The most common way to start a plot is with plt.subplots().

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5# Create figure and axes
6fig, ax = plt.subplots(figsize=(10, 5))
7 
8# Generate data
9x = np.linspace(0, 10, 100)
10y = np.sin(x)
11 
12# Plotting on the Axes object
13ax.plot(x, y, label='Sine Wave', color='#2563eb', linewidth=2.5)
14 
15# Setting attributes on the Axes object
16ax.set_title('Hierarchical Plot Construction', fontsize=16, fontweight='bold')
17ax.set_xlabel('Time (s)')
18ax.set_ylabel('Amplitude')
19ax.grid(True, linestyle='--', alpha=0.6)
20ax.legend()
21 
22plt.show()
23 

Anatomy of a Figure

Each element you see—the title, the tick labels, the lines—is a separate Artist object that can be customized.

ArtistPurpose
Line2DRepresents the data lines.
TextUsed for titles, labels, and annotations.
AxisManages the scale, limits, and tick locations.
PatchUsed for boxes, circles, and bars.

Multiple Subplots

The power of the OO API becomes clear when you have multiple plots in a single figure.

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5x = np.linspace(0, 10, 100)
6 
7# Create a 2x1 grid of plots
8fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 8), sharex=True)
9 
10ax1.plot(x, np.sin(x), 'r')
11ax1.set_title('Sine')
12 
13ax2.plot(x, np.cos(x), 'b')
14ax2.set_title('Cosine')
15 
16# Automatically adjust spacing
17plt.tight_layout()
18plt.show()
19 

In the next lesson, we will explore how to style these components to create publication-quality figures.

Section Detail

Matplotlib: Styling and Customization

Plotting for Publication

Scientists don’t just produce plots; they produce evidence. A good plot should be clear, accessible, and aesthetically pleasing. In this lesson, we explore how to move beyond default settings.

Global Styles with plt.style

Matplotlib comes with several pre-defined style sheets. You can view them all using plt.style.available.

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5# Use a clean, modern style
6plt.style.use('ggplot')
7 
8x = np.random.randn(100)
9y = np.random.randn(100)
10 
11fig, ax = plt.subplots()
12ax.scatter(x, y, alpha=0.5, color='purple')
13ax.set_title('Styled Scatter Plot')
14 
15plt.show()
16 

Working with Colormaps

Choosing the right colormap is critical. Avoid “jet” and other non-perceptually uniform maps. Use viridis, magma, or inferno for continuous data.

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5x = np.linspace(0, 5, 100)
6y = np.linspace(0, 5, 100)
7X, Y = np.meshgrid(x, y)
8Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
9 
10fig, ax = plt.subplots()
11im = ax.imshow(Z, origin='lower', extent=[0, 5, 0, 5], cmap='viridis')
12fig.colorbar(im, label='Intensity')
13ax.set_title('Perceptually Uniform Colormap')
14 
15plt.show()
16 

Annotations: Highlighting Key Data

Sometimes you need to point directly at a feature in your data.

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5fig, ax = plt.subplots()
6 
7t = np.arange(0.0, 5.0, 0.01)
8s = np.cos(2*np.pi*t)
9ax.plot(t, s, lw=2)
10 
11# Point to the peak
12ax.annotate('Local Maximum', xy=(2, 1), xytext=(3, 1.5),
13 arrowprops=dict(facecolor='black', shrink=0.05))
14 
15ax.set_ylim(-2, 2)
16plt.show()
17 

Complex Layouts with GridSpec

While plt.subplots() handles simple grids, GridSpec allows for plots that span multiple rows or columns.

python
1 
2import matplotlib.pyplot as plt
3 
4fig = plt.figure(figsize=(8, 6))
5gs = fig.add_gridspec(3, 3)
6 
7ax1 = fig.add_subplot(gs[0, :])
8ax1.set_title('Top row, full width')
9 
10ax2 = fig.add_subplot(gs[1:, :2])
11ax2.set_title('Bottom left, big')
12 
13ax3 = fig.add_subplot(gs[1, 2])
14ax3.set_title('Small 1')
15 
16ax4 = fig.add_subplot(gs[2, 2])
17ax4.set_title('Small 2')
18 
19plt.tight_layout()
20plt.show()
21 

In the next lesson, we will see how Seaborn makes these complex statistical visualizations much easier to achieve.

Section Detail

Seaborn: Statistical Data Visualization

High-Level Statistical Plotting

If Matplotlib is the “foundational” layer, Seaborn is the “sophisticated” layer. Built on top of Matplotlib, it integrates deeply with Pandas and automates many complex statistical visualization tasks.

Why Seaborn?

  1. DataFrame Integration: You can pass column names directly as strings.
  2. Beautiful Defaults: Better color palettes and styles out of the box.
  3. Statistical Aggregation: It automatically calculates confidence intervals and means for you.

Exploring Relationships with relplot

relplot is the primary entry point for visualizing the relationship between two variables.

python
1 
2import seaborn as sns
3import matplotlib.pyplot as plt
4import pandas as pd
5import numpy as np
6 
7# Create synthetic tips-like data
8df = pd.DataFrame({
9 'total_bill': np.random.uniform(10, 50, 100),
10 'tip': np.random.uniform(1, 10, 100),
11 'smoker': np.random.choice(['Yes', 'No'], 100),
12 'day': np.random.choice(['Thur', 'Fri', 'Sat', 'Sun'], 100)
13})
14 
15# Add a correlation
16df['tip'] = df['total_bill'] * 0.15 + np.random.normal(0, 1, 100)
17 
18sns.set_theme(style="ticks")
19 
20# Plot relationship by smoker status and day
21g = sns.relplot(
22 data=df, x="total_bill", y="tip",
23 col="day", hue="smoker", style="smoker",
24 kind="scatter"
25)
26 
27plt.show()
28 

Categorical Data and Distributions

When dealing with categories, catplot and displot are your best friends.

python
1 
2import seaborn as sns
3import matplotlib.pyplot as plt
4import pandas as pd
5import numpy as np
6 
7data = pd.DataFrame({
8 'Value': np.concatenate([np.random.normal(0, 1, 100), np.random.normal(2, 0.5, 100)]),
9 'Group': ['A']*100 + ['B']*100
10})
11 
12fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
13 
14# Violin plot
15sns.violinplot(data=data, x="Group", y="Value", ax=ax1, palette="muted")
16ax1.set_title("Distribution by Group (Violin)")
17 
18# ECDF (Empirical Cumulative Distribution Function)
19sns.ecdfplot(data=data, x="Value", hue="Group", ax=ax2)
20ax2.set_title("CDF Comparison")
21 
22plt.show()
23 

Regression Models with lmplot

Seaborn can perform linear regression right inside the visualization.

python
1 
2import seaborn as sns
3import matplotlib.pyplot as plt
4 
5df = sns.load_dataset("anscombe").query("dataset == 'I'")
6 
7sns.lmplot(x="x", y="y", data=df, ci=95, scatter_kws={"s": 80})
8plt.title("Statistical Regression Analysis")
9plt.show()
10 

Heatmaps and Matrix Plots

Heatmaps are essential for visualizing correlation matrices.

python
1 
2import seaborn as sns
3import matplotlib.pyplot as plt
4import numpy as np
5import pandas as pd
6 
7# Correlation matrix for synthetic features
8data = np.random.rand(10, 10)
9cols = [f'Feat_{i}' for i in range(10)]
10df = pd.DataFrame(data, columns=cols)
11corr = df.corr()
12 
13plt.figure(figsize=(10, 8))
14sns.heatmap(corr, annot=True, cmap='coolwarm', fmt=".2f")
15plt.title("Feature Correlation Matrix")
16plt.show()
17 

In the final lesson of this module, we will discuss the principles of scientific storytelling and how to choose the right chart for your data.

Section Detail

Scientific Storytelling: The Art of the Visual

Visualization as Communication

Data visualization is the language of science. A poorly designed plot can obscure the truth, while a great one can reveal deep insights that numbers alone cannot convey.

The Principles of Tufte

Edward Tufte, a pioneer in data visualization, emphasized the concept of Data-Ink Ratio. The goal is to maximize the ink used for the data and minimize the ink used for everything else (decorative borders, unnecessary grids, etc.).

Tufte’s Rules:

  1. Above all else, show the data.
  2. Maximize the data-ink ratio.
  3. Erase non-data-ink.
  4. Erase redundant data-ink.
  5. Revise and edit.

Avoiding Common Pitfalls

1. The Truncated Y-Axis

Starting a Y-axis at a non-zero value can exaggerate small differences. While sometimes necessary, it should be done with caution and clear labeling.

2. Overplotting

Too many points in a scatter plot can hide the density. Use alpha transparency or hexbins to solve this.

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5# Generate thousands of points
6n = 10000
7x = np.random.standard_normal(n)
8y = np.random.standard_normal(n)
9 
10fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
11 
12# Bad: Solid points hide density
13ax1.scatter(x, y, s=5, alpha=1)
14ax1.set_title("Opaque (Hidden Density)")
15 
16# Good: Hexbin shows density
17hb = ax2.hexbin(x, y, gridsize=40, cmap='inferno')
18fig.colorbar(hb, ax=ax2, label='Counts')
19ax2.set_title("Hexbin (Clear Density)")
20 
21plt.show()
22 

Choosing the Right Plot

Data TypeBest PlotWhy?
Trend over TimeLine PlotEmphasizes continuity and sequence.
Comparing CategoriesBar Plot / Box PlotClear separation and ranking of groups.
Relationships (2 vars)Scatter PlotShows correlation or lack thereof.
DistributionHistogram / KDEShows the shape and spread of data.
CompositionStacked AreaShows how parts change relative to the whole.

Narrative Visualization

A good figure should walk the reader through a story. Use labels and arrows to highlight the “turning points” in your data.

python
1 
2import matplotlib.pyplot as plt
3import numpy as np
4 
5time = np.linspace(0, 10, 100)
6signal = np.exp(-0.5 * time) * np.sin(2 * np.pi * time)
7 
8fig, ax = plt.subplots(figsize=(10, 5))
9ax.plot(time, signal, color='darkblue', label='Sensor A Output')
10 
11# Add narrative elements
12ax.axhline(0, color='black', alpha=0.3)
13ax.annotate('System Damping Begins', xy=(2.3, 0.2), xytext=(5, 0.5),
14 arrowprops=dict(facecolor='black', shrink=0.05),
15 fontsize=12, fontweight='bold')
16 
17ax.fill_between(time, signal, 0, alpha=0.1, color='blue')
18 
19ax.set_title("Signal Decay Analysis", loc='left', fontsize=18)
20ax.spines['top'].set_visible(False)
21ax.spines['right'].set_visible(False)
22 
23plt.show()
24 

Conclusion of the module

You have now moved from plotting basic lines to constructing complex, data-driven narratives. By combining the power of Matplotlib and Seaborn with the principles of Tufte, you can communicate your scientific findings with clarity and impact.

Advanced Scientific Stack

Section Detail

Capstone: Integrating the Full Scientific Stack

The Power of Integration

Throughout this course, we have looked at libraries in isolation. However, the true strength of the Python scientific ecosystem is how seamlessly these tools work together.

In this final project, we will:

  1. Generate synthetic data using NumPy.
  2. Structure and analyze it using Pandas.
  3. Build a predictive model using Scikit-Learn.
  4. Visualize the results using Matplotlib and Seaborn.

The Scenario: Predicting Sensor Failures

Imagine we have a chemical reactor with two sensors: Temperature and Pressure. We want to predict if the reactor is in a “Stable” or “Unstable” state based on these readings.

python
1 
2import numpy as np
3import pandas as pd
4import matplotlib.pyplot as plt
5import seaborn as sns
6from sklearn.model_selection import train_test_split
7from sklearn.ensemble import RandomForestClassifier
8from sklearn.metrics import confusion_matrix
9 
10# 1. Generate Synthetic Data (NumPy)
11np.random.seed(42)
12n_samples = 200
13temp = np.random.normal(300, 20, n_samples)
14pressure = np.random.normal(50, 10, n_samples)
15 
16# Define stability condition: unstable if temp > 330 or pressure > 65
17stability = ((temp > 330) | (pressure > 65)).astype(int)
18 
19# 2. Structure Data (Pandas)
20df = pd.DataFrame({
21 'Temperature': temp,
22 'Pressure': pressure,
23 'Unstable': stability
24})
25 
26print("First 5 samples of the dataset:")
27print(df.head())
28 
29# 3. Model Training (Scikit-Learn)
30X = df[['Temperature', 'Pressure']]
31y = df['Unstable']
32 
33X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)
34 
35model = RandomForestClassifier(n_estimators=50)
36model.fit(X_train, y_train)
37 
38# 4. Comprehensive Visualization (Matplotlib/Seaborn)
39fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
40 
41# Plot A: The raw data distribution
42sns.scatterplot(data=df, x='Temperature', y='Pressure', hue='Unstable', ax=ax1, palette='coolwarm')
43ax1.set_title("Reactor States: Temperature vs Pressure")
44 
45# Plot B: The Model Performance (Confusion Matrix)
46y_pred = model.predict(X_test)
47cm = confusion_matrix(y_test, y_pred)
48sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax2)
49ax2.set_xlabel('Predicted')
50ax2.set_ylabel('Actual')
51ax2.set_title("Model Accuracy: Confusion Matrix")
52 
53plt.tight_layout()
54plt.show()
55 
56score = model.score(X_test, y_test)
57print(f"\nModel Accuracy on Test Set: {score*100:.1f}%")
58 

Course Conclusion

Congratulations! You have mastered the core components of the Python scientific stack. You are now equipped to:

  • Perform high-performance numerical computing with NumPy.
  • Derive exact mathematical formulas with SymPy.
  • Clean and analyze massive datasets with Pandas.
  • Solve complex scientific problems with SciPy.
  • Build machine learning pipelines with Scikit-Learn.
  • Communicate your insights through professional-grade visualizations with Matplotlib and Seaborn.

The journey doesn’t end here. The ecosystem is constantly evolving with libraries like PyTorch for Deep Learning, Dask for Parallel Computing, and Plotly for Interactive Dashboards.

Happy coding!