Intuition What this page is for
The parent note showed you the map of SciPy submodules and three quick derivations. Here we do
the opposite of skimming: we take the five workhorse submodules and push each into every corner
case it can hit — the "easy" input, the sign flip, the zero/empty input, the limiting value, a
real-world word problem, and an exam-style trap. If you can follow all ten examples below, no SciPy
homework question can surprise you.
Everything here builds on NumPy arrays (the data), and the parent SciPy overview .
Before examples, we list every case class a SciPy question can throw at you. Each row is a "cell".
Every cell is covered by at least one worked example below.
Let us build the vocabulary once so no symbol is unexplained.
Definition The three words behind everything
Integrate ∫ a b f ( x ) d x — read "the area trapped between the curve y = f ( x ) and the
flat ground (the x -axis) from the left wall x = a to the right wall x = b ." The ∫ is a
stretched "S" for "S um", because area = sum of thin slices.
Minimize — find the input x where the curve is at its lowest point (the bottom of a valley).
Solve A x = b — A is a table of numbers (a matrix), x is the list of unknowns, b is the
list of right-hand answers. "Solve" = find the x that makes the equations all true at once.
Worked example Area under
f ( x ) = x 3 from 0 to 2
Forecast (guess first!): The curve y = x 3 rises from 0 up to 2 3 = 8 . The area under it is
some number bigger than a triangle of height 8, base 2 (area 8) would suggest? Or smaller? Jot a guess.
Recall the exact antiderivative. ∫ x 3 d x = 4 x 4 .
Why this step? So we have a hand-computed true value to check SciPy against — never trust a number you can't cross-check.
Evaluate at the walls. 4 2 4 − 4 0 4 = 4 16 − 0 = 4 .
Why this step? The Fundamental Theorem says area = (antiderivative at right wall) − (at left wall).
Ask SciPy.
from scipy import integrate
val, err = integrate.quad( lambda x: x ** 3 , 0 , 2 )
print (val, err) # 4.0 ~4e-14
Why this step? quad slices [ 0 , 2 ] adaptively and sums; err is its self-estimate of how wrong it might be.
Verify: v a l = 4.0 matches our hand answer 4 , and er r ≈ 1 0 − 14 (essentially machine dust) — trust it fully.
The curve y = x 3 never dips below the axis on [ 0 , 2 ] , so all the area counts as positive . That is the "clean" cell. Next we break that assumption.
The figure below is the whole argument in one picture — read it before the steps . The lavender
curve is y = x 3 . Everything shaded coral sits below the horizontal axis (counts as
negative area); everything shaded mint sits above it (counts as positive area). Notice
the two shaded blobs are the same size — that equal-and-opposite pairing is exactly why the answer
will be zero.
Figure C2 — The coral blob on [ − 1 , 0 ] (negative area) and the mint blob on [ 0 , 1 ] (positive area) are mirror images of equal size, so the signed total is 0 .
∫ − 1 1 x 3 d x — a curve that is negative on the left, positive on the right
Forecast: Look at the figure. On [ − 1 , 0 ] the curve is below the axis (shaded coral = "negative area"), on [ 0 , 1 ] it is above (shaded mint = "positive area"). What do you predict the total is?
Read the geometry (point at the figure). For odd powers, ( − x ) 3 = − x 3 : flipping the sign of x flips the sign of the height. So the coral blob on the left is the mint blob on the right, reflected through the origin — same shape, opposite sign.
Why this step? Integrals count area below the axis as negative . Equal-and-opposite pieces should cancel, and the figure lets us see that before computing.
Hand-compute the signed area. 4 x 4 − 1 1 = 4 1 − 4 1 = 0 .
Why this step? Confirms the cancellation the figure suggested is exact, not approximate.
Ask SciPy for the signed area.
val, err = integrate.quad( lambda x: x ** 3 , - 1 , 1 )
print (val) # ~0.0 (like 0.0 or 1e-17)
Now the GEOMETRIC area — integrate ∣ f ∣ . To count both blobs as positive, integrate the absolute value:
import numpy as np
area, err = integrate.quad( lambda x: np.abs(x ** 3 ), - 1 , 1 )
print (area) # 0.5
Why this step? ∣ x 3 ∣ folds the coral blob up above the axis, so its area now adds instead of cancels : 4 1 + 4 1 = 2 1 . This is exactly how you get physical area (e.g. total distance regardless of direction).
Verify: signed v a l ≈ 0 ; geometric a r e a = 0.5 matches 4 1 + 4 1 . Mistake trap: the signed 0 is not "no area" — the figure clearly shows area on both sides, the signs just cancel.
Common mistake "The integral is zero, so the function is flat / has no area."
The fix: A zero signed integral means the below-axis part and above-axis part are equal in size (look again at the two blobs). Integrate abs(f) if you want total geometric area — as Step 4 did, giving 2 1 .
First, one word we lean on all through this example.
x0 — the starting guess
When you call optimize.minimize(f, x0=...), the argument x0 ("x-zero") is the initial
guess : the point on the curve where you place the ball before letting it roll downhill. The
optimizer does not scan the whole curve; it only ever moves downhill from x0 until the slope
is flat. So x0 literally decides which valley you fall into .
Now study the figure — it is the punchline. There are two valleys; a ball placed on the left
rolls into the deep one, a ball placed on the right rolls into the shallow one.
Figure C3 — Two valleys of f ( x ) = x 4 − 3 x 2 + x . Mint dot = deep global min (x ≈ − 1.30 , f ≈ − 3.51 ); butter dot = shallow local min (x ≈ 1.13 , f ≈ − 1.68 ). The coral arrows show a ball dropped at x0=-2 rolling into the LEFT valley and one dropped at x0=2 rolling into the RIGHT valley.
f ( x ) = x 4 − 3 x 2 + x — a "double valley"
Forecast: The figure shows two dips (a left valley near x ≈ − 1.3 and a right valley near x ≈ 1.1 ). One is deeper. If we start our optimizer on the left vs the right , will it always find the deepest? Guess yes/no.
Find slope-zero points by hand. A valley bottom or hill top is where the slope is flat, so we set the derivative to zero: f ′ ( x ) = 4 x 3 − 6 x + 1 = 0 .
Why this step? minimize walks downhill from x0 until the slope is zero; it stops at whatever valley it fell into, not necessarily the deepest.
Locate the three roots by inspection (sign changes of f ′ ). Evaluate f ′ at a few probe points:
f ′ ( − 2 ) = 4 ( − 8 ) − 6 ( − 2 ) + 1 = − 32 + 12 + 1 = − 19 (negative),
f ′ ( − 1 ) = − 4 + 6 + 1 = 3 (positive) → a root sits between − 2 and − 1 , near x ≈ − 1.30 (left valley),
f ′ ( 0 ) = 1 (positive),
f ′ ( 0.5 ) = 0.5 − 3 + 1 = − 1.5 (negative) → a root between 0 and 0.5 , near x ≈ 0.17 (the hill top),
f ′ ( 2 ) = 32 − 12 + 1 = 21 (positive) → a root between 0.5 and 2 , near x ≈ 1.13 (right valley).
Why this step? A continuous curve that changes sign between two probe points must cross zero between them — so three sign flips ⇒ three roots ⇒ two valley bottoms plus one hill top, exactly what the figure shows. No cubic-solving formula needed.
Start on the right, x0=2 (right coral arrow).
from scipy import optimize
r_right = optimize.minimize( lambda x: x ** 4 - 3 * x ** 2 + x, x0 = 2.0 )
print (r_right.x, r_right.fun) # x ~ 1.130, f ~ -1.678
Why this step? From x0=2 the ball rolls left into the nearest valley — the shallower right one (butter dot).
Start on the left, x0=-2 (left coral arrow).
r_left = optimize.minimize( lambda x: x ** 4 - 3 * x ** 2 + x, x0 =- 2.0 )
print (r_left.x, r_left.fun) # x ~ -1.301, f ~ -3.514
Why this step? From x0=-2 it finds the deeper valley (mint dot).
Verify: Two different x0 values → two different answers (− 1.678 vs − 3.514 ). The global minimum is the left one, value ≈ − 3.514 . Lesson: local optimizers depend on x0; try several starts. See Optimization — gradient descent .
{ x + 2 y = 3 2 x + 4 y = 10
Forecast: The second equation is the first multiplied by 2 on the left side (2 x + 4 y = 2 ( x + 2 y ) ) — but the right sides give 2 ⋅ 3 = 6 = 10 . What happens when we ask SciPy to solve it?
Spot the degeneracy. Row 2 = 2·(row 1) on the left, so the matrix A = ( 1 2 2 4 ) is singular (its determinant 1 ⋅ 4 − 2 ⋅ 2 = 0 ).
Why this step? A singular matrix means the two lines are parallel — either identical (infinitely many solutions) or never meeting (no solution).
Check the right-hand side. 2 ⋅ 3 = 6 = 10 , so the lines are parallel but distinct → no solution .
Why this step? Determines which degenerate case we hit.
Ask SciPy — and expect an error.
from scipy import linalg
import numpy as np
A = np.array([[ 1 ., 2 .],[ 2 ., 4 .]]); b = np.array([ 3 ., 10 .])
try :
linalg.solve(A, b)
except linalg.LinAlgError as e:
print ( "singular:" , e) # raises LinAlgError
Why this step? linalg.solve refuses singular systems rather than returning garbage. Read the exception — it's information, not a bug.
Verify: det A = 0 confirms singularity, and 2 ⋅ 3 = 10 confirms inconsistency → the raised LinAlgError is the correct outcome. See Linear algebra — solving Ax=b .
Before the example, one symbol we are about to use as a wall.
∞ (infinity) and np.inf
The symbol ∞ is not a number — it is shorthand for "let the right wall keep marching to
the right without ever stopping ". Writing ∫ 0 ∞ means "compute the area up to wall b ,
then ask what number that area approaches as b grows past every bound." In code, NumPy spells
this idea np.inf , a special floating-point value you can pass straight into quad as a bound.
∫ 0 ∞ e − x d x — an infinite right wall
Forecast: As x grows, e − x shrinks toward 0 very fast. The tail area keeps adding tinier slivers forever. Does the total blow up to ∞ , or settle on a finite number?
Hand-integrate up to a finite wall b . ∫ 0 b e − x d x = [ − e − x ] 0 b = 1 − e − b .
Why this step? Keeping the wall finite first lets us watch what happens as it moves.
Let the wall march to infinity. As b → ∞ , e − b → 0 , so 1 − e − b → 1 .
Why this step? This is exactly what ∫ 0 ∞ means — the limiting value the finite areas approach. It is finite (= 1 ) even though the tail never truly ends.
Ask SciPy — infinity is np.inf.
import numpy as np
from scipy import integrate
val, err = integrate.quad( lambda x: np.exp( - x), 0 , np.inf)
print (val, err) # 1.0000000000000002 ~5e-11
Why this step? quad handles improper (infinite) bounds via a change of variable internally — you just pass np.inf.
Verify: v a l ≈ 1.0 matches the hand answer. The tiny er r shows the infinite tail is well controlled. Contrast: ∫ 1 ∞ x 1 d x would diverge — not every infinite integral is finite.
Worked example A car's velocity is
v ( t ) = 3 t 2 (metres per second) for t from 0 to 4 seconds. How far does it travel?
Forecast: Distance = area under the velocity–time curve. Since v grows fast, guess: more than the "average speed × time" of a straight line? Jot a number.
Translate words → integral. Distance = ∫ 0 4 v ( t ) d t = ∫ 0 4 3 t 2 d t .
Why this step? Velocity is the rate of position change; summing rate × tiny-time over the trip = total distance. That "sum" is exactly an integral.
Hand-check. ∫ 3 t 2 d t = t 3 , so [ t 3 ] 0 4 = 64 − 0 = 64 metres.
Why this step? Gives the expected answer with units (metres).
Ask SciPy.
from scipy import integrate
dist, err = integrate.quad( lambda t: 3 * t ** 2 , 0 , 4 )
print (dist, "metres" ) # 64.0 metres
Verify: 64 metres. Units check: v is m/s, t is s, so v ⋅ t has units (m/s) ⋅ s = m ✓. See Numerical integration (trapezoid & Simpson) .
Worked example A student writes
area = integrate.quad(lambda x: x**2, 0, 3) then does area + 1. What goes wrong, and what is the true area?
Forecast: Will area + 1 give 10.0? Or will Python complain? Guess before reading.
Compute the true area. ∫ 0 3 x 2 d x = 3 x 3 0 3 = 3 27 = 9 .
Why this step? We need the correct number to compare against.
Recall what quad returns. It returns a tuple (value, abserr), not a bare number.
Why this step? area is (9.0, 1.0e-13). Then area + 1 tries to add an int to a tuple → TypeError.
The fix — unpack.
val, err = integrate.quad( lambda x: x ** 2 , 0 , 3 )
print (val + 1 ) # 10.0
Why this step? Unpacking pulls out the number; now arithmetic works.
Verify: True area = 9 , so v a l + 1 = 10.0 . Exam mnemonic: quad gives you two numbers — always val, err = quad(...).
{ 2 x − 3 y = − 4 − x + 4 y = 9 (positive, negative, and mixed coefficients)
Forecast: Both x and y — will they be positive? negative? Guess signs first.
Eliminate by hand. From equation 2: x = 4 y − 9 . Substitute into equation 1: 2 ( 4 y − 9 ) − 3 y = − 4 ⇒ 8 y − 18 − 3 y = − 4 ⇒ 5 y = 14 ⇒ y = 2.8 .
Why this step? Substitution gives a checkable exact answer independent of SciPy.
Back-substitute. x = 4 ( 2.8 ) − 9 = 11.2 − 9 = 2.2 .
Why this step? Now we have the full predicted vector ( x , y ) = ( 2.2 , 2.8 ) .
Ask SciPy.
import numpy as np
from scipy import linalg
A = np.array([[ 2 ., - 3 .],[ - 1 ., 4 .]]); b = np.array([ - 4 ., 9 .])
print (linalg.solve(A, b)) # [2.2 2.8]
Why this step? solve uses LU decomposition — stable even with the negative entries.
Verify: Plug back into eq 1: 2 ( 2.2 ) − 3 ( 2.8 ) = 4.4 − 8.4 = − 4 ✓. Eq 2: − 2.2 + 4 ( 2.8 ) = − 2.2 + 11.2 = 9 ✓. Determinant 2 ⋅ 4 − ( − 3 ) ( − 1 ) = 8 − 3 = 5 = 0 , so the unique solution exists.
Worked example IQ scores follow a normal (bell) curve with mean
μ = 100 , standard deviation σ = 15 . What fraction of people score above 130 ?
Forecast: 130 is 15 130 − 100 = 2 standard deviations above the mean. The famous "68–95–99.7" rule says ~95% lie within 2σ, so ~5% lie outside on both tails combined. So one tail (above) is roughly…? Guess.
Standardize. The z -score = 15 130 − 100 = 2 .
Why this step? It converts "above 130 on this bell" into "above z = 2 on the standard bell", so we can use one universal table.
We want the upper tail P ( X > 130 ) = P ( Z > 2 ) , i.e. the area under the bell to the right of z = 2 .
Why this step? "Fraction scoring above" = area of the right tail. See Probability distributions .
Ask SciPy — sf = survival function = right-tail area.
from scipy import stats
p = stats.norm.sf( 130 , loc = 100 , scale = 15 )
print (p) # 0.0227501319...
Why this step? sf(x) directly gives P ( X > x ) ; equivalently 1 - cdf(x). Using sf avoids precision loss for tiny tails.
Verify: p ≈ 0.0228 = 2.28% . Sanity: the 95% rule said ~5% total in both tails → ~2.5% each; 2.28% agrees ✓.
Worked example Find the root of
f ( x ) = x 2 − 2 between x = 0 and x = 2 (i.e. compute 2 ).
Forecast: f ( 0 ) = 0 2 − 2 = − 2 (below zero) and f ( 2 ) = 2 2 − 2 = 2 (above zero). Somewhere between, the curve must climb through the axis, so f = 0 there. That crossing is x = 2 ≈ 1.41 . Guess the crossing before running.
Confirm a sign change. f ( 0 ) = − 2 < 0 and f ( 2 ) = + 2 > 0 .
Why this step? If a continuous curve is below the axis at one wall and above at the other, it must cross zero somewhere in between (the Intermediate Value Theorem). That crossing is the root, and it guarantees the bracketing solver a valid interval to hunt in.
Ask SciPy — a bracketing root finder.
from scipy import optimize
root = optimize.brentq( lambda x: x ** 2 - 2 , 0 , 2 )
print (root) # 1.4142135623730951
Why this step? brentq needs a bracket [ a , b ] where the function has opposite signs at the two ends; it then repeatedly squeezes the interval inward, always keeping the sign change inside — this makes convergence guaranteed , unlike a downhill optimizer that can get stuck.
Why brentq and not minimize? minimize finds where the curve is lowest ; brentq finds where the curve equals zero . Here we want a zero-crossing, so the bracketing root finder is the right tool.
Why this step? Choosing the tool that answers your question ("where is f = 0 ?") not a neighbouring one ("where is f smallest?") is half of using SciPy well.
Verify: r oo t 2 = 1.41421356 … 2 = 2.0000 … ✓, and r oo t = 2 . Case coverage: this is the sign-change root cell — contrast with g ( x ) = x 2 + 1 , which is + 1 at both x = − 1 and x = 1 (no sign change, never crosses zero), so brentq(..., -1, 1) would raise ValueError ("f(a) and f(b) must have different signs").
Recall Which submodule for each example?
Ex 1,2,5,6 use scipy.integrate ::: area / distance / improper integrals.
Ex 3 uses scipy.optimize.minimize ::: find valley bottoms (local, needs x0).
Ex 4,8 use scipy.linalg.solve ::: systems A x = b (Ex 4 is singular → error).
Ex 9 uses scipy.stats.norm.sf ::: right-tail probability.
Ex 10 uses scipy.optimize.brentq ::: root of f inside a sign-change bracket.
Mnemonic The four verbs on trial here
Integrate, Minimize, Solve, Test — and one hidden fifth, Find-the-root (brentq), which is
"solve f ( x ) = 0 " rather than "solve A x = b ".