All 57 lessons

Lesson 27 of 57 · NumPy for Analysis

NumPy Arrays: Fast Numerical Computing in Python

By Skillancy Editorial Team

Run the code in the free Python Compiler or on your own computer. Lessons that read files use the FreshBasket practice data: download the dataset (ZIP).

Python lists are flexible but slow for numbers. If you have six thousand delivery times and want to subtract the mean from each, a list needs a Python-level loop that handles one value at a time, and every value is a full Python object with its own overhead. NumPy (Numerical Python) stores numbers in a compact block of memory and performs operations on the whole block in fast compiled code. The result is code that is shorter to write and often 50 to 100 times faster to run.

NumPy is also the foundation of the entire Python data stack. A pandas column is built on a NumPy array. Matplotlib plots arrays. Scikit-learn trains models on arrays. Learning NumPy first means that when you reach pandas, the behaviour of its columns (vectorised operations, boolean filtering, NaN handling) will already feel familiar. In this lesson you will build arrays, learn their key properties, index and slice them, and use boolean masks to select data.

Worked examples

Example 1: From a list to an array

Question: Convert a list of delivery times into an array and inspect it.

import numpy as np

minutes_list = [12, 35, 18, 41, 9, 27]
minutes = np.array(minutes_list)

print(minutes)
print(type(minutes).__name__, minutes.dtype, minutes.shape, minutes.ndim, minutes.size)
print(minutes.mean(), minutes.max(), minutes.min(), minutes.sum())
print(np.array([1, 2.5, 3]).dtype, np.array([1, "a"]).dtype)

Output

[12 35 18 41  9 27]
ndarray int64 (6,) 1 6
23.666666666666668 41 9 142
float64 <U21

How it works: np.array builds an array from any sequence. Every array has a fixed dtype (data type); NumPy picks one that fits all values, so mixing integers and a float gives float64, and mixing numbers with text gives a string type. shape is a tuple of dimension sizes, ndim the number of dimensions, and size the total element count. Arrays carry methods such as mean, max and sum.

Reading the result: The array has shape (6,) and dtype int64, with an average of 23.67 minutes and a maximum of 41. Unlike a list, all elements share one type.

Example 2: Creating arrays

Question: What are the common ways to make arrays without typing values?

print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))
print(np.zeros(4), np.ones((2, 3)))
print(np.full((2, 2), 99))
print(np.eye(3, dtype=int))

rng = np.random.default_rng(42)
print(rng.integers(1, 7, size=8))
print(rng.normal(loc=25, scale=5, size=4).round(1))
print(np.arange(1, 13).reshape(3, 4))

Output

[0 2 4 6 8]
[0.   0.25 0.5  0.75 1.  ]
[0. 0. 0. 0.] [[1. 1. 1.]
 [1. 1. 1.]]
[[99 99]
 [99 99]]
[[1 0 0]
 [0 1 0]
 [0 0 1]]
[1 5 4 3 3 6 1 5]
[15.2 18.5 25.6 23.4]
[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]

How it works: arange(start, stop, step) is the array version of range and accepts floats. linspace(a, b, n) gives n evenly spaced points including both ends, which is handy for plotting. zeros, ones, full and eye build arrays of a given shape. The modern random API starts with default_rng(seed), and the seed makes results repeatable. reshape rearranges the same data into a new shape.

Reading the result: The reshape produced a grid with shape (3, 4) from 12 numbers. Passing a tuple such as (2, 3) creates two dimensions.

Example 3: Indexing and slicing

Question: Pick single values and ranges from an array.

a = np.array([10, 20, 30, 40, 50, 60, 70])

print(a[0], a[-1], a[2])
print(a[1:4])
print(a[::2])
print(a[::-1])
print(a[[0, 3, 5]])

b = a[1:4]
b[0] = 999
print(a)

c = a[[0, 1]].copy()
c[0] = -1
print(a[0])

Output

10 70 30
[20 30 40]
[10 30 50 70]
[70 60 50 40 30 20 10]
[10 40 60]
[ 10 999  30  40  50  60  70]
10

