In [ ]:

```
# Let printing work the same in Python 2 and 3
from __future__ import print_function
```

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.

In [ ]:

```
import numpy as np
```

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.

In [ ]:

```
a = np.array([1, 2, 3])
print(a.shape)
print(a.size)
print(a.ndim)
```

In [ ]:

```
x = np.arange(100)
print(x.shape)
print(x.size)
print(x.ndim)
```

In [ ]:

```
y = np.random.rand(5, 80)
print(y.shape)
print(y.size)
print(y.ndim)
```

How to change the shape of an array without a copy!

In [ ]:

```
x.shape = (20, 5)
print(x)
```

NumPy can even automatically figure out the size of at most one dimension for you.

In [ ]:

```
y.shape = (4, 20, -1)
print(y.shape)
```

In [ ]:

```
# Scalar Indexing
print(x[2])
```

In [ ]:

```
# Slicing
print(x[2:5])
```

In [ ]:

```
# 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])
```

In [ ]:

```
# Boolean Indexing
print(x[(x % 2) == 0])
```

In [ ]:

```
# Fancy Indexing -- Note the use of a list, not tuple!
print(x[[1, 3, 8, 9, 2]])
```

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.

In [ ]:

```
print("Shape of X:", x.shape)
print("Shape of Y:", y.shape)
```

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.

In [ ]:

```
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))
```

Another example of broadcasting two 1-D arrays to make a 2-D array.

In [ ]:

```
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)
```

In [ ]:

```
# 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)
```