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: sin, exp, mean, dot, etc.

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

np.array(list)

Create from a Python list

np.array([1, 2, 3])

np.zeros(n)

Array of n zeros

np.zeros(5)[0. 0. 0. 0. 0.]

np.ones(n)

Array of n ones

np.ones(4)[1. 1. 1. 1.]

np.linspace(a, b, n)

n evenly spaced points from a to b (inclusive)

np.linspace(0, 1, 5)[0. 0.25 0.5 0.75 1.]

np.arange(a, b, step)

Values from a to b (exclusive) with step

np.arange(0, 5, 0.5)[0. 0.5 1. ... 4.5]

np.eye(n)

n×n identity matrix

np.eye(3) → 3×3 with 1s on diagonal

np.random.rand(n)

n random values in [0, 1)

np.random.rand(4)[0.23 0.71 ...]

np.full(n, val)

Array of n copies of val

np.full(3, 7.5)[7.5 7.5 7.5]

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

arr[i, j]

Single element at row i, column j

arr[i, :]

Entire row i

arr[:, j]

Entire column j

arr[a:b, c:d]

Sub-array: rows a to b-1, columns c to d-1

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 arrays

  • Index 2D arrays with [row, col] syntax; use : for entire rows or columns

  • Arithmetic operators work element-wise — no loops needed

  • Use axis=0 for column-wise and axis=1 for 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.