so i’ve been going through the freeCodeCamp numpy tutorial by keith galli and honestly it’s one of those things that clicks once you see it used properly. here’s everything from the video, broken down the way it made sense to me.
what even is numpy
numpy stands for numerical python and it’s basically the backbone of almost every data/science/ml library in python. at its core it gives you this thing called an ndarray - an n-dimensional array that’s way faster and more capable than regular python lists.
import numpy as np
that np convention is universal. everyone uses it. just do it.
numpy vs python lists - why bother
okay so why not just use lists? here’s the thing - python lists are slow. like, really slow. they can hold any type of object, which sounds flexible until you realize that flexibility comes with overhead. every element is a pointer to a python object, and doing math on them means looping through in python.
numpy arrays on the other hand are stored as contiguous blocks of memory with a single data type. that means the computer can operate on them in bulk, using compiled C code under the hood.
import time
# python list - doing it the slow way
py_list = list(range(1000000))
start = time.time()
result = [x ** 2 for x in py_list]
print(f"list: {time.time() - start:.4f}s")
# numpy array - zoom zoom
np_arr = np.arange(1000000)
start = time.time()
result = np_arr ** 2
print(f"numpy: {time.time() - start:.4f}s")
you’ll typically see numpy be like 40-50x faster. and that’s not even an exaggeration. once you start working with real data (thousands or millions of rows), this difference becomes night and day.
a few other things numpy gives you that lists don’t:
| thing | python list | numpy array |
|---|---|---|
| types | can mix anything | one type per array |
| math | loop it yourself | just use operators |
| memory | lots of overhead | compact and efficient |
| linear algebra | nope | built in |
| broadcasting | doesn’t exist | very much exists |
a = np.array([1, 2, 3, 4])
b = np.array([5, 6, 7, 8])
# just... do math. on arrays. directly.
print(a + b) # [ 6 8 10 12]
print(a * b) # [ 5 12 21 32]
print(a ** 2) # [ 1 4 9 16]
print(a.dot(b)) # 70 (this is the dot product)
no loops. no list comprehensions. just operators doing the right thing.
where numpy actually gets used
this isn’t just an academic thing. numpy is everywhere:
- machine learning / deep learning - every model uses it. weights are numpy arrays (or tensors that behave like them)
- data science - pandas is built on top of numpy. every csv you load, every column you manipulate - numpy underneath
- image processing - an image is literally a 3D numpy array (height × width × rgb channels). want to flip it? rotate it? apply a filter? just array math
- scientific computing - simulations, physics models, numerical methods
- finance - portfolio analysis, risk modeling, monte carlo simulations
basically if you’re doing anything numerical in python, you’re using numpy.
the basics - creating arrays
the most straightforward way is just passing a python list to np.array():
a = np.array([1, 2, 3])
print(a) # [1 2 3]
print(type(a)) # <class 'numpy.ndarray'>
you can go nested for 2D, 3D, however many dimensions:
# 2D - like a matrix
b = np.array([[1, 2, 3],
[4, 5, 6]])
# 3D - think of it as blocks of matrices
c = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])</code></pre>
<p>but most of the time you won’t be manually typing arrays like this. you’ll use shorthand methods:</p>
<pre class="highlight"><code class="language-python">np.zeros((2, 3)) # 2×3 matrix of zeros
np.ones((4, 2), dtype=int) # 4×2 of ones, as integers
np.full((3, 3), 7) # 3×3 filled with 7
np.eye(4) # 4×4 identity matrix
np.arange(0, 20, 2) # [0, 2, 4, ... 18]
np.linspace(0, 1, 5) # 5 evenly spaced values from 0 to 1
np.identity(3) # same as eye(3) basically</code></pre>
<p>for random stuff:</p>
<pre class="highlight"><code class="language-python">np.random.random((4, 3)) # 4×3 of random floats [0, 1)
np.random.randint(0, 100, size=(3, 3)) # 3×3 of random ints 0-99
np.random.randn(3, 3) # normal distribution (mean=0, std=1)</code></pre>
<h3 id="the-attributes-you-should-know">the attributes you should know</h3>
<p>once you have an array, you’ll want to know things about it:</p>
<pre class="highlight"><code class="language-python">a = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10]])
a.shape # (2, 5) - 2 rows, 5 columns
a.ndim # 2 - number of dimensions
a.size # 10 - total elements
a.dtype # int64 - the data type
a.itemsize # 8 - how many bytes each element takes
a.nbytes # 80 - total memory used</code></pre>
<p><code>shape</code> is probably the one you’ll check the most. when things go wrong with numpy, 90% of the time it’s a shape mismatch.</p>
<hr />
<h2 id="slicing-and-indexing-grabbing-what-you-need">slicing and indexing - grabbing what you need</h2>
<p>this works almost exactly like python lists, but extended to multiple dimensions.</p>
<h3 id="1d">1D</h3>
<pre class="highlight"><code class="language-python">a = np.array([10, 20, 30, 40, 50])
a[0] # 10
a[-1] # 50
a[1:4] # [20 30 40]
a[::2] # [10 30 50] (every other element)</code></pre>
<h3 id="2d-this-is-where-it-gets-interesting">2D - this is where it gets interesting</h3>
<pre class="highlight"><code class="language-python">a = np.array([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10],
[11, 12, 13, 14, 15]])
a[0, 3] # 4 - row 0, column 3
a[1, :] # [6 7 8 9 10] - entire second row
a[:, 0] # [1 6 11] - entire first column
a[0:2, 1:4] # sub-matrix:
# [[2 3 4]
# [7 8 9]]</code></pre>
<p>the syntax is <code>a[rows, columns]</code>. that’s it. once that clicks, everything else follows.</p>
<h3 id="modifying-elements">modifying elements</h3>
<pre class="highlight"><code class="language-python">a = np.array([[1, 2, 3],
[4, 5, 6]])
a[0, 1] = 99 # change one element
a[1, :] = 0 # zero out entire row
a[:, 2] = [10, 20] # set column using broadcasting
print(a)
# [[ 1 99 10]
# [ 0 0 20]]</code></pre>
<h3 id="3d-slicing">3D slicing</h3>
<pre class="highlight"><code class="language-python">a = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
a[0, :, :] # first block: [[1 2] [3 4]]
a[:, 1, :] # second row across both blocks: [[3 4] [7 8]]
a[0, 0, 1] # single value: 2</code></pre>
<p>you can think of 3D indexing as <code>a[block, row, column]</code>.</p>
<hr />
<h2 id="initializing-different-arrays">initializing different arrays</h2>
<p>you’ll often need to create arrays filled with specific values. here’s your toolkit:</p>
<pre class="highlight"><code class="language-python"># all zeros - probably the one you'll use most
np.zeros((2, 3))
# all ones
np.ones((3, 3, 3))
# fill with any value
np.full((2, 2), 99)
# fill with same shape as another array
ref = np.array([[1, 2], [3, 4]])
np.full_like(ref, 42)
# random stuff
np.random.random((4, 3)) # uniform [0, 1)
np.random.randint(0, 100, (3, 3)) # random ints
np.random.randn(3, 3) # standard normal
# identity / diagonal
np.identity(3)
np.diag([1, 2, 3, 4])
# ranges
np.arange(0, 20, 2) # 0, 2, 4, ... 18
np.linspace(0, 1, 5) # 0, 0.25, 0.5, 0.75, 1.0</code></pre>
<hr />
<h2 id="problem-1">problem #1</h2>
<p>how do you create this array?</p>
<pre class="highlight"><code>[[ 1 1 1 1 1]
[ 1 9 9 9 1]
[ 1 9 9 9 1]
[ 1 9 9 9 1]
[ 1 1 1 1 1]]</code></pre>
<details>
<summary>click to reveal</summary>
<pre class="highlight"><code class="language-python">a = np.ones((5, 5), dtype=int)
a[1:-1, 1:-1] = 9</code></pre>
the trick is starting with ones and then filling the inner part. `1:-1` means "skip the first and last" on both axes, which gives you the interior.
</details>
<hr />
<h2 id="be-careful-when-copying">be careful when copying!!</h2>
<p>this one trips everyone up at least once.</p>
<pre class="highlight"><code class="language-python">a = np.array([1, 2, 3])
b = a # this is NOT a copy. b points to the same data.
b[0] = 100
print(a) # [100 2 3] - a got changed too!</code></pre>
<p>when you do <code>b = a</code>, you’re not making a copy. you’re making another name for the same array. it’s like naming your cat “also cat” - it’s still the same cat.</p>
<p>to actually copy:</p>
<pre class="highlight"><code class="language-python">a = np.array([1, 2, 3])
b = a.copy() # now this is independent
b[0] = 100
print(a) # [1 2 3] - a is untouched
print(b) # [100 2 3]</code></pre>
<p>always use <code>.copy()</code> when you need an independent array.</p>
<hr />
<h2 id="basic-mathematics">basic mathematics</h2>
<p>this is where numpy really shines. all math is element-wise by default:</p>
<pre class="highlight"><code class="language-python">a = np.array([1, 2, 3, 4, 5])
a + 2 # [3 4 5 6 7]
a - 2 # [-1 0 1 2 3]
a * 2 # [ 2 4 6 8 10]
a / 2 # [0.5 1. 1.5 2. 2.5]
a ** 2 # [ 1 4 9 16 25]
a % 3 # [1 2 0 1 2]</code></pre>
<p>aggregate functions:</p>
<pre class="highlight"><code class="language-python">np.sum(a) # 15
np.mean(a) # 3.0
np.std(a) # 1.4142...
np.min(a) # 1
np.max(a) # 5
np.prod(a) # 120 (1*2*3*4*5)
np.median(a) # 3.0</code></pre>
<p>trigonometry:</p>
<pre class="highlight"><code class="language-python">angles = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
np.sin(angles)
np.cos(angles)
np.tan(angles)
np.degrees(angles) # [ 0. 30. 45. 60. 90.]
np.radians([0, 90, 180])</code></pre>
<p>other useful ones:</p>
<pre class="highlight"><code class="language-python">np.sqrt(a)
np.log(a) # natural log
np.log2(a)
np.abs([-1, 2, -3]) # [1 2 3]
np.exp(a) # e^x
np.round(np.pi, 2) # 3.14</code></pre>
<p>the key thing to remember: <strong>all of this happens without loops</strong>. that’s the whole point.</p>
<hr />
<h2 id="linear-algebra">linear algebra</h2>
<p>numpy has a whole linear algebra module in <code>np.linalg</code>. here are the essentials:</p>
<pre class="highlight"><code class="language-python">a = np.array([[1, 2],
[3, 4]])
b = np.array([[5, 6],
[7, 8]])</code></pre>
<h3 id="matrix-multiplication">matrix multiplication</h3>
<pre class="highlight"><code class="language-python">np.matmul(a, b)
# or equivalently
a @ b
# result:
# [[19 22]
# [43 50]]</code></pre>
<p>the <code>@</code> operator is the cleanest way to do matrix multiplication. use it.</p>
<h3 id="dot-product">dot product</h3>
<pre class="highlight"><code class="language-python">v1 = np.array([1, 2, 3])
v2 = np.array([4, 5, 6])
np.dot(v1, v2) # 32 (1*4 + 2*5 + 3*6)</code></pre>
<h3 id="other-linalg-stuff">other linalg stuff</h3>
<pre class="highlight"><code class="language-python"># determinant
np.linalg.det(np.array([[3, 7], [1, -4]])) # -19.0
# inverse
np.linalg.inv(np.array([[1, 2], [3, 4]]))
# eigenvalues and eigenvectors
vals, vecs = np.linalg.eig(np.array([[4, 2], [1, 3]]))
# matrix rank
np.linalg.matrix_rank(np.array([[1, 2], [3, 6]])) # 1
# trace (sum of diagonal)
np.trace(np.array([[1, 2], [3, 4]])) # 5
# norm (magnitude of a vector)
np.linalg.norm(np.array([3, 4])) # 5.0
# solve linear equations: Ax = b
A = np.array([[3, 1], [1, 2]])
b = np.array([9, 8])
np.linalg.solve(A, b) # [2. 3.]</code></pre>
<hr />
<h2 id="statistics">statistics</h2>
<pre class="highlight"><code class="language-python">data = np.array([15, 22, 8, 34, 12, 28, 19])
np.mean(data) # 19.714...
np.median(data) # 19.0
np.std(data) # 8.489...
np.var(data) # 72.061...
np.percentile(data, 25) # 13.5
np.percentile(data, 75) # 25.0
# correlation between two variables
x = np.array([1, 2, 3, 4, 5])
y = np.array([2, 4, 5, 4, 5])
np.corrcoef(x, y)
# histogram
values, bins = np.histogram(data, bins=5)
print("counts:", values)
print("bins:", bins)</code></pre>
<hr />
<h2 id="reorganizing-arrays">reorganizing arrays</h2>
<p>once you have data, you’ll often need to reshape it - especially when feeding it into ML models or combining datasets.</p>
<pre class="highlight"><code class="language-python">before = np.array([[1, 2, 3, 4],
[5, 6, 7, 8]])
# reshape - total elements must stay the same
np.reshape(before, (4, 2))
# [[1 2]
# [3 4]
# [5 6]
# [7 8]]
# use -1 to let numpy figure out one dimension
before.reshape(2, -1) # same result
# flatten to 1D
before.flatten() # [1 2 3 4 5 6 7 8]</code></pre>
<h3 id="stacking-arrays-together">stacking arrays together</h3>
<pre class="highlight"><code class="language-python">v1 = np.array([[1, 2],
[3, 4]])
v2 = np.array([[5, 6]])
# vertical stack (stack on top of each other)
np.vstack((v1, v2))
# [[1 2]
# [3 4]
# [5 6]]
# horizontal stack (stack side by side)
h1 = np.array([[1, 2],
[3, 4]])
h2 = np.array([[5, 6],
[7, 8]])
np.hstack((h1, h2))
# [[1 2 5 6]
# [3 4 7 8]]
# split arrays
arr = np.arange(12).reshape(3, 4)
top, bottom = np.vsplit(arr, 2) # split vertically
left, right = np.hsplit(arr, 2) # split horizontally</code></pre>
<hr />
<h2 id="loading-data-from-files">loading data from files</h2>
<p>in real projects you’re not defining arrays by hand - you’re loading them from csv files, text files, etc.</p>
<pre class="highlight"><code class="language-python"># save to csv
data = np.array([[1, 2, 3],
[4, 5, 6]])
np.savetxt("data.csv", data, delimiter=",", header="a,b,c", comments="")
# load from csv
loaded = np.loadtxt("data.csv", delimiter=",", skiprows=1)
# load only specific columns
data = np.loadtxt("data.csv", delimiter=",", usecols=(0, 2))
# binary format - way faster for large arrays
np.save("data.npy", data)
data = np.load("data.npy")</code></pre>
<p>use <code>.npy</code> for anything large. it’s faster to save/load and preserves the exact data types.</p>
<hr />
<h2 id="advanced-indexing-and-boolean-masking">advanced indexing and boolean masking</h2>
<p>this is where numpy goes from “nice arrays” to “incredibly powerful data manipulation tool.”</p>
<h3 id="boolean-indexing">boolean indexing</h3>
<pre class="highlight"><code class="language-python">a = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# this creates a boolean array
a > 5
# [[False False False]
# [False False True]
# [ True True True]]
# use it as a mask to filter values
a[a > 5] # [6 7 8 9]
# multiple conditions (use & and |, NOT `and`/`or`)
a[(a > 3) & (a < 8)] # [4 5 6 7]
this is incredibly useful for data cleaning. imagine you have a dataset and want to grab only the rows where a certain column exceeds a threshold - this is how.
np.where - the ternary operator of numpy
a = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# where condition is true, keep value; otherwise replace with 0
np.where(a > 5, a, 0)
# [[0 0 0]
# [0 0 6]
# [7 8 9]]</code></pre>
<h3 id="fancy-indexing">fancy indexing</h3>
<pre class="highlight"><code class="language-python">a = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# grab specific rows
a[[0, 2]] # rows 0 and 2
# [[1 2 3]
# [7 8 9]]
# grab specific elements
a[[0, 2], [1, 0]] # a[0,1] and a[2,0] → [2 7]</code></pre>
<h3 id="other-useful-stuff">other useful stuff</h3>
<pre class="highlight"><code class="language-python"># check if any/all elements meet a condition
np.any(a > 8) # True
np.all(a > 0) # True
# find indices where condition is true
np.argwhere(a > 5)
# [[1 2]
# [2 0]
# [2 1]
# [2 2]]
# unique values
arr = np.array([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])
np.unique(arr) # [1 2 3 4 5 6 9]
values, counts = np.unique(arr, return_counts=True)
# sort
np.sort(arr) # [1 1 2 3 3 4 5 5 6 9]
# clip values to a range
np.clip(arr, 2, 8) # clamp between 2 and 8</code></pre>
<hr />
<h2 id="problem-2">problem #2</h2>
<p>how do you index these values from this array?</p>
<pre class="highlight"><code class="language-python">a = np.array([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20],
[21, 22, 23, 24, 25]])</code></pre>
<ol>
<li><code>[1, 2, 3, 4, 5]</code> - first row</li>
<li><code>[21, 22, 23, 24, 25]</code> - last row</li>
<li><code>[3, 8, 13, 18, 23]</code> - third column</li>
<li><code>[[17, 18, 19], [22, 23, 24]] - bottom-right corner
[1, 7, 13, 19, 25] - diagonalclick to reveal
a[0, :] # first row
a[-1, :] # last row
a[:, 2] # third column
a[3:, 1:4] # bottom-right 2×3 block
np.diag(a) # main diagonal
quick reference
| what you want | how to do it |
|---|---|
| create array | np.array([1,2,3]) |
| zeros | np.zeros((r, c)) |
| ones | np.ones((r, c)) |
| random | np.random.randint(0, 10, (r, c)) |
| identity | np.identity(n) |
| reshape | arr.reshape(r, c) |
| transpose | arr.T |
| sum along axis | np.sum(arr, axis=0) |
| mean | np.mean(arr) |
| dot product | a @ b |
| filter | arr[arr > 5] |
| copy | arr.copy() |
| save | np.save("f.npy", arr) |
| load | np.load("f.npy") |
based on the freeCodeCamp numpy tutorial by keith galli. probably the best 50 minutes you can spend if you’re getting into data science or ml.