NumPy is the foundational module for numeric computation in Python. Its primary data structure, the NumPy n-dimensional array, can be considered the Python equivalent of a MATLAB matrix. SciPy is a collection of modules built upon NumPy to solve a wide range of computational problems in clustering, differential equations, image processing, interpolation, linear algebra, linear programming, optimization, sparse matrices, statistics, and symbolic mathematics, among others. NumPy and SciPy together span the capabilities of the core MATLAB product plus functions from many toolboxes.
In this chapter, I’ll show how to create, manipulate, subset, aggregate, and serialize NumPy arrays, then survey the broad capabilities found in NumPy and SciPy. Comparisons to MATLAB equivalents are shown throughout.
11.1 NumPy Arrays
A NumPy array, or, more formally, a numpy.ndarray, meaning “n-dimensional array,” is functionally equivalent to a MATLAB matrix. Both are containers for a single (usually numeric) data type, both allow vectorized operations on their values, and both are accompanied by a large library of high performance mathematical, scientific, and statistical functions.
The prefix np. will therefore represent the NumPy module throughout this book.
11.1.1 Formatting NumPy Array Values
We’ll see many numbers in the following sections, so it’s helpful to have control over their appearance. MATLAB’s format command has a collection of useful styles. NumPy offers that and more; it gives control not only of the numeric format but also the line width and summary options if the array exceeds a given size.
Occasionally , in MATLAB you’ll accidentally type the name of a massive array or enter an expression that produces volumes of numbers without a trailing semicolon. The result is many screens full of numbers scrolling by. NumPy is more forgiving in that it will simply print leading and trailing terms:
If you’re fortunate to have a large monitor, setting linewidth=200 (or something similar) can be a big help when poring over data.
11.1.2 Differences Between NumPy Arrays and MATLAB Matrices
The next five sections explain the primary differences between NumPy arrays and MATLAB matrices.
11.1.2.1 MATLAB Matrices Made of Numeric Literals Default to Double Precision
MATLAB: | Python: |
---|---|
>> a = [1 2 3]; >> class(a) 'double' >> a = [1 2. 3]; >> class(a) 'double' | In : a = np.array([1, 2, 3]) In : a.dtype Out: dtype('int64') In : a = np.array([1, 2., 3]) In : a.dtype Out: dtype('float64') |
11.1.2.2 MATLAB Matrices Have a Minimum of Two Dimensions
MATLAB: | Python: |
---|---|
>> a = [1 2 3] >> size(a) 1 3 >> ndims(a) 2 | In : a = np.array([1, 2, 3]) In : a.shape Out: (3,) In : a.ndim Out: 1 |
In the preceding example, a in MATLAB is a 1 x 3 matrix, while Python’s a just has three terms. MATLAB can therefore distinguish between row vectors and column vectors which is useful for some linear algebra expressions.
One-dimensional NumPy arrays have no concept of vertical or horizontal arrangement; thus, their row or column affinity becomes a matter of context. A second dimension can be added explicitly to convey the same row or column sense as a MATLAB vector; this process is explained in Section 11.1.12.
11.1.2.3 MATLAB Numeric Scalars Are 1 x 1 Matrices
MATLAB: | Python: |
---|---|
>> a = 2.7183; >> size(a) 1 1 >> ndims(a) 2 | In : a = 2.7183 In : a.shape AttributeError Traceback ----> 1 a.shape AttributeError: 'float' object has no attribute 'shape' |
11.1.2.4 NumPy Arrays Are Row Major
MATLAB: | Python: |
---|---|
>> a = 1:12 a = 1 2 3 4 5 6 7 8 9 10 11 12 >> reshape(a, [3,4]) 1 4 7 10 2 5 8 11 3 6 9 12 | In : a = np.arange(1, 13) In : a Out: array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) In : a.reshape(3,4) array([[ 1, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12]]) |
NumPy is capable of creating column-major arrays using the order='F' (Fortran-order) optional argument to the numpy.array() function.
11.1.2.5 NumPy Arrays Must Be Explicitly Resized to Add Terms
MATLAB: | Python: |
---|---|
>> a = [1 2; 3 4] a = 1 2 3 4 >> a(3,:) = [5 6] a = 1 2 3 4 5 6 | In : a = np.array([[1,2], [3,4]]) In : a Out: array([[1, 2] [3, 4]]) In : a.resize( (3,2) ) In : a[2,] = [5, 6] In : a Out: array([[1, 2], [3, 4], [5, 6]]) |
While MATLAB’s matrix growing capability is convenient, it comes with a performance penalty. It is much faster to preallocate a MATLAB matrix, for example, by first creating a matrix of zeros, then populating existing rows or columns with values rather than starting with an empty matrix and growing it in a loop. Preallocation is fine when the final size of a matrix is known up front, but this is not always the case. Reading values from a file may require either two passes through the file, once to learn what the final size will be and a second time to populate matrix entries, or reverting to growing the matrix.
11.1.3 NumPy Data Types
NumPy data types
MATLAB Type | NumPy Type | Notes |
---|---|---|
logical | np.bool | 8-bit Boolean |
uint8 | np.ubyte | 8-bit unsigned integer |
uint8 | np.uint8 | 8-bit unsigned integer |
int8 | np.int8 | 8-bit signed integer |
uint16 | np.uint16 | 16-bit unsigned integer |
int16 | np.int16 | 16-bit signed integer |
uint32 | np.uint32 | 32-bit unsigned integer |
int32 | np.int32 | 32-bit signed integer |
uint64 | np.uint64 | 64-bit unsigned integer |
int64 | np.int, np.int64 | 64-bit signed integer |
uint64 | np.uint | 64-bit unsigned integer |
not available | np.float16 | 16-bit (short) float |
single | np.float32 | 32-bit float |
double | np.float64 | 64-bit float |
double | np.float | 64-bit float |
not available | np.float128 | 128-bit (quad precision) float1 |
complex | np.complex | pair of 64-bit floats |
single2 | np.complex64 | Pair of 32-bit floats |
double3 | np.complex128 | Pair of 64-bit floats |
not available | np.complex256 | Pair of 128-bit floats4 |
char | np.str | Fixed-length strings |
datetime | datetime.datetime | Datetime object |
11.1.4 Typecasting Scalars and Arrays
MATLAB’s typecasting mechanism is straightforward: if you have a scalar or array in one data type (values are double precision by default), you just need to prefix the variable with the type name.
MATLAB: | Python: |
---|---|
>> p = 4.32; >> uint8(p) 4 >> p = [4.32 1.23]; >> uint8(p) 4 1 | In : p = 4.32 In : np.uint8(p) Out: 4 In : p = np.array([4.32, 1.23]) In : p.astype(np.uint8) Out: array([4, 1], dtype=uint8) |
MATLAB: | Python: |
---|---|
>> p = [4.32 1.23]; >> q = uint8([0 0]); >> cast(p,'like',q) 1×2 uint8 row vector 4 1 | In : p = np.array([4.32, 1.23]) In : q = np.zeros((1,2),dtype=np.uint8) In : p.astype(q.dtype) Out: array([4, 1], dtype=uint8) |
np.zeros() and np.zeros_like() are described in Section 11.1.6.
11.1.5 Hex, Binary, and Decimal Representations
The content of some numeric data, specifically integers, may have properties that become apparent when represented in different bases. Bit mask operations in particular are more easily understood when their values are shown in binary form.
11.1.5.1 Decimal to Binary
MATLAB: | Python: |
---|---|
>> dec = 1234; >> dec2bin(dec) 10011010010 | In : dec = 1234 In : np.binary_repr(dec) Out: '10011010010' |
11.1.5.2 Binary to Decimal
MATLAB: | Python: |
---|---|
>> bin2dec('10011010010') 1234 | In : np.int('10011010010', 2) Out: 1234 |
11.1.5.3 Decimal to Hexadecimal
MATLAB: | Python: |
---|---|
>> dec2hex(8765) 223D >> sprintf('%x', 8765) 223d >> sprintf('%X', 8765) 223D >> sprintf('%06X', 8765) 00223D | In : '%x' % 8765 Out: '223d' In : '%X' % 8765 Out: '223D' In : '%06X' % 8765 Out: '00223D' |
11.1.5.4 Hexadecimal to Decimal
MATLAB: | Python: |
---|---|
>> hex2dec('223D') 8765 | In : np.int('223D', 16) Out: 8765 |
11.1.6 Creating Arrays
MATLAB: | Python: |
---|---|
>> a = [9 8 7; 6 5 4] a = 9 8 7 6 5 4 | In : a = np.array([[9, 8, 7],[6, 5, 4]]) In : a array([[9, 8, 7], [6, 5, 4]]) |
11.1.6.1 Zeros
Array allocation or initialization is often done by creating a zero-filled array with the desired shape and type. NumPy has two functions, np.zeros() and np.zeros_like(), for this purpose. The np.zeros_like() function is interesting because rather than taking the array size as an argument, it takes an existing matrix, then returns a zero-filled array having the same size and type of the given array. Another array creation option is np.empty() which creates an array without initializing its contents.
MATLAB 2020b: | Python: |
---|---|
>> a = zeros(2,3) 0 0 0 0 0 0 >> b = [2; 4] 2 4 >> c = zeros('like',b) 0 | In : a = np.zeros((2,3)) In : a Out: array([[0, 0, 0], [0, 0, 0]]) In : b = np.array([[2], [4]]) In : b Out: array([[2], [4]]) In : c = np.zeros_like(b) In : c Out: array([[0], [0]]) |
11.1.6.2 Ones
MATLAB: | Python: |
---|---|
>> a = ones(3,1) 1 1 1 | In : a = np.ones((3,1)) In : a Out: array([[1], [1], [1]]) |
11.1.6.3 NaNs
MATLAB: | Python: |
---|---|
>> nan(2,3) NaN NaN NaN NaN NaN NaN | In : np.full((2,3), np.NaN) array([[nan, nan, nan], [nan, nan, nan]]) |
11.1.6.4 Range
MATLAB: | Python: |
---|---|
>> b = -8:-4 b = -8 -7 -6 -5 -4 >> b = -8:1.5:-4 b = -8.00 -6.50 -5.00 | In : b = np.arange(-8,-3) In : b Out: array([-8, -7, -6, -5, -4]) In : b = np.arange(-8,-4,1.5) In : b Out: array([-8. , -6.5, -5. ]) |
11.1.6.5 Identity
MATLAB: | Python: |
---|---|
>> eye(3) 1 0 0 0 1 0 0 0 1 | In : a = np.eye(3) array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) |
11.1.6.6 Diagonal Matrices
MATLAB: | Python: |
---|---|
>> diag([9 8 7]) 9 0 0 0 8 0 0 0 7 >> diag(eye(2)) 1 1 | In : np.diag([9,8,7]) array([[9, 0, 0], [0, 8, 0], [0, 0, 7]]) In : np.diag(np.eye(2)) Out: array([1., 1.]) |
11.1.6.7 Upper and Lower Triangular Matrices
These are formed in MATLAB with triu() and tril() and in NumPy with np.triu() and np.tril(). All four functions support an optional offset from the diagonal.
MATLAB: | Python: |
---|---|
>> A = reshape(5:16, 4,3)' A = 5 6 7 8 9 10 11 12 13 14 15 16 >> triu(A) 5 6 7 8 0 10 11 12 0 0 15 16 >> triu(A,1) 0 6 7 8 0 0 11 12 0 0 0 16 >> tril(A) 5 0 0 0 9 10 0 0 13 14 15 0 >> tril(A,-1) 0 0 0 0 9 0 0 0 13 14 0 0 | In : A = np.arange(5,17).reshape(3,4) In : A array([[ 5, 6, 7, 8], [ 9, 10, 11, 12], [13, 14, 15, 16]]) In : np.triu(A) array([[ 5, 6, 7, 8], [ 0, 10, 11, 12], [ 0, 0, 15, 16]]) In : np.triu(A,1) array([[ 0, 6, 7, 8], [ 0, 0, 11, 12], [ 0, 0, 0, 16]]) In : np.tril(A) array([[ 5, 0, 0, 0], [ 9, 10, 0, 0], [13, 14, 15, 0]]) In : np.tril(A,-1) array([[ 0, 0, 0, 0], [ 9, 0, 0, 0], [13, 14, 0, 0]]) |
11.1.6.8 Random
MATLAB: | Python: |
---|---|
>> rand(2) 0.095692 0.232535 0.813753 0.892878 >> rand(3,2) 0.456106 0.908961 0.274357 0.755147 0.113177 0.900217 | In : np.random.rand(2,2) array([[0.59219917, 0.62549076], [0.21467385, 0.70801235]]) In : np.random.rand(3,2) array([[0.664537, 0.219990], [0.974484, 0.251934], [0.827411, 0.063566]]) |
11.1.6.9 Equally Spaced Distribution (linspace)
MATLAB: | Python: |
---|---|
>> linspace(21,23,5) 21.0 21.5 22.0 22.5 23.0 | In : np.linspace(21,23,5) Out: array([21., 21.5, 22., 22.5, 23. ]) |
11.1.6.10 Logarithmically Spaced Distribution (logspace)
MATLAB: | Python: |
---|---|
>> logspace(0.1, 1, 3) 1.2589 3.5481 10.0000 | In : np.logspace(0.1, 1, 3) Out: array([ 1.25892541, 3.54813389, 10. ]) |
11.1.6.11 Horizontal and Vertical Stacking
MATLAB: | Python: |
---|---|
>> a = [1; 2] 1 2 >> b = [0; -3] 0 -3 >> c = [-1; 0] -1 0 >> d = [a b c] 1 0 -1 2 -3 0 >> e = [a;b;c] 1 2 0 -3 -1 0 | In : a = np.array([[1],[2]]) In : a array([[1], [2]]) In : b = np.array([[0],[-3]]) In : b array([[ 0], [-3]]) In : c = np.array([[-1],[0]]) In : c array([[-1], [ 0]]) In : d = np.hstack([a,b,c]) In : d array([[ 1, 0, -1], [ 2, -3, 0]]) In : e = np.vstack([a,b,c]) In : e array([[ 1], [ 2], [ 0], [-3], [-1], [ 0], |
11.1.6.12 Meshgrid
MATLAB: | Python: |
---|---|
>> [y,x] = meshgrid([1 2 3], [3.5 3.6]) y = 1 2 3 1 2 3 x = 3.5000 3.5000 3.5000 3.6000 3.6000 3.6000 | In : y,x = np.meshgrid([1,2,3], [3.5,3.6]) In : y array([[1, 2, 3], [1, 2, 3]]) In : x array([[3.5, 3.5, 3.5], [3.6, 3.6, 3.6]]) |
11.1.6.13 Inflating Matrices
MATLAB: | Python: |
---|---|
>> a = [4 -4; 4 0] 4 -4 4 0 >> nY = 2*3; nX = 2*2; >> Xs = linspace(0,1,nX); >> Ys = linspace(0,1,nY); >> [Xq,Yq] = meshgrid(Xs, Ys); >> interp2(X,Y,a,Xq,Yq) 4.0000 1.3333 -1.3333 -4.0000 4.0000 1.6000 -0.8000 -3.2000 4.0000 1.8667 -0.2667 -2.4000 4.0000 2.1333 0.2667 -1.6000 4.0000 2.4000 0.8000 -0.8000 4.0000 2.6667 1.3333 0 | In : from scipy.ndimage import zoom In : a = np.array([[4.0, -4], [4, 0]]) In : a array([[ 4., -4.], [ 4., 0.]]) In : zoom(a, [3, 2], order=1) array([[ 4. , 1.3333, -1.3333, -4. ], [ 4. , 1.6 , -0.8 , -3.2 ], [ 4. , 1.8667, -0.2667, -2.4 ], [ 4. , 2.1333, 0.2667, -1.6 ], [ 4. , 2.4 , 0.8 , -0.8 ], [ 4. , 2.6667, 1.3333, 0. ]]) |
11.1.6.14 Test Matrices
Algorithm development typically begins with test data, or in its absence, random data with prescribed properties. The array creation methods of this section can be combined in interesting ways to yield data having properties useful for exploring algorithm robustness, error handling, or exercising various logic branches. Here, we explore some of those methods.
The MATLAB and Python matrices in this section are generated from random number generators and therefore will not match each other.
MATLAB: | Python: |
---|---|
>> floor(10*rand(4,4) - 5) -5 -3 4 -4 -4 4 1 4 -3 0 -1 3 3 -3 0 -4 | In : np.random.randint(-5,6,size=(4,4)) array([[-3, 5, 4, 5], [ 0, -1, 1, 3], [ 0, 3, 1, -2], [-1, -1, 5, -5]]) |
MATLAB: | Python: |
---|---|
>> reshape(1:12, 4,3) 1 5 9 2 6 10 3 7 11 4 8 12 | In : np.arange(1,13).reshape(3,4).T array([[ 1, 5, 9], [ 2, 6, 10], [ 3, 7, 11], [ 4, 8, 12]]) |
MATLAB: | Python: |
---|---|
>> reshape(1:12, 3,4)' 1 2 3 4 5 6 7 8 9 10 11 12 | In : np.arange(1,13).reshape(4,3) array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) |
MATLAB: | Python: |
---|---|
>> a = 100*rand(4,3); >> a(a < 50) = nan a = 69.082 NaN NaN NaN 73.604 NaN NaN 63.711 NaN 51.612 76.674 63.956 | In : a = 100*np.random.rand(4,3) In : a[a < 50] = np.NaN In : a array([[60.551, 99.574, 78.620], [ nan, 66.137, 64.343], [52.505, nan, nan], [96.109, 56.539, nan]]) |
MATLAB and Python generate different random matrices. The two matrices earlier are not intended to match.
11.1.7 Complex Scalars and Arrays
MATLAB: | Python: |
---|---|
>> 1+2i 1.0000 + 2.0000i >> 3 - 4j 3.0000 - 4.0000i >> complex(5,6) 5.0000 + 6.0000i | In : 1+2i 1+2i SyntaxError: invalid syntax In : 3 - 4j Out: (3-4j) In : complex(5,6) Out: (5+6j) |
MATLAB: | Python: |
---|---|
>> N = ones(2,3); >> complex(N, -5*N) 1.00-5.00i 1.00-5.00i 1.00-5.00i 1.00-5.00i 1.00-5.00i 1.00-5.00i | In : N = np.ones((2,3)) In : N - 5*N*(1j) array([[1.-5.j, 1.-5.j, 1.-5.j], [1.-5.j, 1.-5.j, 1.-5.j]]) |
MATLAB: | Python: |
---|---|
>> z = complex([2.2;3.3],... [-4.4;5.5]) 2.2000 - 4.4000i 3.3000 + 5.5000i >> real(z) 2.2000 3.3000 >> imag(z) -4.4000 5.5000 | In : z=np.array([[2.2],[3.3]]) + (1j)* ...: np.array([[-4.4],[5.5]]) In : z array([[2.2-4.4j], [3.3+5.5j]]) In : z.real array([[2.2], [3.3]]) In : z.imag array([[-4.4], [ 5.5]]) |
11.1.8 Linear Indexing
MATLAB matrices allow linear indexing whereby a single scalar index can be used to access terms of a multidimensional array. The scalar index is the offset from the beginning of the array to the desired term, striding along columns (or, more generally, incrementing the index of leftmost dimension most rapidly).
The same is possible with NumPy arrays using either their .take() or .item() methods. Keep in mind, though, that (1) NumPy arrays are row major so a different offset is needed to arrive at the same term, and (2) the argument to .take() is zero index based.
MATLAB: | Python: |
---|---|
>> a = [11 14 17 20 23; 12 15 18 21 24; 13 16 19 22 25]; >> a(12) 22 | In : a = np.array( [[11, 14, 17, 20, 23], [12, 15, 18, 21, 24], [13, 16, 19, 22, 25]]) In : a.take(11) Out: 16 |
MATLAB: | Python: |
---|---|
>> [r,c]=ind2sub([3,5],12) r = 3 c = 4 >> sub2ind([3,5],3,4) 12 | In : np.unravel_index(11,(3,5)) Out: (2, 1) In : np.ravel_multi_index((2,1),(3,5)) Out: 11 |
sub2ind() and np.ravel_multi_index() give the linear offset into a multidimensional array given the indices of the desired term.
The .take() method also accepts lists of indices and an optional argument specifying the dimension to extract terms from. This turns out to be useful when extracting rows or columns of NumPy arrays within MATLAB because MATLAB syntax precludes indexing NumPy arrays with brackets.
Here, we show how the first row and third column of a can be extracted with .take() and the axis keyword argument:
11.1.9 Reading/Writing Arrays to/from Text Files
Sections 7.1 and 7.6 covered text and binary I/O of generic data. Here, we’ll examine I/O for the special case of numeric arrays to and from text files.
11.1.9.1 Reading an Array from a Text File
MATLAB: | Python: |
---|---|
>> a = load('a.txt') a = -3.2000e+00 4.3000e+00 1.0000e-05 2.9000e+00 6.6000e+00 -9.2000e+00 | In : a = np.loadtxt('a.txt') In : a array([[-3.2e+00, 4.3e+00], [ 1.0e-05, 2.9e+00], [ 6.6e+00, -9.2e+00]]) |
(See Section 7.1.5 for reading data from comma-separated value files.)
11.1.9.2 Writing an Array to a Text File
MATLAB: | Python: |
---|---|
>> save("aM.txt","a","-ascii") | In : np.savetxt('aP.txt', a) |
It produces these files:
11.1.10 Reading/Writing Arrays to/from Binary Files
Performing input and output with arrays to and from binary files is much faster, and nearly always more space efficient, than I/O with text files. Binary I/O can be done either with just the raw bytes of numeric data in an array or the raw bytes plus additional metadata storing the array dimensions and data types. Here, we’ll cover the four combinations of reading and writing with and without metadata and describe how to manage little/big endian byte conversions.
11.1.10.1 Reading a Raw Array from a Binary File
MATLAB: | Python: |
---|---|
>> fh = fopen('f64800.bin'); >> sst = fread(fh, [360 ... 180], 'single'); >> fclose(fh); | In : sst = np.fromfile('f64800.bin', dtype=np.float32) In : sst = sst.reshape(360,180) |
Given the same binary file, Python and MATLAB will load the same 1D array of terms. However, if the terms are loaded into a 2D or higher-dimensional array, the row versus column-major ordering difference between the two languages means terms will be ordered differently. In the preceding example, MATLAB’s sst would have to be loaded like this to have sst(i+1,j+1) in MATLAB equal sst[i,j] in Python:
11.1.10.2 Writing a Raw Array to a Binary File
MATLAB: | Python: |
---|---|
>> sst = single(rand(360,180)) >> fh = fopen('f64800.bin','w'); >> fwrite(fh,sst,'single'); >> fclose(fh); | In : sst = np.random.rand(360, 180).astype(np.float32) In : sst.tofile('f64800.bin') |
11.1.10.3 Writing Arrays to a Binary File with Metadata
This creates files that are more convenient to use later because the data type and dimensions are also stored. The disadvantage is such file formats are then harder for other applications to deal with. MATLAB’s save() without arguments writes a binary file with one or more arrays (or other data structures). The saved file, in .mat format, includes MATLAB-specific headers and thus has more than a pure numeric representation of the array’s bytes.
MATLAB: | Python: |
---|---|
>> X=reshape([-4:4], 3,3)' X = -4 -3 -2 -1 0 1 2 3 4 >> Y = [pi e] Y = 3.1416 2.7183 >> save 'XY.mat' X Y | In : X = np.arange(-4,5).reshape(3,3) In : X array([[-4, -3, -2], [-1, 0, 1], [ 2, 3, 4]]) In : Y = np.array([np.pi, np.e]) In : Y Out: array([3.14159265, 2.71828183]) In : np.savez('XY.npz', X=X, Y=Y) |
Saving NumPy arrays with np.save() or np.savez() is more efficient and more portable across Python versions than using the pickle module (Section 7.13).
In Section 11.1.10.4, we’ll see that XY.mat and XY.npz can be loaded with MATLAB’s load() and NumPy’s np.loadz() functions, respectively.
Before loading the arrays back in, we’ll write another pair of binary files, this time saving only bytes of the numeric data. This is useful when writing data for subsequent ingest by code written in other languages—C, C++, Java, Fortran, and so on—that do not understand .mat or .npz formats. A pure binary representation of an array also makes it possible for MATLAB to read arrays written by Python to binary files (although the converse is unnecessary because Python, through the SciPy module, can read .mat files, ref. Section 7.14).
MATLAB: | Python: |
---|---|
>> fh = fopen('Xm.bin','wb'); >> fwrite(fh, X, 'double'); >> fclose(fh); >> ls -l X?.bin -rw-rw-r-- 1 al al 72 Nov 15 15:27 Xm.bin -rw-rw-r-- 1 al al 72 Nov 15 15:28 Xp.bin | In : X.tofile('Xp.bin') |
While both files are 72 bytes, as we’ll see in the next section, their contents are quite different (spoilers: MATLAB’s array is written column-wise, and Python’s is row-wise; MATLAB’s array is of type double, while Python’s is 64-bit integers).
11.1.10.4 Reading Arrays from a Binary File
MATLAB: | Python: |
---|---|
>> load XY % .mat is implied >> X X = -4 -3 -2 -1 0 1 2 3 4 >>Y Y = 3.1416 2.7183 | In : data = np.load('XY.npz') In : data['X'] array([[-4, -3, -2], [-1, 0, 1], [ 2, 3, 4]]) In : data['Y'] Out: array([3.14159265, 2.71828183]) |
Next, we’ll look at the raw binary files Xm.bin and Xp.bin we wrote in the previous section. As these contain only bytes of numeric data, they can be read by any programming language capable of reading a binary file and mapping the data to a numeric array. However, the lack of metadata means we need a priori knowledge of the number of dimensions, the size along each dimension, and the data type.
The Python-written file Xp.bin contains 64-bit (8-byte) signed integers (the default data type from np.arange() when start, end, and increments are integers).
The MATLAB-written file Xm.bin contains 64-bit (8-byte) floats (the MATLAB default for all numbers).
The struct format characters for 64-bit signed integers and 64-bit floats are “q” and “d”, respectively (Table 7-1).
Both files contain 2D arrays of size 3 × 3.
We’ll read each file in Python and MATLAB:
There’s our first surprise—Python loaded a transpose of the MATLAB data. This happens because MATLAB’s arrays are stored internally in column-major order, while NumPy arrays, by default, are stored row-major.
The MATLAB code to read the files is shown as follows. The second surprise appears if we forget the Python values are integers rather than doubles:
11.1.10.5 Endian Conversions
Little-to-big and big-to-little endian conversions are sometimes necessary when transferring numeric data between computers with different architectures. NumPy allows easy determination and manipulation of word endian format. MATLAB allows one to change an array’s endian order but not determine what its current order is.
NumPy arrays have an attribute, .dtype, which stores the data type such as np.float16 or np.complex256. The data type itself is a NumPy object with the interesting property that endianness can be determined from the .str representation of the data type—if it starts with >, it is big-endian; with < it is little-endian:
Endian conversion on I/O can be done directly in NumPy simply by providing np.tofile() or np.fromfile() the explicit dtype to read or write. Revisiting the sea surface temperature example of Section 11.1.10.2, this time explicitly writing and reading a big-endian file looks like this:
MATLAB: | Python: |
---|---|
>> a = int32([1234 5678]); >> b = swapbytes(a) 1×2 int32 row vector -771489792 773193728 >> dec2hex(a) 2×4 char array '04D2' '162E' >> dec2hex(b) 2×8 char array 'D2040000' '2E160000' | In : a = np.array([1234, 5678], dtype=np.int32) In : b = a.byteswap() In : b Out: array([-771489792, 773193728], dtype=int32) In : hex(a[0]),hex(a[1]) Out: ('0x4d2', '0x162e') In : hex(b[0]), hex(b[1]) Out: ('-0x2dfc0000', '0x2e160000') |
Python’s hex() function treats signed numbers differently than MATLAB’s dec2hex(), but we can at least tell from the integer representations that the same operation is being performed.
11.1.11 Primitive Array Operations
11.1.11.1 Addition, Subtraction
MATLAB: | Python: |
---|---|
>> a = [2, -3] a = 2 -3 >> b = [7; 8; 9] b = 7 8 9 >> a+b 9 4 10 5 11 6 | In : a = np.array([[2,-3]]) In : a Out: array([[ 2, -3]]) In : b = np.array([[7],[8],[9]]) In : b array([[7], [8], [9]]) In : a+b array([[ 9, 4], [10, 5], [11, 6]]) |
11.1.11.2 Elementwise Operations
MATLAB: | Python: |
---|---|
>> a = [2 7; 3 8; 4 9]; >> b = [5 -6; -1 2; -3 4]; >> a.^2 4 49 9 64 16 81 >> a.*b 10 -42 -3 16 -12 36 | In : a = np.array([[2,7],[3,8],[4,9]]) In : a**2 array([[ 4, 49], [ 9, 64], [16, 81]]) In : a*b array([[ 10, -42], [ -3, 16], [-12, 36]]) |
11.1.11.3 Bitwise Operations
Bit operations
Operation | MATLAB | Python |
---|---|---|
Left shift bits of x y times | bitshift(x,y) | x << y |
Right shift bits of x y times | bitshift(x,-y) | x >> y |
x AND y | bitand(x,y) | x & y |
x OR y | bitor(x,y) | x | y |
x XOR y | bitxor(x,y) | x ˆ y |
Flip bits of x | bitcmp(x) | ∼ x |
11.1.11.4 Transpose and Hermitian Operations
MATLAB: | Python: |
---|---|
>> a = [1 2; 3 4] >> a = [1 2 ; 3 4] a = 1 2 3 4 >> a' 1 3 2 4 >> a = a + j*eye(2) a = 1 + 1i 2 + 0i 3 + 0i 4 + 1i >> a' 1 - 1i 3 - 0i 2 - 0i 4 - 1i | In : a = np.array([[1,2],[3,4]]) In : a array([[1, 2], [3, 4]]) In : a.T array([[1, 3], [2, 4]]) In : a = a + 1j*np.eye(2) In : a array([[1.+1.j, 2.+0.j], [3.+0.j, 4.+1.j]]) In : a.T array([[1.+1.j, 3.+0.j], [2.+0.j, 4.+1.j]]) In : a.conj().T array([[1.-1.j, 3.-0.j], [2.-0.j, 4.-1.j]]) |
11.1.11.5 Array Multiplication
MATLAB: | Python: |
---|---|
>> a = [2 7; 3 8; 4 9] a = 2 7 3 8 4 9 >> b = [ 1 5] b = 1 5 >> a*b 37 43 49 | In : a = np.array([[2,7],[3,8],[4,9]]) In : a array([[2, 7], [3, 8], [4, 9]]) In : b = np.array([1,5]) In : b Out: array([1, 5]) In : a@b Out: array([37, 43, 49]) In : np.dot(a,b) Out: array([37, 43, 49]) |
MATLAB views all numeric values, whether they are scalars, one-dimensional vectors, or multidimensional matrices, as having at least two dimensions; MATLAB considers the preceding vector as having three rows and one column. In NumPy, one-dimensional arrays have just one dimension; concepts of “row” and “column” do not apply.
The * operator in Python performs an elementwise array product, like .* in MATLAB. This can be a source of bugs when translating MATLAB expressions to Python.
11.1.12 Adding Dimensions
MATLAB’s “all variables have at least two dimensions” property is useful when the distinction between a row and column vector is important. As a simple example, consider the vector products vTv and vvT. The first produces a scalar, while the second gives a square matrix with v’s dimension. MATLAB can express these products as v'*v and v*v'.
In contrast, a one-dimensional NumPy array has no knowledge of row or column orientation; v.T@v and [email protected] (or v.T.dot(v) and v.dot(v.T)) both return vT v. We need an extra dimension to compute the outer product.
By explicitly reshaping it
By passing it to np.atleast 2d()5
By indexing it with np.newaxis or None as an additional subscript
The last method using np.newaxis as a new index appears frequently when broadcasting arrays, a topic covered in the next section. As its name implies, np.newaxis brings into existence a new dimension with size 1 at the designated location. Indexing the one-dimensional array v in the preceding example as v[np.newaxis, :, np.newaxis] yields a 3D array with dimensions 1 × 3 × 1 array.
11.1.13 Array Broadcasting
Broadcasting in both MATLAB and Python refers to implicitly extending array dimensions by copying existing rows or columns (or submatrices) to satisfy rules for numeric operations. Specifically, an array can be broadcast to the needed shape if its trailing dimensions are one or match the trailing dimensions of the other arrays in the computation; the nonmatching dimensions are simply copied to inflate the array to the correct size. np.newaxis often plays a useful role as the last dimension since its size is 1 and therefore pairs with the last dimension of other arrays.
MATLAB: | Python: |
---|---|
>> a = [11 12]; >> b = [4 5 6]; >> a + b error: operator +: noncon- formant arguments (op1 is 1x2, op2 is 1x3) >> a' + b 15 16 17 16 17 18 >> a + b' 15 16 16 17 17 18 | In : a = np.array([11,12]) In : b = np.array([4, 5, 6]) In : a + b ValueError: operands could not be broadcast together with shapes (2,) (3,) In : a[:,np.newaxis] + b array([[15, 16, 17], [16, 17, 18]]) In : a + b[:,np.newaxis] array([[15, 16], [16, 17], [17, 18]]) |
11.1.13.1 Broadcasting Requires More Memory
Broadcasting—in MATLAB as in Python—is generally computationally efficient as it eliminates the need to write explicit loops. However, as more dimensions are copied, broadcasting can become a substantial memory and CPU sink. Say you have 1000 frames captured by a 4k camera where each frame has 4096 x 2160 pixels of uint32 values, and you need to multiply each frame’s pixels by a filter matrix. If you were to use broadcasting, a temporary array containing 1000 copies of the filter would be made first before the multiplication takes place.
Writing a loop to iterate over each frame may be slower but could save you from an out-of-memory error.
11.1.13.2 Broadcasting Example 1: Normalize Vectors
MATLAB: | Python: |
---|---|
>> V = [-7.72 -2.84 -7.55; ... 0.79 4.42 6.10; ... -9.99 4.24 9.29; ... 5.84 7.65 0.20; ... -6.85 3.73 7.56]; >> mag = vecnorm(V'); | In : V = np.array( [[-7.72, -2.84, -7.55], [ 0.79, 4.42, 6.10], [-9.99, 4.24, 9.29], [ 5.84, 7.65, 0.20], [-6.85, 3.73, 7.56]]) In : mag = np.linalg.norm(V,axis=1) |
MATLAB: | Python: |
---|---|
>> V = V./mag'; >> V -0.6914 -0.2544 -0.6762 0.1043 0.5835 0.8054 -0.6993 0.2968 0.6503 0.6067 0.7947 0.0208 -0.6306 0.3434 0.6960 | In : V = V/mag[:,np.newaxis] In : V array([[-0.6914, -0.2544, -0.6762], [ 0.1043, 0.5835, 0.8053], [-0.6993, 0.2968, 0.6504], [ 0.6067, 0.7947, 0.0208], [-0.6306, 0.3434, 0.6960]]) |
11.1.13.3 Broadcasting Example 2: The Distance Matrix
Section 11.12.2 shows how the Traveling Salesman Problem can be solved with the stochastic optimization technique of simulated annealing. The TSP cost function is the sum of distances between adjacent cities in the route. The cities’ locations remain constant, so we can speed up the inner loops of the TSP solvers by precomputing all city-to-city distances up front.
City locations on a Cartesian grid
city0 | city1 | city2 | city3 | |
---|---|---|---|---|
x | 56.2 | 27.7 | 96.1 | 51.7 |
y | 9.7 | 71.0 | 51.9 | 65.1 |
MATLAB: | Python: |
---|---|
>> x = [56.2 27.7 96.1 51.7] x = 56.200 27.700 96.100 51.700 >> x' 56.200 27.700 96.100 51.700 >> x' - x 0.00 28.50 -39.90 4.50 -28.50 0.00 -68.40 -24.00 39.90 68.40 0.00 44.40 -4.50 24.00 -44.40 0.00 | In : x = np.array([56.2,27.7,96.1,51.7]) Out: x array([56.2, 27.7, 96.1, 51.7]) In : x[:,np.newaxis] array([[56.2], [27.7], [96.1], [51.7]]) In : x[:,np.newaxis] - x array([[ 0. , 28.5, -39.9, 4.5], [-28.5, 0. , -68.4, -24. ], [ 39.9, 68.4, 0. , 44.4], [ -4.5, 24. , -44.4, 0. ]]) |
MATLAB: | Python: |
---|---|
>> (x' - x).^2 0.00 812.25 1592.01 20.25 812.25 0.00 4678.56 576.00 1592.01 4678.56 0.00 1971.36 20.25 576.00 1971.36 0.00 | In : (x[:,np.newaxis] - x)**2 array([[ 0. , 812.25, 1592.01, 20.25], [ 812.25, 0. , 4678.56, 576. ], [1592.01, 4678.56, 0. , 1971.36], [ 20.25, 576. , 1971.36, 0. ]]) |
MATLAB: | Python: |
---|---|
>> y = [9.7 71.0 51.9 65.1]; >> D = sqrt((x'-x).^2 + (y'-y).^2) D = 0.0000 67.6013 58.0762 55.5824 67.6013 0.0000 71.0166 24.7145 58.0762 71.0166 0.0000 46.3206 55.5824 24.7145 46.3206 0.0000 | In : y = np.array([9.7,71.0,51.9,65.1]) In : D = np.sqrt((x[:,np.newaxis]-x)**2 + ...: (y[:,np.newaxis]-y)**2) [[ 0. , 67.6013, 58.0762, 55.5824], [67.6013, 0. , 71.0166, 24.7145], [58.0762, 71.0166, 0. , 46.3206], [55.5824, 24.7145, 46.3206, 0. ]]) |
11.1.14 Index Masks
MATLAB and Python allow one to identify and operate on array terms according to the value of an index mask. In addition to working as filters, index masks help one write vectorizable code and therefore are powerful tools for achieving high performance.
MATLAB: | Python: |
---|---|
>> a = reshape(0:8, 3,3)' a = 0 1 2 3 4 5 6 7 8 >> mask=false(3,3); >> mask(2:end,2:end) = true; >> mask 3×3 logical array 0 0 0 0 1 1 0 1 1 >> a(mask)' 4 7 5 8 >> a(mask) = -a(mask) 0 1 2 3 -4 -5 6 -7 -8 | In : a = np.arange(9).reshape(3,3) In : a array([[0, 1, 2], [3, 4, 5], [6, 7, 8]]) In : mask = np.zeros((3,3), dtype=np.bool) In : mask[1:,1:] = True In : mask array([[False, False, False], [False, True, True], [False, True, True]]) In : a[mask] Out: array([4, 5, 7, 8]) In : a[mask] *= -1 In : a array([[ 0, 1, 2], [ 3, -4, -5], [ 6, -7, -8]]) |
11.1.14.1 Creating Index Masks
MATLAB: | Python: |
---|---|
>> a = -2:2 a = -2 -1 0 1 2 >> -1 < a 0 0 1 1 1 >> a < 2 1 1 1 1 0 >> (-1 < a) & (a < 2) 0 0 1 1 0 >> abs(a) > 1 1 0 0 0 1 >> (-1 < a) | (abs(a) > 1) 1 0 1 1 1 | In : a = np.arange(-2,3) In : a Out: array([-2, -1, 0, 1, 2]) In : -1 < a Out: array([False, False, True, True, True]) In : a < 2 Out: array([ True, True, True, True, False]) In : (-1 < a) * (a < 2) Out: array([False, False, True, True, False]) In : np.abs(a) > 1 Out: array([ True, False, False, False, True]) In : (-1 < a) + (np.abs(a) > 1) Out: array([ True, False, True, True, True]) |
11.1.14.2 Using Index Masks
MATLAB: | Python: |
---|---|
>> Lon = [40 -40 60 -120]; >> mask = Lon < 0; >> Lon(mask)=Lon(mask)+360 Lon = 40 320 60 240 | In : Lon = np.array([40, -40, 60, -120]) In : mask = Lon < 0 In : Lon[mask] += 360 In : Lon Out: array([40, 320, 60, 240]) |
MATLAB: | Python: |
---|---|
>> Lon = [40 -40 60 -120]; >> Lon(Lon<0)=Lon(Lon<0)+360 Lon = 40 320 60 240 | In : Lon = np.array([40, -40, 60, -120]) In : Lon[Lon < 0] += 360 In : Lon Out: array([40, 320, 60, 240]) |
11.1.15 Extracting and Updating Submatrices
MATLAB: | Python: |
---|---|
>> a = reshape([1:24], 4,6)' a = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 >> a(4:5,2:4) 14 15 16 18 19 20 >> a(4:5,2:4)=a(4:5,2:4)-20 a = 1 2 3 4 5 6 7 8 9 10 11 12 13 -6 -5 -4 17 -2 -1 0 21 22 23 24 | In : a = np.arange(1,25).reshape(6,4) In : a array([[ 1, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12], [13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]) In : a[3:5,1:4] array([[14, 15, 16], [18, 19, 20]]) In : a[3:5,1:4] -= 20 In : a array([[ 1, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12], [13, -6, -5, -4], [17, -2, -1, 0], [21, 22, 23, 24]]) |
MATLAB can just as easily use arbitrary index arrays instead of index slices to define our submatrix. NumPy makes this harder than it should be, though, because it permits only one dimension to be indexed by an array at a time. Say we wish to extract the 3 × 2 submatrix made from the third, fifth, and sixth rows and the second and fourth columns:
MATLAB: | Python: |
---|---|
>> a = reshape([1:24], 4,6)' a = 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 >> I = [3 5 6]; >> J = [2 4]; >> a(I,J) 10 12 18 20 22 24 | In : a = np.arange(1,25).reshape(6,4) In : a array([[ 1, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12], [13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]) In : I = [2, 4, 5] In : J = [1, 3] In : a[np.ix_(I,J)] array([[10, 12], [18, 20], [22, 24]]) |
Alternatively , we can use index chaining (Section 3.4.5) with a pair of single-dimension index arrays:
The notation a[I][:,J] is unintuitive though and is not encouraged.
11.1.16 Finding Terms of Interest
Both NumPy and MATLAB have functions useful for locating terms within an array that satisfy desired properties. The following sections will use the following 2D arrays to demonstrate searching for terms in an array with and without NaNs. (NaNs are used in fields such as the atmospheric sciences to represent unknown values, e.g., a satellite image of terrain may use NaNs to represent ground pixels hidden by clouds.)
MATLAB: | Python: |
---|---|
>> X = [0.22, -.47; ... 0.97, -.31; ... 0.32, 0.05]; >> Z = [0.22, -.47; ... 0.97, -.31; ... nan , 0.05]; | In : X = np.array( [[0.22, -.47,], [0.97, -.31,], [0.32, 0.05,]]) In : Z = np.array( [[0.22 , -.47,], [0.97 , -.31,], [np.NaN, 0.05,]]) |
11.1.16.1 Find the Smallest Term
MATLAB: | Python: |
---|---|
>> min(X,[],'all') -0.47000 >> min(Z,[],'all') -0.47000 | In : np.min(X) Out: -0.47 In : np.min(Z) Out: nan In : np.nanmin(Z) Out: -0.47 |
MATLAB: | Python: |
---|---|
>> [Rval, Rind] = min(X) Rval = 0.22000 -0.47000 Rind = 1 1 >> [Cval, Cind] = min(Rval) Cval = -0.47000 Cind = 2 >> R = Rind(Cind); C = Cind; >> R, C R = 1 C = 2 >> X(R,C) -0.47000 | In : lin_i = np.argmin(X) In : lin_i Out: 1 In : X.take(lin_i) Out: -0.47 In : R, C = np.unravel_index(lin_i, X.shape) In : R, C Out: (0, 1) In : X[R,C] Out: -0.47 |
The identical process works in MATLAB for the NaN-containing Z array. In Python, however, one must call np.nanargmin(Z) to arrive at –0.47. Calling np.argmin(Z) leads to the linear index of 4, corresponding to the NaN term, instead of 1.
One aspect of finding minimum values that MATLAB and Python share is that if the matrix has multiple copies of the minimum value, both languages return the location of the first occurrence.
11.1.16.2 Find the Largest Term
MATLAB: | Python: |
---|---|
>> [Rval, Rind] = max(X); >> [Cval, Cind] = max(Rval); >> R = Rind(Cind); C = Cind; >> R, C R = 2 C = 1 >> X(R,C) ns = 0.97000 | In : lin_i = np.argmax(X) In : X.take(lin_i) Out: 0.97 In : R, C = np.unravel_index(lin_i, X.shape) In : R, C Out: (1, 0) In : X[R,C] Out: 0.97 |
The call to argmax() in Python can also be written as a method call on the X array; lin_i = X.argmax() is equivalent to lin_i = np.argmax(X).
11.1.16.3 Find Term Nearest a Given Value
MATLAB: | Python: |
---|---|
>> delta = abs(X - (-0.5)); >> [Rval, Rind] = min(delta); >> [Cval, Cind] = min(Rval); >> R = Rind(Cind); C = Cind; >> R, C R = 1 C = 2 >> X(R,C) -0.47000 | In : delta = np.abs(X - (-0.5)) In : lin_i = delta.argmin() In : R, C = np.unravel_index(lin_i, X.shape) In : R, C Out: (0, 1) In : X[R, C] Out: -0.47 |
Once again, in Python the equivalent search over terms of the NaN-containing Z array requires nanargmin() instead of argmin(). The MATLAB code shown earlier works with both X and Z.
11.1.16.4 Find Term Nearest a Given Value in a Sorted 1D Array
A special case of finding the term closest to a given value is when the array to be searched is one-dimensional and already sorted. The sorted data permits bisection searches to locate the desired terms rapidly. A typical use case is working with data accumulated chronologically; subsequent analyses often need to know what data was recorded nearest to a given time T.
MATLAB has an undocumented (and not guaranteed to exist in future versions) bisection search capability oddly invoked as builtin('_ismemberhelper',A,S).6 However, it only works for exact, not nearest, matches. In this case, we’re no better off than the O(N ) min() method of Section 11.1.16.3.
MATLAB: | Python: |
---|---|
X = 1000:1700; for t = [1234.4, 1245.6] delta = abs(X - t); [val, ind] = min(delta); fprintf('%.2f ', X(ind)) end 1234.00 1246.00 | In : A = np.arange(1000.0, 1701.0) In : t = [1234.5, 1245.6] In : ind = A.searchsorted(t) In : ind Out: array([235, 246]) In : A[ind] Out: array([1235., 1246.]) |
11.1.16.5 Find Indices of Terms Satisfying a Condition
The locations of terms satisfying a condition can be found with NumPy’s argwhere() function which returns an array of indices for those matching terms. MATLAB’s find() function does the same thing, but it returns linear indexes into the array instead of index sets (e.g., (i,j) pairs for 2D arrays) as Python does.
MATLAB: | Python: |
---|---|
>> ind = find(abs(X) < .3) 1 6 | In : ind = np.argwhere(np.abs(X)<.3) In : ind array([[0, 0], [2, 1]]) |
MATLAB: | Python: |
---|---|
>> X(ind) 0.220000 0.050000 | In : X[ind] array([[[ 0.22, -0.47], # not [ 0.22, -0.47]], # what [[ 0.32, 0.05], # we [ 0.97, -0.31]]]) # wanted! In : X[tuple(ind[0])] Out: 0.22 In : X[tuple(ind[1])] Out: 0.05 |
Section 11.1.14.1 shows more complex array conditions that include numeric AND and OR operators.
11.1.17 Object-Oriented Programming and Computational Performance
Section 10.2 in the previous chapter mentioned object-oriented programming can have a negative impact on computational performance. To demonstrate this, we’ll create simulations of balls bouncing within a rectangular enclosure. One sim will use conventional procedure–based coding with all data stored in arrays, while the other uses object-oriented techniques. The simulation has two computationally intensive parts: determining collisions between the balls and updating velocity vectors for colliding ball pairs. Here, we’ll just implement the collision detection portion of the simulation.
If we store the ball’s coordinates and radii in conventional arrays, we can take full advantage of vectorization and easily compute collisions between a thousand balls at a rate of 60 Hz with both MATLAB and Python. Both languages take full advantage of broadcasting to compute the collision matrix, an N x N symmetric Boolean matrix which has 1 (MATLAB) or True (Python) at indices (i, j) and (i, j) if balls i and j are in contact for a simulation with N balls.
MATLAB: | Python: |
---|---|
>> run_sim(1000, 10) n balls=1000 n coll=29965 Hz = 59.29 n coll=28679 Hz = 47.92 n coll=29406 Hz = 45.44 n coll=30536 Hz = 62.94 n coll=28888 Hz = 62.21 n coll=29597 Hz = 62.99 n coll=29443 Hz = 62.64 n coll=29441 Hz = 63.83 n coll=30324 Hz = 65.24 n coll=29286 Hz = 63.38 | In : run_sim(1000, 10) n balls=1000 n coll=28836 Hz = 32.22 n coll=28728 Hz = 43.19 n coll=29613 Hz = 45.33 n coll=28559 Hz = 65.21 n coll=30694 Hz = 56.81 n coll=29023 Hz = 64.17 n coll=29755 Hz = 58.66 n coll=28404 Hz = 64.25 n coll=29196 Hz = 60.42 n coll=29824 Hz = 64.44 |
We’ll repeat the simulation, this time defining each ball as a Ball object (a variation of Circle that includes a collision detection function) and adding a class method to detect if the ball is in contact with another given ball.
MATLAB: | Python: |
---|---|
>> run_oo_sim(1000, 10) n balls=1000 n coll=29788 Hz = 0.77 n coll=29959 Hz = 0.84 n coll=29236 Hz = 0.85 n coll=29422 Hz = 0.85 n coll=30776 Hz = 0.85 n coll=29626 Hz = 0.86 n coll=30527 Hz = 0.83 n coll=28534 Hz = 0.85 n coll=29844 Hz = 0.85 n coll=30211 Hz = 0.85 | In : run_oo_sim(1000, 10) n balls=1000 n coll=28442 Hz = 0.96 n coll=28054 Hz = 0.97 n coll=29002 Hz = 0.96 n coll=29128 Hz = 0.97 n coll=28722 Hz = 0.91 n coll=30064 Hz = 0.93 n coll=29300 Hz = 0.94 n coll=28034 Hz = 0.93 n coll=28609 Hz = 0.93 n coll=30782 Hz = 0.94 |
11.2 Linear Algebra
MATLAB’s operators for linear algebra notation are more concise than NumPy’s or SciPy’s.
MATLAB has a uniform collection of linear algebra functions, while Python’s are spread across NumPy and SciPy, which have overlapping capabilities in identically named linear algebra submodules, numpy.linalg and scipy.linalg. In general, SciPy’s linear algebra offerings are more extensive than NumPy’s.
MATLAB’s functions apply to both dense and sparse matrices. In both NumPy and SciPy, one must use differently named functions to operate on sparse matrices. Python’s sparse matrix capability is discussed in Section 11.3.
11.2.1 Linear Equations
11.2.1.1 Ax = b
MATLAB: | Python: |
---|---|
>> A = [1,2,-3; ... 4,-5,6; ... -7,8,9]; >> b = [-1,0; 0,1; 1,0]; >> x = A x = -0.375000 0.175000 -0.250000 0.050000 0.041667 0.091667 | In : import numpy as np In : A = np.array([[1,2,-3], [4,-5,6], [-7,8,9]]) In : b = np.array([[-1,0], [ 0,1], [ 1,0]]) In : x = np.linalg.solve(A,b) In : x array([[-0.375 , 0.175 ], [-0.25 , 0.05 ], [ 0.04166667, 0.09166667]]) |
MATLAB: | Python: |
---|---|
>> A = [1,2,-3; ... 4,-5,6; ... -7,8,9]; >> [L, U, P] = lu(A) L = 1.00000 0.00000 0.00000 -0.14286 1.00000 0.00000 -0.57143 -0.13636 1.00000 U = -7.00000 8.00000 9.00000 0.00000 3.14286 -1.71429 0.00000 0.00000 10.90909 P = Permutation Matrix 0 0 1 1 0 0 0 1 0 | In : import scipy.linalg as spla In : A = np.array([[1,2,-3], [4,-5,6], [-7,8,9]]) In : LU, P = spla.lu_factor(A) In : LU, P (array([[-7. , 8. , 9. ], [-0.14285714, 3.14285714, -1.71428571], [-0.57142857, -0.13636364, 10.90909091]]), array([2, 2, 2], dtype=int32)) |
MATLAB: | Python: |
---|---|
>> b = [1; 3; 5]; >> x = U(L(P*b)) 9.750000000000000e-01 8.500000000000000e-01 5.583333333333333e-01 | In : b = np.array([1,3,5]) In : x = spla.lu_solve( (LU, P), b ) In : x Out: array([0.975 , 0.85 , 0.55833333]) |
11.2.2 Singular Value Decomposition
MATLAB: | Python: |
---|---|
>> A = [9 8 7; 5 4 3; ... -1 2 -1; -5 5 0]; >> [U,S,V] = svd(A,0) U = -8.9152e-01 4.7790e-02 2.0140e-01 -4.5163e-01 -1.9355e-02 -3.8250e-01 2.1957e-03 2.9513e-01 -8.6635e-01 3.4816e-02 9.5407e-01 2.5014e-01 S = 1.5615e+01 0 0 0 7.3818e+00 0 0 0 1.2919e+00 V = -6.6973e-01 -6.4105e-01 -3.7485e-01 -5.6100e-01 7.6749e-01 -3.1021e-01 -4.8656e-01 -2.5282e-03 8.7365e-01 | In : A = np.array([[9,8,7],[5,4,3], ...: [-1,2,-1],[-5,5,0.]]) In : U, S, V = np.linalg.svd(A, ...: full_matrices=False) In : U array([[-0.89152435, 0.04778986, 0.20140207], [-0.45162746, -0.01935495, -0.38250147], [ 0.00219575, 0.29512661, -0.86634739], [ 0.0348158 , 0.95406593, 0.25014403]]) In : S array([15.61537691, 7.38180298, 1.29189342]) In : V array([[-0.66973287, -0.56100049, -0.48655557], [-0.64105237, 0.76749298, -0.00252817], [-0.37484629, -0.3102144 , 0.87364597]]) |
MATLAB: | Python: |
---|---|
>> A - U*S*V' 7.1054e-15 1.7764e-15 4.4409e-15 2.6645e-15 1.7764e-15 1.3323e-15 2.2204e-16 1.5543e-15 -1.1102e-16 8.8818e-16 6.2172e-15 1.1657e-15 | In : A - [email protected](S)@V array( [[ 7.10542736e-15, 1.77635684e-15, 4.44089210e-15], [ 2.66453526e-15, 1.77635684e-15, 1.33226763e-15], [ 2.22044605e-16, 1.55431223e-15,-1.11022302e-16], [ 8.88178420e-16, 6.21724894e-15, 1.12317600e-15]]) |
11.2.3 Eigenvalue Problems
Capabilities of MATLAB, NumPy, and SciPy eigensolvers
dense | sparse | Ax = λx | Ax = λBx | asym. | full | partial | |
---|---|---|---|---|---|---|---|
MATLAB | |||||||
eig | • | • | • | • | • | • | |
eigs | • | • | • | • | • | • | |
numpy.linalg | |||||||
eig | • | • | • | • | |||
eigh | • | • | • | ||||
eigvals | • | • | • | • | |||
eigvalsh | • | • | • | ||||
scipy.linalg | • | • | • | ||||
eig | • | • | • | • | • | ||
eigh | • | • | • | • | |||
eigvals | • | • | • | • | • | ||
eigvalsh | • | • | • | • | |||
scipy.sparse.linalg | |||||||
eig | • | • | • | • | • | ||
eigsh | • | • | • | • |
MATLAB: | Python: |
---|---|
>> n = 3; >> ones = ones(n); >> off = 2*tril(ones,-2); >> A = (n+2)*eye(n) - ones + off A = 4 -1 -1 -1 4 -1 1 -1 4 >> [v,d] = eig(A) v = -0.5774 0.0000 0.7071 -0.5774 0.7071 0.7071 0.5774 -0.7071 -0.0000 d = 4.0000 0 0 0 5.0000 0 0 0 3.0000 >> A*v - v*d 1.0e-14 * -0.0444 0.0020 0 -0.2220 0.2665 0.1776 0.2220 -0.2665 -0.1228 | In : import numpy as np In : n = 3 In : ones = np.ones((n,n)) In : off = 2*np.tril(ones,-2) In : A = (n+2)*np.eye(n) - ones + off In : A Out: array([[ 4., -1., -1.], [-1., 4., -1.], [ 1., -1., 4.]]) In : [d,v] = np.linalg.eig(A) In : v Out: array( [[ 2.22045e-15, 5.77350e-01, 7.07107e-01], [ 7.07107e-01, 5.77350e-01, 7.07107e-01], [-7.07107e-01, -5.77350e-01, 4.44089e-16]]) In : d Out: array([5., 4., 3.]) In : A@v - [email protected](d) Out: array( [[-5.55111e-16, -4.44089e-16, 4.44089e-16], [ 4.88498e-15, 3.10862e-15, 2.22046e-15], [-1.77636e-15, -8.88178e-16, -4.44089e-16]]) |
Aside from Python’s capability fragmentation, solutions, computational performance, and accuracy are comparable those in MATLAB.
11.3 Sparse Matrices
Sparse matrices are central to many areas of numerical analysis including computational fluid dynamics, finite element analysis, network theoretic computations, and neural network computations for machine learning and artificial intelligence. SciPy’s sparse module provides sparse matrix capability to Python. Under the hood, sparse uses the C and Fortran libraries SuperLU and ARPACK to solve systems of equations and eigenproblems.
SciPy’s sparse matrix capability is good but not as convenient as MATLAB’s. MATLAB has two advantages: its sparse matrices are more fully integrated into the language, and there is a single sparse storage scheme which is both efficient and flexible. SciPy provides many sparse matrix operations, but its sparse matrices may be used in only a subset of NumPy functions. Also, before creating a sparse matrix in SciPy, one must decide which of the seven (!) storage formats to use. Each storage format has different indexing capabilities and computational performance. Conversion between formats is easy however.
11.3.1 Sparse Matrix Creation with COO, CSC, CSR
MATLAB: | Python: |
---|---|
>> x = [1 2 0; 0 3 0; 0 4 5] x = 1 2 0 0 3 0 0 4 5 >> y = sparse(x) y = (1,1) 1 (1,2) 2 (2,2) 3 (3,2) 4 (3,3) 5 | import scipy.sparse as sp In : x = np.array([[1, 2, 0], ...: [0, 3, 0],[0, 4, 5]]) In : x array([[1, 2, 0], [0, 3, 0], [0, 4, 5]]) In : y = sp.csr_matrix(x) In : print(y) Out (0, 0) 1 (0, 1) 2 (1, 1) 3 (2, 1) 4 (2, 2) 5 |
MATLAB: | Python: |
---|---|
>> I = [1 2 2 3 3]; >> J = [2 1 3 2 3]; >> V = [1 8 2 4 -3]; >> A = sparse(I,J,V,4,3) A = Compressed Column Sparse (rows = 3, cols = 3, nnz = 5 [56%]) (2, 1) -> 8 (1, 2) -> 1 (3, 2) -> 4 (2, 3) -> 2 (3, 3) -> -3 | import scipy.sparse as sp In : I = [0, 1, 1, 2, 2] In : J = [1, 2, 2, 1, 2] In : V = [1, 8, 2, 4, -3] In : A = sp.coo_matrix((V,(I,J)),(4,3)) In : A <4x3 sparse matrix of type '<class 'numpy.int64'>' with 5 stored elements in COOrdinate format> In : print(A) (0, 1) 1 (1, 2) 8 (1, 2) 2 (2, 1) 4 (2, 2) -3 |
MATLAB: | Python: |
---|---|
>> full(A) 0 1 0 8 0 2 0 4 -3 0 0 0 | In : A.todense() matrix([[ 0, 1, 0], [ 8, 0, 2], [ 0, 4, -3], [ 0, 0, 0]]) |
11.3.1.1 Repeated COO Indices
MATLAB: | Python: |
---|---|
. >> I = [1 1 2 2 ]; >> J = [1 2 2 2 ]; >> V = [1 3 10 -6 ]; >> A = sparse(I,J,V) A = Compressed Column Sparse (rows = 2, cols = 2, nnz = 3 [75%]) (1, 1) -> 1 (1, 2) -> 3 (2, 2) -> 4 | In : import scipy.sparse as sp In : I = [0, 0, 1, 1] In : J = [0, 1, 1, 1] In : V = [1, 3, 10, -6] In : A = sp.coo_matrix((V,(I,J))) In : A <2x2 sparse matrix of type '<class 'numpy.int64'>' with 4 stored elements in COOrdinate format> In : print(A) (0, 0) 1 (0, 1) 3 (1, 1) 10 (1, 1) -6 |
MATLAB: | Python: |
---|---|
>> A * [1; 1] 4 4 | In : A.dot([1,1]) Out: array([4, 4], dtype=int64) |
The Python repeated index terms can be explicitly summed with the sparse matrix object’s .sum_duplicates() method :
11.3.1.2 Extracting Indices and Values
MATLAB: | Python: |
---|---|
>> [I J V] = find(A); >> I' 1 1 2 >> J' 1 2 2 > V' 1 3 4 | In : A.sum_duplicates() In : A.row Out: array([0, 0, 1], dtype=int32) In : A.col Out: array([0, 1, 1], dtype=int32) In : A.data Out: array([1, 3, 4]) |
Without the call to .sum_duplicates(), the index and value attributes would have shown the repeated terms.
Only COO matrices have .row and .col attributes. However, conversion from other formats to COO via matrices’ .tocoo() methods is inexpensive. See Section 11.3.5.2 for a complete list of components available in the other formats.
11.3.1.3 Compressed Row, Column Storage
The Compressed Sparse Row (CSR) and Compressed Sparse Column (CSC) storage schemes allow more rapid matrix operations such as factoring and multiplication. Although their creation functions csr_matrix() and csc_matrix() can take the same arguments as coo_matrix(), it is usually faster to create matrices with coo_matrix() and then convert them to CSR or CSC formats using the matrices’ .tocsr() or .tocsc() methods. Duplicate indices are summed during these conversions.
While csr_matrix() and csc_matrix() can accept explicit row and column index arrays, one can directly supply the tighter index and index pointer arrays native to the CSR and CSC internal representations. For csr_matrix(), this means an array of column indices (J in the preceding example) and an array of pointers that contain counts of terms in each row. The array of pointers, indptr, are defined such that indices for row i are stored in J[indptr[i]:indptr[i + 1]] and their corresponding values are stored in data[indptr[i]:indptr[i + 1]]. These inputs repeat the earlier example:
indptr values for each row
Row | Column Indices | ||
---|---|---|---|
0 | J[ indptr[0]:indptr[1] ] | J[0:1] | [1] |
1 | J[ indptr[1]:indptr[2] ] | J[1:3] | [0,2] |
2 | J[ indptr[2]:indptr[3] ] | J[3:5] | [1,2] |
3 | J[ indptr[3]:indptr[4] ] | J[5:5] | [] |
To convert an array of row indices, I = [1,0,2,1,2] in our case, into index pointers, we can take advantage of a data container, Counter, from the collections module. A Counter takes an iterable such as a list or array and returns a dictionary whose keys are unique entries in the iterable and whose values are the number of times those entries appeared. If the dictionary is keyed by a nonexistent key, rather than raising a KeyError, it simply returns zero .
Index pointer inputs to csc_matrix() work the same way as for csc_matrix(), except one supplies explicit row indices and pointers to column indices instead of the other way around.
11.3.2 Sparse Matrix Creation with LIL, DOK
The three storage formats of the previous section, coordinate and compressed row and column, require explicit entries for each row index, column index, and value to be given up front. Rather than compute locations and values and then create the sparse matrix from these, sparse matrices can be built up incrementally. In addition, many engineering applications, notably finite element analysis, produce sparse matrices from many smaller submatrices. The resulting sparse matrix has small blocks of non-zero terms in adjacent rows and columns.
SciPy has three storage formats, list of lists (LIL) , dictionary of keys (DOK) , and block compressed row (BSR), that enable efficient incremental assembly, access, and manipulation of such structured sparse matrices.
into the second and third rows and columns.
MATLAB: | Python: |
---|---|
. >> A = sparse(4,3) >> A(1,2) = 1; >> A(2,1) = 8; >> A(2:3,2:3) = [0 2;4 -3]; >> A A = Compressed Column Sparse (rows = 4, cols = 3, nnz = 5 [42%]) (2, 1) -> 8 (1, 2) -> 1 (3, 2) -> 4 (2, 3) -> 2 (3, 3) -> -3 | import scipy.sparse as sp A = sp.lil_matrix( (4,3) ) A[0,1] = 1 A[1,0] = 8 A[1:3,1:3] = np.array([[0, 2], [4,-3]]) In : print(A) (0, 1) 1.0 (1, 0) 8.0 (1, 2) 2.0 (2, 1) 4.0 (2, 2) -3.0 |
MATLAB: | Python: |
---|---|
. >> A = sparse(4,3) >> A(1,2) = 1; >> A(2,1) = 8; >> A(2:3,2:3) = [0 2;4 -3]; >> A A = Compressed Column Sparse (rows = 4, cols = 3, nnz = 5 [42%]) (2, 1) -> 8 (1, 2) -> 1 (3, 2) -> 4 (2, 3) -> 2 (3, 3) -> -3 | import scipy.sparse as sp A = sp.dok_matrix( (4,3) ) A[0,1] = 1 A[1,0] = 8 A[1:3,1:3] = np.array([[0, 2], [4,-3]]) In : print(A) (0, 1) 1.0 (1, 0) 8.0 (1, 2) 2.0 (2, 1) 4.0 (2, 2) -3.0 |
MATLAB: | Python: |
---|---|
>> A(2:3,2:3) = ... A(2:3,2:3) + [0 2;4 -3]; | A = sp.dok_matrix( (4,3) ) A[1:3,1:3] += np.array([[0, 2], [4,-3]]) |
11.3.3 Sparse Matrix Creation with BSR, DIA
The two remaining sparse formats, block compressed row (BSR) and diagonal (DIA), are more specialized than the previous five and are therefore seen less frequently.
As its name suggests , the block compressed row format stores a collection of dense submatrices. It has drawbacks though: submatrices must be equally sized, and direct indexing is not supported. The BSR constructor function bsr_matrix() takes as input a 3D array of data blocks, and 1D arrays containing the block column indices, and pointers into the column indices for each row. This example creates an 8 x 6 sparse matrix from four 2 x 2 matrices—the sparse matrix then has four block rows and three block columns:
The indptr array is computed from block row indices (which would be I = [0,1,3,3] in this example) the same way indptr are created for csr_matrix() (refer to Section 11.3.1).
The diagonal storage format, DIA, is well suited for computational fluid dynamics problems that employ iterative solutions on finite difference grids. The dia_matrix() constructor resembles MATLAB’s spdiags() function :
Note that in both MATLAB and Python, terms of data are dropped to accommodate the size reduction as one moves away from the diagonal. In our case, –2 and –4 do not appear above the diagonal, and –13 does not appear below the diagonal.
As with BSR, DIA sparse matrices cannot be subscripted.
11.3.4 Test Matrices
A few examples of dense test matrices were presented in Section 11.1.6.14; here, we do the same for sparse matrices. As with the dense random matrices, the MATLAB sparse random matrices will not match the Python sparse matrices.
11.3.4.1 Random Sparse 4 × 3 Arrays with Density of 40%
MATLAB: | Python: |
---|---|
>> A = sprand(4,3,.4); >> full(A) 0.000 0.000 0.000 0.617 0.000 0.349 0.000 0.000 0.000 0.881 0.165 0.335 | In : A = sp.rand(4, 3, density=0.4, format='dok') In : A.todense() matrix([[0.688, 0.401, 0. ], [0. , 0. , 0. ], [0.337, 0. , 0. ], [0. , 0.023, 0. ]]) |
11.3.4.2 Random Sparse via I,J,V
MATLAB: | Python: |
---|---|
nR = 50; nC = 50; nNZ = 10*nC; I = ceil(nR*rand(nNZ,1)); J = ceil(nC*rand(nNZ,1)); V = ceil(nC*rand(nNZ,1)); A = sparse(I,J,V); spy(A) | import scipy.sparse as sp import numpy as np import matplotlib.pyplot as plt nR = 50 nC = 50 nNZ = 10*nC# number of non-zeros I = np.random.randint(0, nR, size=(nNZ,)) J = np.random.randint(0, nC, size=(nNZ,)) V = np.random.randint(0, nC, size=(nNZ,)) A = sp.coo_matrix((V,(I,J)),shape=(nR,nC)) plt.spy(A) plt.show() |
11.3.4.3 Arrow Matrix
MATLAB: | Python: |
---|---|
>> A = speye(4); >> A(1,:) = 1; >> A(:,1) = 1; >> full(A) 1 1 1 1 1 1 0 0 1 0 1 0 1 0 0 1 | In : A = sp.identity(4).todok() In : A[0,:] = 1 In : A[:,0] = 1 In : A.todense() matrix([[1., 1., 1., 1.], [1., 1., 0., 0.], [1., 0., 1., 0.], [1., 0., 0., 1.]]) |
11.3.5 Sparse Matrix I/O
MATLAB’s save and load commands make it easy to write sparse matrices to .mat files and to read them back. Unlike their NumPy counterparts, SciPy sparse matrices lack built-in methods for file input and output. Ironically, SciPy’s savemat() and loadmat() functions support reading and writing sparse matrices to and from MATLAB .mat files.
11.3.5.1 Write to MATLAB .mat Files
MATLAB: | Python: |
---|---|
>> A = sprand(4,3,.4); >> save A % to A.mat >> clear A >> whos A Name Size Bytes Class Attributes A 4x3 112 double sparse >> clear A >> load A % from A.mat >> whos A Name Size Bytes Class Attributes A 4x3 112 double sparse | In : A = sp.rand(4, 3, 0.4) In : A.todense() matrix([[0. , 0. , 0.264], [0. , 0. , 0. ], [0.334, 0.743, 0. ], [0.059, 0. , 0. ]]) In : import scipy.io as io In : io.savemat('A.mat',{'A':A}) In : A = None In : data = io.loadmat('A.mat') In : data['A'].todense() matrix([[0. , 0. , 0.264], [0. , 0. , 0. ], [0.334, 0.743, 0. ], [0.059, 0. , 0. ]]) |
This method is also useful for passing sparse matrices between MATLAB and Python.
11.3.5.2 Write Component Parts As NumPy Arrays
COO: A.data, A.row, A.col
CSR: A.data, A.indices, A.indptr
CSC: A.data, A.indices, A.indptr
LIL: A.data, A.rows (contains both row and column indices)
BSR: A.data, A.indices, A.indptr
DIA: A.data, A.offsets
DOK is unique in that its components are stored in a dictionary rather than NumPy arrays. Its entries can be retrieved with A.keys() and A.values(). DOK matrices can either be converted to other formats to save as NumPy arrays or written to pickle files as shown in the next section.
After loading the components, the matrix has to be regenerated by calling the appropriate constructor. The process looks like this for a COO matrix:
11.3.5.3 Write a Python Pickle Object
SciPy sparse matrices are conventional Python variables and can therefore be written to pickle files (ref. Section 7.13):
11.3.6 Linear Algebra
SciPy’s sparse matrix linear algebra functions are in the scipy.sparse.linalg module which must be imported independently from scipy.sparse itself.
As a representative matrix, we’ll use a finite element–based symmetric sparse matrix, fidap037,7 from the National Institute of Standards and Technology’s Matrix Market.8 It can be loaded into Python with scipy.io’s mmread() function and into MATLAB with mmread.m.9 SciPy’s mmread() returns a sparse matrix in coordinate form (COO). If the matrix is to be factored, it will need to be converted to a form such as CSC.
MATLAB: | Python: |
---|---|
>> K = mmread('fidap037.mtx'); >> [nR,nC] = size(K); >> b = ones(nR,2); >> b(:,2) = -1; | from scipy.io import mmread import scipy.sparse as sp import scipy.sparse.linalg as spla K = mmread('fidap037.mtx') # COO format K = K.tocsc() # for factorization later nR,nC = K.shape b = np.ones((nR,2)) b[:,1] = -1 |
Matrix summary
MATLAB: | Python: |
---|---|
>> size(K) 3565 3565 >> nnz(K) 67591 >> spy(K) >> condest(K) 226.3745 | In : K.shape Out: (3565, 3565) In : K.nnz Out: 67591 In : import matplotlib.pyplot as plt In : plt.spy(K,markersize=1) In : plt.show() In : spla.norm(K) Out: 1472.080010077052 |
Matrix-vector product
MATLAB: | Python: |
---|---|
>> Kb = K*b; >> Kb(191:195,:) 1.0000 -1.0000 13.4848 -13.4848 52.3418 -52.3418 1.0000 -1.0000 27.2083 -27.2083 | In : Kb = K*b In : Kb[190:195,:] Out: array([[ 1. , -1. ], [ 13.48478231, -13.48478231], [ 52.3417648 , -52.3417648 ], [ 1. , -1. ], [ 27.20825099, -27.20825099]]) |
Solution to linear equations
MATLAB: | Python: |
---|---|
>> x = K; >> x(501:505,:) 1.0000 -1.0000 0.0712 -0.0712 0.0453 -0.0453 1.0000 -1.0000 0.0421 -0.0421 >> [L,U,P,Q] = lu(K); >> x = Q*(U(L(P*b))); >> x(501:505,:) 1.0000 -1.0000 0.0712 -0.0712 0.0453 -0.0453 1.0000 -1.0000 0.0421 -0.0421 | In : x = spla.spsolve(K,b) In : x[500:505,:] Out: array([[ 1. , -1. ], [ 0.07122914, -0.07122914], [ 0.04526046, -0.04526046], [ 1. , -1. ], [ 0.04214178, -0.04214178]]) In : LU = spla.splu(K) In : x = LU.solve(b) In : x[500:505,:] Out: array([[ 1. , -1. ], [ 0.07122914, -0.07122914], [ 0.04526046, -0.04526046], [ 1. , -1. ], [ 0.04214178, -0.04214178]]) |
LU factorization performance is comparable between MATLAB and SciPy. Additionally, both have a collection of similarly named iterative solvers including conjugate gradient and minimum residual methods.
Solution to the standard eigenvalue problem
As indicated in Table 11-4, MATLAB’s eigs() function handles all forms of sparse eigensolutions with options controlled by various arguments. SciPy has two functions, eigs() and eigsh(), to handle asymmetric and symmetric inputs. Both support a shift-and-invert argument, sigma, to search for solutions near a desired eigenvalue region, and both support extracting a given number of eigenpairs.
MATLAB: | Python: |
---|---|
>> [vec,val] = eigs(K,10,-1); > format long e >> diag(val) 9.999999999999947e-01 9.999999999999947e-01 9.999999999999956e-01 9.999999999999969e-01 9.999999999999978e-01 1.000000000000000e+00 1.000000000000000e+00 2.319122761249388e+00 2.390762542515986e+00 2.500701541757246e+00 | In : val,vec = spla.eigsh(K,k=10,sigma=-1) In : val Out: array([1. , 1. , 1. , 1. , 1. , 1. , 2.31912276, 2.39076254, 2.50070154, 2.63542116]) |
Sturm sequence number
Neither MATLAB nor Python sparse eigensolvers can tell if the requested number of solutions is comprehensive, that is, if n eigensolutions were requested near a shift of σ, are the returned n eigenvalues the closest to σ, or were some omitted? The Sturm sequence number for the general eigenvalue problem Kx = λMx, removes guesswork from this question. This number is the count of negative values on the diagonal of the LU factor of K - σM, and it equals the number of eigenvalues below σ.
MATLAB: | Python: |
---|---|
>> sigma = 0.99; >> [L,U] = lu(K-sigma*eye(nR)); >> dU = diag(U); >> sum(dU < 0) 0 >> sigma = 1.01; >> [L,U] = lu(K - sigma*eye(nR)); >> dU = diag(U); >> sum(dU < 0) 1651 | In : sigma = 0.99 In : LU = spla.splu(K - sigma*sp.eye(nR)) In : dU = LU.U.diagonal() In : np.sum(dU < 0) Out: 0 In : sigma = 1.01 In : LU = spla.splu(K - sigma*sp.eye(nR)) In : dU = LU.U.diagonal() In : np.sum(dU < 0) Out: 1651 |
Therefore, we know Kx = λx has 1651 eigenvalues between 0.99 and 1.01.
Solution to the general eigenvalue problem
The general eigenvalue problem, Kx = λMx, arises in structural vibration and buckling problems, among others. MATLAB’s and SciPy’s sparse eigensolvers both accept a second matrix to solve such problems. We’ll manufacture a diagonal M matrix with values linearly spaced from 1 to 3565 to accompany our K.
MATLAB: | Python: |
---|---|
>> M = spdiags([1:nR]', 0, nR, nR); sigma = 1.8; >> [vec,val] = eigs(K,M,10,sigma); >> sort(diag(val)) 1.097093867635186e+00 1.129980804603547e+00 1.362226236223728e+00 1.593598298009953e+00 1.743881417538397e+00 1.837195925535993e+00 2.042699252311738e+00 2.171083950484060e+00 2.236999096156634e+00 2.411152544918247e+00 | M = sp.diags(np.arange(1,nR+1)) sigma = 1.8 val,vec = spla.eigsh(K,M=M, k=10,sigma=sigma) array([1.09709389, 1.12998084, 1.36222621, 1.59359813, 1.74388143, 1.83719593, 2.04269926, 2.1710838 , 2.23699914, 2.4111525 ]) |
11.3.7 Summary of Sparse Formats and Capabilities; Recommendations
Comparison of SciPy sparse storage schemes
Creation | Subscriptable | Change Sparse Structure | Matrix-Vector Product | Linear Algebra | |
---|---|---|---|---|---|
MATLAB | fast | yes | yes | fast | yes |
COO | fast | no | no | medium | no |
CSC/CSR Indices | fast | yes | yes | fast | yes |
CSC/CSR Subscripting | very slow | yes | yes | fast | yes |
LIL | slow | yes | fast | fast | yes |
DOK | very slow | yes | yes | fast | yes |
BSR | medium | no | no | fast | yes |
DIA | very fast | no | no | fast | yes |
Performance measurements suggest two of the seven formats, DOK and BSR, could be dropped from SciPy with no impact on capability. LIL does everything DOK can do, only faster, and BSR, despite being tailored for block matrices, is both slow and inflexible.
The most effective way to work with sparse matrices in Python is to create them as COO, CSC, or CSR matrices using index arrays. COO matrices can be created marginally more quickly than CSC or CSR, but COO matrices cannot be subscripted or used for linear algebra operations beyond matrix multiplication. Converting COO matrices to CSC or CSR can be done quickly though.
Subscripted growth, that is, creation with A[i,j] += v, should be avoided if performance is a concern. Conversion between formats is cheap, usually less than 1% of the effort it took to create the matrix, so if many indexed updates are necessary, it may pay off to convert a matrix from COO/CSR/CSC to LIL (which can do subscripted updates the fastest), make the subscripted update, then convert back to CSC or CSR.
One wonders if these format switches could be abstracted away to give a single, seamless sparse matrix experience—like MATLAB’s.
11.4 Interpolation
Numerical data must often be resampled at intervals different than originally recorded. This is especially true of observations taken from in situ measurements, test equipment, and so on.
While both NumPy and SciPy have interpolation functions, SciPy’s offer more capability.
11.4.1 One-Dimensional Interpolation
The following figure shows five points on the interval 0 to 1. We’ll demonstrate linear and spline interpolation methods to estimate values between known points. Extrapolation before the first and beyond the last points is best done with a curve fitting method such as linear regression (Section 11.5.1).
11.4.1.1 One-Dimensional Linear Interpolation
The interpolated points (x_desired, y_desired) are shown as blue plus signs:
The extrapolated values are merely copies of the first and last known points. This can be changed to other values (such as np.NaN) with the optional keyword arguments left= and right= to np.interp().
11.4.1.2 One-Dimensional Spline Interpolation
Options for kind in scipy.interpolate.interp1d()
kind | Description |
---|---|
'linear' | Linear; same as np.interp() |
'nearest' | Nearest neighbor |
'zero' | Zeroth-order spline |
'slinear' | First-order spline |
'quadratic' | Second-order spline |
'cubic' | Third-order spline |
'previous' | Value of the previous point |
'next' | Value of the next point |
interp1d()’s return value is a new function which takes the array of desired x values as its sole argument. The return value from this function is the y array for the given x’s. The code below uses the same x_known, y_known, and x_desired as in Section 11.4.1.1.
which yields
11.4.2 Two-Dimensional Interpolation
Two-dimensional interpolation is a common task in the fields of geographic information systems (GIS), mapping, and remote sensing. As with 1D interpolation, 2D interpolation takes values at known locations and returns estimated values at neighboring locations, typically a regularly spaced grid. Values on a regular grid are well suited for visualization as heat maps, contour plots, or map overlays.
As a simple example, let’s look at the function f(x, y) = x+ 2(cos 7x+ sin 5y)+ 3y sin 6x on the square bounded by −0.6 < x < 1 and −1 < y < 0.4. A contour plot of f(x, y) looks like this:
Both MATLAB and Python employ a function named griddata() to perform 2D interpolation of irregularly spaced data onto regular grids; Python’s exists in SciPy’s scipy.interpolate module. griddata()’s input arguments are the coordinates of the points at which we know data values, the data values themselves, then a pair of two-dimensional arrays holding the X and Y of coordinates at which we want the function to interpolate values. The X and Y arrays typically contain coordinates on a regularly spaced grid as returned by mgrid[] or np.meshgrid() (explained later); however, any arrangement of points may be employed. Points whose indices differ by one are assumed to be topologically adjacent for the interpolation computations.
MATLAB: | Python: |
---|---|
>> nX = 3; nY = 4; >> format short >> [X, Y] = meshgrid(... linspace(-.6, 1,nX),... linspace(-1 ,-.4,nY)) X = -0.6000 0.2000 1.0000 -0.6000 0.2000 1.0000 -0.6000 0.2000 1.0000 -0.6000 0.2000 1.0000 Y = -1.0000 -1.0000 -1.0000 -0.8000 -0.8000 -0.8000 -0.6000 -0.6000 -0.6000 -0.4000 -0.4000 -0.4000 | In : nX, nY = 3, 4 In : X, Y = np.meshgrid( ...: np.linspace(-.6, 1,nX), ...: np.linspace(-1 ,-.4,nY)) In : X array([[-0.6, 0.2, 1. ], [-0.6, 0.2, 1. ], [-0.6, 0.2, 1. ], [-0.6, 0.2, 1. ]]) In : Y array([[-1. , -1. , -1. ], [-0.8, -0.8, -0.8], [-0.6, -0.6, -0.6], [-0.4, -0.4, -0.4]]) |
MATLAB: | Python: |
---|---|
matlab code >> nX = 3; nY = 4; >> format short >> [X, Y] = meshgrid(... linspace(-.6, 1,nX),... linspace(-1 ,-.4,nY)) X = -0.6000 0.2000 1.0000 -0.6000 0.2000 1.0000 -0.6000 0.2000 1.0000 -0.6000 0.2000 1.0000 Y = -1.0000 -1.0000 -1.0000 -0.8000 -0.8000 -0.8000 -0.6000 -0.6000 -0.6000 -0.4000 -0.4000 -0.4000 | In : nX, nY = 3, 4 In : Y, X = np.mgrid[-1.0:-0.4:nY*1j, ...: -0.6: 1.0:nX*1j] In : X array([[-0.6, 0.2, 1. ], [-0.6, 0.2, 1. ], [-0.6, 0.2, 1. ], [-0.6, 0.2, 1. ]]) In : Y array([[-1. , -1. , -1. ], [-0.8, -0.8, -0.8], [-0.6, -0.6, -0.6], [-0.4, -0.4, -0.4]]) |
MATLAB: | Python: |
---|---|
% code/griddata/spiral_interp.m K = 0.005; % subsample f(x,y) at 114 points t = linspace(0,800,1100); X = [K*t.*cos(t)+1; K*t.*sin(t)-1]; in_range = (-0.6 < X(1,:)) & ... (X(1,:) < 1.0) & ... (-1.0 < X(2,:)) & ... (X(2,:) < 0.4); x_known = X(1,in_range); y_known = X(2,in_range); F_known = f(x_known, y_known); % make a 200 x 200 regular grid nX = 200; nY = 200; [X, Y] = meshgrid(... linspace(-.6, 1,nX),... linspace(-1 ,-.4,nY)); % interpolate to the regular grid F_near = griddata(... x_known, y_known, ... F_known, X,Y, 'nearest'); F_linear = griddata(... x_known, y_known,... F_known, X,Y, 'linear'); F_cubic = griddata(... x_known, y_known,... F_known, X,Y, 'cubic'); function [S] = f(x,y) S = x + 2*(cos(7*x) + ... sin(5*y)) + 3*y.*sin(6*x); end | # code/griddata/spiral_interp.py import numpy as np from scipy.interpolate import griddata def f(x,y): return x + 2*(np.cos(7*x)+ np.sin(5*y)) + 3*y*np.sin(6*x) K = 0.005 # subsample f(x,y) at 114 points t = np.linspace(0,800,num=1100) X = np.array([K*t*np.cos(t)+1, K*t*np.sin(t)-1]) in_range = (-0.6 < X[0,:]) * (X[0,:] < 1.0) * (-1.0 < X[1,:]) * (X[1,:] < 0.4) x_known = X[0,in_range] y_known = X[1,in_range] F_known = f(x_known, y_known) # make a 200 x 200 regular grid nX, nY = 200, 200 Y, X = np.mgrid[-1.0:-0.4:nY*1j, -0.6: 1.0:nX*1j] # interpolate to the regular grid F_near = griddata( (x_known, y_known), F_known, (X,Y), method="nearest") F_linear = griddata( (x_known, y_known), F_known, (X,Y),method='linear') F_cubic = griddata( (x_known, y_known), F_known, (X,Y),method='cubic') |
Which interpolation method is best? As with many problems, the answer is “it depends.” The cubic method most faithfully captures the gradients of the original function, especially where there is a concentration of known data points. However, both the cubic and linear methods fail to capture the local peak at the top edge.
This failure is caused by griddata()’s linear and cubic methods’ use of a Delaunay triangulation over the known data points. The triangulation fills a convex hull around the known points with elements, and any desired point inside the convex hull will receive an interpolated value from its bounding element. Points along the top and left edges are joined by thin slivers of elements that unhelpfully associate remotely spaced points.
11.4.3 Two-Dimensional Interpolation on a Grid
A simplified, but still common, form of two-dimensional interpolation takes evenly spaced values on a coarse grid and interpolates the values to a finer grid. In image processing, this interpolation is referred to as image zoom since the finer grid has many more data points (or pixels) than the original grid.
Both interp2 in MATLAB and griddata in Python, discussed in the previous section, can be used for this task. Both of those, however, require additional arguments that define the interpolation point positions. A simpler method is available in Python’s scipy.ndimage module. Its zoom() function needs only two arguments: the input array and the amount to grow (or reduce it by). By default, zoom() performs a cubic spline interpolation, but the spline order can be set between 0 and 5. The following example interpolates values of a 3 x 5 matrix to fill a 10 x 12 matrix.
The second argument describes the amount by which each dimension should grow. We want 10 rows instead of 3 and 12 columns instead of 5, so the scaling argument is [10/3, 12/5]:
If the scaling argument were a single value, both dimensions would be increased by the same factor.
The negative values are an artifact of the cubic spline that fits the step function across the 0/5 boundary. A bilinear interpolation can be done by setting (order=1):
11.5 Curve Fitting
11.5.1 Linear Regression
Linear regression finds the slope and vertical intercept of a line which best fits a collection of points. The “best fit” is defined as the line which minimizes the sum of the square of errors—the distances the points are away from the line. Motivations for solving linear regression problems generally fall into two categories: the need for just the slope and intercept of the best-fit line and the need for a comprehensive solution that returns error residuals, skew, kurtosis, and numerous other characteristic values.
In this section, we’ll concern ourselves with the simple version of the problem. The comprehensive solution, including weighted least squares, appears in Section 11.7.
MATLAB: | Python: |
---|---|
>> Pts = [7.312 15.878; 7.657 16.308; 7.934 16.690; 7.962 16.902; 8.614 17.013; 8.623 17.766]; >> polyfit(Pts(:,1),Pts(:,2),1) 1.1346 7.6638 | from numpy.polynomial.polynomial import polyfit In : Pts = np.array([ [ 7.312, 15.878], [ 7.657, 16.308], [ 7.934, 16.690], [ 7.962, 16.902], [ 8.614, 17.013], [ 8.623, 17.766]]) In : b, m = polyfit(Pts[:,0],Pts[:,1],1) In : m, b Out: (1.134557, 7.663752) |
11.5.2 Fitting Higher-Order Polynomials
The polyfit() function shown in the previous section can of course be used to find coefficients of best-fit polynomials with a degree greater than one. Here, we compute the coefficients of a cubic which best fits another collection of points. Note that in this case the points are not sorted in ascending value of X coordinate—point order is irrelevant.
MATLAB: | Python: |
---|---|
Pts = [-0.938 16.875; 0.326 21.290; 1.787 22.317; 2.968 28.767; 4.038 10.210; 5.358 -53.774]; >> polyfit(Pts(:,1),Pts(:,2),3) -1.356 4.306 2.967 15.960 | from numpy.polynomial.polynomial import polyfit In : Pts = np.array([ [ -0.938, 16.875], [ 0.326, 21.290], [ 1.787, 22.317], [ 2.968, 28.767], [ 4.038, 10.210], [ 5.358, -53.774]]) In : polyfit(Pts[:,0],Pts[:,1],3) Out: array([15.960, 2.967, 4.306, -1.356]) |
11.5.3 Fitting to Models
Few real-world datasets fit linear or polynomial models, at least not for wide ranges. More common distributions, at least among physical phenomena, are Gaussian, sinusoidal, and exponential or logarithmic decay or growth—or combinations of these.
The scipy.optimize module has a number of functions that can be used to fit models to data. Here, we’ll examine two of them, curve_fit() and differential_evolution(). MATLAB has a powerful Curve Fitting Toolbox, but for simpler curves the generic optimization function fminsearch() is often effective.
For such data we’ll need a hardier method. One such option is the differential evolution algorithm [3], implemented in scipy.optimize.differential_evolution(). It is less sensitive to local minima in the objective function and often finds excellent solutions over wide input domains. A MATLAB implementation of this algorithm is available on the FileExchange.10
11.5.3.1 Case 1: Logarithmic Growth
The points of Figure 11-3 were created with the function
f (t) = A ln Bt + C
using A = 5, B = 14, and C = -13.7 and then adding noise in the form of random values:
MATLAB: | Python: |
---|---|
% code/optim/log_test_data.py A = 5; B = 14; C = -13.7; Fn = @(t,A,B,C)(A*log(B*t) + C); rand('seed',234567); n_pts = 1000; t = 2 + linspace(.2,30,n_pts)+... .3*rand(1,n_pts); y_true = Fn(t, A, B, C); y_meas = y_true + 0.2*A*(0.5 -... rand(1,n_pts)); t_meas = t + 0.1*A*(0.5 -... rand(1,n_pts)); | #!/usr/bin/env python3 # code/optim/log_test_data.py import numpy as np A, B, C = 5, 14, -13.7 def Fn(t, A, B, C): return A*np.log(B*t) + C np.random.seed(234567) n_pts = 1000 t = 2 + np.linspace(.2,30,n_pts)+ .3*np.random.rand(n_pts) y_true = Fn(t, A, B, C) y_meas = y_true + 0.2*A*(0.5 - np.random.rand(n_pts)) t_meas = t + 0.1*A*(0.5 - np.random.rand(n_pts)) |
Our task is to determine values for A, B, and C using only the noisy data points and the model equation, f(t) = A ln Bt + C. Before we begin we'll need a way to assess how well a given set of A, B, and C values fit our model. A reasonable choice is the R2 coefficient of determination.
11.5.3.2 R2 to Score the Solution
A plot overlaying the noisy data on a curve of the model equation with the solution parameters will give us a subjective idea on how good the solution is. While the plot is useful, a numeric estimate of the “goodness of fit” is also valuable. On the Python side, we’ll use the r2_score() function from the sklearn.metrics (machine learning) module. It returns the R2 coefficient of determination between known and estimated values; 1.0 represents excellent agreement, and 0.0 (or worse, a negative score) means terrible agreement. MATLAB's Statistics and Machine Learning Toolbox has functions to compute R2. Implementations can also be found on the FileExchange.11
11.5.3.3 Fitting Logarithmic Growth with curve_fit()
The curve_fit() function has only three required arguments: the model function (Fn()), the independent variable (t_meas), and the dependent variable (y_meas). It returns two variables, popt, the optimal values of the parameters, and pcov, the estimated covariance of the optimal values.
MATLAB: | Python: |
---|---|
% code/optim/log_test_data_full.py A = 5; B = 14; C = -13.7; Fn = @(t,A,B,C)(A*log(B*t) + C); rand('seed',234567); n_pts = 1000; t = 2 + linspace(.2,30,n_pts)+... .3*rand(1,n_pts); y_true = Fn(t, A, B, C); y_meas = y_true + 0.2*A*(0.5 -... rand(1,n_pts)); t_meas = t + 0.1*A*(0.5 -... rand(1,n_pts)); opt_Fn = @(x)sum_sq(x,t_meas,y_meas); popt = fminsearch(opt_Fn,[1 1 1]) function ss = sum_sq(x,t,ydata) A = x(1); B = x(2); C = x(3); ss = sum((ydata - A*log(B*t) + C).^2); end | In : run log_test_data.py In : from scipy.optimize import curve_fit In : from sklearn.metrics import r2_score In : popt, pcov = curve_fit(Fn,t_meas,y_meas) In : popt Out: array([ 5.00663437, 12.05087493, -12.99449958]) In : r2_score(y_meas, y_curve_fit) Out: 0.9920846142243493 |
MATLAB’s results aren’t great. They are also sensitive to the initial guess:
The Python R2 score of 0.99 is excellent. Our estimates of A = 5.007, B = 12.05, and C = –12.99 are reasonably close to the true values of 5, 14, and –13.7; more impressively, the resulting curve matches the true curve so closely as to be indistinguishable from it:
11.5.3.4 Case 2: Sinusoidal Decay
More complex models require increasingly more fiddling with curve_fit()’s options such as parameter bounds, initial values, uncertainty estimates, and Jacobian matrix. Instead we’ll switch to the more robust (but more computationally expensive) differential_evolution() function.
We’ll apply this model of an underdamped system:
f(t) = Ae−Bt sin Ct + D (11.1)
to estimate values of A, B, C, and D that best fit the points shown in Figure 11-4. The points in that figure are made with the following code. It begins with known values for A…D and then adds noise in the form of random values to the model function.
11.5.3.5 Fitting Sinusoidal Decay with curve_fit()
Sending our noisy data and model function to curve_fit() leads to an unsatisfactory result.
The R2 score of 0.03 means our solution is worthless. The green curve of Figure 11-5 in the next section shows exactly how bad it is.
11.5.3.6 Fitting Sinusoidal Decay with differential_evolution()
Rather than attempting to solve this more challenging problem with MATLAB’s fminsearch(), the recipe in Section 11.6 shows how to solve the curve fit in MATLAB using SciPy’s differential_evolution(). Of course, if the FileExchange implementation of differential_evolution() is available to you, it will be a more convenient alternative than using Python. It goes without saying that the Curve Fitting Toolbox is better still.
11.6 Recipe 11-1: Curve Fitting with differential_evolution()
The Curve Fitting Toolbox is the package of choice for determining equations of best-fit curves to data. In its absence, differential_evolution() from SciPy’s optimize module is a more powerful alternative than fminsearch from the core MATLAB product. Here, we demonstrate using the Python implementation of differential_evolution() with MATLAB to find the coefficients A, B, C, and D of the decaying sinusoid function, as was done using only Python in Section 11.5.3.6.
This recipe reveals a limitation of MATLAB’s py module : MATLAB functions passed as arguments to Python functions cannot be called from Python. This prevents us from calling optimizers such as differential_evolution() directly from MATLAB since their first argument is the function to be minimized or maximized.
There are at least two possible solutions: (1) write the cost function in Python and perform the optimization entirely in a Python bridge module or (2) call the MATLAB cost function from Python through the matlab.engine module (Section 6.10). The second method is much more powerful since it should be able to work with any MATLAB function. In practice, though, Python calls to matlab.engine objects frequently fail with the MATLAB error “failed to connect to matlab.engine: Unable to connect to MATLAB session.” For this reason, only the first method is demonstrated.
When evaluating the cost function in Python, the MATLAB portion merely passes data points to a Python bridge module which contains both the cost function and the call to the optimizer. A function in the bridge module then returns the best-fit coefficients to MATLAB.
The following MATLAB driver sends parameter bounds and test data to the Python compute_ABCD() function which returns a dictionary containing the solution—if one is found:
The solution from MATLAB matches the Python solution of Section 11.5.3.6:
11.7 Regression
11.7.1 Ordinary Least Squares
The Python module statsmodels has its own statistics-flavored linear regression, or least squares, solvers that return richer results than NumPy’s polyfit() function described in Section 11.5.1. The primary functions are OLS(), for ordinary least squares, and WLS(), for weighted least squares.
MATLAB: | Python: |
---|---|
% create random X,Y m = 3.77; b = -5.5; nPts = 20; X = -5 + 12*rand(nPts,1); X = sort(X); noise = 8*(rand(nPts,1)-.5); Y = m*X + b + noise; % recover m and b mb = polyfit(X,Y,1); m_ls = mb(1); b_ls = mb(2); fprintf("ML : %f %f ", ... m_ls,b_ls) Y_pred = m_ls*X + b_ls; plot(X,Y,'g.') hold on plot(X,Y_pred,'r-') grid ---------- Output: ML : 3.558505 -4.843925 | import numpy as np import statsmodels.api as stats import matplotlib.pyplot as plt # create random X,Y m = 3.77 # true slope b = -5.5 # true intercept nPts = 20 X = -5 + 12*np.random.rand(nPts) X.sort() noise = 8*(np.random.rand(nPts)-.5) Y = m*X + b + noise % recover m and b Xb = stats.add_constant(X,0) ols = stats.OLS(Y, Xb).fit() m_ls, b_ls = ols.params print(f'OLS {m_ls} {b_ls}') Y_pred = ols.predict(Xb) plt.plot(X,Y,'g.') plt.plot(X,Y_pred,'r-') plt.grid(True) plt.show() ---------- Output: OLS 3.86476 -6.23757 |
The MATLAB and Python implementations will see different point pairs, so we do not expect their respective solutions for m and b to match for small sample sizes. If we were to use 1000 points instead of 20, both Python and MATLAB will return values within 1% of the true values.
11.7.2 Weighted Least Squares
Data points may have disproportionate importance—imagine a measurement campaign performed with different models of test equipment. Values observed with more accurate equipment should be accorded greater significance when aggregate properties of all points are computed.
MATLAB can perform weighted least squares fits with the fittype() and fit() functions from the Curve Fitting Toolbox or the nlinfit() function from the Statistics and Machine Learning Toolbox. Python’s statsmodule solves such problems with WLS(), which takes the same inputs as OLS() shown in the previous section plus additional argument for weights.
The following example repeats the ordinary least squares example with weights assigned to the points linearly from left to right. Point size represents weight in the following plot:
Solve the weighted least squares problem in MATLAB with the recipe in Section 11.8.
11.7.3 Confidence and Prediction Intervals
The coefCI() function from MATLAB’s Statistics and Machine Learning Toolbox returns confidence intervals for coefficients of linear regression models . Plotting these takes a bit of extra effort though. In the absence of this toolbox, one can make confidence interval plots quite easily with the regplot() function from the Seaborn Python module.
This code repeats a portion of the ordinary least squares problem, increases the noise, and adds more points:
Prediction intervals are arguably more useful than confidence intervals. They can be computed with the predint() function in MATLAB’s Curve Fitting Toolbox or with the predband() function [1] in Python, shown as follows:
11.8 Recipe 11-2: Weighted Least Squares in MATLAB
A direct invocation of Python’s weighted least squares function from MATLAB leads to a disappointing result. Here is such an attempt using the example problem of the previous section:
The reason for the failure is the wls object returned by py.statsmodels.api.WLS() lacks a .params attribute—as well as many other attributes and methods—so the attempt to access the nonexistent .params throws an error.
We’ll need a small bridge module which contains a function that takes primitive MATLAB inputs, in this case the X, Y, and W arrays, calls statsmodels.api.WLS(), then returns desired portions of the solution in a form MATLAB understands. In other words, the call to WLS() happens entirely in Python without a traversal of the MATLAB/Python interface. The entire module defines only one function in ten lines:
The returned dictionary is recast by py2mat() as a struct containing the slope (.m), intercept (.b), and array of predicted y values (.Y_predict). This MATLAB code calls Python’s weighted least squares correctly:
The Python and MATLAB codes for this example create points randomly, so their locations on the two plots will differ.
11.9 Recipe 11-3: Confidence and Prediction Intervals in MATLAB
The following MATLAB programs replicate the confidence and prediction interval plots shown in Section 11.7.3. The only catch is Seaborn wants NumPy arrays, so X and Y are explicitly converted from MATLAB-native to Python-native variables with the mat2py() utility.
Confidence interval:
Prediction interval:
The following program imports both model_function() and predband() from the pure Python version of the prediction interval example:
11.10 Finding Roots
11.10.1 Univariate
The root_scalar() function from SciPy’s optimization module is the analog to MATLAB’s fzero() function. root_scalar() offers eight algorithms for finding roots of a scalar equation: bisect, brentq, brenth, ridder, toms748, newton, secant, halley. The first five require limits on the independent variable, while the newton, secant, and halley methods work on unbounded domains. Additional information such as a starting guess or a slope function may be provided to accelerate convergence. If no method is specified, the solver chooses the best option for the given inputs (whether or not bounds were specified, slope function provided, and so on).
MATLAB: | Python: |
---|---|
>> F = @(x)(2*x.^3-7*x.^2+12*x-4); >> fzero(F, 0) 4.265215061673047e-01 | In : import numpy as np In : import scipy.optimize as so In : def F(x): In : return 2*x**3-7*x**2+12*x-4 In : x = np.linspace(-1,2, num=100) In : f_zero = so.root_scalar(F, ...: bracket=[0, 1]) In : f_zero Out[14]: converged: True flag: 'converged' function_calls: 9 iterations: 8 root: 0.42652150616730566 In : f_zero.root Out: 0.42652150616730566 |
MATLAB: | Python: |
---|---|
>> F = @(x)(2*x.^3-7*x.^2+12*x-4); >> fzero(F, 100) 4.265215061673049e-01 | In : f_zero = so.root_scalar(F, x0=100, ...: x1=200, method="secant") Out[25]: converged: True flag: 'converged' function_calls: 25 iterations: 24 root: 0.4265215061673047 |
11.10.2 Multivariate
MATLAB can solve this with fsolve() from the Optimization Toolkit. Alternatively, MATLAB can use scipy.optimize.root() to find the solution; this is demonstrated with the recipe in Section 11.11.
Selecting a starting point far from the solution can make the method fail:
11.11 Recipe 11-4: Solving Simultaneous Nonlinear Equations
The plane/sphere/ellipsoid intersection problem of Section 11.10.2 can be solved in MATLAB with SciPy’s multivariate root finder. As with the curve fitting recipe (Section 11.6), we will need to implement the function to be solved in Python because Python cannot evaluate MATLAB functions passed as arguments.
Here’s the bridge module. The function that wraps the call to the solver, F_roots(), uses (0,0,0) as a default starting point if one isn’t given:
Calling it from MATLAB is simple:
Once more, this time supplying a bad starting location:
11.12 Optimization
SciPy’s scipy.optimize module has many functions to solve optimization problems. We already saw one of its most powerful global optimizers, scipy.optimize.differential_evolution(), in action Section 11.5.3. Here, we’ll examine a wider scope of problems including local uni- and multivariate optimization with linear and nonlinear constraints, linear programming, and combinatorial optimization for problems such as route planning.
11.12.1 Linear Programming
Linear programming (LP) problems are ubiquitous in industrial engineering, operations research, logistics, and scheduling. MATLAB’s Optimization Toolbox and SciPy have a function named linprog() that can solve real-valued LP problems. More difficult mixed-integer LP problems require modules not found in SciPy; for these we’ll turn to PuLP, which can be added to Anaconda installations via conda install.
Computer categories and properties
Cores | Power [kW] | Volume [ft3] | Charge Back [$/hr] | |
---|---|---|---|---|
Small | 4 | 50 | 10 | 0.10 |
Medium | 8 | 105 | 20 | 0.60 |
Large | 12 | 192 | 40 | 0.96 |
Huge | 48 | 391 | 40 | 2.83 |
The total power consumption is less than 30,000 kW.
The total volume needed to house the servers is less than 80,000 ft3.
The aggregate count of compute cores exceeds 1000.
The number of cores in Large servers exceeds the count of cores in Huge servers.
The objective function is to maximize the charge-back
maximize 0.1S + 0.6M + 0.96L + 2.83H
As the values for S … H can only take on integer values, this is an integer-valued LP problem. Nonetheless, we’ll see how close the real-valued solutions of SciPy’s linprog() function match fully integer-based solutions from PuLP.
11.12.1.1 SciPy linprog()
Calling arguments to SciPy’s linprog() are nearly identical to those for the Optimization Toolbox’s version. Inputs are coefficients of the function to minimize, matrices of coefficients that describe the inequality relationships, equality relationships (if such exist), and upper and lower bounds. Inequality relationships must be expressed as less-than inequalities with independent variables on the left and constant values on the right. Greater-than relationships can be remapped to less-than by changing signs on both sides.
The left-hand side coefficients define the matrix A, the right-hand side terms define b, and the coefficients of the cost function define f. A final constraint is that we seek only positive solutions so the first value in each of the four (lower, upper) bound tuples in variable UL_bounds is zero.
The Python solution looks like this:
The function returns an object, sol in our example, with these attributes:
A solution status of 0 indicates a successful convergence, and the solution to our independent variables is in attribute .x. The closest integer values to our solution leaves us with S = 0, M = 0, L = 104, H = 26, which corresponds to an aggregate charge value of $172.42 per hour.
If you don’t have the Optimization Toolbox, you can still solve the problem in MATLAB by calling SciPy’s linprog():
Running the preceding code gives the correct solution, but, frustratingly, the solution components are not accessible in MATLAB:
MATLAB: | Python: |
---|---|
LP = py.importlib.import_module(... 'bridge_linprog'); py.importlib.reload(LP); f = [ -0.1; -0.6; -0.96; -2.83]; A = [ 50 105 192 391; ... 10 20 40 40; ... -4 -8 -12 -48; ... 0 0 -12 48 ]; b = [ 30000; 80000; -1000; 0]; UL_bounds = { py.tuple({0, py.None}), ... py.tuple({0, py.None}), ... py.tuple({0, py.None}), ... py.tuple({0, py.None}) }; py_sol = LP.solver(f,A,b,pyargs(... 'bounds',py.list(UL_bounds))); m_sol = py2mat(py_sol); | # file bridge_linprog.py from scipy.optimize import linprog def solver(f,A,b,bounds=None): sol = linprog(f,A,b,bounds=bounds) return { 'fun' : sol.fun, 'message' : sol.message, 'status' : sol.status, 'success' : sol.success, 'x' : sol.x} |
Now the five components we captured in the bridge module can be used in MATLAB:
11.12.1.2 PuLP
The Python PuLP module has two appealing aspects: one can define variables as being integer or real, and constraints can be expressed in a more natural form than other modules:
11.12.2 Simulated Annealing
Simulated annealing is a stochastic global optimization method based on the concept of crystal formation in metals as temperature drops. Briefly, the algorithm makes a random guess at a solution, then either uses a perturbation of that solution for the next iteration or makes a completely new guess. The decision as to whether to perturb the solution or make a new guess depends on the solution’s cost and how far along the “annealing schedule” the algorithm has progressed. Early on, a guess that is worse than its predecessor will likely be accepted, but as iterations progress and the “temperature” drops, improvements to the solution are more likely to survive to the next round.
A class which inherits from simanneal’s Annealer class and provides a constructor (the __init__() function), a move() function which perturbs an existing solution, and an energy() function which evaluates the solution’s cost function
An initial guess to the solution
An annealing schedule
To illustrate how these three items might be implemented, we’ll solve the Traveling Salesman Problem (TSP) using a simplification of the TSP solution given in simanneal’s documentation.12
The Annealer-derived class is defined with lines 12–22. Our problem’s cost function is defined with the energy() method on line 20. The .state attribute is the collection of variables we wish to optimize—in our case, it is a list of city indices that defines the route. For us, the cost of a solution is the sum of distances between adjacent cities in the route. Here, we access the .distance_matrix attribute which is a look-up table of distances between cities; it is an N x N matrix where the term at index (i, j) is the distance between cities i and j. The distance matrix is populated in the class’s constructor at line 13. Aside from adding .distance_matrix as an attribute, the constructor is entirely boilerplate. Finally, the move() method at line 16 defines how to apply a random perturbation to the current state, in other words, how to make a random change to our route.
The compute_distances() function at line 24 computes the distance matrix using the broadcasting technique mentioned in Section 11.1.13.
Lines 31–34 define 80 cities arranged on an 8 x 10 grid. The simplicity of this layout makes it easy to identify optimal solutions.
The annealing schedule is defined on line 42. The argument tsp.auto(minutes=0.5) tells the module to estimate schedule parameters so that a solution is returned in roughly 30 seconds of elapsed time. Alternatively, you can control the parameters yourself with
In practice, the automatic estimator provides an ideal balance between convenience, solution fitness, and knowledge of how long you’ll need to wait for an answer. If your solution isn’t sufficiently optimal, simply increase the amount of time you’ll let the annealer to run (via the minutes parameter to tsp.auto()).
11.13 Differential Equations
SciPy’s collection of differential equation solvers is comparable to MATLAB’s family of ode*() routines. The calling method is quite different though. MATLAB’s routines return the full solution (if one can be found), while SciPy’s return an iterator that must be called in a loop.
MATLAB: | Python: |
---|---|
% file: code/ode/duffing.m Time = [0 40]; U_0 = [0; 0]; c = 0.1; k = 0.7; k3 = 1; A = 44.5; omega = 1.1; options = odeset('RelTol',1e-7); [t,u] = ode45(@(t,u)Duff(... t,u,k,c,k3,A,omega), ... Time,U_0,options); figure plot(u(:,1),u(:,2)); xlabel('Position'); ylabel('Velocity'); title('Duffing oscillator') function u = Duff(t,u,k,c,k3,A,omega) u = [u(2); -k*u(1)-c*u(2)- ... k3*u(1)^3+A*sin(omega*t)]; end | # file: code/ode/duffing.py import numpy as np from scipy.integrate import RK45 import matplotlib.pyplot as plt def Duff(t,u,k=0.7,c=0.1,k3=1, A=44.5,omega=1.1): return [u[1], -k*u[0]-c*u[1] - k3*u[0]**3+A*np.sin(omega*t)] T0, Tn = 0, 40 U_0 = [0, 0] t, u = [], [] sol = RK45(Duff, T0, U_0, Tn, rtol=1e-7) while sol.t < Tn: t.append(sol.t) u.append([sol.y[0], sol.y[1]]) sol.step() u = np.array(u) plt.plot(u[:,0], u[:,1]) plt.xlabel('Position'); plt.ylabel('Velocity'); plt.title('Duffing oscillator'); plt.show() |
The single line call to ode45() on the left corresponds to a call to RK45() followed by a while loop that successively calls the solver object’s .step() method in Python. Both solvers are configured to return solutions with a relative accuracy of 10−7, but MATLAB needs 5093 iterations, while Python only does 1002. A phase space plot of the solution shows good agreement:
11.14 Symbolic Mathematics
SymPy code generators
Language | sympy.printing Function |
---|---|
C | ccode() |
C++ | cxxcode() |
Fortran | fcode() |
GLSL | glsl_code() |
JavaScript | jscode() |
Julia | julia_code() |
Maple | maple_code() |
Mathematica | mathematica_code() |
MATLAB | octave_code() |
Python | pycode() |
R | rcode() |
Rust | rust_code() |
The examples shown in this section barely scratch the surface of SymPy’s power.
11.14.1 Defining Symbolic Variables
Variables are identified in SymPy by importing them from the sympy.abc module. For example, the expression from sympy.abc import x, y, phi identifies x, y, and phi as variables to be treated symbolically.
11.14.2 Derivatives
and MATLAB code for the input and output functions can be produced like so:
11.14.3 Integrals
is found with
11.14.4 Solving Equations
describe a circle and a line. The following code solves for the points of intersection (x, y) in terms of R, m, and b:
11.14.5 Linear Algebra
11.14.5.1 Perform a Symbolic LU Factorization of
The matrix
is factored to lower and upper triangular matrices using exact arithmetic with the following code:
11.14.5.2 Symbolically Solve Ax = b
This series of three equations and three unknowns
is solved symbolically for x with Gauss-Jordan elimination using this code:
11.14.6 Series
can be found:
11.15 Recipe 11-5: Using SymPy in MATLAB
The series summation from Section 11.14.6 can be done from MATLAB after defining symbolic variables and SymPy-specific constants for i and π. We’ll prefix all Python variables with “p” to avoid name collisions with built-in MATLAB variables even though in this case only pi would have caused a collision.
The output from s.doit() is a Python variable within MATLAB; to get a MATLAB character string containing the solution, we merely need to access the Python solution’s .char attribute :
11.16 Recipe 11-6: Compute Laplace Transforms
11.17 Unit Systems
The Python module pint allows one to assign units to numeric scalars and arrays, much like the similar capability in MATLAB’s Symbolic Toolbox. Once units are assigned to variables, attempts to mix terms with incompatible dimensions in a computation—for example, adding an area to a temperature–raise a DimensionalityError. Mixing terms with compatible dimensions but different unit systems—adding a length in meters to a length in inches—works correctly because the appropriate scale factors are applied first.
In both pint and the Symbolic Toolbox, one must first initialize the units object (u in the following examples for both languages). Subsequent variables can receive units by multiplying a numeric value or variable by one of the units object’s many attributes. Here, we show how one can convert metric values for aluminum 6061’s15 modulus of elasticity and density from the metric system to the US customary system.
Both pint and the Symbolic Toolbox support hundreds of base and derived units; complete lists can be found at https://github.com/hgrecco/pint/blob/master/pint/default_en.txt and www.mathworks.com/help/symbolic/units-list.html.
Units can be assigned to a NumPy array; operations on individual terms or slices from that array will work only with unitless or other consistent unit variables. It is not possible to create a NumPy array with mixed units, though. This precludes the use of pint in applications that employ arrays whose rows or columns have different unit types. One such example is finite element matrices that contain both linear and rotational stiffnesses.
11.17.1 Defining Units in pint
Arbitrary units can be defined easily. In the next example, currency units of “dollar” and “euro” are defined. From these, prices per unit area are defined and then applied to a collection of real estate lots with four properties per lot. Finally, the price of each collection is given in dollars and euros. Note that pint does not automatically simplify the compound units when the cost vector is computed; we have to explicitly convert to dollars or euros:
The pint documentation says adding units to variables causes a performance hit when performing computations with those variables. That’s true each time pint has to check units. If the variable is a NumPy array, though, this units check is done only once for the entire array, not for each term in the array. By and large, programs whose performance is dominated by numeric array computations run as quickly with units as without.
11.18 Recipe 11-7: Using pint in MATLAB
The units example of Section 11.17 requires some modification for use in MATLAB. The primary difference is that units cannot be accessed from the registry variable using dot notation. Instead, we have to use the .Units() accessor function. The “meter” unit, for example, must be referenced in MATLAB as u.Unit('meter') instead of just u.meter as it is in Python. Also note that the a matrix is defined as the transpose of a 4 × 5 to get the same row-major values as Python.
The numeric value of the Python units array can be converted to a MATLAB matrix like this:
11.19 References
- [1]
John Hedengren. Programming for Engineers: Regression Statistics with Python. 2021. URL: https://apmonitor.com/che263/index.php/Mai/PythonRegressionStatistics
- [2]
MathWorks Support Team. “How do I perform a binary search of a presorted array?” In: (June 2017). URL: www.mathworks.com/matlabcentral/answers/92533-how-do-i-perform-a-binary-search-of-a-presorted-array
- [3]
Storn, R.; Price, K. (1997). “Differential evolution – a simple and efficient heuristic for global optimization over continuous spaces.” Journal of Global Optimization. 11 (4): 341–359. doi:10.1023/A:1008202821328