How it works: Indexing and slicing follow the list rules (zero-based, negative from the end, start:stop:step). Passing a list of positions ("fancy indexing") picks those elements. One crucial difference from lists: a basic slice is a view of the same memory, not a copy, so changing b changed a. Use .copy() when you need independence.

Reading the result: After the slice assignment, a became [ 10 999 30 40 50 60 70], because b shared memory with it. The copy did not affect a, which still starts with 10.

Example 4: Two-dimensional arrays

Question: Store the orders of three stores over four weeks and index the grid.

weekly = np.array([
    [310, 295, 330, 342],
    [220, 245, 238, 260],
    [405, 390, 421, 433],
])

print(weekly.shape)
print(weekly[1, 2])
print(weekly[0])
print(weekly[:, 3])
print(weekly[1:, :2])

print(weekly.sum(axis=0))
print(weekly.sum(axis=1))
print(weekly.mean().round(1), weekly.mean(axis=1).round(1))

Output

(3, 4)
238
[310 295 330 342]
[342 260 433]
[[220 245]
 [405 390]]
[ 935  930  989 1035]
[1277  963 1649]
324.1 [319.2 240.8 412.2]

How it works: A 2-D array is indexed as [row, column]. A bare colon means "all". The axis argument says along which dimension to combine: axis=0 collapses the rows (giving one result per column), and axis=1 collapses the columns (one result per row). Think of axis as the dimension that disappears.

Reading the result: The three stores sold [1277 963 1649] orders in total, and the four weeks had totals of [ 935 930 989 1035]. Store 3's average week is 412.2 orders.

Example 5: Vectorised arithmetic

Question: Apply a calculation to every element without a loop.

prices = np.array([68, 45, 96, 62, 120])
qty = np.array([2, 1, 3, 1, 2])

