NumPy · lesson 6 of 13
NumPy Universal Functions (ufuncs)
Use NumPy ufuncs, axis aggregations, statistics, sorting, top-k and set operations with real output, including NaN handling and the removed np.in1d.
By SkillByExample · Updated
Elementwise Math
A ufunc applies the same operation to every element and returns an array of the same shape.
import numpy as np
arr = np.array([1.0, 4.0, 9.0, 16.0])
print(np.sqrt(arr))
print(np.exp([0.0, 1.0, 2.0]).round(3))
print(np.log10([1.0, 10.0, 1000.0]))
print(np.clip([-2.0, 0.5, 1.5], 0.0, 1.0))
print(np.maximum([1, 5, 3], [4, 2, 6]))
$ python math_ufuncs.py
[1. 2. 3. 4.]
[1. 2.718 7.389]
[0. 1. 3.]
[0. 0.5 1. ]
[4 5 6]
Plain Python lists are accepted too — np.exp([0.0, 1.0, 2.0]) converts the list to an array
first. np.maximum is a two-input ufunc: it compares the arrays position by position, unlike
max(), which would return a single value.
Division by Zero and Invalid Values
import warnings
import numpy as np
a = np.array([1.0, 0.0, -1.0])
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
print(a / 0)
print(np.log(a))
for w in caught:
print(f"{w.category.__name__}: {w.message}")
$ python warnings_demo.py
[ inf nan -inf]
[ 0. -inf nan]
RuntimeWarning: divide by zero encountered in divide
RuntimeWarning: invalid value encountered in divide
RuntimeWarning: divide by zero encountered in log
RuntimeWarning: invalid value encountered in log
NumPy does not raise on these: it returns inf, -inf or nan and issues a RuntimeWarning.
Normally Python prints each warning to stderr with the script’s path and line number; here
catch_warnings(record=True) collects them so they can be printed after the results. 1/0 is inf, 0/0 is nan, and log(-1) is nan. The program keeps running,
so silent NaNs can spread through a pipeline — check with np.isnan or np.isfinite.
Writing into an Existing Array with out=
import numpy as np
x = np.array([1.0, 2.0, 3.0])
result = np.empty(3)
np.multiply(x, 10, out=result)
np.sqrt(result, out=result)
print(result)
$ python out_param.py
[3.16227766 4.47213595 5.47722558]
Both calls wrote into result instead of allocating new arrays, and the second computed a
square root in place. In tight loops over large arrays this avoids repeated allocations.
Aggregations and axis
import numpy as np
sales = np.array([[10, 20, 30],
[40, 50, 60]])
print(sales.sum())
print(sales.sum(axis=0))
print(sales.sum(axis=1))
print(sales.mean(axis=0, keepdims=True).shape)
print(np.cumsum([1, 2, 3, 4]))
print(np.argmax([88, 45, 91, 67]))
$ python aggregations.py
210
[50 70 90]
[ 60 150]
(1, 3)
[ 1 3 6 10]
2
axis=0 collapsed the two rows into one total per column; axis=1 collapsed the three columns
into one total per row. keepdims=True keeps the collapsed axis with length 1 — shape (1, 3)
— so the result still broadcasts against the original. argmax returns a position, not a value.
NaN-Aware Aggregations
import numpy as np
readings = np.array([21.5, np.nan, 23.0, 22.5])
print(readings.mean())
print(np.nanmean(readings))
print(np.isnan(readings).sum(), "missing")
$ python nan_aware.py
nan
22.333333333333332
1 missing
One missing value turned the plain mean into nan. np.nanmean ignores NaN entries and
averages the remaining three; there are matching nansum, nanstd, nanmax and so on.
Statistics
import numpy as np
rng = np.random.default_rng(42)
heights = rng.normal(loc=170, scale=10, size=1000)
print(f"mean {heights.mean():.2f} median {np.median(heights):.2f} std {heights.std():.2f}")
q1, q3 = np.percentile(heights, [25, 75])
print(f"IQR {q3 - q1:.2f}")
x = rng.standard_normal(100)
y = 0.8 * x + 0.5 * rng.standard_normal(100)
print(f"corr {np.corrcoef(x, y)[0, 1]:.3f}")
$ python stats.py
mean 169.71 median 170.06 std 9.89
IQR 12.86
corr 0.780
For normally distributed data the mean and median sit close together, and the IQR comes out
near the 1.35 standard deviations theory predicts (12.86 / 9.89 ≈ 1.30 for this sample). np.corrcoef returns a 2 × 2 matrix; the
off-diagonal [0, 1] entry is the correlation between x and y, strongly positive by
construction.
Sorting and Top-k
import numpy as np
arr = np.array([3, 1, 4, 1, 5, 9, 2, 6])
print(np.sort(arr))
print(np.argsort(arr))
top3 = np.argsort(arr)[-3:][::-1]
print(top3, arr[top3])
print(np.sort(np.array([[3, 1, 2], [6, 4, 5]]), axis=1))
$ python sorting.py
[1 1 2 3 4 5 6 9]
[1 3 6 0 2 4 7 5]
[5 7 4] [9 6 5]
[[1 2 3]
[4 5 6]]
argsort gives the positions that would sort the array. Its last three entries point at the
three largest values in ascending order, so [::-1] reverses them to largest-first. Note the
two 1s: their order in the argsort output follows the default quicksort, which is not
guaranteed stable — pass kind="stable" when ties must keep their original order.
Unique Values and Set Operations
import numpy as np
arr = np.array([1, 2, 3, 2, 1, 3, 3])
values, counts = np.unique(arr, return_counts=True)
print(values, counts)
a = np.array([1, 2, 3, 4, 5])
b = np.array([3, 4, 5, 6, 7])
print(np.intersect1d(a, b), np.union1d(a, b), np.setdiff1d(a, b))
print(np.isin(a, b))
print(np.any(a > 4), np.all(a > 3))
$ python sets.py
[1 2 3] [2 2 3]
[3 4 5] [1 2 3 4 5 6 7] [1 2]
[False False True True True]
True False
np.unique returns sorted distinct values and, with return_counts=True, how often each
appears. np.isin is the membership test — the older np.in1d name no longer exists in
NumPy 2.5, as the next example shows.
The Removed np.in1d
import numpy as np
try:
print(np.in1d([1, 2, 3], [2]))
except AttributeError as e:
print("AttributeError:", e)
print(np.isin([1, 2, 3], [2]))
$ python in1d_removed.py
AttributeError: module 'numpy' has no attribute 'in1d'
[False True False]
Code written for NumPy 1.x that calls np.in1d crashes on current NumPy. np.isin is the
drop-in replacement.
Practice
- Compute the per-row maximum and its column position for
[[4, 9, 2], [7, 1, 8]]. - Standardize each column of
[[1., 10.], [2., 20.], [3., 30.]]to mean 0 and std 1 usingkeepdims=True. - Find the two smallest values of
[8, 3, 9, 1, 7]with their original positions. - Average
[4.0, nan, 6.0, nan, 5.0], ignoring the missing values.
Solution 1
import numpy as np
m = np.array([[4, 9, 2], [7, 1, 8]])
print(m.max(axis=1), m.argmax(axis=1))
$ python practice_rowmax.py
[9 8] [1 2]
axis=1 collapses each row, so both calls return one entry per row.
Solution 2
import numpy as np
X = np.array([[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]])
Z = (X - X.mean(axis=0, keepdims=True)) / X.std(axis=0, keepdims=True)
print(Z.round(3))
print(Z.mean(axis=0), Z.std(axis=0))
$ python practice_standardize.py
[[-1.225 -1.225]
[ 0. 0. ]
[ 1.225 1.225]]
[0. 0.] [1. 1.]
Each column is standardized independently, so both come out identical even though the second column is ten times larger.
Solution 3
import numpy as np
arr = np.array([8, 3, 9, 1, 7])
idx = np.argsort(arr)[:2]
print(idx, arr[idx])
$ python practice_bottom2.py
[3 1] [1 3]
The first two argsort positions are the two smallest values, already in ascending order.
Solution 4
import numpy as np
print(np.nanmean(np.array([4.0, np.nan, 6.0, np.nan, 5.0])))
$ python practice_nanmean.py
5.0
The two NaNs are skipped, leaving the mean of 4, 6 and 5.