Table of Contents
- Why NumPy?
- Module 1 — Understanding NumPy Basics
- Module 2 — Working with NumPy Arrays
- Module 3 — Statistics with NumPy
- Module 4 — Analyzing Data with NumPy
- Reference Tables
Why NumPy?
Python is the reference tool for scientific and engineering applications. NASA and SpaceX use it extensively. But native Python is slow for massive computations on numerical collections. NumPy solves this problem.
NumPy (Numerical Python) was created by Travis Oliphant in 2005, combining Python’s pre-existing numerical libraries. It works in tandem with Matplotlib for visualization.
Why is NumPy so fast?
- NumPy arrays are stored densely and contiguously in memory (unlike Python lists which are arrays of pointers).
- Operations are implemented in C under the hood.
- Vectorized operations allow processing thousands of elements without explicit Python loops.
import numpy as np # universal alias
Module 1 — Understanding NumPy Basics
What Is NumPy?
NumPy revolves around a central structure: the ndarray (n-dimensional array). An ndarray is a collection of numbers organized in a single, indexable, multidimensional variable.
graph TD
A["ndarray (N-Dimensional Array)"]
A --> B["shape\n(e.g.: 3, 4)"]
A --> C["dtype\n(e.g.: float64, int32)"]
A --> D["strides\n(memory step per axis)"]
A --> E["data\ncontiguous memory block"]
B --> B1["Tuple describing\neach dimension size"]
C --> C1["Type of each element\n(all identical)"]
D --> D1["Number of bytes to advance\nto get to the next element"]
E --> E1["Contiguous C buffer\n→ maximum performance"]
Unlike Python lists:
| Characteristic | Python List | NumPy ndarray |
|---|---|---|
| Element types | Heterogeneous | Homogeneous (same dtype) |
| Memory storage | Scattered pointers | Contiguous block |
| Computation speed | Slow (interpreted) | Fast (C, vectorized) |
| Math operations | Manual (loops) | Directly vectorized |
Installation and Validation
pip install numpy
# simple.py — First NumPy script
import numpy as np
# Native Python data (list)
data = [78, 85, 92, 61, 95, 88, 72, 90]
# Convert to ndarray
array = np.array(data)
# Compute the mean
grade = np.mean(array)
print(f"Class average: {grade:.2f}")
# Output: Class average: 82.63
Simple Arrays and Creation Functions
flowchart LR
subgraph "Array Creation"
A["np.array(list)"] --> R1["Array from existing data"]
B["np.zeros(n)"] --> R2["Array filled with zeros"]
C["np.ones(n)"] --> R3["Array filled with ones"]
D["np.linspace(start, stop, n)"] --> R4["Regularly spaced values"]
E["np.arange(start, stop, step)"] --> R5["Values with fixed step (integers)"]
F["np.full(shape, val)"] --> R6["Array filled with a value"]
G["np.eye(n)"] --> R7["n×n identity matrix"]
H["np.random.rand(n)"] --> R8["Random values [0,1)"]
end
np.zeros — Initialization Array
# Create an array of 5 zeros
init = np.zeros(5)
print(init)
# [0. 0. 0. 0. 0.]
# Typical usage: pre-allocate an array before filling it
altitude = np.zeros(101)
Why zeros? To initialize an array before filling it, as an input mask, or to define a target array shape.
np.linspace — Regularly Spaced Values
# linspace(start, stop, num_points)
t = np.linspace(0, 10, 5)
print(t)
# [ 0. 2.5 5. 7.5 10. ]
# Step formula: step = (stop - start) / (n - 1)
Use case: represent a time or space sequence with a precise number of points.
Complete Demo — Launch Trajectory
# altviz.py
import numpy as np
import matplotlib.pyplot as plt
# 101 time points between 0 and 100 seconds
t = np.linspace(0, 100, 101)
# Initialize altitude array to zero
altitude = np.zeros(101)
# Fill with a parabolic curve (h = 0.01 * t²)
for i, time in enumerate(t):
altitude[i] = 0.01 * time ** 2
# Visualization
plt.plot(t, altitude)
plt.xlabel("Time (s)")
plt.ylabel("Altitude (km)")
plt.title("Altitude during launch")
plt.grid(True)
plt.show()
Module 2 — Working with NumPy Arrays
Multidimensional Arrays and ndarrays
The term ndarray means n-dimensional array — an array with N dimensions. Here is the nomenclature by number of dimensions:
graph LR
subgraph "ndarray Dimensions"
D0["0D — Scalar\ne.g.: np.array(42)\nshape: ()"]
D1["1D — Vector\ne.g.: [1, 2, 3]\nshape: (3,)"]
D2["2D — Matrix / Grid\ne.g.: [[1,2],[3,4]]\nshape: (2,2)"]
D3["3D — Tensor\ne.g.: series of matrices\nshape: (n, rows, cols)"]
end
D0 --> D1 --> D2 --> D3
Vocabulary: In AI/ML, arrays of 3D and above are often called tensors. A tensor is simply an ndarray of order ≥ 3.
The shape Concept
The shape describes the extent of each dimension as a tuple:
import numpy as np
# 1D: 5 elements
a = np.linspace(0, 100, 5)
print(a.shape) # (5,)
# 2D: 3 rows × 4 columns
b = np.zeros((3, 4))
print(b.shape) # (3, 4)
# 3D: 2 planes × 3 rows × 4 columns
c = np.ones((2, 3, 4))
print(c.shape) # (2, 3, 4)
Attention: shape (3,) is 1D with 3 elements. Shape (3, 1) is 2D with 3 rows and 1 column. They are not the same!
ndarray Attributes
# trajectory.py
import numpy as np
t = np.linspace(0, 2 * np.pi, 100)
x = np.cos(t)
y = np.sin(t)
z = t / 2
# Combine x, y, z into a single 2D array
trajectory = np.column_stack((x, y, z))
# --- Attributes ---
print("Shape trajectory:", trajectory.shape) # (100, 3)
print("Shape t: ", t.shape) # (100,)
print("Size trajectory: ", trajectory.size) # 300
print("Size t: ", t.size) # 100
print("Dtype trajectory:", trajectory.dtype) # float64
print("Dtype t: ", t.dtype) # float64
print("Ndim trajectory: ", trajectory.ndim) # 2
print("Ndim t: ", t.ndim) # 1
| Attribute | Description | Example |
|---|---|---|
.shape | Dimension tuple | (100, 3) |
.size | Total number of elements | 300 |
.dtype | Data type | float64 |
.ndim | Number of dimensions | 2 |
.strides | Memory step per axis (bytes) | (24, 8) |
Slicing — Array Slicing
NumPy slicing is identical to Python, Rust, R, and other languages. It allows extracting or modifying subsets of an array.
flowchart TD
subgraph "Syntax: array[start:stop:step]"
S["start"] --> N1["Start index (inclusive)\nDefault: 0"]
E["stop"] --> N2["End index (exclusive!)\nDefault: last index"]
T["step"] --> N3["Step\nDefault: 1"]
end
Key rule:
stopis non-inclusive (exclusive). To include the last element, go one index beyond.
1D Slicing
import numpy as np
arr = np.array([10, 20, 30, 40, 50])
print(arr[1:4]) # [20 30 40] — indices 1, 2, 3
print(arr[:3]) # [10 20 30] — from start to index 2
print(arr[2:]) # [30 40 50] — from index 2 to end
print(arr[::2]) # [10 30 50] — every other element
print(arr[::-1]) # [50 40 30 20 10] — reversed
2D Slicing — Think Dimension by Dimension
data = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
# All rows, column 1 only
print(data[:, 1]) # [2 5 8]
# Rows 0 and 1, all columns
print(data[:2, :]) # [[1 2 3] [4 5 6]]
# Bottom-right submatrix
print(data[1:, 1:]) # [[5 6] [8 9]]
Practical Application — Filling by Slicing
# Equivalent to column_stack, but via assignment
trajectory = np.zeros((100, 3), dtype=float)
trajectory[:, 0] = x # Column 0 = X coordinate
trajectory[:, 1] = y # Column 1 = Y coordinate
trajectory[:, 2] = z # Column 2 = Z coordinate
Indexing and Masking
NumPy offers three ways to access data:
1. Classic Indexing
arr = np.array([10, 20, 30, 40, 50])
print(arr[2]) # 30 — 0-based index
print(arr[-1]) # 50 — index from end
# 2D array
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(matrix[1, 2]) # 6 — row 1, column 2
2. Boolean Masking
Masking creates a boolean array (True/False) evaluated for each element, then uses it to filter.
import numpy as np
x = np.array([-3, -1, 0, 2, 5, -2])
# Mask for positive values
mask_positive = x > 0
print(mask_positive) # [False False False True True False]
print(x[mask_positive]) # [2 5]
# Inline mask (equivalent)
print(x[x > 0]) # [2 5]
# Mask for multiples of 2
mask_even = x % 2 == 0
print(x[mask_even]) # [0 2 -2]
Masking Applied to Trajectory Data
# Filter points below 3 km altitude (z coordinate)
z_threshold = 3.0
# Select all rows where z < threshold
mask = trajectory[:, 2] < z_threshold
# Extract corresponding points (all x, y, z columns)
low_altitude_points = trajectory[mask, :]
print(f"Points below {z_threshold} km: {low_altitude_points.shape[0]}")
Module 3 — Statistics with NumPy
Vectorized Operations
Vectorized operations allow applying arithmetic calculations on entire arrays without writing a loop. This is the core of NumPy’s power.
Concrete example: unit conversion (foot-pounds → Newtons)
import numpy as np
# Data: force (ft-lbs) and duration (seconds)
burn_data = np.array([
[150.0, 2.5],
[200.0, 3.0],
[175.0, 2.0]
])
# Conversion factor
FOOT_LBS_TO_NEWTONS = 4.44822
# Extract columns
forces_ftlbs = burn_data[:, 0] # Column 0: forces
times = burn_data[:, 1] # Column 1: durations
# Vectorized operation: multiply EACH force by the factor
forces_newtons = forces_ftlbs * FOOT_LBS_TO_NEWTONS
# No loop! NumPy applies the operation to all elements.
# Reconstruct the corrected array
corrected_data = np.column_stack((forces_newtons, times))
# Compute impulse (force × time)
impulse_ftlbs = forces_ftlbs * times
impulse_newtons = forces_newtons * times
print("Original impulse (ft-lbs·s):", impulse_ftlbs)
print("Corrected impulse (N·s): ", impulse_newtons)
Historical context: In 1999, the Mars Climate Orbiter crashed because the software team was using English units (foot-pounds) instead of the metric system (Newtons). A simple vectorized conversion like the above would have prevented this $327 million disaster.
Broadcasting
Broadcasting is the automatic reconciliation of different shapes during vectorized operations.
flowchart TD
subgraph "Broadcasting Compatibility Rules"
R1["Same number of dimensions\nand compatible shapes"]
R2["OR one array is a scalar (0D)"]
R3["OR one dimension equals 1\n(it gets stretched)"]
end
subgraph "Example"
A["Array A shape (2,3)\n[[1,2,3],[4,5,6]]"]
B["Array B shape (3,)\n[10,20,30]"]
C["Result (2,3)\n[[11,22,33],[14,25,36]]"]
A -- "+ B" --> C
B -- "broadcast over rows" --> C
end
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
# Broadcasting: b is "stretched" over the 2 rows of a
result = a + b
print(result)
# [[11 22 33]
# [14 25 36]]
# INVALID example — incompatible shapes
b_bad = np.array([10, 20]) # shape (2,) — doesn't match (2,3)
# a + b_bad → ValueError: operands could not be broadcast together
Vectorization vs broadcasting difference:
- Vectorization: operations on arrays with same shape.
- Broadcasting: automatically reconciles different shapes to allow vectorized operations.
Statistical Operations
import numpy as np
# Rocket engine vibration data (simulated values)
vibrations = np.array([
30, 25, 32, 41, 28, 35, 30, 22, 30, 45,
31, 29, 30, 27, 33, 30, 100, 28, 30, 20
])
# --- Descriptive statistics ---
mean_val = np.mean(vibrations) # Mean
std_val = np.std(vibrations) # Standard deviation
median_val = np.median(vibrations) # Median
sum_val = np.sum(vibrations) # Sum
min_val = np.min(vibrations) # Minimum
max_val = np.max(vibrations) # Maximum
# --- Mode (not native) ---
# bincount counts occurrences of each integer 0..max
bin_counts = np.bincount(vibrations)
mode_val = np.argmax(bin_counts) # Index with max count = mode
print(f"Mean : {mean_val:.2f}")
print(f"Std dev : {std_val:.2f}")
print(f"Median : {median_val:.2f}")
print(f"Mode : {mode_val}")
print(f"Sum : {sum_val}")
print(f"Min : {min_val} Max: {max_val}")
Interpretation:
- A large standard deviation (e.g., 25) relative to the data range means values are highly dispersed.
- When mean ≈ mode, data is roughly symmetric.
- When median and mean diverge, there are extreme values (outliers) pulling the mean.
Module 4 — Analyzing Data with NumPy
Data Cleansing
Fundamental principle: cleaning must be based on sensor limits, not on how the data looks. Correct what is physically impossible, not what you dislike.
Recommended strategy:
- Identify rules based on the physical capabilities of the source.
- Replace invalid values with
np.nan(Not a Number), never with zero. - Work on a copy (non-destructive).
import numpy as np
# Raw data from an infrared light sensor
raw_data = np.array([
45.2, 67.1, -5.3, 88.0, 50.0, 214.47,
110.0, 30.2, 42.1, -12.0, 95.0, 73.3
])
# Rules based on sensor limits:
# 1. No negative light (physical minimum = 0)
# 2. Values > mean + 7*std = cosmic anomaly (cosmic rays)
mean_val = np.mean(raw_data)
std_val = np.std(raw_data)
# Outlier mask
outlier_mask = (
(np.abs(raw_data - mean_val) > 7 * std_val) | # Cosmic anomaly
(raw_data < 0) # Physically impossible value
)
# Non-destructive copy
clean_data = raw_data.copy()
# Replace outliers with NaN
clean_data[outlier_mask] = np.nan
print(f"Number of outliers: {np.sum(outlier_mask)}")
print(f"Detected outliers : {raw_data[outlier_mask]}")
print(f"Cleaned data : {clean_data}")
# Compute mean ignoring NaN
print(f"Mean (without NaN): {np.nanmean(clean_data):.2f}")
Why
np.nanrather than0? Zero is a valid value and can skew calculations.np.nanis recognized bynp.nanmean,np.nanstd, etc. and automatically ignored.
Stacking — Array Stacking
There are three types of stacking for combining arrays along different axes:
graph TD
subgraph "Stacking Types"
H["hstack\n(horizontal)\nAdds columns"]
V["vstack\n(vertical)\nAdds rows"]
D["dstack\n(depth)\nAdds a dimension"]
CS["column_stack\nHstack for 1D/2D\n(columns)"]
end
subgraph "Example with a=[1,2] and b=[3,4]"
H2["hstack → [1,2,3,4]\nshape: (4,)"]
V2["vstack → [[1,2],[3,4]]\nshape: (2,2)"]
D2["dstack → [[[1,3],[2,4]]]\nshape: (1,2,2)"]
end
H --> H2
V --> V2
D --> D2
import numpy as np
a = np.array([1, 2])
b = np.array([3, 4])
# Horizontal stack: concatenation on column axis
h = np.hstack((a, b))
print("hstack:", h) # [1 2 3 4] shape: (4,)
# Vertical stack: adding rows
v = np.vstack((a, b))
print("vstack:", v) # [[1 2]
# [3 4]] shape: (2,2)
# Depth stack: adding a dimension
d = np.dstack((a, b))
print("dstack:", d) # [[[1 3]
# [2 4]]] shape: (1,2,2)
# --- With 2D arrays ---
arr1 = np.array([[1, 2], [3, 4]])
arr2 = np.array([[5, 6], [7, 8]])
print("hstack 2D:", np.hstack((arr1, arr2)))
# [[1 2 5 6]
# [3 4 7 8]]
print("vstack 2D:", np.vstack((arr1, arr2)))
# [[1 2]
# [3 4]
# [5 6]
# [7 8]]
Reshaping
import numpy as np
# Create a 1D array of 12 elements
arr = np.arange(1, 13) # [1, 2, ..., 12]
# Reshape to 3×4 matrix
matrix = arr.reshape(3, 4)
print(matrix)
# [[ 1 2 3 4]
# [ 5 6 7 8]
# [ 9 10 11 12]]
# Flatten back to 1D
flat = matrix.ravel() # view (fast)
flat2 = matrix.flatten() # copy (independent)
# Transpose
transposed = matrix.T # shape (4, 3)
Integration with Pandas
import numpy as np
import pandas as pd
# Convert ndarray → DataFrame
array_2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
df = pd.DataFrame(array_2d, columns=['A', 'B', 'C'])
# Convert DataFrame → ndarray
array_back = df.values # or df.to_numpy()
# NumPy operations on a column
col_mean = np.mean(df['A'].values)
# Create ndarray from CSV (via Pandas)
df_csv = pd.read_csv('data.csv')
data_array = df_csv.select_dtypes(include='number').to_numpy()
Reference Tables
Array Creation Functions
| Function | Parameters | Description |
|---|---|---|
np.array(data) | list / nested list | Array from existing data |
np.zeros(n) | int or tuple | Array filled with zeros |
np.ones(n) | int or tuple | Array filled with ones |
np.full(shape, val) | shape, fill value | Array filled with a value |
np.linspace(a,b,n) | start, stop, points | n evenly spaced values |
np.arange(a,b,s) | start, stop, step | Values with fixed step |
np.eye(n) | int | n×n identity matrix |
np.random.rand(n) | int or tuple | Random uniform values [0,1) |
np.random.randn(n) | int or tuple | Random standard normal values |
np.column_stack() | tuple of arrays | 1D arrays → 2D matrix (columns) |
Common dtypes
| dtype | Full Name | Size | Range / Precision |
|---|---|---|---|
float64 | 64-bit float (double) | 8 bytes | ~15 decimal digits |
float32 | 32-bit float | 4 bytes | ~7 decimal digits |
int64 | 64-bit integer | 8 bytes | ±9.2 × 10¹⁸ |
int32 | 32-bit integer | 4 bytes | ±2.1 × 10⁹ |
int16 | 16-bit integer | 2 bytes | ±32,767 |
bool_ | Boolean | 1 byte | True / False |
str_ | String | Variable | — |
# Specify dtype at creation
a = np.array([1, 2, 3], dtype=np.float32)
b = np.zeros((3, 3), dtype=np.int16)
# Convert dtype
c = a.astype(np.int64)
Statistical Operations
| Function | Description | NaN-safe Variant |
|---|---|---|
np.mean(a) | Arithmetic mean | np.nanmean(a) |
np.median(a) | Median | np.nanmedian(a) |
np.std(a) | Standard deviation | np.nanstd(a) |
np.var(a) | Variance | np.nanvar(a) |
np.min(a) / np.max(a) | Min / Max | np.nanmin / np.nanmax |
np.sum(a) | Sum | np.nansum(a) |
np.cumsum(a) | Cumulative sum | — |
np.argmin(a) / np.argmax(a) | Index of min / max | — |
np.bincount(a) | Integer counts | — |
np.percentile(a, q) | Percentile q | np.nanpercentile |
np.corrcoef(a, b) | Correlation matrix | — |
Statistics by axis: add
axis=0(by column) oraxis=1(by row):np.mean(matrix, axis=0)
Reshaping and Stacking Operations
| Operation | Function | Description |
|---|---|---|
| Reshape | a.reshape(shape) | Change shape (same element count) |
| Transpose | a.T or np.transpose(a) | Swap axes |
| Flatten (view) | np.ravel(a) | → 1D, view (fast) |
| Flatten (copy) | a.flatten() | → 1D, independent copy |
| Horizontal stack | np.hstack((a,b)) | Concatenate on column axis |
| Vertical stack | np.vstack((a,b)) | Concatenate on row axis |
| Depth stack | np.dstack((a,b)) | Add a dimension |
| Column stack | np.column_stack((a,b)) | Columns (1D → 2D) |
| Concatenation | np.concatenate((a,b), axis=n) | General purpose |
| Split | np.split(a, n) | Divide into n sub-arrays |
| Squeeze | np.squeeze(a) | Remove size-1 dimensions |
| Expand dims | np.expand_dims(a, axis) | Add a size-1 dimension |
Search Terms
numpy · python · foundations · data · analysis · engineering · analytics · array · operations · slicing · arrays · masking · creation · functions · indexing · reshaping · stacking · statistical · trajectory