9 NumPy 2D-Arrays

(CSE331) Python for Data Science

Author
Affiliation

Md Rasel Biswas

IASDS, University of Dhaka

A 2D array arranges values in rows and columns. We will use it both as a data table and as a matrix for numerical calculations.

import numpy as np

1 Creating and Inspecting a 2D Array

Create a rectangular array from a list of equal-length lists. NumPy converts the values to a common dtype.

A = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8],
              [9, 10, 11, 12]])
print(A)
print(A.ndim)   # 2
print(A.shape)  # (3, 4): 3 rows, 4 columns
print(A.size)   # 12: total number of elements
print(len(A))   # 3: length of the first axis, not total size
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]
2
(3, 4)
12
3

For an array with shape (rows, columns), axis 0 is the row dimension and axis 1 is the column dimension.

2 Indexing and Slicing

Use array[row, column]. Both positions start at zero.

print(A[1, 2])   # 7: second row, third column
print(A[2, -1])  # 12: last element in the third row
print(A[:2, :3]) # first two rows and first three columns
7
12
[[1 2 3]
 [5 6 7]]

A lone colon selects all positions along that dimension:

print(A[1:, :])  # second row onward, all columns
print(A[1, :])   # entire second row
print(A[:, 3])   # entire fourth column
[[ 5  6  7  8]
 [ 9 10 11 12]]
[5 6 7 8]
[ 4  8 12]

2.1 Keeping one or two dimensions

An integer index removes that dimension. A slice retains it, even if it selects only one row or column.

Selection Shape Result
A[1, :] (4,) 1D array
A[1:2, :] (1, 4) 2D row
A[:, 3] (3,) 1D array
A[:, 3:4] (3, 1) 2D column
print(A[:, 3].shape)
print(A[:, 3:4].shape)
(3,)
(3, 1)

2.2 Changing part of an array

B = A.copy()
B[0, 0] = 99
B[:, 1] = 0
print(B)
print(A)  # unchanged because B was copied
[[99  0  3  4]
 [ 5  0  7  8]
 [ 9  0 11 12]]
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

Basic slices share data, as in Lecture 8. For an independent rectangular subset, use A[:2, :3].copy().

3 Shape and Reshaping

reshape() returns an array with a different shape. The total number of elements must remain the same. By default, values are read and placed in row order.

reshaped = A.reshape(2, 6)
print(reshaped)
print(reshaped.shape)
print(A.shape)  # A itself still has shape (3, 4)
[[ 1  2  3  4  5  6]
 [ 7  8  9 10 11 12]]
(2, 6)
(3, 4)

Reshaping preserves the sequence of values, but may return a view or a copy. Do not assume the result has independent data; add .copy() if independence is needed.

3.1 Let NumPy infer one dimension

print(A.reshape(4, -1))  # 12 elements / 4 rows = 3 columns
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]

Only one dimension can be -1, and the inferred dimension must be a whole number. Reshaping this array to (5, -1) would fail.

3.2 A single row or column

v = np.array([3, 1, 4])
row = v.reshape(1, -1)
column = v.reshape(-1, 1)
print(row, row.shape)
print(column, column.shape)
[[3 1 4]] (1, 3)
[[3]
 [1]
 [4]] (3, 1)

The shapes (3,), (1, 3), and (3, 1) are different and can behave differently in calculations.

4 Special Arrays

Many array constructors accept a shape tuple:

print(np.zeros((2, 3)))
print(np.ones((3, 2)))
[[0. 0. 0.]
 [0. 0. 0.]]
[[1. 1.]
 [1. 1.]
 [1. 1.]]

5 Elementwise Operations

Arrays of the same shape can be combined element by element:

x1 = np.array([[7, 0, 8],
               [5, 6, 2]])
x2 = np.array([[2, 5, 3],
               [7, 1, 4]])
print(x1 + x2)
print(x1 * x2)
print(x1**x2)
[[ 9  5 11]
 [12  7  6]]
[[14  0 24]
 [35  6  8]]
[[   49     0   512]
 [78125     6    16]]

* multiplies corresponding elements. It is not matrix multiplication. A scalar also applies to every element, as in x1 + 10.

6 Summaries by Row or Column

A reduction combines values along an axis. The selected axis is removed from the output unless keepdims=True.

values = np.array([[1, 2, 3, 4],
                   [5, 6, 7, 8]])
print(np.sum(values))          # 36: all values
print(np.sum(values, axis=0))  # [6 8 10 12]: one sum per column
print(np.sum(values, axis=1))  # [10 26]: one sum per row
36
[ 6  8 10 12]
[10 26]
Argument What is combined? Result for shape (2, 4)
No axis specified All elements One value
axis=0 Rows, within each column Shape (4,)
axis=1 Columns, within each row Shape (2,)

The same pattern applies to other summaries:

print(np.mean(values, axis=0))
print(np.mean(values, axis=1))
print(np.std(values, axis=0, ddof=1))  # sample SD for each column
print(np.prod(values, axis=0))
[3. 4. 5. 6.]
[2.5 6.5]
[2.82842712 2.82842712 2.82842712 2.82842712]
[ 5 12 21 32]

