NumPy Fundamentals: A Comprehensive Guide to Numerical Computing

Overview of NumPy

NumPy (Numerical Python) is a powerful extension libray for Python that provides support for large, multi-dimensional arrays and matrices. It also offers a comprehensive collection of mathematical functions to operate on these arrays efficiently.

NumPy's origins trace back to Numeric, originally developed by Jim Hugunin along with other contributors. In 2005, Travis Oliphant combined Numeric with another similar library called Numarray, adding additional extensions to create what we now know as NumPy. The project remains open-source and continues to be actively maintained by a large community of developers.

Core Purpose

NumPy is designed for high-performance numerical computing. Its primary function is to enable fast array operations, serving as the foundation for scientific computing in Python.

Key Components

The library includes several essential components:

  • ndarray: A powerful N-dimensional array object that serves as the foundation for all numerical operations
  • Broadcasting capabilities: Functions that enable operations between arrays of different shapes
  • Integration tools: Utilities for integrating C, C++, and Fortran code
  • Mathematical functions: Support for linear algebra, Fourier transforms, random number generation, and more

Performance Advantages

NumPy offers significant advantages over pure Python for numerical computations:

  • Simplified syntax for array operations compared to equivalent Python loops
  • Superior memory efficiency with array storage and I/O performance exceeding Python's built-in data structures
  • Performance improvements that scale proportionally with array size
  • Implementation primarily in C, ensuring optimal algorithmic performance

Ecosystem Integration

NumPy typically works alongside SciPy (Scientific Python) and Matplotlib (plotting library) to form a comprehensive scientific computing environment. This combination serves as a powerful alternative to MATLAB and is widely used for data science and machine learning applications.

SciPy provides an open-source collection of algorithms and tools for scientific computing, including modules for optimization, linear algebra, integration, interpolation, special functions, fast Fourier transforms, signal processing, image processing, and ordinary differential equation solving.

Matplotlib serves as the visualization interface for Python and NumPy, providing an API for embedding plots in applications using GUI toolkits such as Tkinter, wxPython, Qt, or GTK+.

Installation

pip install numpy

The ndarray Object

Introduction

The ndarray (N-dimensional array) represents NumPy's most fundamental data structure. It consists of a collection of elements of the same type, indexed starting from zero. Each element occupies a contiguous block of memory with identical storage size.

Structure Components

An ndarray comprises four essential elements:

  1. Data pointer: Points to the underlying data storage (either in memory or a memory-mapped file)
  2. Data type (dtype): Describes the fixed-size values stored in array cells
  3. Shape tuple: A tuple indicating the size of each dimension
  4. Stride tuple: Integers representing the number of bytes to skip when advancing to the next element in each dimension

Data Types in NumPy

Type System Overview

NumPy supports an extensive range of data types that map closely to C language types, with some corresponding to Python's built-in types. These types are actually instances of dtype objects, each associated with a unique character code.

Integer Types

Type Description Range
bool_ Boolean type True or False
int_ Default integer type Platform-dependent (int32 or int64)
intc C int equivalent Platform-dependent
intp Indexing integer Platform-dependent
int8 8-bit signed -128 to 127
int16 16-bit signed -32768 to 32767
int32 32-bit signed -2147483648 to 2147483647
int64 64-bit signed -9223372036854775808 to 9223372036854775807
uint8 8-bit unsigned 0 to 255
uint16 16-bit unsigned 0 to 65535
uint32 32-bit unsigned 0 to 4294967295
uint64 64-bit unsigned 0 to 18446744073709551615

Floating-Point Types

Type Description Components
float_ Alias for float64 -
float16 Half precision 1 sign bit, 5 exponent bits, 10 mantissa bits
float32 Single precision 1 sign bit, 8 exponent bits, 23 mantissa bits
float64 Double precision 1 sign bit, 11 exponent bits, 52 mantissa bits

Complex Types

Type Description
complex_ Alias for complex128
complex64 Two 32-bit floats (real and imaginary)
complex128 Two 64-bit floats (real and imaginary)

Character Codes

