NumPy (Numerical Python) is the fundamental numerical-computing library in the Python data-analysis ecosystem. While basic NumPy focuses on creating arrays and performing simple operations, Advanced NumPy focuses on efficient computation, multidimensional data manipulation, broadcasting, vectorization, memory management, linear algebra, and statistical operations.
6.1.1Learning Objectives
By the end of this topic, you should be able to
- Work confidently with multidimensional NumPy arrays.
- Understand NumPy's memory model and array dimensions.
- Use broadcasting effectively.
- Perform vectorized calculations without Python loops.
- Apply advanced indexing and Boolean filtering.
- Reshape, transpose, split, stack, and combine arrays.
- Perform statistical calculations efficiently.
- Work with matrices and linear algebra.
- Generate random data for analysis and simulations.
- Understand views versus copies.
- Optimize numerical Python code.
6.1.2NumPy Array Fundamentals
A NumPy array is an object of type numpy.ndarray.
import numpy as np data = np.array([10, 20, 30, 40, 50]) print(data) print(type(data)) print(data.ndim) print(data.shape) print(data.size) print(data.dtype)
Output
<class 'numpy.ndarray'>
1
(5,)
5
int64
Important properties
| Property | Meaning |
|---|---|
| ndim | Number of dimensions |
| shape | Size of each dimension |
| size | Total number of elements |
| dtype | Data type |
| itemsize | Bytes occupied by one element |
| nbytes | Total memory occupied |
Example
data = np.array([
[10, 20, 30],])
print(data.ndim) print(data.shape) print(data.size) print(data.dtype) print(data.nbytes)
The shape is
(2, 3)
This means
- 2 rows
- 3 columns
- 6 total elements
6.1.3Multidimensional Arrays
NumPy supports arrays with multiple dimensions.
1D
a = np.array([10, 20, 30])
Shape
(3,)
2D
b = np.array([
[10, 20, 30],])
Shape
(2, 3)
3D
c = np.array([
[
[1, 2],],
[
[5, 6],
]
])
Shape
(2, 2, 2)
You can think of a 3D array as a collection of 2D tables.
6.1.4Array Data Types
NumPy supports specialized numerical data types.
a = np.array([1, 2, 3], dtype=np.int32) print(a.dtype)
Common types include
- int8
- int16
- int32
- int64
- float32
- float64
- bool
- complex64
- complex128
You can convert an array using astype()
prices = np.array([100, 200, 300]) prices_float = prices.astype(float) print(prices_float) print(prices_float.dtype)
Why is this important?
Data type affects
- memory usage
- computation speed
- precision
- compatibility with other libraries
For example
data = np.array([1, 2, 3, 4], dtype=np.int8)
uses significantly less memory than
data = np.array([1, 2, 3, 4], dtype=np.int64)
This becomes important when processing millions of records.
6.1.5Advanced Array Creation
NumPy provides several methods for creating arrays.
arange()
numbers = np.arange(1, 11) print(numbers)
Output
With a step
numbers = np.arange(0, 20, 2) print(numbers)
Output
linspace()
linspace() generates a specified number of equally spaced values.
numbers = np.linspace(0, 100, 11) print(numbers)
Output
This is especially useful for mathematical calculations and plotting.
zeros()
a = np.zeros((3, 4)) print(a)
Creates a 3 × 4 array containing zeros.
ones()
a = np.ones((2, 3)) print(a)
full()
a = np.full((3, 3), 100) print(a)
Output
Identity Matrix
identity = np.eye(4) print(identity)
Output
6.1.6Advanced Indexing
Basic indexing
data = np.array([10, 20, 30, 40, 50]) print(data[0]) print(data[-1])
Output
- 10
- 50
- Slicing
print(data[1:4])
Output
You can also use a step
print(data[::2])
Output
6.1.72D Array Indexing
data = np.array([
[10, 20, 30],
[40, 50, 60],])
Access a specific element
print(data[1, 2])
Output
- 60
- The first number represents the row and the second represents the column.
- data[row, column]
- Selecting a row
print(data[1])
Output
Selecting a column
print(data[:, 1])
Output
The : means "all rows".
6.1.8Boolean Indexing
Boolean indexing is one of the most useful NumPy techniques in data analysis.
sales = np.array([100, 250, 80, 450, 300]) high_sales = sales[sales > 200] print(high_sales)
Output
You can create the Boolean condition separately
condition = sales > 200 print(condition)
Output
Then
print(sales[condition])
Multiple Conditions
Use & for AND
sales = np.array([100, 250, 80, 450, 300]) result = sales[(sales > 200) & (sales < 400)] print(result)
Output
Use | for OR
result = sales[(sales < 100) | (sales > 400)] print(result)
Output
6.1.9Fancy Indexing
Fancy indexing allows you to select specific positions.
data = np.array([10, 20, 30, 40, 50]) indexes = [0, 2, 4] print(data[indexes])
Output
For a 2D array
data = np.array([
[10, 20, 30],
[40, 50, 60],])
rows = [0, 2] cols = [1, 2] print(data[rows, cols])
Output
6.1.10Broadcasting
Broadcasting allows NumPy to perform operations between arrays with compatible shapes.
Consider
prices = np.array([100, 200, 300]) discount = 10 result = prices - discount print(result)
Output
NumPy automatically applies 10 to every element.
This is broadcasting.
Broadcasting with 2D Arrays
sales = np.array([
[100, 200, 300],])
tax = np.array([10, 20, 30]) result = sales + tax print(result)
Output
The tax array is effectively applied to every row.
6.1.11Broadcasting Rules
When NumPy performs operations between arrays, dimensions are compared from right to left.
Two dimensions are compatible when
They are equal, or
One of them is 1.
For example
Array A: (3, 4)
Array B: (4,)
These are compatible.
Another example
Array A: (3, 1)
Array B: (1, 4)
These are also compatible.
The result becomes
(3, 4)
Understanding broadcasting is extremely important for efficient numerical programming.
6.1.12Vectorization
Vectorization means performing operations on entire arrays instead of processing elements individually using Python loops.
Traditional Python
numbers = [1, 2, 3, 4, 5]
result = []
for number in numbers:
result.append(number * 2)print(result)
NumPy
numbers = np.array([1, 2, 3, 4, 5]) result = numbers * 2 print(result)
Output
NumPy's approach is generally faster and more concise for numerical operations.
6.1.13Universal Functions — ufuncs
NumPy provides optimized functions called universal functions, or ufuncs.
Examples
numbers = np.array([1, 4, 9, 16]) print(np.sqrt(numbers)) print(np.square(numbers)) print(np.exp(numbers)) print(np.log(numbers))
Other useful functions include
np.abs() np.sin() np.cos() np.tan() np.ceil() np.floor() np.round()
Example
prices = np.array([100.4, 200.6, 300.2]) print(np.round(prices))
Output
6.1.14Aggregation Functions
NumPy provides efficient statistical functions.
sales = np.array([100, 200, 300, 400, 500]) print(np.sum(sales)) print(np.mean(sales)) print(np.median(sales)) print(np.min(sales)) print(np.max(sales)) print(np.std(sales)) print(np.var(sales))
Common functions
| Function | Purpose |
|---|---|
| np.sum() | Total |
| np.mean() | Average |
| np.median() | Median |
| np.min() | Minimum |
| np.max() | Maximum |
| np.std() | Standard deviation |
| np.var() | Variance |
| np.percentile() | Percentile |
6.1.15Axis Operations
Consider
sales = np.array([
[100, 200, 300],])
Sum of all values
print(np.sum(sales))
Output
2100
Sum by column
print(np.sum(sales, axis=0))
Output
Sum by row
print(np.sum(sales, axis=1))
Output
A useful way to remember
axis=0 → operate down rows → result for each column axis=1 → operate across columns → result for each row
6.1.16Reshaping Arrays
Suppose
data = np.arange(1, 13) print(data)
Output
Convert it into a 3 × 4 matrix
matrix = data.reshape(3, 4) print(matrix)
Output
You can use -1 when NumPy should calculate one dimension
matrix = data.reshape(3, -1) print(matrix)
6.1.17Flattening Arrays
Convert a multidimensional array into one dimension.
data = np.array([
[1, 2, 3],])
flat = data.flatten() print(flat)
Output
Another option
flat = data.ravel()
There is an important difference
flatten() generally returns a copy.
ravel() attempts to return a view where possible.
6.1.18Transpose
Transpose converts rows into columns.
data = np.array([
[1, 2, 3],])
print(data.T)
Output
You can also use
np.transpose(data)
6.1.19Stacking Arrays
Vertical stacking
a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) result = np.vstack((a, b)) print(result)
Output
Horizontal stacking
result = np.hstack((a, b)) print(result)
Output
For multidimensional arrays
a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6], [7, 8]]) result = np.vstack((a, b)) print(result)
6.1.20Concatenation
a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) result = np.concatenate((a, b)) print(result)
Output
For 2D arrays
a = np.array([
[1, 2],])
b = np.array([
[5, 6],])
result = np.concatenate((a, b), axis=0) print(result)
6.1.21Splitting Arrays
data = np.arange(1, 10) parts = np.split(data, 3) for part in parts: print(part)
Output
Other functions
np.hsplit() np.vsplit() np.array_split()
array_split() is useful when the array cannot be divided equally.
6.1.22Sorting
data = np.array([50, 10, 40, 20, 30]) print(np.sort(data))
Output
To get the indexes that would sort the array
indexes = np.argsort(data) print(indexes)
This is especially useful when working with related arrays.
6.1.23Searching
Find the index of the maximum value
sales = np.array([100, 500, 200, 800, 300]) print(np.argmax(sales))
Output
3
Find the minimum
print(np.argmin(sales))
Find all values satisfying a condition
indexes = np.where(sales > 300) print(indexes)
Output
(array([1, 3]),)
6.1.24np.where()
np.where() is extremely useful for conditional transformations. sales = np.array([100, 500, 200, 800, 300]) status = np.where(sales >= 500, "High", "Low") print(status)
Output
This is similar to a SQL CASE WHEN.
For example
CASE
- WHEN sales >= 500 THEN 'High'
- ELSE 'Low'
- END
This makes NumPy particularly useful for people working with SQL and analytics.
6.1.25Unique Values
customers = np.array([
"A", "B", "A", "C", "B", "D"
])print(np.unique(customers))
Output
You can also get counts
values, counts = np.unique(
customers,
return_counts=True
)
print(values) print(counts)
Output
6.1.26Views vs Copies
This is an important advanced NumPy concept.
Consider
data = np.array([10, 20, 30, 40]) view = data[1:3]
view[0] = 999
print(data)
The original array may now contain
because slicing generally produces a view into the original array.
To create an independent copy
copy_data = data[1:3].copy()
copy_data[0] = 1000
Now changes to copy_data do not modify the original array.
Key distinction
View → shares underlying data
Copy → independent data
This is important when working with large datasets because unnecessary copies can consume significant memory.
6.1.27Memory Layout
NumPy arrays store data efficiently in memory.
You can inspect memory information
data = np.array([1, 2, 3, 4, 5]) print(data.itemsize) print(data.nbytes)
For example, if each integer occupies 8 bytes
itemsize = 8 nbytes = 40
For large-scale analytics, memory-efficient data types can make a significant difference.
6.1.28Linear Algebra
NumPy provides a dedicated linear algebra module
np.linalg
Matrix multiplication
A = np.array([
[1, 2],])
B = np.array([
[5, 6],])
result = A @ B print(result)
Output
You can also use
np.matmul(A, B)
Matrix inverse
A = np.array([
[1, 2],])
inverse = np.linalg.inv(A) print(inverse)
Determinant
det = np.linalg.det(A) print(det)
Eigenvalues
values, vectors = np.linalg.eig(A)
print(values) print(vectors)
Linear algebra is particularly important in machine learning, statistics, optimization, and scientific computing.
6.1.29Random Number Generation
NumPy provides a modern random-number API.
rng = np.random.default_rng(42) numbers = rng.integers(1, 101, size=10) print(numbers)
The 42 is a seed, which makes the result reproducible.
Random floating-point numbers
numbers = rng.random(5) print(numbers)
Normal distribution
values = rng.normal(
loc=100,
scale=15,
size=1000
)Here
loc = mean scale = standard deviation size = number of observations
6.1.30Statistical Analysis
Consider a sales dataset
sales = np.array([
1200, 1500, 1800, 1100,
2200, 2500, 1700, 1900
])Calculate statistics
print("Mean:", np.mean(sales))
print("Median:", np.median(sales))
print("Minimum:", np.min(sales))
print("Maximum:", np.max(sales))
print("Standard Deviation:", np.std(sales))Percentiles
print(np.percentile(sales, 25)) print(np.percentile(sales, 50)) print(np.percentile(sales, 75))
These statistics are frequently used during Exploratory Data Analysis (EDA).
6.1.31Handling NaN Values
NumPy also provides functions specifically for missing numerical values represented as NaN.
data = np.array([
10,
20,
np.nan,
40,
50
])Regular mean
print(np.mean(data))
This produces nan.
Instead
print(np.nanmean(data))
Output
30.0
Other useful functions
np.nansum() np.nanmean() np.nanmedian() np.nanmin() np.nanmax() np.nanstd()
6.1.32Practical Example — Sales Analysis
Suppose a company has monthly sales
sales = np.array([
[12000, 15000, 14000, 16000],
[18000, 21000, 19000, 22000],])
Each row represents a region and each column represents a month.
Total sales
total_sales = np.sum(sales) print(total_sales)
Sales by region
region_sales = np.sum(sales, axis=1) print(region_sales)
Sales by month
monthly_sales = np.sum(sales, axis=0) print(monthly_sales)
Average monthly sales
average_sales = np.mean(sales, axis=0) print(average_sales)
Highest-performing region
best_region = np.argmax(region_sales) print(best_region)
This demonstrates how NumPy can perform large-scale calculations without writing explicit loops.
6.1.33NumPy vs Python Lists
Consider
numbers = [1, 2, 3, 4, 5]
Python lists are general-purpose containers.
NumPy arrays are optimized for numerical computation.
| Feature | Python List | NumPy Array |
|---|---|---|
| Numerical operations | Limited | Excellent |
| Vectorization | No | Yes |
| Broadcasting | No | Yes |
| Multidimensional data | Limited | Excellent |
| Mathematical functions | Manual | Built-in |
| Memory efficiency | Lower | Higher |
| Numerical performance | Lower | Higher |
| Linear algebra | Not built-in | Built-in |
6.1.34NumPy and SQL Comparison
Since data analysts frequently work with SQL, several NumPy concepts can be related to SQL.
| SQL | NumPy |
|---|---|
| WHERE sales > 500 | sales[sales > 500] |
| CASE WHEN | np.where() |
| SUM() | np.sum() |
| AVG() | np.mean() |
| MIN() | np.min() |
| MAX() | np.max() |
| ORDER BY | np.sort() |
| Position of maximum | np.argmax() |
| Distinct values | np.unique() |
For example, SQL
SELECT * FROM sales
WHERE amount > 500;
Equivalent NumPy filtering
result = sales[sales > 500]
This comparison helps bridge the gap between SQL-based analytics and Python-based analytics.
6.1.35Performance: Loop vs Vectorization
Consider
numbers = np.arange(1_000_000)
A Python loop
result = []
for number in numbers:
result.append(number * 2)Vectorized NumPy
result = numbers * 2
The second approach is generally preferable because the operation is performed using NumPy's optimized numerical machinery.
General principle
Instead of
for row in data:
calculate(row)look for opportunities to perform
result = operation(data)
This is one of the most important habits in effective NumPy programming.
6.1.36Common NumPy Mistakes
Mistake 1 — Using and instead of &
Incorrect
data[(data > 10) and (data < 50)]
Correct
data[(data > 10) & (data < 50)]
Mistake 2 — Forgetting parentheses
Incorrect
data > 10 & data < 50
Correct
(data > 10) & (data < 50)
Mistake 3 — Confusing shape
a = np.array([1, 2, 3]) print(a.shape)
Output
(3,)
This is not
(1, 3)
If you need a row vector
a.reshape(1, 3)
Mistake 4 — Unexpected modification through views
subset = data[1:4]
Remember that slicing can create a view.
Use
subset = data[1:4].copy()
when an independent array is required.
6.1.37Mini Project — Regional Sales Analysis
Dataset
import numpy as np
sales = np.array([
[12000, 15000, 14000, 16000, 18000, 19000],
[18000, 21000, 19000, 22000, 23000, 25000],
[10000, 12000, 13000, 14000, 15000, 16000],])
Assume
Rows → Regions
- Columns → Months
- Tasks
- Task 1: Calculate total company sales.
total_sales = np.sum(sales)
Task 2: Calculate total sales for each region.
region_sales = np.sum(sales, axis=1)
Task 3: Calculate sales for each month.
monthly_sales = np.sum(sales, axis=0)
Task 4: Find the highest-performing region.
best_region = np.argmax(region_sales)
Task 5: Find the month with the highest sales.
best_month = np.argmax(monthly_sales)
Task 6: Calculate average sales.
average_sales = np.mean(sales)
Task 7: Find all sales values greater than ₹20,000.
high_sales = sales[sales > 20000]
6.1.38Interview Questions
- Beginner/Intermediate
- What is NumPy?
- What is an ndarray?
- What is the difference between shape, size, and ndim?
- What is vectorization?
- What is broadcasting?
- What is Boolean indexing?
- What is fancy indexing?
- What is the difference between reshape() and resize()?
- What is the difference between flatten() and ravel()?
- What does axis=0 mean?
Advanced
- What are NumPy views and copies?
- How does broadcasting work?
- Why is NumPy faster than Python loops?
- What are ufuncs?
- What is the difference between np.array() and Python lists?
- What is np.where()?
- What does np.argsort() return?
- How do you handle NaN values in NumPy?
- How can you reduce NumPy memory consumption?
- What is the difference between @, np.dot(), and np.matmul()?
- How does NumPy store multidimensional arrays?
- What are strides?
- How does NumPy avoid unnecessary data copies?
- How would you optimize a NumPy calculation involving millions of records?
Explain broadcasting with an example.
6.1.39Key Takeaways
The most important concepts to master are
For data analysis, the five concepts you should become especially strong at are Boolean indexing, broadcasting, vectorization, axis-based aggregation, and views vs copies. These form the foundation for advanced Pandas and efficient Python data-processing workflows.