6.1 Retaining the reduced dimension

row_means = np.mean(values, axis=1, keepdims=True)
print(row_means)        # [[2.5], [6.5]]
print(row_means.shape)  # (2, 1)
[[2.5]
 [6.5]]
(2, 1)

This shape is useful when subtracting each row’s mean from that row.

7 Broadcasting

Broadcasting lets compatible shapes participate in elementwise arithmetic. Compare dimensions from the right: sizes must match, or one must be 1. A missing leading dimension is treated as 1.

y = np.array([[3, 1, 0],
              [5, 2, 7]])
row_offsets = np.array([[10], [20]])  # shape (2, 1)
column_weights = np.array([10, 20, 30])  # shape (3,)

print(y + row_offsets)
print(y * column_weights)
[[13 11 10]
 [25 22 27]]
[[ 30  20   0]
 [ 50  40 210]]

The first calculation adds 10 to row 1 and 20 to row 2. The second multiplies columns by 10, 20, and 30.

Shapes Compatible? Result shape
(2, 3) and (3,) Yes (2, 3)
(2, 3) and (2, 1) Yes (2, 3)
(2, 3) and (2,) No Error

To apply one value per row, use a column shape such as (2, 1). Broadcasting avoids explicitly duplicating the smaller input, although the resulting calculation still requires memory.

8 Worked Example: Scores by Student and Quiz

Each row is a student and each column is a quiz, scored out of 100.

scores = np.array([[60, 70, 80],
                   [40, 50, 60],
                   [80, 85, 90],
                   [30, 40, 50]], dtype=float)

student_means = np.mean(scores, axis=1)
quiz_means = np.mean(scores, axis=0)
print("Student means:", student_means)
print("Quiz means:", quiz_means)
Student means: [70. 50. 85. 40.]
Quiz means: [52.5  61.25 70.  ]

Select whole rows using one Boolean value per student:

print(scores[student_means >= 60, :])
[[60. 70. 80.]
 [80. 85. 90.]]

A mask with the same shape as the full array selects individual elements and produces a 1D result:

print(scores[scores >= 80])
[80. 80. 85. 90.]

Centre each quiz by subtracting its column mean:

centered = scores - quiz_means
print(centered)
print(np.mean(centered, axis=0))  # zeros, apart from possible roundoff
[[  7.5    8.75  10.  ]
 [-12.5  -11.25 -10.  ]
 [ 27.5   23.75  20.  ]
 [-22.5  -21.25 -20.  ]]
[0. 0. 0.]

To centre each student’s row instead:

row_centered = scores - np.mean(scores, axis=1, keepdims=True)
print(row_centered)
[[-10.   0.  10.]
 [-10.   0.  10.]
 [ -5.   0.   5.]
 [-10.   0.  10.]]

9 Matrix Multiplication

9.1 Inner product of two vectors

u = np.array([2, 5, 1])
v = np.array([3, 4, 2])
print(np.dot(u, v))  # 2*3 + 5*4 + 1*2 = 28
28

9.2 Multiplying matrices

If M has shape (m, k) and N has shape (k, n), their matrix product has shape (m, n). The inner dimensions must agree.

M = np.array([[1, 2], [3, 4]])
N = np.array([[5, 6, 7], [8, 9, 0]])
print(M @ N)
print(np.dot(M, N))  # same result for these 2D arrays
[[21 24  7]
 [47 54 21]]
[[21 24  7]
 [47 54 21]]

Use @ for matrix multiplication. For 2D arrays it agrees with np.matmul() and np.dot(). These functions have different rules for higher-dimensional inputs, which are outside this lecture.

N @ M  # incompatible inner dimensions: 3 and 2
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[24], line 1
----> 1 N @ M

ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)

This deliberately produces a ValueError.

9.3 Matrix-vector multiplication

weights = np.array([0.2, 0.3, 0.5])
weighted_scores = scores @ weights
print(weighted_scores)  # [73, 53, 86.5, 43]
[73.  53.  86.5 43. ]

The shape calculation is (4, 3) @ (3,), giving one weighted score per student, with shape (4,).

10 Other Matrix Operations

10.1 Transpose

print(M.T)
print(N.T.shape)  # (3, 2)
[[1 3]
 [2 4]]
(3, 2)

Transpose swaps the two axes of a 2D array. For a 1D array, v.T still has shape (3,); use reshape(-1, 1) to create a 2D column.

10.2 Determinant and inverse

C = np.array([[2.0, 1.0],
              [5.0, 3.0]])
print(np.linalg.det(C))
inverse_C = np.linalg.inv(C)
print(inverse_C)
print(C @ inverse_C)  # approximately the identity matrix
1.0000000000000009
[[ 3. -1.]
 [-5.  2.]]
[[ 1.00000000e+00  2.22044605e-16]
 [-8.88178420e-16  1.00000000e+00]]

