A 2-row × 3-column grid centered column by column. The first nested loop computes each column's mean; the second subtracts the column mean from every element in that column. The trace shows means filling element by element, then result accumulating one centered row at a time.

By hand

Two passes over the data. First, loop over each column index j: sum every row's value at j into col_sum, divide by the row count, and append to means. Second, loop over each row in grid: for each column j subtract means[j] from row[j] and collect into new_row.

naive.py
Replay: real traced execution (multi-file project)
grid = [[1, 2, 3], [3, 6, 9]]
cols = len(grid[0])
means = []
for j in range(cols):
    col_sum = 0
    for row in grid:
        col_sum += row[j]
    means.append(col_sum / len(grid))
result = []
for row in grid:
    new_row = []
    for j in range(cols):
        new_row.append(row[j] - means[j])
    result.append(new_row)
print('RESULT:', result)
  1. grid ← [[1, 2, 3], [3, 6, 9]]

    1grid = [[1, 2, 3], [3, 6, 9]]2cols = len(grid[0])
    values this step[[1, 2, 3], [3, 6, 9]]grid
  2. cols ← 3

    1grid = [[1, 2, 3], [3, 6, 9]]2cols = len(grid[0])3means = []
    values this step3cols
  3. means ← []

    2cols = len(grid[0])3means = []4for j in range(cols):
    values this step[]means
  4. j ← 0

    3means = []4for j in range(cols):5    col_sum = 0
    values this step0j
  5. col_sum ← 0

    4for j in range(cols):5    col_sum = 06    for row in grid:
    values this step0col_sum
  6. row ← [1, 2, 3]

    5col_sum = 06for row in grid:7    col_sum += row[j]
    values this step[1, 2, 3]row
  7. col_sum ← 1

    6for row in grid:7    col_sum += row[j]8means.append(col_sum / len(grid))
    values this step0 1col_sum
  8. row ← [3, 6, 9]

    5col_sum = 06for row in grid:7    col_sum += row[j]
    values this step[1, 2, 3] [3, 6, 9]row
  9. col_sum ← 4

    6for row in grid:7    col_sum += row[j]8means.append(col_sum / len(grid))
    values this step1 4col_sum
  10. for row in grid:

    5col_sum = 06for row in grid:7    col_sum += row[j]
  11. means ← [2.0]

    7        col_sum += row[j]8    means.append(col_sum / len(grid))9result = []
    values this step[] [2.0]means
  12. j ← 1

    3means = []4for j in range(cols):5    col_sum = 0
    values this step0 1j
  13. col_sum ← 0

    4for j in range(cols):5    col_sum = 06    for row in grid:
    values this step4 0col_sum
  14. row ← [1, 2, 3]

    5col_sum = 06for row in grid:7    col_sum += row[j]
    values this step[3, 6, 9] [1, 2, 3]row
  15. col_sum ← 2

    6for row in grid:7    col_sum += row[j]8means.append(col_sum / len(grid))
    values this step0 2col_sum
  16. row ← [3, 6, 9]

    5col_sum = 06for row in grid:7    col_sum += row[j]
    values this step[1, 2, 3] [3, 6, 9]row
  17. col_sum ← 8

    6for row in grid:7    col_sum += row[j]8means.append(col_sum / len(grid))
    values this step2 8col_sum
  18. for row in grid:

    5col_sum = 06for row in grid:7    col_sum += row[j]
  19. means ← [2.0, 4.0]

    7        col_sum += row[j]8    means.append(col_sum / len(grid))9result = []
    values this step[2.0] [2.0, 4.0]means
  20. j ← 2

    3means = []4for j in range(cols):5    col_sum = 0
    values this step1 2j
  21. col_sum ← 0

    4for j in range(cols):5    col_sum = 06    for row in grid:
    values this step8 0col_sum
  22. row ← [1, 2, 3]

    5col_sum = 06for row in grid:7    col_sum += row[j]
    values this step[3, 6, 9] [1, 2, 3]row
  23. col_sum ← 3

    6for row in grid:7    col_sum += row[j]8means.append(col_sum / len(grid))
    values this step0 3col_sum
  24. row ← [3, 6, 9]

    5col_sum = 06for row in grid:7    col_sum += row[j]
    values this step[1, 2, 3] [3, 6, 9]row
  25. col_sum ← 12

    6for row in grid:7    col_sum += row[j]8means.append(col_sum / len(grid))
    values this step3 12col_sum
  26. for row in grid:

    5col_sum = 06for row in grid:7    col_sum += row[j]
  27. means ← [2.0, 4.0, 6.0]

    7        col_sum += row[j]8    means.append(col_sum / len(grid))9result = []
    values this step[2.0, 4.0] [2.0, 4.0, 6.0]means
  28. for j in range(cols):

    3means = []4for j in range(cols):5    col_sum = 0
  29. result ← []

    8    means.append(col_sum / len(grid))9result = []10for row in grid:
    values this step[]result
  30. row ← [1, 2, 3]

    9result = []10for row in grid:11    new_row = []
    values this step[3, 6, 9] [1, 2, 3]row
  31. new_row ← []

    10for row in grid:11    new_row = []12    for j in range(cols):
    values this step[]new_row
  32. j ← 0

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
    values this step2 0j
  33. new_row ← [-1.0]

    12for j in range(cols):13    new_row.append(row[j] - means[j])14result.append(new_row)
    values this step[] [-1.0]new_row
  34. j ← 1

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
    values this step0 1j
  35. new_row ← [-1.0, -2.0]

    12for j in range(cols):13    new_row.append(row[j] - means[j])14result.append(new_row)
    values this step[-1.0] [-1.0, -2.0]new_row
  36. j ← 2

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
    values this step1 2j
  37. new_row ← [-1.0, -2.0, -3.0]

    12for j in range(cols):13    new_row.append(row[j] - means[j])14result.append(new_row)
    values this step[-1.0, -2.0] [-1.0, -2.0, -3.0]new_row
  38. for j in range(cols):

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
  39. result ← [[-1.0, -2.0, -3.0]]

    13        new_row.append(row[j] - means[j])14    result.append(new_row)15print('RESULT:', result)
    values this step[] [[-1.0, -2.0, -3.0]]result
  40. row ← [3, 6, 9]

    9result = []10for row in grid:11    new_row = []
    values this step[1, 2, 3] [3, 6, 9]row
  41. new_row ← []

    10for row in grid:11    new_row = []12    for j in range(cols):
    values this step[-1.0, -2.0, -3.0] []new_row
  42. j ← 0

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
    values this step2 0j
  43. new_row ← [1.0]

    12for j in range(cols):13    new_row.append(row[j] - means[j])14result.append(new_row)
    values this step[] [1.0]new_row
  44. j ← 1

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
    values this step0 1j
  45. new_row ← [1.0, 2.0]

    12for j in range(cols):13    new_row.append(row[j] - means[j])14result.append(new_row)
    values this step[1.0] [1.0, 2.0]new_row
  46. j ← 2

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
    values this step1 2j
  47. new_row ← [1.0, 2.0, 3.0]

    12for j in range(cols):13    new_row.append(row[j] - means[j])14result.append(new_row)
    values this step[1.0, 2.0] [1.0, 2.0, 3.0]new_row
  48. for j in range(cols):

    11new_row = []12for j in range(cols):13    new_row.append(row[j] - means[j])
  49. result ← [[-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]]

    13        new_row.append(row[j] - means[j])14    result.append(new_row)15print('RESULT:', result)
    values this step[[-1.0, -2.0, -3.0]] [[-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]]result
  50. for row in grid:

    9result = []10for row in grid:11    new_row = []
  51. stdout ← RESULT: [[-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]]

    14    result.append(new_row)15print('RESULT:', result)
    values this stepRESULT: [[-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]]stdout

