<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Fundamentals of NumPy
This notebook aims to quickly walk you through the most fundamental bits of NumPy, including
Step1:  1. ndarray in NumPy
NumPy’s main object is the homogeneous multidimensional array (ndarray). It is a table of elements (usually numbers), all of the same type, indexed by a tuple of positive integers.
A list of elements can be expressed as an ndarray of rank 1, i.e. a 1D array; a matrix can be expressed as an ndarray of rank 2.
Here lists some important attributes of ndarray
Step2:  2. Array Creation
There are several ways to create ndarrays.
2.1 Using the array function
You can create a 1D ndarray from an existing list/array easily using the array function.
Step3:  Note that there is only one argument. So never do this
Step4:  To create a matrix, call array on a sequence of sequence.
Step5:  2.2 Using zeros, ones, empty
When the contents of the array to be created are unknown, but its dimensions are known, use one of zeros, ones, empty.
Step6:  The default dtype is numpy.float64 for these functions.
2.3 Using arange, linspace
Akin to range in Python, arange in NumPy returns a sequence of numbers in a ndarray. Use the dtype parameter to change the type, or use astype() function to cast into another type.
Step7:  Due to the finite precision of floating point numbers, however, it's better to use linspace when we are trying to create a sequence of floating point numbers, specifying how many elements we want, instead of the step.
Step8:  3. Playing with the Shapes of ndarrays
Step9:  3.1 How to Get the Shape of an ndarray
Step10:  3.2 How to Reshape the ndarray
Step11:  The reshape function returns a new ndarray with the shape changed without modifying the original one.
Step12:  To directly modify the shape of an ndarray
Step13:  Note that 1d arrays and 2d arrays are different.
Step14:  4. Basic Operations
4.1 Arithmetic Operators
Arithmetic operators on ndarrays apply elementwise (so the operation is vectorized). A new ndarray will be created to hold the result.
Step15:  4.2 Dot Products
Given two ndarrays with proper shapes
Step16:  To calculate the dot product, the sentence in the following cell is intuitive but WRONG
Step17:  To correctly calculate the dot products of two ndarrays, use numpy.dot or the dot function on the ndarray object.
Step18:  Both ways create a new ndarray to hold the results without modifying the original ones.
4.3 Norms
See https
Step19:  4.4 Matrix multiplication
$$
\begin{bmatrix}
  1 & 2\
  3 & 4
\end{bmatrix}
\begin{bmatrix}
  -1 & 1\
  2 & 1
\end{bmatrix}
=
%% do it column wise
\begin{bmatrix}
\left(
    -1 \cdot
    \begin{bmatrix}
      1 \
      3 
    \end{bmatrix}
    + 2 \cdot
    \begin{bmatrix}
      2\
      4
    \end{bmatrix}
\right)
& 
\left(
    1 \cdot
    \begin{bmatrix}
      1 \
      3 
    \end{bmatrix}
    + 1 \cdot
    \begin{bmatrix}
      2\
      4
    \end{bmatrix}
\right)
\end{bmatrix}
=
\begin{bmatrix}
  3 & 3\
  5 & 7