Each built-in type has a unique character code:

Code Meaning
b Boolean
i Signed integer
u Unsigned integer
f Floating point
c Complex floating point
m Timedelta
M Datetime
O Python object
S, a Byte string
U Unicode
V Void (raw data)

Creating dtype Objects

The dtype constructor follows this syntax:

numpy.dtype(object, align=False, copy=False)

Parameters:

  • object: The object to convert to a dtype
  • align: If True, pads fields similar to C structures
  • copy: If False, references the built-in dtype; otherwise creates a copy
import numpy as np

# Creating dtype objects from different specifications
int32_dtype = np.dtype(np.int32)      # Using NumPy type
int32_dtype = np.dtype('int32')       # Using string
int32_dtype = np.dtype('i4')          # 4 bytes = 32 bits
little_endian = np.dtype('<i4')       # Little-endian 32-bit int

# Creating structured (compound) dtypes
person_dtype = np.dtype([
    ('name', 'S20'),   # 20-byte string
    ('age', 'i4'),     # 4-byte integer
    ('score', 'f4')    # 4-byte float
])

# Using structured dtype to create arrays
people = np.array([
    ('alice', 22, 95.5),
    ('bob', 19, 87.0),
    ('charlie', 24, 92.3)
], dtype=person_dtype)

print(people['name'])           # Access 'name' field
print(people[0]['name'])        # Access first element's name field

Byte Order Specification

Byte order is controlled through type prefixes:

  • < (little-endian): Least significant byte stored at lowest address
  • > (big-endian): Most significant byte stored at lowest address

File I/O Operations

import numpy as np

# Generate random arrays
random_data = np.random.randint(0, 100, size=(3, 5))
standard_normal = np.random.randn(3, 5)

# Save single array (default .npy extension)
np.save('./dataset', random_data)
loaded_data = np.load('./dataset.npy')

# Save multiple arrays (.npz archive)
np.savez('./multi_dataset.npz', 
         first=random_data, 
         second=standard_normal)
loaded_archive = np.load('./multi_dataset.npz')
print(loaded_archive['first'], loaded_archive['second'])

# Compressed archive
np.savez_compressed('./compressed.npz', 
                    x=random_data, 
                    y=standard_normal)

# Text file I/O
np.savetxt('./data.txt', random_data, fmt='%0.2f', delimiter=',')
loaded_text = np.loadtxt('./data.txt', delimiter=',')

Creating ndarray Arrays

Important Note

NumPy arrays require homogeneous data types by default. If input data contains mixed types, NumPy applies type coercion with the priority order: string > float > integer.

The array() Function

np.array(object, dtype=None, copy=True, order='A', subok=False, ndmin=0)

Parameters:

  • object: Array or sequence of sequences
  • dtype: Desired data type (optional)
  • copy: Whether to copy the input (default True)
  • order: Memory layout - 'C' (row-major), 'F' (column-major), 'A' (any)
  • subok: Return subclass if applicable (default False)
  • ndmin: Minimum number of dimensions
import numpy as np

# From lists and tuples
arr1 = np.array([1, 2, 3, 4, 5])
arr2 = np.array((1, 2, 3, 4, 5))

# Two-dimensional arrays
arr3 = np.array([[1, 2, 3], [4, 5, 6]])

# Enforcing minimum dimensions
arr4 = np.array([1, 2, 3, 4, 5, 6, 7], ndmin=2)

# Specifying data type
arr5 = np.array([1, 2, 3], dtype=np.int32)

# With structured dtype
person_dtype = np.dtype([('name', 'S20'), ('age', 'i4'), ('score', 'f4')])
people = np.array([
    ('david', 25, 88.5),
    ('emma', 23, 91.2)
], dtype=person_dtype)

The asarray() Function

np.asarray(a, dtype=None, order='A')

Similar to array() but with fewer parameters. Primarily used for type conversion.

Array Creation Functions

empty()

Creates uninitialized arrays with specified shape and dtype:

np.empty(shape, dtype=float, order='C')

zeros()

Creates arrays filled with zeros:

