Chapter 10: Basic Array Methods#
Topics Covered:
Creating and inspecting NumPy arrays
Indexing and slicing (1D and 2D)
Element-wise operations and broadcasting
Reshaping and combining arrays
Practical chemical engineering examples
10.1 Introduction to NumPy Arrays#
NumPy (Numerical Python) is the core library for scientific computing in Python. At its heart is the ndarray — a fast, memory-efficient array that supports vectorized operations.
Why use NumPy instead of Python lists?
Feature |
Python List |
NumPy Array |
|---|---|---|
Speed |
Slow (interpreted loops) |
Fast (compiled C under the hood) |
Operations |
Element-by-element with loops |
Vectorized — operates on entire arrays at once |
Math support |
Manual implementation |
Built-in: |
In chemical engineering, we constantly work with large datasets — temperature profiles, concentration measurements, pressure readings — and NumPy makes processing them efficient.
import numpy as np
time = np.linspace(0, 10, 100)
time
array([ 0. , 0.1010101 , 0.2020202 , 0.3030303 , 0.4040404 ,
0.50505051, 0.60606061, 0.70707071, 0.80808081, 0.90909091,
1.01010101, 1.11111111, 1.21212121, 1.31313131, 1.41414141,
1.51515152, 1.61616162, 1.71717172, 1.81818182, 1.91919192,
2.02020202, 2.12121212, 2.22222222, 2.32323232, 2.42424242,
2.52525253, 2.62626263, 2.72727273, 2.82828283, 2.92929293,
3.03030303, 3.13131313, 3.23232323, 3.33333333, 3.43434343,
3.53535354, 3.63636364, 3.73737374, 3.83838384, 3.93939394,
4.04040404, 4.14141414, 4.24242424, 4.34343434, 4.44444444,
4.54545455, 4.64646465, 4.74747475, 4.84848485, 4.94949495,
5.05050505, 5.15151515, 5.25252525, 5.35353535, 5.45454545,
5.55555556, 5.65656566, 5.75757576, 5.85858586, 5.95959596,
6.06060606, 6.16161616, 6.26262626, 6.36363636, 6.46464646,
6.56565657, 6.66666667, 6.76767677, 6.86868687, 6.96969697,
7.07070707, 7.17171717, 7.27272727, 7.37373737, 7.47474747,
7.57575758, 7.67676768, 7.77777778, 7.87878788, 7.97979798,
8.08080808, 8.18181818, 8.28282828, 8.38383838, 8.48484848,
8.58585859, 8.68686869, 8.78787879, 8.88888889, 8.98989899,
9.09090909, 9.19191919, 9.29292929, 9.39393939, 9.49494949,
9.5959596 , 9.6969697 , 9.7979798 , 9.8989899 , 10. ])
10.1.1 Creating Arrays#
NumPy provides several built-in functions to create arrays:
Function |
Description |
Example |
|---|---|---|
|
Create from a Python list |
|
|
Array of |
|
|
Array of |
|
|
|
|
|
Values from |
|
|
|
|
|
|
|
|
Array of |
|
import numpy as np
# From a Python list
temperatures = np.array([300, 350, 400, 450, 500])
print("From list:", temperatures)
temperatures
From list: [300 350 400 450 500]
array([300, 350, 400, 450, 500])
# Array of zeros
zeros = np.zeros(5)
print("Zeros:", zeros)
Zeros: [0. 0. 0. 0. 0.]
# Array of ones
ones = np.ones(4)
print("Ones:", ones)
Ones: [1. 1. 1. 1.]
# Evenly spaced values (start, stop, num_points)
pressures = np.linspace(1, 10, 5)
print("Linspace:", pressures)
Linspace: [ 1. 3.25 5.5 7.75 10. ]
# Evenly spaced values with step size (start, stop_exclusive, step)
time = np.arange(0, 5, 0.5)
print("Arange:", time)
Arange: [0. 0.5 1. 1.5 2. 2.5 3. 3.5 4. 4.5]
10.1.2 Arrays vs. Lists#
A common mistake is to assume NumPy arrays behave like Python lists. They don’t — and the difference matters.
# List: + concatenates
py_list = [1, 2, 3]
print(type(py_list))
print("List + List:", py_list + py_list) # [1, 2, 3, 1, 2, 3]
print("List * 2: ", py_list * 2) # [1, 2, 3, 1, 2, 3]
<class 'list'>
List + List: [1, 2, 3, 1, 2, 3]
List * 2: [1, 2, 3, 1, 2, 3]
np_array = np.array([1, 2, 3])
type(np_array)
print("List + List:", np_array + np_array)
print("List * 2: ", np_array * 2)
List + List: [2 4 6]
List * 2: [2 4 6]
py_list
result_list = []
for n1, n2 in zip(py_list, py_list):
result = n1+n2
result_list.append(result)
print(result_list)
[2, 4, 6]
import time
start = time.time()
time_array = np.linspace(0, 10, 100_000_000) # 100 points from 0 to 10
#print(time)
concentration = 5.0 * np.exp(-0.3 * time_array) # First-order decay
print("Concentration as array:", concentration)
end = time.time()
print(end-start)
Concentration as array: [5. 4.99999985 4.9999997 ... 0.24893536 0.24893535 0.24893534]
1.1255130767822266
import time
start = time.time()
#print(start)
time_list = list(time_array)
conc_list = []
for t in time_list:
c = 5.0 * np.exp(-0.3 * t)
conc_list.append(c)
end = time.time()
#print(end)
print(conc_list == list(concentration))
print(end-start)
10.2 Array Properties & Indexing#
10.2.1 Array Properties#
Every NumPy array has attributes that describe its structure:
# 1D array
temps = np.array([300.0, 350.0, 400.0, 450.0, 500.0])
print("Array: ", temps)
print("Shape: ", temps.shape) # (5,) — 1D with 5 elements
print("Size: ", temps.size) # 5 total elements
print("Dimensions:", temps.ndim) # 1
print("Data type: ", temps.dtype) # float64
Array: [300. 350. 400. 450. 500.]
Shape: (5,)
Size: 5
Dimensions: 1
Data type: float64
# 2D array (matrix)
data = np.array([
[300, 1.5, 0.92],
[350, 2.1, 0.85],
[400, 3.0, 0.78],
[450, 4.2, 0.70]
])
print("Array:\n", data)
print("Shape: ", data.shape) # (4, 3) — 4 rows, 3 columns
print("Size: ", data.size) # 12 total elements
print("Dimensions:", data.ndim) # 2
Array:
[[300. 1.5 0.92]
[350. 2.1 0.85]
[400. 3. 0.78]
[450. 4.2 0.7 ]]
Shape: (4, 3)
Size: 12
Dimensions: 2
10.2.2 Indexing and Slicing — 1D Arrays#
NumPy indexing works like Python list indexing (0-based), but with additional power for multi-dimensional arrays.
temps = np.array([300, 350, 400, 450, 500])
# 0 1 2 3 4
print("First element: ", temps[0]) # 300
print("Last element: ", temps[-1]) # 500
print("Slice [1:3]: ", temps[1:3]) # [350, 400]
print("Every other: ", temps[::2]) # [300, 400, 500]
print("Reversed: ", temps[::-1]) # [500, 450, 400, 350, 300]
First element: 300
Last element: 500
Slice [1:3]: [350 400]
Every other: [300 400 500]
Reversed: [500 450 400 350 300]
10.2.3 Indexing and Slicing — 2D Arrays#
For 2D arrays, use [row, column] syntax. This is different from nested list indexing (list[row][col]).
# Experiment data: rows = trials, columns = [Temp(K), Pressure(atm), Conversion]
data = np.array([
[300, 1.5, 0.92],
[350, 2.1, 0.85],
[400, 3.0, 0.78],
[450, 4.2, 0.70]
])
# 0. 1. 2
# 0 [300, 1.5, 0.92],
# 1 [350, 2.1, 0.85],
# 2 [400, 3.0, 0.78],
# 3 [450, 4.2, 0.70]
print("Single element [1, 2]:", data[1, 2]) # Row 1, Col 2 → 0.85
print("Entire row 0: ", data[0, :]) # [300, 1.5, 0.92]
print("Entire column 1: ", data[:, 1]) # [1.5, 2.1, 3.0, 4.2]
print("Sub-array [0:2, 1:]:\n", data[0:2, 1:]) # First 2 rows, last 2 columns
Single element [1, 2]: 0.85
Entire row 0: [300. 1.5 0.92]
Entire column 1: [1.5 2.1 3. 4.2]
Sub-array [0:2, 1:]:
[[1.5 0.92]
[2.1 0.85]]
Key syntax for 2D indexing:
Syntax |
Meaning |
|---|---|
|
Single element at row |
|
Entire row |
|
Entire column |
|
Sub-array: rows |
10.3 Array Operations#
10.3.1 Element-wise Arithmetic#
Arithmetic operators work element-by-element on NumPy arrays. This is called vectorization — no loops needed.
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
print("a + b =", a + b) # [11, 22, 33, 44]
print("a - b =", a - b) # [-9, -18, -27, -36]
print("a * b =", a * b) # [10, 40, 90, 160]
print("b / a =", b / a) # [10., 10., 10., 10.]
print("a ** 2 =", a ** 2) # [1, 4, 9, 16]
a + b = [11 22 33 44]
a - b = [ -9 -18 -27 -36]
a * b = [ 10 40 90 160]
b / a = [10. 10. 10. 10.]
a ** 2 = [ 1 4 9 16]
Compare this to achieving the same result with a Python list — you’d need a loop:
# With lists (slow, verbose)
result = []
for i in range(len(a)):
result.append(a[i] + b[i])
# With NumPy (fast, clean)
result = a + b
10.3.2 Broadcasting#
Broadcasting is NumPy’s way of handling operations between arrays of different shapes. The most common case: an operation between an array and a scalar.
temps_C = np.array([25, 50, 75, 100])
temps_K = temps_C + 273.15 # adds 273.15 to every element
print(temps_K)
[298.15 323.15 348.15 373.15]
# Scalar + Array: the scalar is "broadcast" to every element
temps_C = np.array([25, 50, 75, 100])
temps_K = temps_C + 273.15 # adds 273.15 to every element
print("Celsius: ", temps_C)
print("Kelvin: ", temps_K)
# Convert pressures from atm to Pa (1 atm = 101325 Pa)
pressures_atm = np.array([1.0, 2.5, 5.0, 10.0])
pressures_Pa = pressures_atm * 101325
print("\nPressures (atm):", pressures_atm)
print("Pressures (Pa): ", pressures_Pa)
Celsius: [ 25 50 75 100]
Kelvin: [298.15 323.15 348.15 373.15]
Pressures (atm): [ 1. 2.5 5. 10. ]
Pressures (Pa): [ 101325. 253312.5 506625. 1013250. ]
10.3.3 Useful Array Functions#
NumPy provides built-in functions for common calculations. These are faster than writing your own loops.
import numpy as np
measurements = np.array([4.52, 4.48, 4.55, 4.60, 4.50, 4.53])
print("Sum: ", np.sum(measurements))
manual_sum = 0
for value in measurements:
manual_sum+=value
print("Manual Sum: ", manual_sum)
Sum: 27.18
Manual Sum: 27.18
print("Mean: ", np.mean(measurements))
print("Std: ", np.std(measurements))
manual_mean = manual_sum / len(measurements)
print("Manual Mean: ", manual_mean)
manual_variance_sum = 0
for value in measurements:
manual_variance_sum += (value - manual_mean) ** 2
manual_varaince = manual_variance_sum / len(measurements)
manual_std = np.sqrt(manual_varaince)
print("Manual Std: ", manual_std)
Mean: 4.53
Std: 0.03829708431025333
Manual Mean: 4.53
Manual Std: 0.03829708431025333
measurements = np.array([4.52, 4.48, 4.55, 4.60, 4.50, 4.53])
#0 1 2 3 4 5
print("Sum: ", np.sum(measurements))
print("Mean: ", np.mean(measurements))
print("Std: ", np.std(measurements))
print("Min: ", np.min(measurements))
print("Max: ", np.max(measurements))
print("Argmin (index of min):", np.argmin(measurements))
print("Argmax (index of max):", np.argmax(measurements))
Sum: 27.18
Mean: 4.53
Std: 0.03829708431025333
Min: 4.48
Max: 4.6
Argmin (index of min): 1
Argmax (index of max): 3
measurements.shape
(6,)
10.3.4 The axis Argument#
For 2D arrays, you can apply functions along a specific axis:
axis=0→ operate down each column (across rows)axis=1→ operate across each row (across columns)
Think of it as: the axis you specify is the one that collapses.
# 3 experiments, each measuring 4 temperatures (K)
data = np.array([
[305, 310, 315, 320], # Experiment 1
[302, 308, 312, 318], # Experiment 2
[307, 311, 316, 322], # Experiment 3
])
print(np.mean(data))
print(np.mean(data, axis=0))
print(np.mean(data, axis=1))
312.1666666666667
[304.66666667 309.66666667 314.33333333 320. ]
[312.5 310. 314. ]
# 3 experiments, each measuring 4 temperatures (K)
data = np.array([
[305, 310, 315, 320], # Experiment 1
[302, 308, 312, 318], # Experiment 2
[307, 311, 316, 322], # Experiment 3
])
print("Data shape:", data.shape) # (3, 4)
print("Data:\n", data)
# Mean across experiments (down columns) — average temp at each time point
print("\nMean along axis=0 (column means):", np.mean(data, axis=0))
# Mean across time points (across rows) — average temp per experiment
print("Mean along axis=1 (row means): ", np.mean(data, axis=1))
# Overall mean
print("Overall mean: ", np.mean(data))
10.3.5 Mathematical Functions#
NumPy includes all common math functions that work element-wise on arrays:
print(np.pi)
3.141592653589793
x = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2])
print("sin(x):", np.sin(x))
print("cos(x):", np.cos(x))
print("exp(x):", np.exp(x))
print("log(x):", np.log(np.array([1, np.e, np.e**2]))) # natural log
print("sqrt(x):", np.sqrt(np.array([4, 9, 16, 25])))
sin(x): [0. 0.5 0.70710678 0.8660254 1. ]
cos(x): [1.00000000e+00 8.66025404e-01 7.07106781e-01 5.00000000e-01
6.12323400e-17]
exp(x): [1. 1.68809179 2.19328005 2.84965391 4.81047738]
log(x): [0. 1. 2.]
sqrt(x): [2. 3. 4. 5.]
10.4 Reshaping & Combining Arrays#
When preparing data for linear algebra or organizing experimental results, you often need to change the shape of an array or combine multiple arrays together.
10.4.1 Reshaping#
# reshape: change dimensions without changing data
a = np.array([1, 2, 3, 4, 5, 6])
print("Original (1D):", a, " shape:", a.shape, a.ndim)
# Reshape to 2x3
b = a.reshape(2, 3)
print("Reshaped (2x3):\n", b)
# # c = a.reshape(2, 3)
# # # Reshape to 3x2
# c = a.reshape(3, 2)
# print("Reshaped (3x2):\n", c)
# d = a.reshape(1,6)
# print(d, d.shape, d.ndim)
# Flatten: 2D → 1D
print("Flattened:", b.flatten())
Original (1D): [1 2 3 4 5 6] shape: (6,) 1
Reshaped (2x3):
[[1 2 3]
[4 5 6]]
Flattened: [1 2 3 4 5 6]
10.4.2 Transpose#
The transpose swaps rows and columns. This is essential for linear algebra (Chapter 11).
A = np.array([
[1, 2, 3],
[4, 5, 6]
])
print("Original (2x3):\n", A)
print("Transposed (3x2):\n", A.T)
print(np.transpose(A))
Original (2x3):
[[1 2 3]
[4 5 6]]
Transposed (3x2):
[[1 4]
[2 5]
[3 6]]
[[1 4]
[2 5]
[3 6]]
10.4.3 Combining Arrays#
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print("Concatenate:", np.concatenate([a, b]))
Concatenate: [1 2 3 4 5 6]
[1,2,3] + [4, 5, 6]
[1, 2, 3, 4, 5, 6]
# vstack: stack vertically (add rows)
row1 = np.array([300, 1.5, 0.92])
row2 = np.array([350, 2.1, 0.85])
stacked = np.vstack([row1, row2])
print("\nvstack:\n", stacked)
vstack:
[[300. 1.5 0.92]
[350. 2.1 0.85]]
col1 = np.array([300, 1.5, 0.92])
col2 = np.array([350, 2.1, 0.85])
stacked = np.hstack([col1, col2])
print("\nhstack:\n", stacked)
hstack:
[300. 1.5 0.92 350. 2.1 0.85]
# Concatenate: join arrays along an existing axis
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print("Concatenate:", np.concatenate([a, b])) # [1, 2, 3, 4, 5, 6]
# vstack: stack vertically (add rows)
row1 = np.array([300, 1.5, 0.92])
row2 = np.array([350, 2.1, 0.85])
stacked = np.vstack([row1, row2])
print("\nvstack:\n", stacked)
# hstack: stack horizontally (add columns)
col1 = np.array([[1], [2], [3]])
col2 = np.array([[10], [20], [30]])
print("\nhstack:\n", np.hstack([col1, col2]))
Concatenate: [1 2 3 4 5 6]
vstack:
[[300. 1.5 0.92]
[350. 2.1 0.85]]
hstack:
[[ 1 10]
[ 2 20]
[ 3 30]]
10.5 Practical Examples#
10.5.1 Tabulating Experimental Data#
Suppose you ran 4 experiments measuring reactor temperature at 5 time points each. You can store all the data in a single 2D array and quickly compute statistics.
# Rows = experiments, Columns = time points (0, 5, 10, 15, 20 min)
# Values = reactor temperature (K)
reactor_temps = np.array([
[298, 315, 340, 358, 370], # Experiment 1
[298, 312, 335, 352, 365], # Experiment 2
[298, 318, 345, 362, 375], # Experiment 3
[298, 310, 332, 350, 362], # Experiment 4
])
time_points = np.array([0, 5, 10, 15, 20]) # minutes
print("Temperature data (K):")
print(reactor_temps)
print(f"\nShape: {reactor_temps.shape} (4 experiments x 5 time points)")
avg_per_time = np.mean(reactor_temps, axis=0)
# print(avg_per_time)
avg_per_exp = np.mean(reactor_temps, axis=1)
print(avg_per_exp)
std_per_time = np.std(reactor_temps, axis=0)
std_per_exp = np.std(reactor_temps, axis=1)
print(std_per_time)
print(std_per_exp)
Temperature data (K):
[[298 315 340 358 370]
[298 312 335 352 365]
[298 318 345 362 375]
[298 310 332 350 362]]
Shape: (4, 5) (4 experiments x 5 time points)
[336.2 332.4 339.6 330.4]
[0. 3.03108891 4.94974747 4.76969601 4.94974747]
[26.61127581 24.7111311 28.21772493 23.87969849]
10.5.2 Vectorized Ideal Gas Law#
Instead of computing PV = nRT one condition at a time with a loop, we can compute pressure for many temperatures at once using vectorized operations.
temperatures = np.arange(200, 501, 50)
temperatures
array([200, 250, 300, 350, 400, 450, 500])
# Ideal gas law: P = nRT/V
R = 8.314 # J/(mol·K)
n = 1.0 # mol
V = 0.0224 # m³ (≈ 22.4 L)
# Compute pressure at many temperatures simultaneously
temperatures = np.arange(200, 501, 50) # 200 K to 500 K
pressures = n * R * temperatures / V # vectorized — no loop!
print(pressures)
# pressures_list = []
# for temp in temperatures:
# press = n * R * temp / V
# pressures_list.append(press)
# print(pressures_list)
print("T (K) | P (Pa)")
print("-" * 25)
for T, P in zip(temperatures, pressures):
print(f" {T} | {P:>10.1f}")
[ 74232.14285714 92790.17857143 111348.21428571 129906.25
148464.28571429 167022.32142857 185580.35714286]
T (K) | P (Pa)
-------------------------
200 | 74232.1
250 | 92790.2
300 | 111348.2
350 | 129906.2
400 | 148464.3
450 | 167022.3
500 | 185580.4
Key Takeaways:
NumPy arrays are the foundation for scientific computing in Python — fast, compact, and vectorized
Use
np.array(),np.zeros(),np.ones(),np.linspace(),np.arange()to create arraysIndex 2D arrays with
[row, col]syntax; use:for entire rows or columnsArithmetic operators work element-wise — no loops needed
Use
axis=0for column-wise andaxis=1for row-wise operations.reshape(),.T,np.vstack(),np.hstack()let you restructure arrays
Coming up in Chapter 11: These 2D arrays are matrices — and NumPy provides tools for matrix multiplication, solving systems of linear equations, and other linear algebra operations essential for chemical engineering.