5.4.8 · D4Scientific Computing (Python)

Exercises — SciPy — overview of submodules

2,644 words12 min readBack to topic

Before we begin, a reminder of two words we will lean on:

  • An array is just a fast grid of numbers (see NumPy arrays). SciPy never invents its own data type — it works on these grids.
  • A submodule is one drawer of the SciPy toolbox, named after the maths it does: linalg, optimize, integrate, interpolate, stats, sparse, fft, …

Level 1 — Recognition

Goal: given a task, name the right drawer. No coding yet — just point.

L1.1

For each task, name the single best core submodule.

  1. Solve the system .
  2. Find the that makes smallest.
  3. Compute numerically.
  4. Draw a smooth curve through the scattered data points .
  5. Get the mean and standard deviation of a normal distribution.
Recall Solution

A verb hides in each task; match verb → drawer.

  1. solve a linear system → ==scipy.linalg== (see Linear algebra — solving Ax=b).
  2. minimize → ==scipy.optimize== (see Optimization — gradient descent).
  3. integrate → ==scipy.integrate== (see Numerical integration (trapezoid & Simpson)).
  4. interpolate (curve through points) → ==scipy.interpolate==.
  5. distribution / statistics → ==scipy.stats== (see Probability distributions).

L1.2

quad returns two numbers. What are they, and why two?

Recall Solution

It returns the tuple (value, abserr) — the estimate of the integral and an absolute error bound. Two numbers because a numerical answer is worthless without knowing how much to trust it. Always unpack both: val, err = quad(...).


Level 2 — Application

Goal: write the call and predict the number before running it.

L2.1

Using scipy.linalg.solve, solve Predict by hand first, then give the code.

Recall Solution

By hand. From the first equation . Substitute into the second: Then . So .

from scipy import linalg
import numpy as np
A = np.array([[2, 1], [1, 3]], dtype=float)
b = np.array([5, 10], dtype=float)
print(linalg.solve(A, b))   # [1. 3.]

Why solve and not inv(A) @ b? solve uses LU decomposition — it factors once and back-substitutes, which is faster and numerically steadier than forming explicitly.

L2.2

Compute with integrate.quad. Predict the exact value.

Recall Solution

By hand.

from scipy import integrate
import numpy as np
val, err = integrate.quad(np.sin, 0, np.pi)
print(val, err)   # 2.0  ~2e-14

The estimate is and the error bound is around — essentially exact.

L2.3

Minimize with optimize.minimize, starting from x0=0. Predict the minimizer and the minimum value.

Recall Solution

By hand. The minimum of a squared term is where the inside is zero, so . The added shifts the value up but not the location: . Check with calculus: .

from scipy import optimize
res = optimize.minimize(lambda x: (x-3)**2 + 4, x0=0.0)
print(res.x, res.fun)   # [3.0000...] [4.0000...]

Level 3 — Analysis

Goal: reason about trade-offs, correctness, and when a routine breaks.

L3.1

You must store a matrix of float64 where all but million entries are zero. Estimate the memory a dense array needs, and say which submodule you should use instead.

Recall Solution

A float64 takes 8 bytes. A dense array stores every cell: That will never fit in RAM. Because it is mostly zeros, use `scipy.sparse`, > which stores only the nonzero entries plus their positions. Roughly nonzeros need on the order of tens of megabytes — a factor of about smaller.

L3.2

Two students solve the same : one with numpy.linalg.solve, one with scipy.linalg.solve. They get the same answer on a system. Does that prove the two are interchangeable? Give one concrete reason to prefer scipy.linalg.

Recall Solution

No — agreeing on one tiny problem proves nothing about the general case. scipy.linalg is always compiled against BLAS/LAPACK and offers more routines (lu, qr, schur, expm, …) that numpy.linalg simply lacks. For serious numerical work — decompositions, matrix functions, large systems — prefer scipy.linalg. See Linear algebra — solving Ax=b.

L3.3

The trapezoid rule approximates by summing straight-topped strips. Look at the figure: on a curve that bends upward (convex), does one trapezoid strip over-estimate or under-estimate the true area? Explain from the picture.

Figure — SciPy — overview of submodules
Recall Solution

For a curve that bends upward, the straight chord joining the two endpoints lies above the curve (red chord above the black curve in the figure). The trapezoid's flat top therefore covers more area than the region under the curve, so a single strip over-estimates. This is exactly the wiggliness integrate.quad fixes by refining adaptively — it adds more, thinner strips (or higher-order pieces) where the curve bends, driving the error down. See Numerical integration (trapezoid & Simpson).


Level 4 — Synthesis

Goal: chain two or three submodules into one small pipeline.

L4.1

A physical process is described by the density on .

  1. Confirm it is a valid probability density by integrating it over (integrate.quad), checking the returned error bound is tiny.
  2. Then find the point where the running area equals (the median), by solving with optimize.brentq.