np.zeros(shape, dtype=float, order='C')

ones()

Creates arrays filled with ones:

np.ones(shape, dtype=float, order='C')

full()

Creates arrays filled with a specified value:

np.full(shape, fill_value, dtype=None, order='C')

eye()

Creates identity matrices with ones on the diagonal:

np.eye(N, M=None, k=0, dtype=float, order='C')

Parameters:

  • N: Number of rows
  • M: Number of columns (defaults to N)
  • k: Diagonal offset (0 for main diagonal)

arange()

Creates arrays with evenly spaced values:

np.arange(start, stop, step, dtype=None)
# Examples
np.arange(10)          # [0, 1, 2, ..., 9]
np.arange(2, 10)       # [2, 3, 4, ..., 9]
np.arange(0, 10, 2)    # [0, 2, 4, 6, 8]

frombuffer()

Creates arrays from buffer objects:

np.frombuffer(buffer, dtype=float, count=-1, offset=0)
data = b'Hello World'
arr = np.frombuffer(data, dtype='S1', count=5, offset=6)  # ['W', 'o', 'r', 'l', 'd']

fromiter()

Creates arrays from iterable objects:

np.fromiter(iterable, dtype, count=-1)
generator = (x ** 2 for x in range(5))
arr = np.fromiter(generator, dtype='i4')  # [0, 1, 4, 9, 16]

linspace()

Creates arrays with evenly spaced numbers over a specified interval:

np.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)
# 50 evenly spaced points from 0 to 10
arr = np.linspace(0, 10, 50)
# With step value returned
arr, step = np.linspace(0, 10, 5, retstep=True)  # [0, 2.5, 5, 7.5, 10], step=2.5

logspace()

Creates arrays with numbers spaced logarithmically:

np.logspace(start, stop, num=50, endpoint=True, base=10.0, dtype=None)
# 10^0 to 10^2 (1 to 100)
arr = np.logspace(0, 2, 5)  # [1, 3.16, 10, 31.62, 100]

Random Number Generation

rand()

Generates random floats in [0, 1):

np.random.rand(d0, d1, ..., dn)
np.random.rand()           # Single value
np.random.rand(5)          # 1D array with 5 elements
np.random.rand(3, 4)       # 3x4 array

random()

Similar to rand() but uses size parameter:

np.random.random(size=None)

randint()

Generates random integers:

np.random.randint(low, high=None, size=None, dtype=int)
np.random.randint(0, 10)           # Single value in [0, 10)
np.random.randint(0, 10, 5)         # 1D array with 5 elements
np.random.randint(0, 10, (3, 4))    # 3x4 array

randn()

Generates samples from standard normal distribution N(0, 1):

np.random.randn(d0, d1, ..., dn)

normal()

Generates samples from normal distribution with specified parameters:

np.random.normal(loc=0.0, scale=1.0, size=None)

Parameters:

  • loc: Mean (center of distribution)
  • scale: Standard deviation (width of distribution)
  • size: Output shape
np.random.normal(0, 1, 1000)  # 1000 samples from N(0, 1)

Array Attributes

Understanding Axes

In NumPy terminology, an array's dimension is called its rank, and each dimension is an axis. For example, a 2D array consists of rows (axis 0) and columns (axis 1). Operations along axis=0 operate on columns, while axis=1 operates on rows.

Core Attributes

Attribute Description
ndim Number of dimensions (axes)
shape Tuple representing array dimensions (rows, columns, ...)
size Total number of elements
dtype Data type of array elements
itemsize Size of each element in bytes
flags Memory layout information
real Real parts of complex elements
imag Imaginary parts of complex elements
data Buffer containing actual array elements

Flags Attribute Details

a = np.array([[1, 2], [3, 4]])
print(a.flags)

Output fields:

  • C_CONTIGUOUS: Data stored in contiguous C-style memory
  • F_CONTIGUOUS: Data stored in contiguous Fortran-style memory
  • OWNDATA: Array owns its memory or borrows from another object
  • WRITEABLE: Data region can be modified
  • ALIGNED: Data and elements properly aligned
  • WRITEBACKIFCOPY: Array is a copy; source updates when this array is freed