\end{bmatrix}
$$
Since Python 3.5, we can use the infix operator * for element-wise multiplication, and the infix operator @ for matrix multiplication.
Step20:  To solve $\mathbf{A z = B}$, we have $\mathbf{z = A^{-1} B}$
<ASSISTANT_TASK:>
Python Code:
import numpy as np
Explanation: Fundamentals of NumPy
This notebook aims to quickly walk you through the most fundamental bits of NumPy, including:
1. how to create/initiate 1D arrays and 2D matrices,
2. how to get/set the shape of numpy arrays,
3. and how to calculate the dot product of two NumPy arrays, and take the norms.
This notebook is created by cherry-picking from the official documents of NumPy. Please refer to that page for more information.
End of explanation
arr = np.array(range(1, 10))
print('arr\t\t', arr)
print('arr.ndim\t', arr.ndim)
print('arr.shape\t', arr.shape)
print('arr.size\t', arr.size)
print('arr.dtype\t', arr.dtype)
Explanation: 1. ndarray in NumPy
NumPy’s main object is the homogeneous multidimensional array (ndarray). It is a table of elements (usually numbers), all of the same type, indexed by a tuple of positive integers.
A list of elements can be expressed as an ndarray of rank 1, i.e. a 1D array; a matrix can be expressed as an ndarray of rank 2.
Here lists some important attributes of ndarray:
ndarray.ndim: the rank of the ndarray. For instance, a matrix has a rank of 2.
ndarray.shape: the dimensions of the ndarray as a tuple of integers. For an array having 10 elements, the shape is (10,) (note the trailing comma, not the same as (10, 1)); for a matrix having 20 rows and 30 columns, the shape is (20, 30).
ndarray.size: the total number of elements in the ndarray, which is equal to the product of the dimensions.
ndarray.dtype: an object describing the type of the elements in the array.
End of explanation
np.array([1, 2, 3, 4])
Explanation: 2. Array Creation
There are several ways to create ndarrays.
2.1 Using the array function
You can create a 1D ndarray from an existing list/array easily using the array function.
End of explanation
# Do not do this
#np.array(1, 2, 3, 4)
Explanation: Note that there is only one argument. So never do this:
End of explanation
x = np.array([[1, 2], [3, 4], [5, 6]])
x
x.shape
Explanation: To create a matrix, call array on a sequence of sequence.
End of explanation
np.zeros((2, 3))  # the elements are explicitly initialized to zeros
np.ones((2, 3))  # the elements are explicitly initialized to ones
np.empty((2, 3))  # the elements are not explicitly initialized; expect random values
Explanation: 2.2 Using zeros, ones, empty
When the contents of the array to be created are unknown, but its dimensions are known, use one of zeros, ones, empty.
End of explanation
np.arange(1, 3, 0.2)
Explanation: The default dtype is numpy.float64 for these functions.
2.3 Using arange, linspace
Akin to range in Python, arange in NumPy returns a sequence of numbers in a ndarray. Use the dtype parameter to change the type, or use astype() function to cast into another type.
End of explanation
np.linspace(1, 3, 7)
Explanation: Due to the finite precision of floating point numbers, however, it's better to use linspace when we are trying to create a sequence of floating point numbers, specifying how many elements we want, instead of the step.
End of explanation
arr = np.arange(1, 10)
arr
Explanation: 3. Playing with the Shapes of ndarrays
End of explanation
arr.shape
Explanation: 3.1 How to Get the Shape of an ndarray
End of explanation
arr.reshape((3, 3))
Explanation: 3.2 How to Reshape the ndarray:
End of explanation
arr
arr.reshape((9,1))
Explanation: The reshape function returns a new ndarray with the shape changed without modifying the original one.
End of explanation
arr.shape = (3, 3)
arr
Explanation: To directly modify the shape of an ndarray:
End of explanation
xx = arr.reshape((1,9))
xx
xx.shape
xx[0]
arr
arr.shape
Explanation: Note that 1d arrays and 2d arrays are different.
End of explanation
arr = np.array([1, 2])
arr + 1
arr * 2
arr ** 2
Explanation: 4. Basic Operations
4.1 Arithmetic Operators
Arithmetic operators on ndarrays apply elementwise (so the operation is vectorized). A new ndarray will be created to hold the result.
End of explanation
a = np.array([1, 2])
a
b = np.array([[1], [2]])
b
Explanation: 4.2 Dot Products
Given two ndarrays with proper shapes:
End of explanation
a * b  # calculating elementwise product!
Explanation: To calculate the dot product, the sentence in the following cell is intuitive but WRONG:
End of explanation
a.dot(b)
np.dot(a, b)
Explanation: To correctly calculate the dot products of two ndarrays, use numpy.dot or the dot function on the ndarray object.
End of explanation
a = np.arange(3)-1
a
np.linalg.norm(a)
np.linalg.norm(a, ord=1)
np.linalg.norm(a, ord=np.inf)
Explanation: Both ways create a new ndarray to hold the results without modifying the original ones.
4.3 Norms
See https://docs.scipy.org/doc/numpy/reference/generated/numpy.linalg.norm.html for more details.
End of explanation
A = np.array([[1,2],[3,4]])
B = np.array([[-1,1],[2,1]])
print(np.matmul(A, B)) # preferred over np.dot()
print(A @ B) # A and B must be numpy arrays
print(A * B)
print(B @ A)
x = np.array([1, 2]).reshape((2,1))
print(A @ x)   
Explanation: 4.4 Matrix multiplication
$$
\begin{bmatrix}
  1 & 2\
  3 & 4
\end{bmatrix}
\begin{bmatrix}
  -1 & 1\
  2 & 1
\end{bmatrix}
=
%% do it column wise
\begin{bmatrix}
\left(
    -1 \cdot
    \begin{bmatrix}
      1 \
      3 
    \end{bmatrix}
    + 2 \cdot
    \begin{bmatrix}
      2\
      4
    \end{bmatrix}
\right)
& 
\left(
    1 \cdot
    \begin{bmatrix}
      1 \
      3 
    \end{bmatrix}
    + 1 \cdot
    \begin{bmatrix}
      2\
      4
    \end{bmatrix}
\right)
\end{bmatrix}
=
\begin{bmatrix}
  3 & 3\
  5 & 7
\end{bmatrix}
$$
Since Python 3.5, we can use the infix operator * for element-wise multiplication, and the infix operator @ for matrix multiplication.
End of explanation
z = np.linalg.inv(A) @ B
z
A @ z
Explanation: To solve $\mathbf{A z = B}$, we have $\mathbf{z = A^{-1} B}$
End of explanation
<END_TASK>