Beginner

Getting Started with NumPy

NumPy basics — arrays, statistics and analyzing data with the numerical computing foundation of Python.

Table of Contents


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:

CharacteristicPython ListNumPy ndarray
Element typesHeterogeneousHomogeneous (same dtype)
Memory storageScattered pointersContiguous block
Computation speedSlow (interpreted)Fast (C, vectorized)
Math operationsManual (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
AttributeDescriptionExample
.shapeDimension tuple(100, 3)
.sizeTotal number of elements300
.dtypeData typefloat64
.ndimNumber of dimensions2
.stridesMemory 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: stop is 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:

  1. Identify rules based on the physical capabilities of the source.
  2. Replace invalid values with np.nan (Not a Number), never with zero.
  3. 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.nan rather than 0? Zero is a valid value and can skew calculations. np.nan is recognized by np.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

FunctionParametersDescription
np.array(data)list / nested listArray from existing data
np.zeros(n)int or tupleArray filled with zeros
np.ones(n)int or tupleArray filled with ones
np.full(shape, val)shape, fill valueArray filled with a value
np.linspace(a,b,n)start, stop, pointsn evenly spaced values
np.arange(a,b,s)start, stop, stepValues with fixed step
np.eye(n)intn×n identity matrix
np.random.rand(n)int or tupleRandom uniform values [0,1)
np.random.randn(n)int or tupleRandom standard normal values
np.column_stack()tuple of arrays1D arrays → 2D matrix (columns)

Common dtypes

dtypeFull NameSizeRange / Precision
float6464-bit float (double)8 bytes~15 decimal digits
float3232-bit float4 bytes~7 decimal digits
int6464-bit integer8 bytes±9.2 × 10¹⁸
int3232-bit integer4 bytes±2.1 × 10⁹
int1616-bit integer2 bytes±32,767
bool_Boolean1 byteTrue / False
str_StringVariable
# 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

FunctionDescriptionNaN-safe Variant
np.mean(a)Arithmetic meannp.nanmean(a)
np.median(a)Mediannp.nanmedian(a)
np.std(a)Standard deviationnp.nanstd(a)
np.var(a)Variancenp.nanvar(a)
np.min(a) / np.max(a)Min / Maxnp.nanmin / np.nanmax
np.sum(a)Sumnp.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 qnp.nanpercentile
np.corrcoef(a, b)Correlation matrix

Statistics by axis: add axis=0 (by column) or axis=1 (by row): np.mean(matrix, axis=0)

Reshaping and Stacking Operations

OperationFunctionDescription
Reshapea.reshape(shape)Change shape (same element count)
Transposea.T or np.transpose(a)Swap axes
Flatten (view)np.ravel(a)→ 1D, view (fast)
Flatten (copy)a.flatten()→ 1D, independent copy
Horizontal stacknp.hstack((a,b))Concatenate on column axis
Vertical stacknp.vstack((a,b))Concatenate on row axis
Depth stacknp.dstack((a,b))Add a dimension
Column stacknp.column_stack((a,b))Columns (1D → 2D)
Concatenationnp.concatenate((a,b), axis=n)General purpose
Splitnp.split(a, n)Divide into n sub-arrays
Squeezenp.squeeze(a)Remove size-1 dimensions
Expand dimsnp.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

Interested in this course?

Contact us to book it or get a custom training plan for your team.