Array Indexing and Slicing

Basic Indexing

import numpy as np

# One-dimensional arrays
arr = np.arange(20)

# Single element access
value = arr[5]           # Element at index 5

# Multiple indices (fancy indexing)
values = arr[[1, 3, 5, 7]]

# Slicing (start:stop, inclusive:exclusive)
slice1 = arr[2:8]        # Elements at indices 2, 3, 4, 5, 6, 7
slice2 = arr[:5]         # Elements at indices 0-4
slice3 = arr[10:]        # Elements from index 10 to end

# Slicing with step
slice4 = arr[::2]        # Every other element
slice5 = arr[1::3]       # Starting at index 1, every 3rd element
slice6 = arr[::-1]       # Reverse order
slice7 = arr[::-2]       # Reverse, every other element
slice8 = arr[5::-2]      # Reverse from index 5, every 2nd

Two-Dimensional Array Indexing

matrix = np.random.randint(0, 50, size=(10, 10))

# Row access
row = matrix[2]                  # Third row
rows = matrix[[0, 3, 5]]         # Multiple rows

# Element access
element = matrix[3, 4]           # Row 3, Column 4

# Column selection within rows
cols = matrix[4, [2, 5, 7]]      # Row 4, columns 2, 5, 7

# Combined slicing
sub = matrix[2:7, 1::2]          # Rows 2-6, columns 1, 3, 5, ...
sub = matrix[::3, ::2]           # Every 3rd row, every 2nd column

# Negative indexing
last_element = matrix[-1, -1]    # Bottom-right element
elements = matrix[-2, [-2, -3, -4]]

Fancy Indexing

Returns a copy rather than a view:

indices = np.array([1, 3, 4, 5])
fancy_result = arr[indices]      # Copy of elements at those indices

# Boolean indexing
a = np.random.randint(0, 151, size=(5, 3))
mask = a >= 100
filtered = a[mask]              # Elements where condition is True

# Complex boolean conditions
scores = np.random.randint(0, 151, size=(100, 3))
condition = (scores[:, 0] >= 80) & (scores[:, 1] >= 80) & (scores[:, 2] >= 80)
eligible = scores[condition]

Array Iteration

The nditer object provides efficient multi-dimensional iteration:

a = np.arange(12).reshape(3, 4)

# Default row-major (C) order
for element in np.nditer(a):
    print(element, end=' ')

# Column-major (Fortran) order
for element in np.nditer(a, order='F'):
    print(element, end=' ')

# Modify elements during iteration
for element in np.nditer(a, op_flags=['readwrite']):
    element[...] = element * 2

# Broadcasting iteration
b = np.arange(3)
for x, y in np.nditer([a, b]):
    print(f"x={x}, y={y}")

nditer flags:

  • c_index: C-order indexing
  • f_index: Fortran-order indexing
  • multi-index: Track multiple index types
  • external_loop: Provide one-dimensional arrays

Array Operations

Broadcasting

Broadcasting enables operations between arrays of different shapes. The rules are:

  1. Dimensions are compared from right to left
  2. Dimensions must be equal or one of them must be 1
  3. Output shape is the maximum of each dimension
  4. If dimensions don't match and neither is 1, an error is raised
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[10], [20]])
# b broadcasts to [[10, 10, 10], [20, 20, 20]]
result = a + b  # [[11, 12, 13], [24, 25, 26]]

Arithmetic Operations

a = np.random.randint(0, 10, 5, dtype=np.int8)
b = np.random.randint(0, 10, 5, dtype=np.int8)

# Element-wise operations
print(a + 1)           # Add scalar
print(a + b)           # Add arrays
print(a - 1)           # Subtract scalar
print(a * 2)           # Multiply by scalar
print(a * b)           # Multiply arrays
print(a / 2)           # Divide by scalar
print(a / b)           # Divide arrays
print(a ** 2)          # Power (element-wise)
print(np.power(a, b))  # Using power function

