<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 NumPy
The NumPy package provides the "ndarray" object. The NumPy array is used to contain data of uniform type with an arbitrary number of dimensions. NumPy then provides basic mathematical and array methods to lay down the foundation for the entire SciPy ecosystem. The following import statement is the generally accepted convention for NumPy.
Step1:  Array Creation
There are several ways to make NumPy arrays. An array has three particular attributes that can be queried
Step2:  Array Manipulation
How to change the shape of an array without a copy!
Step3:  NumPy can even automatically figure out the size of at most one dimension for you.
Step4:  Array Indexing
Step5:  Broadcasting
Broadcasting is a very useful feature of NumPy that will let arrays with differing shapes still be used together. In most cases, broadcasting is faster, and it is more memory efficient than the equivalent full array operation.
Step6:  Now, here are three identical assignments. The first one takes full advantage of broadcasting by allowing NumPy to automatically add a new dimension to the left. The second explicitly adds that dimension with the special NumPy alias "np.newaxis". These first two creates a singleton dimension without any new arrays being created. That singleton dimension is then implicitly tiled, much like the third example to match with the RHS of the addition operator. However, unlike the third example, the broadcasting merely re-uses the existing data in memory.
Step7:  Another example of broadcasting two 1-D arrays to make a 2-D array.
<ASSISTANT_TASK:>
Python Code:
import numpy as np
Explanation: NumPy
The NumPy package provides the "ndarray" object. The NumPy array is used to contain data of uniform type with an arbitrary number of dimensions. NumPy then provides basic mathematical and array methods to lay down the foundation for the entire SciPy ecosystem. The following import statement is the generally accepted convention for NumPy.
End of explanation
a = np.array([1, 2, 3])
print(a.shape)
print(a.size)
print(a.ndim)
x = np.arange(100)
print(x.shape)
print(x.size)
print(x.ndim)
y = np.random.rand(5, 80)
print(y.shape)
print(y.size)
print(y.ndim)
Explanation: Array Creation
There are several ways to make NumPy arrays. An array has three particular attributes that can be queried: shape, size and the number of dimensions.
End of explanation
x.shape = (20, 5)
print(x)
Explanation: Array Manipulation
How to change the shape of an array without a copy!
End of explanation
y.shape = (4, 20, -1)
print(y.shape)
Explanation: NumPy can even automatically figure out the size of at most one dimension for you.
End of explanation
# Scalar Indexing
print(x[2])
# Slicing
print(x[2:5])
# Advanced slicing
print("First 5 rows\n", x[:5])
print("Row 18 to the end\n", x[18:])
print("Last 5 rows\n", x[-5:])
print("Reverse the rows\n", x[::-1])
# Boolean Indexing
print(x[(x % 2) == 0])
# Fancy Indexing -- Note the use of a list, not tuple!
print(x[[1, 3, 8, 9, 2]])
Explanation: Array Indexing
End of explanation
print("Shape of X:", x.shape)
print("Shape of Y:", y.shape)
Explanation: Broadcasting
Broadcasting is a very useful feature of NumPy that will let arrays with differing shapes still be used together. In most cases, broadcasting is faster, and it is more memory efficient than the equivalent full array operation.
End of explanation
a = x + y
print(a.shape)
b = x[np.newaxis, :, :] + y
print(b.shape)
c = np.tile(x, (4, 1, 1)) + y
print(c.shape)
print("Are a and b identical?", np.all(a == b))
print("Are a and c identical?", np.all(a == c))
Explanation: Now, here are three identical assignments. The first one takes full advantage of broadcasting by allowing NumPy to automatically add a new dimension to the left. The second explicitly adds that dimension with the special NumPy alias "np.newaxis". These first two creates a singleton dimension without any new arrays being created. That singleton dimension is then implicitly tiled, much like the third example to match with the RHS of the addition operator. However, unlike the third example, the broadcasting merely re-uses the existing data in memory.
End of explanation
x = np.arange(-5, 5, 0.1)
y = np.arange(-8, 8, 0.25)
print(x.shape, y.shape)
z = x[np.newaxis, :] * y[:, np.newaxis]
print(z.shape)
# More concisely
y, x = np.ogrid[-8:8:0.25, -5:5:0.1]
print(x.shape, y.shape)
z = x * y
print(z.shape)
Explanation: Another example of broadcasting two 1-D arrays to make a 2-D array.
End of explanation
<END_TASK>