print(prices * 1.05)
print(prices * qty)
print((prices * qty).sum())
print(prices > 60)
print(np.sqrt(prices).round(2))
print(prices + np.array([1, 2, 3, 4, 5]))
print(prices // 10, prices % 10)

Output

[ 71.4   47.25 100.8   65.1  126.  ]
[136  45 288  62 240]
771
[ True False  True  True  True]
[ 8.25  6.71  9.8   7.87 10.95]
[ 69  47  99  66 125]
[ 6  4  9  6 12] [8 5 6 2 0]

How it works: Arithmetic operators work element by element on arrays of the same shape, and a single number is applied to all elements. This is called vectorisation. Comparison operators return boolean arrays. Mathematical functions such as np.sqrt, np.log and np.exp also work on whole arrays.

Reading the result: The basket's line totals are [136 45 288 62 240] with a grand total of ₹771. Comparing with 60 gave a boolean array: [ True False True True True].

Example 6: Lists versus arrays: speed

Question: Just how much faster is NumPy?

import time

big = list(range(2_000_000))
arr = np.arange(2_000_000)

t0 = time.perf_counter()
doubled_list = [x * 2 for x in big]
t_list = time.perf_counter() - t0

t0 = time.perf_counter()
doubled_arr = arr * 2
t_arr = time.perf_counter() - t0

print(doubled_list[:3], doubled_arr[:3])
print(t_arr < t_list)
print(arr.nbytes / 1e6, "MB")

Output

[0, 2, 4] [0 2 4]
True
16.0 MB

How it works: The list comprehension runs Python bytecode for two million iterations, while arr * 2 runs one tight loop in compiled C. The array also stores raw 8-byte integers rather than full Python objects.

Reading the result: The array version was faster (True); on a typical machine it is 20 to 100 times faster. The array occupies 16 MB. Remember this rule: avoid Python loops over arrays.

Example 7: Boolean masks

Question: Select deliveries that were late.

minutes = np.array([12, 35, 18, 41, 9, 27, 33, 50])

late = minutes > 30
print(late)
print(minutes[late])
print(late.sum(), late.mean())
print(minutes[(minutes > 15) & (minutes < 35)])
print(minutes[(minutes < 10) | (minutes > 45)])
print(minutes[~late])
print(np.where(minutes > 30, "late", "ok"))

Output

[False  True False  True False False  True  True]
[35 41 33 50]
4 0.5
[18 27 33]
[ 9 50]
[12 18  9 27]
['ok' 'late' 'ok' 'late' 'ok' 'ok' 'late' 'late']

How it works: Putting a boolean array inside the brackets selects the elements where it is True. Combine conditions with & (and), | (or) and ~ (not), always wrapping each condition in brackets because of operator precedence; the Python keywords and and or do not work on arrays. Summing booleans counts the True values, and their mean is the proportion. np.where(cond, a, b) chooses between two values per element.

Reading the result: 4 of 8 deliveries were late (50.0%), and those times were [35 41 33 50]. This mask idea becomes df[df["col"] > 30] in pandas.

Example 8: Missing values

Question: What happens when data contains blanks?

times = np.array([12.0, 35.0, np.nan, 41.0, np.nan, 27.0])

print(times.mean())
print(np.nanmean(times), np.nanmax(times))
print(np.isnan(times))
print(times[~np.isnan(times)])
print(np.nan == np.nan)
print(np.nan_to_num(times, nan=0.0))

Output

nan
28.75 41.0
[False False  True False  True False]
[12. 35. 41. 27.]
False
[12. 35.  0. 41.  0. 27.]

How it works: np.nan ("not a number") is a special float that marks missing data. Any calculation involving it returns nan, so use the nan-aware versions (nanmean, nansum, nanmax). Test for it with np.isnan, never with ==, because nan is not even equal to itself. Cancelled FreshBasket orders have a blank delivery time, which pandas loads as NaN.

Reading the result: The plain mean is nan, but the nan-aware mean is 28.75 using the 4 real values.

Example 9: Real order data

Question: Load the delivery times into an array and analyse them.

import csv

raw = []
with open("orders.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        raw.append(float(row["delivery_minutes"]) if row["delivery_minutes"] else np.nan)

dm = np.array(raw)
print(dm.shape, np.isnan(dm).sum())

valid = dm[~np.isnan(dm)]
print(valid.size, valid.mean().round(2), np.median(valid), valid.std().round(2))
print(np.percentile(valid, [10, 50, 90]))
print((valid > 45).sum(), valid.min(), valid.max())

Output

(6000,) 370
5630 23.67 24.0 6.93
[14. 24. 32.]
7 8.0 54.0

How it works: We converted blanks to nan while reading, then filtered them out with a mask. NumPy provides median, std and percentile directly. Pandas will do the reading and the blank handling for you, but the underlying array is the same.

Reading the result: There are 6,000 orders of which 370 have no delivery time (cancelled), leaving 5,630 deliveries with a mean of 23.7 minutes. The 90th percentile is 32 minutes: nine in ten deliveries arrive within that time.

Example 10: Reshaping and combining

Question: Rearrange and join arrays.

a = np.arange(6)
print(a.reshape(2, 3))
print(a.reshape(3, -1).shape)
print(a.reshape(2, 3).T)
print(a.reshape(2, 3).flatten())

x = np.array([1, 2, 3])
y = np.array([4, 5, 6])
print(np.concatenate([x, y]))
print(np.vstack([x, y]))
print(np.column_stack([x, y]))
print(np.sort(np.array([3, 1, 2]))[::-1], np.argsort(np.array([30, 10, 20])))
print(np.unique(np.array([3, 1, 3, 2, 1]), return_counts=True))

Output

[[0 1 2]
 [3 4 5]]
(3, 2)
[[0 3]
 [1 4]
 [2 5]]
[0 1 2 3 4 5]
[1 2 3 4 5 6]
[[1 2 3]
 [4 5 6]]
[[1 4]
 [2 5]
 [3 6]]
[3 2 1] [1 2 0]
(array([1, 2, 3]), array([2, 1, 2]))

How it works: reshape changes the shape without changing the data; -1 means "work it out". .T transposes rows and columns, and flatten returns one dimension. vstack stacks arrays as rows, column_stack as columns. argsort returns the positions that would sort the array, and unique with return_counts=True is a quick frequency table.

Reading the result: The six numbers reshaped to (2, 3), and unique-with-counts told us the values [1 2 3] occur [2 1 2] times respectively.

Common mistakes

1. Using and/or instead of &/| on arrays.

x = np.array([1, 5, 9])
x[(x > 2) and (x < 8)]

Output

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    x[(x > 2) and (x < 8)]
      ^^^^^^^^^^^^^^^^^^^
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

2. Forgetting the brackets around conditions: x > 2 & x < 8 is evaluated in the wrong order.

3. Treating a slice as a copy and being surprised when the original changes.

4. Comparing with == np.nan instead of np.isnan.

5. Looping over arrays with for when a vectorised expression exists.

Practice questions

Practice 1 (Easy): Discounted prices

Given prices [250, 499, 999, 1299], compute the prices after a 12% discount, rounded to 2 decimals.

Show solution
prices = np.array([250, 499, 999, 1299])
print((prices * 0.88).round(2))

Output

[ 220.    439.12  879.12 1143.12]

One expression handles all four values: [ 220. 439.12 879.12 1143.12].

Practice 2 (Medium): Normalise delivery times

Create the z-score of the valid delivery times, (x - mean) / std, and count how many have a z-score above 2.

Show solution
z = (valid - valid.mean()) / valid.std()
print(z.round(2)[:5])
print((z > 2).sum(), (np.abs(z) > 2).sum())

Output

[ 1.49  0.05  1.92  0.77 -0.53]
146 265

Subtracting the mean and dividing by the standard deviation expresses each time in standard-deviation units. 146 deliveries were unusually slow (z above 2), and 265 were unusual in either direction.

Practice 3 (Hard): Weekly growth

Using weekly from Example 4, compute the week-over-week percentage growth for each store (a 3×3 array) and print the average growth per store.

Show solution
growth = (weekly[:, 1:] - weekly[:, :-1]) / weekly[:, :-1] * 100
print(growth.round(1))
print(growth.mean(axis=1).round(2))

Output

[[-4.8 11.9  3.6]
 [11.4 -2.9  9.2]
 [-3.7  7.9  2.9]]
[3.55 5.92 2.37]

Slicing the columns 1: and :-1 lines up each week with the previous one, so the subtraction happens for all stores at once. The first store grew 3.55% a week on average.

Key takeaways

  • NumPy arrays store one dtype in compact memory and run vectorised operations quickly.
  • Know the array's shape, ndim, size and dtype.
  • Indexing follows list rules, and slices are views, not copies.
  • Use axis to summarise rows or columns.
  • Boolean masks with &, | and ~ select data; np.where chooses values.
  • np.nan marks missing data: use isnan and the nan-aware functions.
  • Avoid Python loops over arrays: vectorise instead.

Frequently asked questions

Why are NumPy arrays faster than lists?

They hold raw numbers of one type in contiguous memory and run operations in compiled C, avoiding Python's per-element overhead.

What is the difference between a view and a copy?

A view shares memory with the original (basic slices produce views), so edits show up in both. A copy is independent. Fancy indexing and boolean masks produce copies.

What does axis mean?

It is the dimension along which an operation runs. For a 2-D array, axis=0 goes down the rows (one answer per column) and axis=1 goes across the columns (one answer per row).

What is broadcasting?

The rule that lets arrays of different shapes work together, such as adding a single number to a whole array or a row to every row of a grid. The next lesson covers it.

Should I use NumPy or pandas?

Use pandas for labelled tables and mixed types, and NumPy for pure numerical arrays and maths. In practice you will use both together, since pandas columns are NumPy arrays.

What are dtypes for?

They control how much memory a value takes and how it behaves (int64, float32, bool, datetime64, object). Choosing smaller dtypes can cut memory use for huge datasets.

Newsletter

Keep learning Python

One email a week with Python and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Ready to practise? Try the Python interview questions