# In-place operations
c = np.array([1, 2, 3, 4])
c += 1                 # c becomes [2, 3, 4, 5]
c *= 2                 # c becomes [4, 6, 8, 10]

Mathematical Functions

a = np.array([1, 2, 3, 4])

# Exponential and logarithm
print(np.exp(a))          # e^1, e^2, e^3, e^4
print(np.log(np.e ** 2))  # log_e(2)
print(np.log10(1000))     # log_10(1000) = 3
print(np.log2(1024))      # log_2(1024) = 10

# Trigonometric functions
print(np.sin(np.pi / 2))  # 1.0
print(np.cos(0))          # 1.0
print(np.tan(np.pi / 4))  # 1.0

# Inverse trigonometric
print(np.arcsin(0.5))     # pi/6
print(np.arccos(0.5))     # pi/3
print(np.arctan(1))       # pi/4

# Rounding
print(np.around(3.7))     # 4.0
print(np.around(3.2))     # 3.0
print(np.floor(3.7))      # 3.0
print(np.ceil(3.2))       # 4.0

Comparison Operations

a = np.array([1, 2, 3, 4, 5])
b = np.array([5, 4, 3, 2, 1])

print(a > b)   # [False, False, False, True, True]
print(a >= b)  # [False, False, True, True, True]
print(a < b)   # [True, True, False, False, False]
print(a <= b)  # [True, True, False, False, False]
print(a == b)  # [False, False, True, False, False]
print(a != b)  # [True, True, False, True, True]

Copies and Views

No Copy (Reference Assignment)

a = np.array([1, 2, 3, 4, 5])
b = a  # Both variables point to the same memory
b[0] = 100  # Also modifies a
print(a)  # [100, 2, 3, 4, 5]

Shallow Copy (View)

a = np.array([1, 2, 3, 4, 5])
b = a.view()  # Different array object, same underlying data
b[0] = 100    # Also modifies a
print(a)      # [100, 2, 3, 4, 5]
print(a.base is b.base)  # True - they share data

Deep Copy

a = np.array([1, 2, 3, 4, 5])
b = a.copy()  # Completely independent arrays
b[0] = 100    # Does NOT modify a
print(a)      # [1, 2, 3, 4, 5]

# Memory-efficient pattern for large arrays
a = np.arange(1e8)  # 100 million elements
necessary_data = a[[1, 2, 3, 4, 5, 6, 7]].copy()
del a  # Free memory

Array Shape Manipulation

reshape()

Reshapes array without changing data:

a = np.arange(12)
b = a.reshape(3, 4)  # 3x4 array, shares memory with a

flatten()

Returns flattened copy:

a = np.array([[1, 2], [3, 4]])
b = a.flatten(order='C')  # Row-major order
c = a.flatten(order='F')  # Column-major order

ravel()

Returns flattened view (may share memory):

a = np.array([[1, 2], [3, 4]])
b = a.ravel(order='C')
b[0] = 100  # May modify a (depends on memory layout)

transpose() and T

a = np.arange(12).reshape(3, 4)
b = np.transpose(a)    # Transpose
c = a.T               # Same as transpose

rollaxis()

Rolls an axis backwards:

a = np.arange(24).reshape(2, 3, 4)
b = np.rollaxis(a, axis=2, start=0)  # Shape becomes (4, 2, 3)

swapaxes()

Swaps two axes:

a = np.arange(24).reshape(2, 3, 4)
b = np.swapaxes(a, 0, 2)  # Shape becomes (4, 3, 2)

broadcast Objects

Simulates broadcasting behavior:

a = np.array([[1], [2], [4]])
b = np.array([10, 20, 30])
c = np.broadcast(a, b)
print(c.shape)  # (3, 3)

# Manual broadcasting calculation
result = np.empty(c.shape, dtype='i4')
result.flat = [x + y for x, y in c]
print(result)  # [[11, 21, 31], [12, 22, 32], [14, 24, 34]]

broadcast_to()

Broadcasts array to new shape:

a = np.array([1, 2, 3])
b = np.broadcast_to(a, (3, 3))

