Vectorized Arithmetic, Ufuncs, Comparison and Logical Operations
This is where NumPy really pays off. Instead of writing a loop that handles one number at a time, you write the maths once for the whole array. This is called vectorization. The functions that work element by element, such as np.sqrt and np.maximum, are called ufuncs (universal functions).
How to run the examples
Create a new file named vectorized_ufuncs.py in your NumpyTutorials folder (see the setup in lesson 01). Any name works; this one just matches the topic. Start the file with these lines:
import numpy as np
Then copy the examples of this page into the same file, one after another, in the order shown, and run it with python vectorized_ufuncs.py (with your virtual environment active). Some examples use variables created in earlier ones, so keep everything in the one file. Each Output box shows what that example prints.
1. Element-wise arithmetic
All the normal maths operators work on arrays, element by element. One side can be an array of the same shape or a single number:
a = np.array([10, 20, 30, 40])
b = np.array([1, 2, 3, 4])
print("a + b :", a + b)
print("a - b :", a - b)
print("a * b :", a * b)
print("a / b :", a / b)
print("a // b :", a // b, "(whole number division)")
print("a % 3 :", a % 3, "(remainder)")
print("b ** 2 :", b ** 2, "(power)")
print("-a :", -a)
print("a * 0.5:", a * 0.5)
try:
a + np.array([1, 2, 3]) # different lengths
except ValueError as error:
print("Error:", error)
| Operator | Meaning | Note |
|---|---|---|
+ - * | add, subtract, multiply | element by element |
/ | divide | result is always float |
// | whole-number division | drops the remainder |
% | remainder | 10 % 3 = 1 |
** | power | b ** 2 squares every element |
Two arrays of different lengths, such as 4 and 3 values, cannot be matched element by element, so NumPy raises an error. (Some different shapes can work; that is broadcasting, the next lesson.)
2. Loop vs vectorized
prices = np.array([250.0, 120.0, 980.0, 45.0])
with_gst_loop = []
for price in prices: # the "normal Python" way
with_gst_loop.append(price * 1.18)
with_gst = prices * 1.18 # the NumPy way
print("loop :", np.round(with_gst_loop, 2))
print("vectorized:", with_gst.round(2))
Same result, but the vectorized version is one short line, easier to read, and much faster on big arrays. Whenever you are about to write for x in array:, first ask whether there is a NumPy operation that does it in one go. There almost always is.
3. Ufuncs with one input
x = np.array([1, 4, 9, 16])
print("sqrt :", np.sqrt(x))
print("square:", np.square(x))
print("exp :", np.exp(np.array([0, 1, 2])).round(3))
print("log :", np.log(np.array([1, np.e, 100])).round(3))
print("log10 :", np.log10(np.array([1, 10, 1000])))
print("abs :", np.abs(np.array([-3, 0, 5])))
print("sin :", np.sin(np.array([0, np.pi / 2, np.pi])).round(3))
print("sign :", np.sign(np.array([-8, 0, 2])))
np.sqrt,np.square,np.exp,np.log(natural log),np.log10,np.abs,np.sin/cos/tanandnp.signeach work on every element.np.piandnp.eare the constants π and e.- We use
.round(3)so results like 2.718281828 are easier to read.
Use np.sqrt, not math.sqrt. The math module only works on single numbers and gives an error for arrays.
4. Ufuncs with two inputs
print("np.add(a, b) :", np.add(a, b), "(same as a + b)")
print("np.multiply(a, b) :", np.multiply(a, b))
print("np.power(b, 3) :", np.power(b, 3))
print("np.maximum(a, 25) :", np.maximum(a, 25), "(at least 25)")
print("np.minimum(a, 25) :", np.minimum(a, 25), "(at most 25)")
print("np.maximum(a, b * 12):", np.maximum(a, b * 12), "(bigger of each pair)")
result = np.empty(4)
np.multiply(a, 2, out=result) # write into an existing array
print("out= parameter :", result)
np.add,np.multiplyandnp.powerare the function versions of+,*and**.np.maximum(a, 25)compares each element with 25 and keeps the bigger one, so every value becomes "at least 25".np.minimummakes "at most 25".- Don't confuse
np.maximum(element by element, two inputs) withnp.max(one biggest value of an array). out=writes the result into an existing array instead of creating a new one; this saves memory (lesson 16).
5. Comparison
target = np.array([30, 25, 30, 35])
print("a :", a)
print("target :", target)
print("a == target:", a == target)
print("a != target:", a != target)
print("a > target :", a > target)
print("a >= 20 :", a >= 20)
print("array_equal(a, a.copy()):", np.array_equal(a, a.copy()), "(whole arrays equal?)")
Comparison operators return a boolean array, one True/False per pair. These are the masks from lesson 05. To ask "are these two whole arrays equal?", use np.array_equal, which returns one True or False.
6. Logical operations
To combine True/False arrays, use the logical functions or their operator shortcuts:
in_stock = np.array([True, True, False, True])
on_sale = np.array([False, True, True, True])
print("logical_and :", np.logical_and(in_stock, on_sale))
print("logical_or :", np.logical_or(in_stock, on_sale))
print("logical_not :", np.logical_not(in_stock))
print("logical_xor :", np.logical_xor(in_stock, on_sale), "(only one of the two)")
print("& | ~ work too:", in_stock & on_sale, in_stock | on_sale, ~in_stock)
print("any in stock?:", in_stock.any(), " all in stock?:", in_stock.all())
| Function | Operator | True when |
|---|---|---|
np.logical_and | & | both are True |
np.logical_or | | | at least one is True |
np.logical_not | ~ | the value is False |
np.logical_xor | ^ | exactly one is True |
.any() asks "is at least one True?" and .all() asks "are all True?".
7. Division by zero
with np.errstate(divide="ignore", invalid="ignore"): # hide the warnings here
print("[1, 0, -1] / 0 :", np.array([1, 0, -1]) / 0)
print("safe divide with np.divide(where=):",
np.divide(np.array([10, 5, 8]), np.array([2, 0, 4]), out=np.zeros(3), where=np.array([2, 0, 4]) != 0))
- Python raises an error for
1 / 0. NumPy instead givesinf(infinity) for1/0,-inffor-1/0andnan(not a number) for0/0, plus a warning.np.errstatehides the warning inside thewithblock. - For a safe division,
np.divide(..., out=np.zeros(3), where=b != 0)only divides where the divisor is not 0 and leaves 0 elsewhere.
8. Real example: shop bill
items = np.array(["Rice", "Oil", "Soap", "Tea"])
unit_price = np.array([60.0, 180.0, 35.0, 250.0])
quantity = np.array([5, 2, 6, 1])
discount_pct = np.array([0, 10, 5, 0])
amount = unit_price * quantity
discounted = amount * (1 - discount_pct / 100)
gst = discounted * 0.05
for name, amt, disc, tax in zip(items, amount, discounted, gst):
print(f"{name:<5} {amt:>8.2f} {disc:>8.2f} {tax:>6.2f}")
print(f"Total to pay: {(discounted + gst).sum():.2f}")
Four arrays, one per column of the bill, and every calculation (amount, discount, GST) is a single line that works on all items at once. The final sum() gives the total.
Exercises
Try these exercises on your own before moving on:
- Convert np.array([0, 25, 37, 100]) Celsius to Fahrenheit (C * 9/5 + 32).
- Find the square root of np.arange(1, 11), rounded to 2 decimals.
- Two arrays of exam marks: make an array of the better mark of each pair.
- Find the products that are cheaper than 100 AND in stock.
Summary
- Arithmetic operators work element by element on arrays of the same shape, or with a single number.
- Vectorized code is shorter and much faster than loops.
- Ufuncs such as
np.sqrt,np.log,np.maximumapply a function to every element. - Comparisons give boolean arrays; combine them with
&|~or thelogical_*functions. - Division by zero gives
infornaninstead of an error; usewhere=for safe division.
Next: Lesson 10: Broadcasting.