Welcome to your first lesson in the DS 215: Introduction to Data Science course. We're starting with the absolute cornerstone of numerical and scientific computing in Python: the NumPy library.
This lesson is the first step in building your data scientist's toolkit. Our goal is to master the manipulation of N-dimensional arrays for mathematical operations using NumPy. We'll explore why NumPy is fundamental for performance, how its core object—the ndarray—is structured for efficiency, and how to use it for creating, manipulating, and performing calculations on data.
Given your background in mechanical engineering and your interests in mathematics and physics, you'll find NumPy to be an incredibly powerful tool. It provides a highly optimized environment for the kind of vector and matrix operations that are common in physical sciences and engineering, all within the flexible Python ecosystem.
1. Why Not Just Use Python Lists?
Before we dive into NumPy, it's essential to understand why it's the preferred library for numerical data over Python's built-in lists. While lists are flexible, they are not optimized for large-scale numerical operations. NumPy arrays offer three key advantages:
- Speed: NumPy operations are executed by pre-compiled C code, making them significantly faster than operations on Python lists, which require slow Python loops.
- Memory Efficiency: NumPy arrays are homogeneous (all elements are of the same type), which allows for a more compact memory layout than Python lists that need to store type information for each element.
- Convenience: NumPy enables you to perform operations on entire arrays at once. This is called vectorization, and it allows for cleaner, more readable code without explicit loops.
To see a practical comparison, the following video provides a clear demonstration.
NumPy Full Course (2025) | NumPy Python Tutorial For Beginners | Learn NumPy in 2 Hours |Intellipaat
This video from Intellipaat provides a concise comparison of NumPy arrays and Python lists, focusing on speed, memory usage, and the convenience of direct operations.
Watch the first segment of the video, from the beginning to 05:27, which contrasts Python lists with NumPy arrays and highlights the key advantages of using NumPy.
2. The Anatomy of a NumPy Array
The heart of NumPy is the ndarray, or N-dimensional array. It's a grid of values, all of the same type. To use it effectively, we need to understand its core attributes.

The most important attributes of an ndarray are:
ndarray.ndim: The number of axes (dimensions) of the array.ndarray.shape: A tuple of integers indicating the size of the array in each dimension. For a matrix withnrows andmcolumns,shapewill be(n, m).ndarray.size: The total number of elements in the array.ndarray.dtype: An object describing the data type of the elements (e.g.,int64,float64).ndarray.itemsize: The size in bytes of each element.
The following quickstart guide from the official NumPy documentation provides a concise summary of these attributes.
Let's read the 'The basics' section from the NumPy quickstart guide. It introduces the ndarray object and its key attributes with a clear example.
Read the section titled 'The basics'. Focus on the descriptions of ndarray.ndim, ndarray.shape, ndarray.size, ndarray.dtype, and ndarray.itemsize.
Under the Hood: The Strided Memory Model
One of the secrets to NumPy's efficiency is how it handles data in memory. An ndarray is essentially a block of contiguous memory, and NumPy uses a concept called strides to know how to "step" through that memory to find the elements of the array.