expand_dims()

Adds new axis at specified position:

a = np.array([1, 2, 3])
b = np.expand_dims(a, axis=0)  # Shape (1, 3)
c = np.expand_dims(a, axis=1)  # Shape (3, 1)

squeeze()

Removes axes of length 1:

a = np.arange(6).reshape(1, 2, 3, 1)
b = np.squeeze(a)  # Shape (2, 3)

Array Concatenation

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])

# Concatenate along axis 0 (rows)
c = np.concatenate((a, b), axis=0)   # [[1, 2], [3, 4], [5, 6], [7, 8]]

# Concatenate along axis 1 (columns)
d = np.concatenate((a, b), axis=1)   # [[1, 2, 5, 6], [3, 4, 7, 8]]

# Stack along new axis
e = np.stack((a, b), axis=0)  # Shape (2, 2, 2)

# Horizontal stack (column-wise)
f = np.hstack((a, b))  # [[1, 2, 5, 6], [3, 4, 7, 8]]

# Vertical stack (row-wise)
g = np.vstack((a, b))  # [[1, 2], [3, 4], [5, 6], [7, 8]]

Array Splitting

a = np.arange(12)

# Split into equal parts
parts = np.split(a, 3)        # 3 equal parts

# Split at specified positions
parts = np.split(a, [2, 5, 7])  # Split at indices 2, 5, 7

# Horizontal and vertical split
matrix = np.arange(12).reshape(3, 4)
rows = np.vsplit(matrix, 3)    # 3 row arrays
cols = np.hsplit(matrix, 2)    # 2 column arrays

resize()

Returns array with new shape (repeats if smaller):

a = np.array([1, 2, 3])
b = np.resize(a, (3, 4))  # [[1, 2, 3, 1], [2, 3, 1, 2], [3, 1, 2, 3]]

append(), insert(), delete()

a = np.array([1, 2, 3, 4, 5])

# Append (returns new array)
b = np.append(a, 6)           # [1, 2, 3, 4, 5, 6]
b = np.append(a, [6, 7])      # [1, 2, 3, 4, 5, 6, 7]

# Insert (returns new array)
c = np.insert(a, 2, 99)       # [1, 2, 99, 3, 4, 5]

# Delete (returns new array)
d = np.delete(a, [1, 3])      # [1, 3, 5]

unique()

Finds unique elements:

a = np.array([1, 2, 2, 3, 3, 3, 4, 5, 5])

# Basic unique
unique_values = np.unique(a)

# With indices
values, indices = np.unique(a, return_index=True)

# With reconstruction mapping
values, inverse = np.unique(a, return_inverse=True)
reconstructed = values[inverse]

# With counts
values, counts = np.unique(a, return_counts=True)

String Functions

NumPy's char module provides vectorized string operations:

import numpy as np

# String concatenation
print(np.char.add(['hello'], ['world']))                    # ['helloworld']
print(np.char.add(['red', 'blue'], ['car', 'bike']))        # ['redcar', 'bluebike']

# String repetition
print(np.char.multiply('test*', 3))                         # 'test*test*test*'

# Center string with padding
print(np.char.center(['cat', 'dog'], 10, fillchar='-'))      # ['---cat---', '---dog---']

# Capitalize first letter
print(np.char.capitalize('hello world'))                    # 'Hello world'

# Title case (each word)
print(np.char.title('hello world'))                         # 'Hello World'

# Case conversion
print(np.char.lower('HELLO'))                               # 'hello'
print(np.char.upper('hello'))                               # 'HELLO'

# Split strings
print(np.char.split('hello world', sep=' '))                # ['hello', 'world']

# Split by lines
print(np.char.splitlines('hello\nworld'))                  # ['hello', 'world']

# Strip characters
print(np.char.strip(['***test***', '***demo***'], '*'))     # ['test', 'demo']

# Replace substrings
print(np.char.replace('good morning', 'good', 'great'))     # 'great morning'

# Encode and decode
encoded = np.char.encode('hello', 'utf-8')
decoded = np.char.decode(encoded, 'utf-8')

