Chapter 3: Simple Arithmetic
simple arithmetic ufuncs in NumPy — written as if I’m sitting next to you at the computer, explaining slowly, showing small realistic examples, comparing with pure Python, answering typical student questions, and helping you understand how these basic operations become extremely powerful when used correctly.
Let’s pretend we’re working together in a notebook.
|
0 1 2 3 4 5 6 |
import numpy as np |
1. The four basic arithmetic ufuncs you already use every day
These four symbols are actually ufuncs when used with NumPy arrays:
|
0 1 2 3 4 5 6 7 8 9 |
+ → np.add - → np.subtract * → np.multiply / → np.true_divide (or np.divide) |
There are also related integer-only versions:
|
0 1 2 3 4 5 6 7 8 9 |
// → np.floor_divide % → np.mod / np.remainder ** → np.power |
Very important sentence:
When you write a + b, a * 2, arr ** 3 or data / 10 with NumPy arrays → you are already using ufuncs.
2. Basic examples – scalar vs array
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Scalar operations (very simple) print(5 + 3) # 8 print(5 * 3) # 15 # Now with arrays – same syntax, very different behavior a = np.array([2, 5, 8, 1, 9]) print(a + 10) # [12 15 18 11 19] print(a * 3) # [ 6 15 24 3 27] print(a ** 2) # [ 4 25 64 1 81] print(a / 2) # [1. 2.5 4. 0.5 4.5] |
Student question I always get:
“Why does a + 10 add 10 to every element?”
Answer: Because + is a ufunc. NumPy sees that one operand is an array and the other is a scalar → it automatically broadcasts the scalar to the shape of the array.
3. Array vs array arithmetic
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
a = np.array([2, 5, 8, 1, 9]) b = np.array([10, 20, 30, 40, 50]) print("a + b =", a + b) # [12 25 38 41 59] print("a - b =", a - b) # [ -8 -15 -22 -39 -41] print("a * b =", a * b) # [ 20 100 240 40 450] print("a / b =", a / b.round(3)) # [0.2 0.25 0.267 0.025 0.18 ] print("a ** b =", a ** b) # very large numbers |
Important detail: Division a / b always returns floating-point results, even if both arrays contain integers.
|
0 1 2 3 4 5 6 7 |
print(np.array([10, 20, 30]) / np.array([2, 4, 6])) # [5. 5. 5.] |
If you want integer division (floor division):
|
0 1 2 3 4 5 6 7 |
print(np.array([10, 20, 30]) // np.array([2, 4, 6])) # [5 5 5] |
4. Broadcasting – the magic that makes arithmetic ufuncs so powerful
NumPy automatically stretches smaller arrays to match the shape of larger ones.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
a = np.arange(12).reshape(3, 4) print("a =\n", a) # Add scalar → broadcasts to whole array print("\na + 100:\n", a + 100) # Add 1D row vector row = np.array([100, 200, 300, 400]) print("\na + row:\n", a + row) # Add column vector (needs newaxis or reshape) col = np.array([10, 20, 30])[:, np.newaxis] print("\na + col:\n", a + col) |
Quick broadcasting rule (memorize this):
Shapes are compared from the right to the left. Two dimensions are compatible if:
- they are equal, or
- one of them is 1 (it gets stretched)
5. Realistic patterns – arithmetic ufuncs in real code
Pattern 1 – Normalize / standardize data
|
0 1 2 3 4 5 6 7 8 9 |
X = np.random.randn(10000, 15) # 10,000 samples × 15 features # Standardize each column (mean=0, std=1) X_norm = (X - X.mean(axis=0)) / X.std(axis=0) |
Pattern 2 – Scale pixel values to [0,1]
|
0 1 2 3 4 5 6 7 8 9 |
img = np.random.randint(0, 256, (300, 400, 3), dtype=np.uint8) # Common normalization for neural networks img_float = img / 255.0 |
Pattern 3 – Compute percentage change
|
0 1 2 3 4 5 6 7 8 9 |
prices = np.array([120.5, 118.2, 125.7, 122.9, 130.4]) pct_change = (prices[1:] - prices[:-1]) / prices[:-1] * 100 print("Daily % changes:", pct_change.round(2)) |
Pattern 4 – Element-wise clipping / bounding
|
0 1 2 3 4 5 6 7 8 9 |
values = np.random.normal(100, 25, 1000) # Keep values between 60 and 140 bounded = np.clip(values, 60, 140) |
Pattern 5 – Simple financial return calculation
|
0 1 2 3 4 5 6 7 8 |
investment = np.array([1000, 1200, 900, 1500, 1100]) returns = investment / 1000 - 1 print("Returns:", returns.round(3)) |
6. Summary – Arithmetic ufuncs Quick Reference
| Symbol / Operation | ufunc name | Notes / Common use |
|---|
- | np.add | a + b, a + scalar
- | np.subtract | a – b
- | np.multiply | a * b, a * 2 / | np.true_divide | always float result // | np.floor_divide | integer division % | np.mod, np.remainder | remainder ** | np.power | a ** 2, a ** b np.clip(a, min, max) | – | bound values np.round / floor / ceil | – | rounding
Final teacher messages (very important)
Golden rule #1 If you are writing a for loop to do simple arithmetic on every element of a NumPy array → you are almost always doing it wrong. Just write a + b, a * 5, arr ** 2 directly.
Golden rule #2 When you write a + 10, arr / 255.0, data – mean → you are already using ufuncs + broadcasting. That’s why NumPy feels so fast and expressive.
Golden rule #3 Use np. prefix when you want to be explicit (np.add(a, b) instead of a + b) — especially in larger code or when teaching others.
Would you like to continue with any of these next?
- Broadcasting in more depth (3D/4D arrays, common mistakes)
- Using arithmetic ufuncs with out= parameter (in-place)
- Combining arithmetic with masking & np.where
- Realistic mini-project: vectorized financial calculation or image processing
- Difference between true_divide vs floor_divide vs remainder
Just tell me what feels most useful for you right now! 😊
