Chapter 1: ufunc Intro
1. What does “ufunc” actually mean?
ufunc = universal function
A ufunc is a NumPy function that:
- works element-by-element on entire arrays (or scalars)
- is extremely fast (written in compiled C / Fortran code)
- automatically handles broadcasting (different shapes get matched smartly)
- supports many extra features like where, out, dtype control, etc.
- almost always returns a new array (very rarely modifies in place)
The most important sentence you should remember forever:
In NumPy, almost every mathematical, logical or comparison operation you want to apply to an array is actually a ufunc.
That’s why when you write a + b, np.sin(x), a > 5 or np.sqrt(arr) — you are using ufuncs.
2. Why do ufuncs feel like magic? (comparison with pure Python)
Pure Python way (slow, verbose, not vectorized)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# List of temperatures in Celsius temps_c = [23.4, 25.1, 19.8, 28.7, 22.0, 30.5] # Convert to Fahrenheit (classic slow way) temps_f = [] for t in temps_c: f = t * 1.8 + 32 temps_f.append(round(f, 1)) print(temps_f) |
NumPy ufunc way (fast, clean, beautiful)
|
0 1 2 3 4 5 6 7 8 9 10 |
temps_c = np.array([23.4, 25.1, 19.8, 28.7, 22.0, 30.5]) temps_f = temps_c * 1.8 + 32 print(np.round(temps_f, 1)) # [74.1 77.2 67.6 83.7 71.6 86.9] |
Same result — completely different speed & style.
3. The four big categories of ufuncs you will use every day
Category 1 – Arithmetic ufuncs
These are the ones you use most often:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
a = np.arange(1, 11) b = np.arange(10, 0, -1) print("a =", a) print("b =", b) print("a + b =", a + b) print("a - b =", a - b) print("a * b =", a * b) print("a / b =", a / b.round(2)) # floating point print("a // b =", a // b) # integer division print("a ** 2 =", a ** 2) print("a % 3 =", a % 3) print("a + 100 =", a + 100) # broadcasting scalar |
Category 2 – Mathematical & transcendental ufuncs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
x = np.linspace(0, 2*np.pi, 13) print("x (radians) =", x.round(3)) print("sin(x) =", np.sin(x).round(3)) print("cos(x) =", np.cos(x).round(3)) print("tan(x) =", np.tan(x).round(3)) print("exp(x) =", np.exp(x).round(2)) print("log(x+1) =", np.log1p(x).round(3)) # log(1+x) — safer for small x print("sqrt(x) =", np.sqrt(x).round(3)) print("abs(x-3) =", np.abs(x-3).round(3)) |
Category 3 – Rounding & truncation ufuncs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
values = np.array([-2.7, -1.4, -0.8, 0.3, 1.6, 2.9, 3.2]) print("floor :", np.floor(values)) print("ceil :", np.ceil(values)) print("trunc :", np.trunc(values)) print("round :", np.round(values, decimals=0)) print("rint :", np.rint(values)) # round to nearest even |
Category 4 – Comparison & logical ufuncs
They return boolean arrays — extremely useful for masking/filtering.
|
0 1 2 3 4 5 6 7 8 9 10 11 |
a = np.array([3, 8, 1, 9, 4, 6, 2, 7, 5, 0]) print("a > 5 :", a > 5) print("a == 4 :", a == 4) print("a >= 3 :", a >= 3) print("(a % 2) == 0 :", (a % 2) == 0) # even numbers |
4. Broadcasting – the feature that makes ufuncs feel magical
ufuncs automatically stretch 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) # Scalar broadcasts to whole array print("\na + 100:\n", a + 100) # 1D row vector broadcasts across rows row_vec = np.array([100, 200, 300, 400]) print("\na + row_vec:\n", a + row_vec) # Column vector (needs shape (3,1) or use [:, np.newaxis]) col_vec = np.array([10, 20, 30])[:, np.newaxis] print("\na + col_vec:\n", a + col_vec) |
Golden rule about broadcasting:
NumPy compares shapes from right to left. Dimensions are compatible if they are equal or one of them is 1 (it gets stretched).
5. Very common realistic patterns you will write again and again
Pattern 1 – Normalize / standardize features
|
0 1 2 3 4 5 6 7 8 |
X = np.random.randn(10000, 20) X_norm = (X - X.mean(axis=0)) / X.std(axis=0) |
Pattern 2 – Clip values to valid range
|
0 1 2 3 4 5 6 7 8 |
pixels = np.random.randint(-80, 350, size=(200, 300)) valid_pixels = np.clip(pixels, 0, 255) |
Pattern 3 – Element-wise conditional replacement
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
values = np.random.randn(5000) # Replace negatives with 0 values[values < 0] = 0 # Or using np.where (also a ufunc!) cleaned = np.where(values < 0, 0, values) |
Pattern 4 – Vectorized math on images
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
img = np.random.randint(0, 256, (300, 400, 3), dtype=np.uint8) # Increase contrast contrasted = np.clip(1.3 * img - 40, 0, 255).astype(np.uint8) # Simple grayscale gray = np.mean(img, axis=2).astype(np.uint8) |
Pattern 5 – Find elements that satisfy conditions
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
scores = np.random.randint(40, 101, 300) passed = scores >= 60 print("Pass rate:", passed.mean().round(3)) # Get actual passing scores passing_scores = scores[passed] |
Summary – ufunc Quick Reference (keep this handy)
| Category | Typical ufuncs you will type often |
|---|---|
| Arithmetic | +, -, *, /, //, %, **, np.add, np.subtract… |
| Trigonometric | np.sin, np.cos, np.tan, np.arcsin, np.arccos… |
| Exponential & log | np.exp, np.log, np.log10, np.log1p, np.expm1 |
| Rounding | np.floor, np.ceil, np.round, np.trunc, np.rint |
| Comparison / logical | >, >=, <, <=, ==, !=, np.logical_and, np.logical_or |
| Maximum / minimum | np.maximum, np.minimum, np.fmax, np.fmin |
| Absolute / sign | np.abs, np.sign, np.negative |
| Specialized | np.sqrt, np.square, np.cbrt, np.reciprocal, np.hypot |
Final teacher advice (very important)
Golden rule #1 If you are writing a Python for loop to apply the same math operation to every element of a NumPy array → you are almost certainly doing it wrong.
Golden rule #2 When you think “I need to do [some math] to every element”, try to write it directly with ufuncs + broadcasting first.
Golden rule #3 When in doubt → look at the NumPy cheat sheet or type np.<tab> in Jupyter — you will almost always find a ufunc that does exactly what you want.
Would you like to go deeper into any of these topics?
- Advanced broadcasting examples with 3D/4D arrays
- ufuncs with out= parameter (in-place operations)
- Creating your own ufunc (np.frompyfunc, np.vectorize)
- Common performance traps when using ufuncs
- Realistic mini-project: vectorized image processing / data cleaning
Just tell me what feels most useful for you right now! 😊