Statistical Functions

Finding Extremes

a = np.arange(12).reshape(3, 4)

# Overall min/max
print(np.min(a))    # 0
print(np.max(a))    # 11

# Along axis
print(np.min(a, axis=0))  # Column minima
print(np.max(a, axis=1))  # Row maxima

Percentile

scores = np.array([65, 70, 75, 80, 85, 90, 92, 95, 98])
p25 = np.percentile(scores, 25)   # 25th percentile
p50 = np.percentile(scores, 50)   # Median
p75 = np.percentile(scores, 75)   # 75th percentile

Range (ptp)

a = np.array([1, 5, 10, 15])
print(np.ptp(a))         # 14 (15 - 1)
print(np.ptp(a, axis=0)) # Along specified axis

Central Tendency

a = np.array([1, 2, 3, 4, 5])

# Arithmetic mean
print(np.mean(a))                    # 3.0
print(np.mean(a, axis=0))            # Along axis

# Median
print(np.median(a))                  # 3.0

# Weighted average
weights = np.array([1, 2, 3, 4, 5])
weighted_avg = np.average(a, weights=weights)  # (1*1 + 2*2 + 3*3 + 4*4 + 5*5) / 15

Dispersion

a = np.array([1, 2, 3, 4])

# Standard deviation
std = np.std(a)  # sqrt(mean((x - mean)^2)) = sqrt(1.25) ≈ 1.118

# Variance
var = np.var(a)  # mean((x - mean)^2) = 1.25

Sorting Functions

Comparison of Sorting Algorithms

Algorithm Speed Worst Case Space Stable
quicksort 1 O(n²) 0 No
mergesort 2 O(n log n) ~n/2 Yes
heapsort 3 O(n log n) 0 No

sort()

a = np.array([3, 1, 4, 1, 5, 9, 2, 6])

# Basic sort
sorted_a = np.sort(a)  # Returns new sorted array

# With axis
matrix = np.array([[3, 1], [4, 2]])
print(np.sort(matrix, axis=0))  # Sort columns
print(np.sort(matrix, axis=1))  # Sort rows

# With dtype field
dtype = np.dtype([('name', 'S10'), ('score', 'f4')])
people = np.array([('bob', 85), ('alice', 92)], dtype=dtype)
print(np.sort(people, order='score'))

argsort()

Returns indices that would sort the array:

a = np.array([3, 1, 4, 1, 5])
indices = np.argsort(a)  # [1, 3, 0, 2, 4]
sorted_values = a[indices]

lexsort()

Lexicographic (multi-key) sort:

a = np.array([('math', 90), ('english', 85), ('math', 95), ('science', 88)])

# Sort by second column, then first column
indices = np.lexsort((a[:, 0], a[:, 1]))
result = a[indices]

partition()

Partitions array around kth element:

a = np.array([3, 4, 2, 1, 5])
print(np.partition(a, 2))  # [1, 2, 3, 5, 4]
print(np.partition(a, (1, 3)))  # [1, 2, 3, 4, 5]

argpartition()

Returns indices for partitioning:

a = np.array([3, 4, 2, 1, 5])
indices = np.argpartition(a, 2)
print(a[indices])  # Partially sorted

Searching Functions

argmax() and argmin()

a = np.array([3, 1, 4, 1, 5, 9, 2, 6])
print(np.argmax(a))  # 5 (index of 9)
print(np.argmin(a))  # 1 (index of 1)

nonzero()

a = np.array([0, 2, 0, 4, 0, 6])
indices = np.nonzero(a)  # (array([1, 3, 5]),)

where()

a = np.arange(10)
condition = a > 5
print(np.where(condition, a, -1))  # [-1, -1, -1, -1, -1, -1, 6, 7, 8, 9]

extract()

a = np.arange(12)
condition = a % 2 == 0
print(np.extract(condition, a))  # [0, 2, 4, 6, 8, 10]

Tags: Numpy python data-science numerical-computing Arrays

Posted on Fri, 24 Jul 2026 16:50:57 +0000 by paparts