NumPy ufunc
1. What is a ufunc really? (honest explanation)
ufunc = universal function
A ufunc is a NumPy function that:
- operates element-by-element on entire arrays
- does it very fast (written in compiled C code)
- supports broadcasting automatically
- can be applied to scalars, arrays, or mixtures
- usually returns a new array (rarely modifies in place)
Most important sentence to remember:
In NumPy, almost every mathematical operation you want to do on arrays is a ufunc.
Examples of very common ufuncs:
|
0 1 2 3 4 5 6 7 8 9 |
+ - * / // % ** & | ^ ~ > >= < <= == != np.sin np.cos np.tan np.exp np.log np.log10 np.sqrt np.abs np.floor np.ceil np.round np.trunc np.maximum np.minimum np.fmax np.fmin |
2. Why ufuncs are so much faster than Python loops
Classic beginner mistake:
|
0 1 2 3 4 5 6 7 8 9 10 |
# Slow Python way a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] result = [] for x in a: result.append(x ** 2 + 3 * x - 7) |
NumPy ufunc way — 10–100× faster, cleaner, more readable:
|
0 1 2 3 4 5 6 7 8 9 |
a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) result = a ** 2 + 3 * a - 7 print(result) # [ -3 3 11 21 33 47 63 81 101 123] |
Why is it so fast?
- No Python loop overhead
- Vectorized execution (SIMD instructions on CPU)
- Contiguous memory layout
- Compiled C/Fortran code under the hood
3. The most important ufuncs you will use every day
Arithmetic ufuncs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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) # floor division print("a ** 2 =", a ** 2) print("a % 3 =", a % 3) |
Mathematical ufuncs
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
x = np.array([0, np.pi/6, np.pi/4, np.pi/3, np.pi/2]) 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-2) =", np.abs(x-2).round(3)) |
Rounding & truncation
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
vals = np.array([-1.7, -1.4, -0.6, 0.3, 1.2, 2.7, 3.1]) print("floor :", np.floor(vals)) print("ceil :", np.ceil(vals)) print("trunc :", np.trunc(vals)) print("round :", np.round(vals)) print("rint :", np.rint(vals)) # round to nearest even |
Comparison ufuncs — return boolean arrays
|
0 1 2 3 4 5 6 7 8 9 10 11 |
a = np.array([3, 8, 1, 9, 4, 6, 2, 7]) b = np.array([5, 2, 7, 1, 6, 3, 8, 4]) print("a > b :", a > b) print("a == b :", a == b) print("a >= 5 :", a >= 5) |
4. Broadcasting — the superpower of ufuncs
ufuncs automatically “stretch” smaller arrays to match larger ones.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
a = np.arange(12).reshape(3, 4) print(a) # Add scalar → broadcasts to whole array print("\na + 100:") print(a + 100) # Add 1D row vector print("\na + [100, 200, 300, 400]:") print(a + np.array([100, 200, 300, 400])) # Add column vector (needs shape (3,1)) print("\na + [[10],[20],[30]]:") print(a + np.array([[10],[20],[30]])) |
5. Very common realistic patterns (you will write these often)
Pattern 1 – Normalize data
|
0 1 2 3 4 5 6 7 8 9 |
X = np.random.randn(10000, 20) # 10,000 samples × 20 features # Standardize each column (mean=0, std=1) 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(-50, 300, size=(100, 100)) clean_pixels = np.clip(pixels, 0, 255) |
Pattern 3 – Replace invalid values
|
0 1 2 3 4 5 6 7 |
measurements = np.random.normal(100, 15, 5000) measurements[measurements < 0] = np.nan # or 0, or mean, etc. |
Pattern 4 – Element-wise conditions
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
scores = np.random.randint(40, 101, 200) passed = scores >= 60 print("Pass rate:", passed.mean().round(3)) # Replace failing with 0 scores[~passed] = 0 |
Pattern 5 – 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 brightness brighter = np.clip(img + 40, 0, 255) # Convert to grayscale (simple average) gray = np.mean(img, axis=2).astype(np.uint8) |
Summary – ufunc Quick Reference
| Operation type | Typical ufuncs |
|---|---|
| Arithmetic | +, -, *, /, //, %, **, np.add, np.subtract… |
| Trigonometric | np.sin, np.cos, np.tan, np.arcsin… |
| Exponential & log | np.exp, np.log, np.log10, np.log1p, np.expm1 |
| Rounding | np.floor, np.ceil, np.round, np.trunc, np.rint |
| Comparison | >, >=, <, <=, ==, !=, np.greater, np.equal… |
| Maximum / minimum | np.maximum, np.minimum, np.fmax, np.fmin |
| Absolute / sign | np.abs, np.sign, np.negative |
| Specialized | np.sqrt, np.cbrt, np.square, np.reciprocal |
Final teacher advice
Golden rule #1: If you are writing a 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 in doubt → try to write it with ufuncs + broadcasting. 90% of the time it will work and be much faster.
Golden rule #3: Use np. prefix when you want to be explicit (np.sin vs np.sin) — helps readability and avoids conflicts with math module.
Would you like to go deeper into any of these areas?
- Advanced broadcasting rules with examples
- ufuncs with out= parameter (in-place operations)
- Creating your own ufunc with np.frompyfunc / np.vectorize
- Common ufunc performance traps
- Realistic mini-project: vectorized image processing / data cleaning
Just tell me what you want to focus on next! 😊