Mathematically, a square matrix is invertible exactly when its determinant is nonzero. With floating-point data, testing det(C) == 0 is not a reliable numerical diagnostic; nearly singular matrices can also give inaccurate results. NumPy may raise LinAlgError for a singular matrix.

10.3 Solving a linear system

For \(C\mathbf{x}=\mathbf{b}\), solve directly rather than calculating the inverse explicitly:

\[ \begin{aligned} 2x_1+x_2 &= 5,\\ 5x_1+3x_2 &= 13. \end{aligned} \]

b = np.array([5.0, 13.0])
solution = np.linalg.solve(C, b)
print(solution)  # [2. 1.]
print(np.allclose(C @ solution, b))  # True
[2. 1.]
True

solve() requires a square, full-rank coefficient matrix for this unique-solution problem. np.allclose() checks whether all corresponding values are approximately equal, allowing for small floating-point differences.

10.4 Rank and trace

D = np.array([[1, 2, 3],
              [2, 4, 6],
              [1, 1, 1]])
print(np.linalg.matrix_rank(D))  # 2
print(np.trace(D))              # 6
2
6

Rank counts linearly independent rows or columns; NumPy estimates it numerically using a tolerance. For a square matrix, the trace is the sum of its main diagonal.

10.5 Eigenvalues and eigenvectors

F = np.array([[4.0, 2.0],
              [1.0, 3.0]])
eigenvalues, eigenvectors = np.linalg.eig(F)
print(eigenvalues)
print(eigenvectors)
[5. 2.]
[[ 0.89442719 -0.70710678]
 [ 0.4472136   0.70710678]]

Eigenvectors are stored in columns. Column i corresponds to eigenvalues[i] and satisfies \(F\mathbf{v}_i=\lambda_i\mathbf{v}_i\).

v0 = eigenvectors[:, 0]
print(np.allclose(F @ v0, eigenvalues[0] * v0))
True

Eigenvalue order and eigenvector signs may differ between valid outputs. General real matrices can have complex eigenvalues and eigenvectors.

Operation NumPy expression
Elementwise product A * B
Matrix product A @ B
Transpose A.T
Determinant np.linalg.det(A)
Inverse np.linalg.inv(A)
Solve a square linear system np.linalg.solve(A, b)
Rank np.linalg.matrix_rank(A)
Trace np.trace(A)
Eigenvalues and eigenvectors np.linalg.eig(A)

11 Stacking Arrays

For 2D arrays, horizontal stacking joins columns and vertical stacking joins rows.

a1 = np.array([[11, 12], [13, 14], [15, 16]])
a2 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
a3 = np.array([[-1, -2], [-3, -4]])

print(np.hstack([a1, a2]))  # same number of rows
print(np.vstack([a1, a3]))  # same number of columns
[[11 12  1  2  3]
 [13 14  4  5  6]
 [15 16  7  8  9]]
[[11 12]
 [13 14]
 [15 16]
 [-1 -2]
 [-3 -4]]

concatenate() expresses the same operations using an axis:

print(np.concatenate([a1, a2], axis=1))
print(np.concatenate([a1, a3], axis=0))
[[11 12  1  2  3]
 [13 14  4  5  6]
 [15 16  7  8  9]]
[[11 12]
 [13 14]
 [15 16]
 [-1 -2]
 [-3 -4]]

All dimensions except the joining axis must match. These calls return new arrays. For 1D inputs, hstack() joins the sequences into a longer 1D array; the row/column interpretation above applies to 2D inputs.

12 Exercises

  1. Create a (3, 4) array containing integers 1 through 12. Display its second row, last column, and top-left (2, 2) block.
  2. Select its first column as a 1D array and as a 2D column. Display both shapes.
  3. Reshape it to (2, 6) and (4, 3). Use -1 in one call.
  4. Calculate its column means, row sums, and sample standard deviation within each column.
  5. Add [10, 20, 30, 40] to its columns. Then add 100, 200, and 300 to its first, second, and third rows using broadcasting.
  6. For the worked score data, select students whose average is at least 60. Calculate the mean score for each quiz among those students.
  7. Centre every row of the score array and verify that the resulting row means are approximately zero.
  8. With P = np.array([[1, 2], [3, 4]]) and Q = np.array([[2, 0], [1, 3]]), calculate P * Q, P @ Q, and Q @ P. Explain the differences.
  9. Solve \(3x+y=9\) and \(x+2y=8\) using np.linalg.solve(). Check the solution by multiplication.
  10. For H = np.array([[4, 1], [1, 3]]), obtain its transpose, determinant, inverse, rank, trace, and eigenvalues. Verify one eigenvector equation with np.allclose().
  11. Join a1 and a2 horizontally, and a1 and a3 vertically. State the resulting shapes before running the code.

13 Summary

  • Read a 2D shape as (rows, columns); indexing can remove a dimension.
  • axis=0 gives column summaries and axis=1 gives row summaries.
  • Inspect shapes before broadcasting, reshaping, stacking, or matrix multiplication.
  • Distinguish * from @; solve linear systems directly with np.linalg.solve().
  • Use approximate comparisons when checking floating-point results.

14 Further reading