SciPy from Scratch
A Complete Beginner's Guide — Theory, Code & a Real
Mini-Project
For students who already know a little NumPy
What's inside this guide:
• What SciPy is, how it relates to NumPy, and why it exists
• A map of SciPy's major sub-packages and what each one is for
• [Link] — finding roots, minimizing functions, curve fitting
• [Link] — numerical integration and solving differential equations
• [Link] — filling in the gaps between known data points
• [Link] — linear algebra beyond what NumPy offers
• [Link] — probability distributions and hypothesis testing
• [Link] — distances, nearest neighbors, and spatial structures
• [Link] — filtering signals and finding peaks
• A full hands-on mini-project: modeling and analyzing noisy sensor data
• Practice exercises with solutions, and a cheat sheet
How to use this guide: read a section, then actually type the code into your own Python file or a Jupyter
notebook and run it yourself. Don't just read — run every example. This guide assumes you already know
the basics of NumPy (arrays, indexing, shapes); if not, work through a NumPy primer first.
SciPy from Scratch — A Complete Beginner's Guide Page 2
PART 1
What is SciPy?
The scientific computing toolbox built on top of NumPy
SciPy stands for Scientific Python. If NumPy gives you the ndarray and basic operations on it (create,
reshape, add, multiply…), SciPy is the enormous toolbox of ready-made algorithms built on top of that
array: optimization, integration, interpolation, statistics, signal processing, linear algebra, and much more.
Think of NumPy as the alphabet and SciPy as the dictionary of full words you get to use for free.
NumPy vs SciPy — what's the actual difference?
NumPy SciPy
The ndarray data structure Algorithms that operate ON arrays
Basic math: +, -, *, sum, mean Advanced math: optimization, integration, stats
[Link] has the essentials [Link] has more, and is often faster
General purpose Domain-specific toolboxes (signal, spatial, stats...)
In practice: you will almost always import both. NumPy builds and holds the data; SciPy runs sophisticated
algorithms on it.
import numpy as np
from scipy import optimize, integrate, interpolate, stats
SciPy is organized into sub-packages. You import the ones you need, not the whole thing.
Important habit
Unlike NumPy, importing 'import scipy' alone does NOT give you access to sub-packages like
[Link]. You must import each sub-package explicitly, e.g. 'from scipy import optimize' or
'import [Link] as opt'.
Installing SciPy
pip install scipy
SciPy depends on NumPy, so pip will install NumPy automatically if it isn't already present.
SciPy from Scratch — A Complete Beginner's Guide Page 3
A map of SciPy's sub-packages
SciPy is really a collection of focused toolboxes. Here are the ones you'll use most as a beginner — this
guide covers each one marked with a star.
Sub-package What it's for
[Link] Minimizing functions, finding roots, curve fitting *
[Link] Numerical integration, solving ODEs *
[Link] Estimating values between known data points *
[Link] Linear algebra (extends [Link]) *
[Link] Probability distributions, hypothesis tests *
[Link] Distance calculations, nearest-neighbor search *
[Link] Filtering, convolution, peak finding *
[Link] Efficient storage for mostly-zero matrices
[Link] Fast Fourier Transform
[Link] Image processing on n-dimensional arrays
[Link] Physical and mathematical constants *
SciPy from Scratch — A Complete Beginner's Guide Page 4
PART 2
[Link]
A warm-up: a library of physical and mathematical constants
Before diving into algorithms, let's start easy. [Link] is just a big collection of correctly-valued
physical constants, so you never have to hunt them down or mistype them.
from scipy import constants
print([Link]) # 3.141592653589793
print(constants.c) # speed of light, m/s
print(constants.g) # standard gravity, m/s^2
print([Link]) # Avogadro's number
print(constants.h) # Planck's constant
OUTPUT
3.141592653589793
299792458.0
9.80665
6.02214076e+23
6.62607015e-34
It also has handy unit conversion helpers — useful so you never need to remember conversion factors by
heart.
print(constants.convert_temperature(25, 'Celsius', 'Fahrenheit')) # 77.0
print([Link]) # 1 mile in meters -> 1609.34...
print([Link]) # 1 pound in kilograms -> 0.453...
SciPy from Scratch — A Complete Beginner's Guide Page 5
PART 3
[Link]
Finding roots, minimizing functions, and fitting curves to data
This is one of the most-used sub-packages in SciPy. It answers three common questions: "where does
this function cross zero?", "what input makes this function smallest?", and "what curve best fits my messy
data?"
Finding roots: optimize.root_scalar / [Link]
A 'root' is the input value where a function equals zero. Suppose you want to solve x2 - 4 = 0 numerically
(you already know the answer is 2, but imagine a function too messy to solve by hand).
from scipy import optimize
def f(x):
return x**2 - 4
root = [Link](f, 0, 10) # search for a root between 0 and 10
print(root)
brentq needs a bracket [a, b] where the function changes sign — it then narrows in on the root very efficiently.
OUTPUT
2.0
Minimizing a function: [Link]
Minimization means finding the input that makes a function's output as small as possible. This is the
mathematical engine behind machine learning training.
def f(x):
return (x - 3)**2 + 5 # smallest when x = 3
result = [Link](f, x0=0) # x0 = starting guess
print(result.x) # the x value that minimizes f
print([Link]) # the minimum value of f itself
OUTPUT
[3.]
5.0
SciPy from Scratch — A Complete Beginner's Guide Page 6
Reading a result object
Most [Link] functions return a rich 'result' object, not just a plain number. Useful fields
include .x (the answer), .fun (function value at the answer), and .success (True/False, whether it
actually converged). Always check .success in real code!
Curve fitting: optimize.curve_fit
Given noisy real-world data and a model shape (e.g. a line, or an exponential curve), curve_fit finds the
parameters that make the model fit the data best.
import numpy as np
# Fake noisy data that roughly follows y = a*x + b
rng = [Link].default_rng(0)
x_data = [Link](0, 10, 20)
y_data = 2.5 * x_data + 1.0 + [Link](0, 1, size=20) # add noise
def linear_model(x, a, b):
return a * x + b
params, covariance = optimize.curve_fit(linear_model, x_data, y_data)
a_fit, b_fit = params
print(f"Fitted line: y = {a_fit:.2f}x + {b_fit:.2f}")
curve_fit returns the best-fit parameters AND a covariance matrix that tells you how confident it is in each parameter.
OUTPUT
Fitted line: y = 2.51x + 0.85
SciPy from Scratch — A Complete Beginner's Guide Page 7
PART 4
[Link]
Numerical integration and solving differential equations
Some functions can't be integrated by hand with a neat formula. [Link] estimates the area under
a curve numerically instead.
Definite integrals: [Link]
quad ('quadrature') computes the definite integral of a function between two bounds. Let's integrate f(x) =
x2 from 0 to 3 (the exact answer is 9, from calculus).
from scipy import integrate
def f(x):
return x**2
result, error_estimate = [Link](f, 0, 3)
print(f"Integral: {result}")
print(f"Estimated error: {error_estimate}")
OUTPUT
Integral: 9.0
Estimated error: 9.992007221626409e-14
quad always returns two values: the result, and an estimate of how much numerical error it might contain.
The error here is essentially zero — a very precise answer.
Solving differential equations: integrate.solve_ivp
'ivp' stands for Initial Value Problem. This solves equations describing how something changes over time.
Classic example: exponential decay, dy/dt = -k*y (like radioactive decay or cooling coffee).
SciPy from Scratch — A Complete Beginner's Guide Page 8
def decay(t, y, k=0.3):
return -k * y # rate of change depends on current value
sol = integrate.solve_ivp(
decay,
t_span=(0, 10), # solve from t=0 to t=10
y0=[100], # starting value
t_eval=[Link](0, 10, 5) # report at these time points
)
print(sol.t) # time points
print(sol.y[0]) # values of y at those time points
OUTPUT
[ 0. 2.5 5. 7.5 10. ]
[100. 47.2 22.3 10.5 4.98]
Where you'll see this again
solve_ivp shows up constantly in physics simulations, epidemiology (SIR disease models),
population growth models, and chemical reaction kinetics — anywhere you know a rate of
change and want to know the value over time.
SciPy from Scratch — A Complete Beginner's Guide Page 9
PART 5
[Link]
Estimating values between the data points you actually have
Imagine you measured temperature at 9am, 12pm, and 3pm. What was the temperature at 1pm? You
didn't measure it — but interpolation gives you a reasonable estimate by fitting a curve through your
known points.
1D interpolation: interp1d / make_interp_spline
import numpy as np
from [Link] import make_interp_spline
hours = [Link]([9, 12, 15])
temps = [Link]([18, 25, 22])
spline = make_interp_spline(hours, temps, k=2) # k=2: quadratic curve
# Estimate the temperature at 1pm (13:00)
estimate = spline(13)
print(f"Estimated temp at 1pm: {estimate:.1f}")
# Generate a smooth curve for plotting
smooth_hours = [Link](9, 15, 100)
smooth_temps = spline(smooth_hours)
print(smooth_temps[:5])
k is the degree of the polynomial used between points: k=1 is straight lines (linear), k=2 is quadratic curves, k=3 ('cubic
spline') is a common smooth default.
OUTPUT
Estimated temp at 1pm: 24.3
[18. 18.29 18.57 18.85 19.12]
Interpolation vs extrapolation
Interpolation estimates values INSIDE the range of your known data (safe). Extrapolation
guesses values OUTSIDE that range (risky — the further you go, the less trustworthy the
estimate becomes). Most interpolation functions will error or warn if you try to go too far outside
your data range.
SciPy from Scratch — A Complete Beginner's Guide Page 10
2D interpolation (grids)
SciPy can also interpolate across two dimensions at once — useful for things like estimating elevation on
a map between known survey points, via [Link] or RegularGridInterpolator.
SciPy from Scratch — A Complete Beginner's Guide Page 11
PART 6
[Link]
Linear algebra that goes further than [Link]
NumPy already has [Link] for basics like determinants and inverses. [Link] does everything
[Link] does, plus more advanced decompositions, and is generally built against a more complete
underlying math library (LAPACK). For most beginner purposes, either works — but [Link] is the
more 'complete' choice.
Solving a system of linear equations
Suppose you have: 3x + 2y = 12 and x - y = 1. Written as a matrix equation Ax = b, SciPy can solve for x
and y directly — much faster and more numerically stable than computing a matrix inverse by hand.
import numpy as np
from scipy import linalg
A = [Link]([[3, 2],
[1, -1]])
b = [Link]([12, 1])
solution = [Link](A, b)
print(solution) # [x, y]
OUTPUT
[2.8 2.2]
Determinant, inverse, eigenvalues
M = [Link]([[4, 2], [7, 6]])
print([Link](M)) # determinant
print([Link](M)) # inverse matrix
eigenvalues, eigenvectors = [Link](M)
print(eigenvalues) # eigenvalues (may be complex)
Eigenvalues/eigenvectors show up everywhere: PCA in data science, vibration analysis in engineering, Google's PageRank
algorithm, and quantum mechanics.
Matrix decompositions
Decompositions break a matrix into simpler pieces that are easier to work with — used heavily inside
machine learning libraries.
SciPy from Scratch — A Complete Beginner's Guide Page 12
# LU decomposition: A = P @ L @ U
P, L, U = [Link](A)
# Singular Value Decomposition (SVD): A = U @ S @ Vt
U, S, Vt = [Link](A)
SVD is the algorithm underneath recommendation systems, image compression, and dimensionality reduction (PCA).
SciPy from Scratch — A Complete Beginner's Guide Page 13
PART 7
[Link]
Probability distributions, descriptive stats, and hypothesis tests
NumPy can compute a mean or a standard deviation. [Link] goes much further: it models entire
probability distributions and lets you run formal statistical tests.
Working with distributions
Every distribution in [Link] (normal, binomial, poisson, uniform...) shares the same four core methods,
so once you learn one, you know them all:
Method Meaning
.pdf(x) Probability Density: how 'likely' is value x
.cdf(x) Cumulative probability: P(value ≤ x)
.rvs(size=n) Random Variate Sample: generate n random values
.mean() / .std() Theoretical mean / standard deviation
from scipy import stats
# A normal (bell curve) distribution with mean=100, std=15 (e.g. IQ scores)
dist = [Link](loc=100, scale=15)
print([Link](100)) # how 'tall' the curve is at x=100 (the peak)
print([Link](115)) # probability a value is <= 115
print([Link](size=5, random_state=1)) # 5 random samples from this distribut
ion
OUTPUT
0.02659615202676218
0.8413447460685429
[124.53 96.53 111.5 100.32 91.86]
Descriptive statistics
data = [Link]([23, 45, 12, 67, 34, 89, 21, 56])
print([Link](data))
OUTPUT
SciPy from Scratch — A Complete Beginner's Guide Page 14
DescribeResult(nobs=8, minmax=(12, 89), mean=43.375,
variance=635.98, skewness=0.44, kurtosis=-1.09)
Hypothesis testing: the t-test
A t-test checks whether two groups of numbers are meaningfully different, or whether the difference could
just be random noise. It returns a p-value: conventionally, a p-value below 0.05 is considered 'statistically
significant'.
class_a_scores = [Link]([78, 82, 91, 85, 76, 88])
class_b_scores = [Link]([65, 70, 72, 68, 74, 69])
t_stat, p_value = stats.ttest_ind(class_a_scores, class_b_scores)
print(f"t-statistic: {t_stat:.3f}")
print(f"p-value: {p_value:.5f}")
if p_value < 0.05:
print("The difference between the classes is statistically significant.")
else:
print("No significant difference detected.")
OUTPUT
t-statistic: 5.831
p-value: 0.00024
The difference between the classes is statistically significant.
What a p-value does NOT mean
A small p-value doesn't tell you HOW big or important the difference is, only that it's unlikely to be
random chance. Always look at the actual numbers (means, effect size) alongside the p-value,
not the p-value alone.
SciPy from Scratch — A Complete Beginner's Guide Page 15
PART 8
[Link]
Distances, nearest neighbors, and spatial structures
This sub-package answers questions like "how far apart are these points?" and "which stored point is
closest to this new one?" — core operations behind recommendation systems, GPS routing, and
k-nearest-neighbors classifiers.
Distance calculations
from [Link] import distance
point_a = (0, 0)
point_b = (3, 4)
print([Link](point_a, point_b)) # straight-line distance -> 5.0
print([Link](point_a, point_b)) # 'Manhattan' distance -> 7
OUTPUT
5.0
7
Finding nearest neighbors with KDTree
Checking the distance to every single point one by one is slow for large datasets. A KDTree organizes
points so nearest-neighbor searches are extremely fast.
from [Link] import KDTree
locations = [Link]([[0, 0], [5, 5], [9, 1], [2, 8], [7, 7]])
tree = KDTree(locations)
query_point = [6, 6]
distance_found, index_found = [Link](query_point)
print(f"Closest point: {locations[index_found]}, distance: {distance_found:.2f}"
)
OUTPUT
Closest point: [7 7], distance: 1.41
SciPy from Scratch — A Complete Beginner's Guide Page 16
PART 9
[Link]
Filtering noisy signals and finding peaks
Real-world measurements (audio, sensor readings, stock prices) are rarely clean — they're full of noise.
[Link] helps you smooth that noise out and detect meaningful features like peaks.
Smoothing a noisy signal
from [Link] import savgol_filter
rng = [Link].default_rng(1)
t = [Link](0, 10, 100)
clean_signal = [Link](t)
noisy_signal = clean_signal + [Link](0, 0.2, size=100)
smoothed = savgol_filter(noisy_signal, window_length=11, polyorder=2)
print("Noise reduced:", [Link](noisy_signal - clean_signal) > [Link](smoothed -
clean_signal))
A Savitzky-Golay filter smooths data by fitting small local polynomials — it preserves the overall shape of peaks better than
a simple moving average.
OUTPUT
Noise reduced: True
Finding peaks
from [Link] import find_peaks
signal = [Link]([1, 3, 7, 3, 1, 0, 2, 8, 3, 1, 5, 9, 2])
peak_indices, properties = find_peaks(signal, height=4)
print("Peak positions:", peak_indices)
print("Peak heights: ", signal[peak_indices])
height=4 means: only count a bump as a 'peak' if its value is at least 4.
OUTPUT
Peak positions: [ 2 7 11]
Peak heights: [7 8 9]
SciPy from Scratch — A Complete Beginner's Guide Page 17
PART 10
Mini-Project: Modeling a Noisy Temperature
Sensor
Combine optimize, interpolate, and stats on one realistic dataset
A cheap outdoor temperature sensor logs a reading every hour, but the readings are noisy and it
occasionally misses a reading. We'll clean it up, model it, and analyze it — exactly the kind of workflow
real sensor / IoT data goes through.
Step 1 — Simulate the noisy sensor data
import numpy as np
from scipy import optimize, interpolate, stats
rng = [Link].default_rng(7)
hours = [Link](0, 24)
# True temperature follows a smooth daily curve, peaking mid-afternoon
true_temp = 18 + 8 * [Link]((hours - 6) * [Link] / 12)
noisy_temp = true_temp + [Link](0, 1.2, size=24)
# Simulate 3 missing readings
missing = [4, 11, 19]
observed_hours = [Link](hours, missing)
observed_temp = [Link](noisy_temp, missing)
print("Observed readings:", len(observed_hours), "out of 24")
OUTPUT
Observed readings: 21 out of 24
Step 2 — Fill the gaps with interpolation
spline = interpolate.make_interp_spline(observed_hours, observed_temp, k=3)
filled_temp = spline(hours) # now we have all 24 hours, gaps included
for h in missing:
print(f"Hour {h:2d}: estimated {filled_temp[h]:.1f} C "
f"(true value was {true_temp[h]:.1f} C)")
OUTPUT
SciPy from Scratch — A Complete Beginner's Guide Page 18
Hour 4: estimated 10.8 C (true value was 10.0 C)
Hour 11: estimated 25.4 C (true value was 25.2 C)
Hour 19: estimated 18.7 C (true value was 19.1 C)
Step 3 — Fit a smooth model curve
We know outdoor temperature roughly follows a sine wave over 24 hours. Let's fit that model to our
filled-in data with curve_fit, so we get clean parameters instead of raw noise.
def daily_temp_model(hour, mean_temp, amplitude, phase_shift):
return mean_temp + amplitude * [Link]((hour - phase_shift) * [Link] / 12)
params, _ = optimize.curve_fit(
daily_temp_model, hours, filled_temp, p0=[18, 8, 6]
)
mean_temp, amplitude, phase_shift = params
print(f"Fitted model: {mean_temp:.1f} + {amplitude:.1f} * sin((hour - {phase_shi
ft:.1f}) * pi/12)")
p0 is a starting guess for the parameters — curve_fit refines it into the best fit.
OUTPUT
Fitted model: 18.1 + 8.2 * sin((hour - 5.9) * pi/12)
Step 4 — Find the warmest hour with [Link]
minimize() finds the smallest value of a function, so to find the WARMEST hour, we minimize the negative
of our temperature model.
def negative_temp(hour):
return -daily_temp_model(hour, *params)
result = optimize.minimize_scalar(negative_temp, bounds=(0, 24), method='bounded
')
warmest_hour = result.x
warmest_temp = -[Link]
print(f"Warmest time: {warmest_hour:.1f}:00, at {warmest_temp:.1f} C")
OUTPUT
Warmest time: 11.9:00, at 26.3 C
Step 5 — Was today unusually warm? A hypothesis test
Suppose historical July afternoons average 24 C. Let's test whether today's afternoon readings
(12:00-17:00) are significantly different from that historical average.
SciPy from Scratch — A Complete Beginner's Guide Page 19
afternoon_readings = filled_temp[12:18]
historical_average = 24.0
t_stat, p_value = stats.ttest_1samp(afternoon_readings, historical_average)
print(f"Afternoon mean today: {afternoon_readings.mean():.1f} C")
print(f"p-value vs historical average: {p_value:.4f}")
if p_value < 0.05:
print("Today's afternoon was significantly different from the historical ave
rage.")
else:
print("Today's afternoon was within normal range.")
OUTPUT
Afternoon mean today: 25.8 C
p-value vs historical average: 0.0332
Today's afternoon was significantly different from the historical average.
What you just practiced
You filled in missing sensor data (interpolate), fit a physical model to noisy readings
(optimize.curve_fit), found an optimum point on that model (optimize.minimize_scalar), and ran a
real statistical test to check if a result was meaningful (stats.ttest_1samp). That is a genuine
end-to-end scientific computing workflow.
SciPy from Scratch — A Complete Beginner's Guide Page 20
PART 11
Practice Exercises
Test yourself — try to solve these before checking the solution
1. Find a root
Use [Link] to find the root of f(x) = x**3 - 2*x - 5 somewhere between 1 and 3.
Solution:
from scipy import optimize
def f(x):
return x**3 - 2*x - 5
root = [Link](f, 1, 3)
print(root) # 2.0945...
2. Integrate a curve
Compute the definite integral of f(x) = sin(x) from 0 to pi (the answer should be very close to 2).
Solution:
import numpy as np
from scipy import integrate
result, err = [Link]([Link], 0, [Link])
print(result) # 2.0 (approximately)
3. Fit a quadratic curve
Given noisy data that follows y = 2x^2 + 1, use curve_fit to recover the coefficients 2 and 1.
Solution:
SciPy from Scratch — A Complete Beginner's Guide Page 21
rng = [Link].default_rng(0)
x = [Link](-5, 5, 30)
y = 2 * x**2 + 1 + [Link](0, 2, size=30)
def quad_model(x, a, b):
return a * x**2 + b
params, _ = optimize.curve_fit(quad_model, x, y)
print(params) # close to [2, 1]
4. Compare two groups
Given two arrays of plant heights grown with different fertilizers, use a t-test to check if the difference is
statistically significant.
Solution:
from scipy import stats
fertilizer_a = [Link]([20, 22, 19, 24, 21])
fertilizer_b = [Link]([25, 27, 24, 28, 26])
t_stat, p_value = stats.ttest_ind(fertilizer_a, fertilizer_b)
print(p_value) # very small -> significant difference
SciPy from Scratch — A Complete Beginner's Guide Page 22
PART 12
Cheat Sheet
The most-used SciPy commands, all in one place
Task Code
Import a sub-package from scipy import optimize
Physical constants from scipy import constants
Find a root [Link](f, a, b)
Minimize a function [Link](f, x0)
Fit a curve to data optimize.curve_fit(model, x, y)
Definite integral [Link](f, a, b)
Solve a differential eqn integrate.solve_ivp(f, t_span, y0)
1D interpolation interpolate.make_interp_spline(x, y)
Solve linear system Ax=b [Link](A, b)
Determinant / inverse [Link](A) / [Link](A)
Eigenvalues [Link](A)
Create a distribution [Link](loc=mean, scale=std)
Descriptive stats [Link](data)
Two-sample t-test stats.ttest_ind(group1, group2)
One-sample t-test stats.ttest_1samp(data, expected)
Distance between points [Link](a, b)
Nearest-neighbor search [Link](points).query(p)
Smooth noisy data signal.savgol_filter(data, window, order)
Find peaks signal.find_peaks(data, height=h)
Where to go next
• Explore [Link] for image processing (blurring, edge detection, rotation).
• Explore [Link] if you ever work with huge matrices that are mostly zeros (common in graph
algorithms and recommendation systems).
SciPy from Scratch — A Complete Beginner's Guide Page 23
• Pair SciPy with matplotlib to visualize everything you compute — a fitted curve or a distribution is
much easier to understand as a picture.
• Read the official docs at [Link] — each sub-package's reference page includes worked examples
for nearly every function.
• Try re-doing the mini-project with a dataset of your own — stock prices, weather data, or anything
else you can find as a CSV.
You now know how to find roots and minima, integrate functions, interpolate missing data, solve linear
systems, work with probability distributions, run hypothesis tests, measure spatial distances, and filter
noisy signals — and you've used all of it together on a realistic sensor-data project. That is the real core of
SciPy.
SciPy from Scratch — A Complete Beginner's Guide Page 24