Predict both answers by hand first.

Recall Solution

Step 1 — is it a density? A density must integrate to : Step 2 — the median. The running area from to is Set it to :

from scipy import integrate, optimize
f = lambda x: 3*x**2
area, area_err = integrate.quad(f, 0, 1)
assert area_err < 1e-10          # honour L1.2: read the error bound
print(area, area_err)            # 1.0  ~1e-14  -> valid density
 
def running_area(m):
    val, err = integrate.quad(f, 0, m)   # unpack BOTH, per L1.2
    return val - 0.5                     # root here => median
m = optimize.brentq(running_area, 0, 1)
print(m)                          # 0.7937005...

Why brentq? We need a root of on a known bracket where changes sign (, ). brentq reliably brackets and squeezes onto that sign change. Note we still unpack val, err inside running_area — the advice from L1.2 does not lapse just because quad sits in a helper. This is the optimize verb (root-finding) feeding on the integrate verb — two drawers in one pipeline. See Probability distributions.

L4.2

Fit a straight line to the exact points using scipy.optimize.curve_fit, and predict and .

Recall Solution

The points lie exactly on a line: from we read ; the slope between and is , so . Every point satisfies .

from scipy import optimize
import numpy as np
x = np.array([0, 1, 2], dtype=float)
y = np.array([1, 3, 5], dtype=float)
popt, _ = optimize.curve_fit(lambda x, m, c: m*x + c, x, y)
print(popt)   # [2. 1.]  -> m=2, c=1

Which drawer is this? curve_fit lives in ==scipy.optimize==, not scipy.linalg — it wraps MINPACK's Levenberg–Marquardt least-squares solver, and least-squares fitting is an optimization verb ("minimize the squared residuals"). scipy.linalg also can do least squares (via linalg.lstsq, a decomposition), but the moment you phrase the task as "fit these parameters," the natural drawer is optimize. See Optimization — gradient descent.


Level 5 — Mastery

Goal: reason about correctness, edge cases, and numerical behaviour like an expert.

L5.1

optimize.minimize on starting at x0=0 returns . But minimizers only guarantee a local minimum. Construct a function where the starting guess changes the answer, and explain why.

Recall Solution

Take . Its derivative is zero at . The two minima are at (each giving ); is a local maximum. A downhill search starting at x0 = -1 slides to ; starting at x0 = +1 slides to . Same function, different basin, different reported minimizer.

from scipy import optimize
f = lambda x: x**4 - 8*x**2
print(optimize.minimize(f, x0=-1.0).x)   # ~[-2.]
print(optimize.minimize(f, x0= 1.0).x)   # ~[ 2.]

Lesson: local optimizers find the valley you start in. For global minima you must try multiple starts or a global method. See Optimization — gradient descent.

L5.2

The figure shows with both starting guesses marked. State, from the picture, which minimum each guess falls into and why the point traps neither.

Figure — SciPy — overview of submodules
Recall Solution

The red dot at sits on the left downslope, so the search rolls left into the valley at . The red dot at sits on the right downslope and rolls right into . The hump at is a local maximum — its slope is zero but the curve bends down on both sides, so any nudge escapes it. A gradient search will never rest there because it always steps in the downhill direction and has downhill on either side.

L5.3

Sample-and-transform mastery. Draw samples from a normal distribution with scipy.stats.norm.rvs (the sample verb), then take the Fast Fourier Transform of those samples with scipy.fft.fft (the transform verb). Predict two sanity checks before running: (a) the sample mean should sit near , and (b) the FFT output must have exactly complex entries. Explain why these two drawers are different verbs even though both crunch the same array.

Recall Solution
import numpy as np
from scipy import stats, fft
N = 1024
samples = stats.norm.rvs(loc=0, scale=1, size=N, random_state=0)  # SAMPLE verb
print(round(samples.mean(), 1))   # ~0.0 (law of large numbers)
print(round(samples.std(), 1))    # ~1.0
spectrum = fft.fft(samples)       # TRANSFORM verb
print(spectrum.shape)             # (1024,)  -> same length, now complex
print(spectrum.dtype)             # complex128

(a) With draws from , the sample mean is very close to and the sample standard deviation close to — that is what "sampling from" a distribution means: rvs = random variates (see Probability distributions). (b) fft.fft returns exactly one complex number per input point, so the output has shape — same length as the input, but now living in the frequency domain instead of the time/sample domain (see Fourier transform (FFT)). Why different drawers? scipy.stats.rvs answers "give me random numbers shaped like this distribution" (the sample verb); scipy.fft.fft answers "which frequencies is this signal built from?" (the transform verb). Two distinct verbs, two distinct drawers, even though both operate on the very same NumPy array.


Recall One-line summary of the whole ladder

Match the verb → pick the drawer → predict by hand → run → read the error / check the bracket / question the local optimum. That loop is the entire skill.