This "view" mechanism is powerful. When you slice an array, you are typically creating a new view of the original data. Modifying the view will modify the original array. This is a critical feature that saves memory and boosts performance, but also one to be mindful of to avoid unintended side effects.
For a deeper dive into this concept, the following paper provides an excellent explanation.
The NumPy array: a structure for efficient numerical computation
This paper, 'The NumPy array: a structure for efficient numerical computation', gives a fantastic, high-level overview of the ndarray's structure, including the strided memory model.
Read Section 1, 'The structure of a NumPy array: a view on memory'. This will solidify your understanding of how strides allow for efficient operations like slicing and transposing by creating views instead of copies.
3. Creating and Populating Arrays
Now let's get practical. There are many ways to create NumPy arrays, depending on your needs. The video below provides a hands-on tutorial.
NumPy Tutorial: For Physicists, Engineers, and Mathematicians
This tutorial by Mr. P Solver is geared towards a technical audience and efficiently demonstrates the most common array creation methods.
Watch the section from 00:41 to 03:40. Pay attention to the different functions used to create arrays: np.array, np.zeros, np.ones, np.random.random, np.linspace, and np.arange.
Here is a summary of the most important creation functions:
| Function | Description | Example |
|---|---|---|
np.array() |
Create an array from a Python list or tuple. | np.array([1, 2, 3]) |
np.zeros() |
Create an array filled with zeros. | np.zeros((2, 3)) |
np.ones() |
Create an array filled with ones. | np.ones((2, 3)) |
np.arange() |
Create an array with evenly spaced values within a given interval (step-based). | np.arange(0, 10, 2) |
np.linspace() |
Create an array with evenly spaced numbers over a specified interval (number-based). | np.linspace(0, 1, 5) |
np.random.rand() |
Create an array of a given shape and populate it with random samples from a uniform distribution over [0, 1). |
np.random.rand(2, 3) |
np.random.randn() |
Create an array of a given shape and populate it with random samples from a standard normal distribution. | np.random.randn(2, 3) |
np.random.randint() |
Create an array of a given shape and populate it with random integers from a given range. | np.random.randint(0, 10, size=(2, 3)) |
4. Mathematical Operations on Arrays
This is where NumPy's power truly shines.
Vectorization: Element-wise Operations
Instead of looping through array elements, you can apply mathematical operations to the entire array at once.
import numpy as np
# Create an array
x = np.array([1, 2, 3, 4])
# Basic arithmetic
print(x + 5) # Adds 5 to each element
# Output: [ 6 7 8 9]
print(x * 2) # Multiplies each element by 2
# Output: [2 4 6 8]
# Operations between two arrays
y = np.array([10, 20, 30, 40])
print(y - x)
# Output: [ 9 18 27 36]
NumPy also provides a rich library of universal functions (ufuncs) that operate element-wise, such as np.sin(), np.sqrt(), and np.exp().
print(np.sqrt(x))
# Output: [1. 1.41421356 1.73205081 2. ]
Broadcasting
Broadcasting is a powerful mechanism that allows NumPy to work with arrays of different shapes when performing arithmetic operations. Subject to certain constraints, the smaller array is "broadcast" across the larger array so that they have compatible shapes.
The simplest example is operating between an array and a scalar, as we saw with x + 5. The scalar 5 was effectively stretched into an array [5, 5, 5, 5] to match the shape of x.
A more complex example is adding a 1D array to a 2D array:
matrix = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
vector = np.array([10, 20, 30])
# Add the vector to each row of the matrix
result = matrix + vector
print(result)
# Output:
# [[11 22 33]
# [14 25 36]
# [17 28 39]]
Here, vector was broadcast across the rows of matrix. Broadcasting avoids the need to create explicit copies of data, making it both memory-efficient and fast.
Linear Algebra
For linear algebra, NumPy distinguishes between element-wise multiplication (*) and matrix multiplication, which uses the @ operator or the .dot() method.
A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
# Element-wise product
print(A * B)
# Output:
# [[ 5 12]
# [21 32]]
# Matrix product
print(A @ B)
# Output:
# [[19 22]
# [43 50]]
5. Accessing and Manipulating Array Data
Being able to create and operate on arrays is useful, but often you need to select, slice, and reshape your data.
Indexing and Slicing
- Indexing works similarly to Python lists for 1D arrays (
arr[i]). For multi-dimensional arrays, you provide a tuple of indices:arr[row, column]. - Slicing allows you to select subarrays. For example,
arr[0:2, 1:3]selects the first two rows and columns 1 and 2. Remember that slices create views, not copies. - Boolean Indexing lets you select elements based on a condition. This is extremely powerful for filtering data.
- Fancy Indexing lets you use an array of indices to select elements.
The following video provides an excellent visual demonstration of these concepts, including a more complex example of boolean indexing.
NumPy Tutorial: For Physicists, Engineers, and Mathematicians
Let's return to the Mr. P Solver tutorial to see indexing and slicing in action. This section covers basic, boolean, and more advanced indexing techniques.
Watch the segment from 04:27 to 15:19. Focus on how to use square brackets [] for basic indexing, colons : for slicing, and how to use a boolean array to filter elements.
Reshaping and Transposing
You can change an array's shape without changing its data.
arr.reshape(new_shape)returns a new array (as a view if possible) with a different shape.arr.Treturns the transpose of the array.arr.ravel()andarr.flatten()convert a multi-dimensional array into a 1D array.ravel()returns a view if possible, whileflatten()always returns a copy.
6. Axis-based and Aggregate Operations
NumPy provides functions to perform calculations across the entire array or along a specific axis.
- Aggregate functions like
arr.sum(),arr.mean(),arr.std(),arr.min(), andarr.max()compute a single value from the array. - Axis-based operations allow you to perform these aggregations along a specific dimension (e.g., row-wise or column-wise). In a 2D array,
axis=0refers to columns, andaxis=1refers to rows.
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
# Sum of all elements
print(matrix.sum()) # Output: 21
# Sum along columns (axis=0)
print(matrix.sum(axis=0)) # Output: [5 7 9]
# Sum along rows (axis=1)
print(matrix.sum(axis=1)) # Output: [ 6 15]
Self-Test and Practice
Now it's time to apply what you've learned. Try to solve the following problems using NumPy.
- Create a 1D NumPy array containing all integers from 10 to 50.
- Create a 5x5 matrix where the values on the border are 1 and the values inside are 0.
- Create a 3x3 identity matrix and then find its determinant and inverse using
np.linalg.det()andnp.linalg.inv(). - Generate a 10x10 array of random integers between 1 and 100.
- Find the minimum and maximum values in the array.
- Replace all values greater than 50 with the value 50.
- Normalize the array: subtract the mean of the array from each element and then divide by the standard deviation.
- Challenge: Create a 2D plot of the "sombrero" function, , for x and y in the range [-10, 10].
- Hint: Use
np.linspace()to createxandyvectors. Then, usenp.meshgrid()to create coordinate matrices from them. Finally, calculatezand usematplotlib.pyplot.imshow()to visualize the result. This problem combines array creation, broadcasting, and universal functions.
- Hint: Use
Conclusion
In this lesson, you've taken the first crucial step into the world of data science with Python. You've learned:
- NumPy's
ndarrayis the high-performance data structure for numerical data in Python. - Vectorization and broadcasting allow for fast, efficient, and readable code by eliminating explicit loops.
- NumPy provides a comprehensive toolkit for creating, manipulating, and performing mathematical and linear algebra operations on N-dimensional arrays.
- Understanding the memory model (views vs. copies) is key to writing efficient and bug-free code.
In our next lesson, we will build directly on this foundation. We'll introduce Pandas, a library that uses NumPy arrays under the hood to provide a more powerful and intuitive object for working with labeled, tabular data—the DataFrame. This is the primary tool for data loading, cleaning, and exploratory analysis in Python.