With NumPy

a.mean(axis=0) reduces along axis 0 (rows), returning a (3,) array of column means. Subtracting that (3,) vector from the (2, 3) matrix broadcasts across both rows — the same rule as add-vector-to-rows but with a computed vector rather than a literal one.

library.py
import numpy as np

grid = [[1, 2, 3], [3, 6, 9]]
a = np.array(grid, dtype=float)
means = a.mean(axis=0)
result = a - means
print('means: shape:', means.shape, 'dtype:', means.dtype, 'values:', means.tolist())
print('result: shape:', result.shape, 'dtype:', result.dtype)
print('result values:', result.tolist())
print('RESULT:', result.tolist())
means: shape: (3,) dtype: float64 values: [2.0, 4.0, 6.0]
result: shape: (2, 3) dtype: float64
result values: [[-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]]
RESULT: [[-1.0, -2.0, -3.0], [1.0, 2.0, 3.0]]

Implementation notes

  • axis=0 means "reduce along rows" — collapse all rows into one value per column. a.mean(axis=0) on a (2, 3) array yields a (3,) means vector.
  • Broadcasting then stretches that (3,) vector across both rows of a without copying memory — the trailing dimension matches, exactly as in add-vector-to-rows.
  • After centering, every column's mean is zero (within floating-point precision). This is the first step in standardization (z-scores), where you also divide each column by its standard deviation — that lesson is in the roadmap statistics chapter.
  • Axis-based reductions (mean, sum, std, max) are covered in depth in the upcoming reductions chapter.
  • Shape, dtype, and values are shown explicitly here because ndarray.__repr__ output varies with NumPy version and print options.