Basic objects and NumPy arrays

Author

Karl Gregory

Basic Python objects

The basic Python objects are the list, the tuple, and the dictionary.

The Python list

The list is the basic type of “container” object in Python. We can create one by typing values within square brackets, separated by commas:

x = [1,2,3]
type(x)
list
y = ["a", "b", "c"]
type(y)
list
z = [True, False, False, True]
type(z)
list

Note that when accessing list elements, the first entry is indexed at 0 (different from R!):

y[1]
'b'
y[0]
'a'

We may find it useful at this point to note that the print function in Python is in some ways like the paste() function in R.

print("The first value in y is '",y[0],"'.",sep="")
The first value in y is 'a'.

You can also concatenate strings using +, as below:

"The first value is y is " + y[0] + "."
'The first value is y is a.'

We can coerce a numeric value to a string with the str function:

str(x[0])
'1'

You can make a list with a sequence of values, for example, like this:

u = list(range(0,10,2))
u
[0, 2, 4, 6, 8]

Note that for any Python function, you can type ?function to read the help documentation.

A Python list does not have to have elements all of one type:

v = [True, 0, "whatever",[1,2]]
type(v[2])
str

In Python we can assign values to multiple objects in one like of code like this:

t, u, v = 3, [4,5], "hello"
print(t)
print(u)
print(v)
3
[4, 5]
hello

We can edit a value in a list as follows:

tuv = [t,u,v]
print(tuv)
tuv[2] = 'good-bye'
print(tuv)
[3, [4, 5], 'hello']
[3, [4, 5], 'good-bye']

To see that lists are fundamentally quite different objects from vectors in R, look what happens if you try the arithmetic operation * on a list:

x*2
[1, 2, 3, 1, 2, 3]

The Python tuple

A tuple is rather like a list, but cannot be edited after it is created. In Python-speak tuples are “immutable”. This makes them more efficient users of a computer’s memory.

We make a tuple with ordinary parentheses:

salutations = ('hey', 'hi', 'ahoy', 'sup')
salutations
('hey', 'hi', 'ahoy', 'sup')

We access elements of a tuple just as we would the elements of a list:

salutations[0]
'hey'

If we try to edit a value in tuple we get an error message.

We will mostly use tuples used as arguments in Python functions.

We can turn a tuple into a list with the list() function:

list(salutations)
['hey', 'hi', 'ahoy', 'sup']

The Python dictionary

The Python dictionary object is a collection of what are called key-value pairs. You can access a value in a dictionary by giving the key. Here is an example:

latin = {'sine qua non' : 'essential part',
         'pax vobiscum' : 'peace be with you'}

print(latin['pax vobiscum'])
peace be with you

But it doesn’t have to be so dictionary-like as that. Here is another example:

stat_540 = {'nstudents' : 29,
            'time' : '1:15 - 2:30 pm',
            'days' : ['Tuesday', 'Thursday'],
            'instructor' : 'Dr. G',
            'location' : 'LeConte College 103'}

print(stat_540['time'])
print(stat_540['nstudents'])
print(stat_540['days'])
1:15 - 2:30 pm
29
['Tuesday', 'Thursday']

We can create dictionaries as above or with the dict function:

vehicle = dict(make = 'Honda', 
               model = 'Odyssey', 
               issues = ['slow tire leak', 'vexingly indecisive check engine light'], 
               miles = 249000, 
               col = 'Aggie Maroon')
vehicle
{'make': 'Honda',
 'model': 'Odyssey',
 'issues': ['slow tire leak', 'vexingly indecisive check engine light'],
 'miles': 249000,
 'col': 'Aggie Maroon'}

We can edit values in a dictionary like this:

vehicle['col'] = 'Gamecock Garnet'
vehicle
{'make': 'Honda',
 'model': 'Odyssey',
 'issues': ['slow tire leak', 'vexingly indecisive check engine light'],
 'miles': 249000,
 'col': 'Gamecock Garnet'}

While we can use Python lists and dictionaries for many things, for our Statistics and Data Science applications we will more often use what are called NumPy arrays…

NumPy arrays

NumPy arrays are more like the vectors and matrices we have encountered in R. They must contain values all of one type. “NumPy” refers to a library of functions written for Python. To have access to these functions we need to import this library. When you import a library into Python, you specify an alias or a name by which you can refer to the library. It looks like this:

import numpy as np

Now all the functions in the NumPy library will be available to us, and we can invoke these functions with the prefix np. Here is an example:

x = np.array([0,1,2])
x
array([0, 1, 2])

We accessed the function array belonging to the NumPy library by typing np.array, where np is the alias we have given to the NumPy library. You could give it some alias other than np, but everyone uses np for NumPy, so to make your code understandable to others, it is best to use np.

type(x)
numpy.ndarray

We see that the type of object is numpy.ndarray. Let’s spend a moment playing with these.

We can create NumPy array containing a sequence of values with the np.arange() function:

a = -1
b = 1
seq = np.arange(a,b,1/4)
seq
array([-1.  , -0.75, -0.5 , -0.25,  0.  ,  0.25,  0.5 ,  0.75])

This array is one-dimensional, so we would call it a vector in R; however, in Python, there is no distinction between vectors and matrices. These are both simply arrays. We can learn about the dimensions of an array with the np.ndim and np.shape functions, as well as get the total number of entries in an array with the np.size function:

np.ndim(seq)
1
np.shape(seq)
(8,)
np.size(seq) 
8

To make an array with two dimensions, i.e. what we would call a matrix in R, we can give the np.array function a list of lists of equal length.

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

Here the numeric entries will be coerced to character strings, since NumPy arrays must have values all of one type:

np.array([[1,2,3],["a","b","c"]])
array([['1', '2', '3'],
       ['a', 'b', 'c']], dtype='<U21')

We can fill an array with a list of values generated from some function like np.linspace:

The np.ones and np.zeros functions are very handy for creating arrays of all one and all zeros, respectively. You must provide the number of rows and columns as a tuple, in the form (rows,columns):

np.ones((2,3)) # our first use of a tuple as a function argument!
array([[1., 1., 1.],
       [1., 1., 1.]])
np.zeros((4,3))
array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]])

The np.full function fills an array with a single value.

np.full((3,7),4)
array([[4, 4, 4, 4, 4, 4, 4],
       [4, 4, 4, 4, 4, 4, 4],
       [4, 4, 4, 4, 4, 4, 4]])

We can create a sequence of equally spaced values on an interval with np.linspace:

seq2 = np.linspace(0,1,21)
seq2
array([0.  , 0.05, 0.1 , 0.15, 0.2 , 0.25, 0.3 , 0.35, 0.4 , 0.45, 0.5 ,
       0.55, 0.6 , 0.65, 0.7 , 0.75, 0.8 , 0.85, 0.9 , 0.95, 1.  ])

We can make a diagonal matrix with the function np.diag(), where the argument is a one-dimensional NumPy array giving the entries to be placed on the diagonal of the matrix:

np.diag(np.ones(5)) # identity matrix
array([[1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0.],
       [0., 0., 1., 0., 0.],
       [0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 1.]])

The np.outer() function can be used to compute the outer product of two vectors:

x = np.arange(1,4)
y = np.arange(1,5)
np.outer(x,y) # each entry is the product of an entry of x and an entry of y
array([[ 1,  2,  3,  4],
       [ 2,  4,  6,  8],
       [ 3,  6,  9, 12]])

Accessing array entries

We access entries of an array with square brackets, as shown below. Note that this array has only one dimension (so it is like a vector).

print(seq[0])
-1.0

You can also, with NumPy arrays, use a colon somewhat like it is used in R, to subset a sequence of entries in an array:

print(seq[2:4])
[-0.5  -0.25]

But the colon works a little differently: To select all entries starting from an index until the end, you can use this:

print(seq[3:])
[-0.25  0.    0.25  0.5   0.75]

To obtain all the entries from the beginning up to an index, you can use this:

print(seq[:4])
[-1.   -0.75 -0.5  -0.25]

To take every, say, third entry, use this:

print(seq[::3])
[-1.   -0.25  0.5 ]

To take every, say, second entry, starting at the index 3, use this:

print(seq[3::2])
[-0.25  0.25  0.75]

In contrast to R, we cannot “drop” values from an array using negative indexes. In Python, a negative index is interpreted as the number of entries from the end of the array:

print(seq[-1]) # gives last entry
0.75
print(seq[[-1,-2]])
[0.75 0.5 ]
print(M[0,:])
[1 2 3]
print(M[:,1])
[2 5]
print(M[1,2])
6
print(M[1,-1]) # same entry as the previous
6

You can replace an entry like this:

M[1,2] = 7 
print(M)
[[1 2 3]
 [4 5 7]]

We can “drop” or de-select indices by using ~ in python:

print(M[~0,:]) # remove first row
print(M[:,~1]) # remove second column
even = M % 2 == 0
M[~even]
[4 5 7]
[2 5]
array([1, 3, 5, 7])

Reshaping arrays

You can convert one array to an array with different dimensions using np.reshape. Keep in mind that the dimensions of the two arrays must be compatible:

np.reshape(np.linspace(0,1,21),(3,7))
array([[0.  , 0.05, 0.1 , 0.15, 0.2 , 0.25, 0.3 ],
       [0.35, 0.4 , 0.45, 0.5 , 0.55, 0.6 , 0.65],
       [0.7 , 0.75, 0.8 , 0.85, 0.9 , 0.95, 1.  ]])
np.reshape(np.linspace(0,1,21),(7,3))
array([[0.  , 0.05, 0.1 ],
       [0.15, 0.2 , 0.25],
       [0.3 , 0.35, 0.4 ],
       [0.45, 0.5 , 0.55],
       [0.6 , 0.65, 0.7 ],
       [0.75, 0.8 , 0.85],
       [0.9 , 0.95, 1.  ]])

Note that np.reshape fills a two-dimensional array across rows. If you want it to fill across columns, you can transpose the result using the function np.transpose, which returns the transpose of an array (corresponds to the matrix transpose for a two-dimensional array).

np.transpose(np.reshape(np.linspace(0,1,21),(3,7)))
array([[0.  , 0.35, 0.7 ],
       [0.05, 0.4 , 0.75],
       [0.1 , 0.45, 0.8 ],
       [0.15, 0.5 , 0.85],
       [0.2 , 0.55, 0.9 ],
       [0.25, 0.6 , 0.95],
       [0.3 , 0.65, 1.  ]])

We can make arrays of more than two dimensions (like arrays in R):

A = np.reshape(np.linspace(0,1.15,24),(4,3,2))
A
array([[[0.  , 0.05],
        [0.1 , 0.15],
        [0.2 , 0.25]],

       [[0.3 , 0.35],
        [0.4 , 0.45],
        [0.5 , 0.55]],

       [[0.6 , 0.65],
        [0.7 , 0.75],
        [0.8 , 0.85]],

       [[0.9 , 0.95],
        [1.  , 1.05],
        [1.1 , 1.15]]])
np.ndim(A)
3

Accessing “methods” for Python objects

Python offers an interesting way for users to apply functions to Python objects which has no direct analogue in R. If some function can be applied to a Python object, then that function can be called by naming the object and then, after a ., giving the function.

Here is an example: The code below shows to ways of transposing a NumPy array.

B = np.array([[1,2],[3,4]])
print(B)
print(np.transpose(B))
print(B.transpose())
[[1 2]
 [3 4]]
[[1 3]
 [2 4]]
[[1 3]
 [2 4]]

And here are two ways of taking the sum of all the entries in a NumPy array:

print(B.sum())
print(np.sum(B))
10
10

We could call this way of applying a function to a Python object “accessing a method” for the object, where a “method” is a function specifically designed for a certain type of object.

Subsetting with a boolean mask

One way to subset a NumPy array is by defining an array of the same dimensions which is filled with boolean (True/False) values and put this array in the square brackets. Such an array of booleans is sometimes called a “boolean mask”:

A[A < 0.5] = 0 # set to zero all entries in A which are less than 0.5
A
array([[[0.  , 0.  ],
        [0.  , 0.  ],
        [0.  , 0.  ]],

       [[0.  , 0.  ],
        [0.  , 0.  ],
        [0.  , 0.55]],

       [[0.6 , 0.65],
        [0.7 , 0.75],
        [0.8 , 0.85]],

       [[0.9 , 0.95],
        [1.  , 1.05],
        [1.1 , 1.15]]])

Accessing attributes of a NumPy array

Attributes of a NumPy array, like the number of dimensions and the size can be accessed not only with the functions np.ndim and np.size, but by appending .ndim and .size to a NumPy array, like this:

A.ndim
3
A.size
24
A.shape
(4, 3, 2)

An attribution to which it is important to pay attention is the dtype attribute, which tells the data type, the type of values, the array contains. This could take many values such as int for integer, bool_ for boolean or logical, float64 for double precision float, and many more.

A.dtype
dtype('float64')
Z = np.reshape(np.array([0,1,2,3,4,5]),(3,2))
Z
array([[0, 1],
       [2, 3],
       [4, 5]])
Z.dtype
dtype('int64')

If you have a NumPy array with an integer dtype, you will not be able to store non-integers in it! See what happens if you try:

Z[0,0] = 0.234
Z
array([[0, 1],
       [2, 3],
       [4, 5]])

Creating aliases and copies of array subsets

If we define a new array as the subset of an array, Python does not actually create a new array; instead it creates an alias for the subset of the original array, so that if the alias is edited, the edit will be applied to the original array. Here is an example:

A = np.reshape(np.arange(0,24),(4,6))
A
array([[ 0,  1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17],
       [18, 19, 20, 21, 22, 23]])
A0 = A[:2,:2] # first three rows and columns
A0
array([[0, 1],
       [6, 7]])
A0[1,1] = -99
A # the corresponding entry of the original A matrix has been changed!
array([[  0,   1,   2,   3,   4,   5],
       [  6, -99,   8,   9,  10,  11],
       [ 12,  13,  14,  15,  16,  17],
       [ 18,  19,  20,  21,  22,  23]])

If we want to make a totally new object out of a subset of an array (not just an alias for the subset), we need to append .copy() to the original array, like this:

A0_copy = A[:2,:2].copy()
A0_copy[0,0] = -99
A0_copy
array([[-99,   1],
       [  6, -99]])

We see that the original array was not changed by the edit to A0_copy:

A 
array([[  0,   1,   2,   3,   4,   5],
       [  6, -99,   8,   9,  10,  11],
       [ 12,  13,  14,  15,  16,  17],
       [ 18,  19,  20,  21,  22,  23]])

Generating random numbers in NumPy

The NumPy library comes with a collection of functions for generating NumPy arrays filled with realizations of random variables. The prescribed way of using these functions is, firstly, to instantiate a random number generator with

rng = np.random.default_rng()
rng
Generator(PCG64) at 0x1036482E0

Having done this, one can access functions for generating values from many probability distributions. These will be accessed as rng.poission(), rng.normal(), etc.

X = rng.poisson(lam = 2, size = 20)
X
array([3, 2, 3, 0, 2, 1, 3, 0, 2, 1, 0, 1, 4, 1, 1, 2, 3, 2, 1, 1])

If we put in a tuple for the size argument we can get a higher-dimensional array filled with realizations:

X = rng.poisson(lam = 2, size = (10,3))
X
array([[3, 3, 0],
       [2, 1, 4],
       [0, 1, 3],
       [0, 2, 4],
       [3, 1, 4],
       [4, 4, 0],
       [3, 1, 5],
       [2, 1, 3],
       [0, 1, 1],
       [0, 0, 1]])

Combining arrays

Use the np.concatenate() function to put one-dimensional arrays together:

x = np.array([1,2,3])
y = np.array([98,99,100])
z0 = [x,y] # this is a list of two NumPy arrays
z = np.concatenate([x,y]) # this is a new NumPy array
print(z0)
print(z)
[array([1, 2, 3]), array([ 98,  99, 100])]
[  1   2   3  98  99 100]

To put two dimensional arrays together, one can use the np.hstack function to set them side-by-side or the np.vstack function to put one on top of the other.

u = rng.binomial(1,1/2,(4,3)) # 4 by 3
v = rng.random((2,3))         # 2 by 3
np.vstack([u,v])               # 6 by 3
array([[1.        , 0.        , 0.        ],
       [1.        , 0.        , 0.        ],
       [1.        , 0.        , 0.        ],
       [0.        , 1.        , 1.        ],
       [0.24216803, 0.32514053, 0.16111029],
       [0.19964467, 0.6857881 , 0.19545409]])
u = rng.binomial(1,1/2,(2,2))  # 2 by 2
v = rng.random((2,3))          # 2 by 3
np.hstack([u,v])       # 2 by 5
array([[1.        , 1.        , 0.31155041, 0.29368222, 0.1297975 ],
       [1.        , 0.        , 0.31834739, 0.20851219, 0.57584194]])

Splitting arrays

We can use the np.split function to split an array into multiple arrays. For a one-dimensional array, the function works like this:

x = np.arange(0,12)
x
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
x1, x2, x3 = np.split(x,[3,6])
x1
array([0, 1, 2])
x2
array([3, 4, 5])
x3
array([ 6,  7,  8,  9, 10, 11])

To split a two-dimensional array, we can use np.hsplit or np.vsplit:

X = np.random.normal(0,1,(3,5))
X
array([[-0.46093709,  0.04695658, -0.60580969, -0.89236439,  0.33281347],
       [ 2.19465439,  0.67994171, -0.46527918, -0.62016361,  0.83775327],
       [ 0.25382317, -1.50409825,  0.54917573,  0.40913954, -0.65428778]])
X1, X2 = np.hsplit(X,[2])
X1
array([[-0.46093709,  0.04695658],
       [ 2.19465439,  0.67994171],
       [ 0.25382317, -1.50409825]])
X2
array([[-0.60580969, -0.89236439,  0.33281347],
       [-0.46527918, -0.62016361,  0.83775327],
       [ 0.54917573,  0.40913954, -0.65428778]])
X1, X2 = np.vsplit(X,[2])
X1
array([[-0.46093709,  0.04695658, -0.60580969, -0.89236439,  0.33281347],
       [ 2.19465439,  0.67994171, -0.46527918, -0.62016361,  0.83775327]])
X2
array([[ 0.25382317, -1.50409825,  0.54917573,  0.40913954, -0.65428778]])

Arithmetic on NumPy arrays

The arithmetic operators +, -, *, / as well as the exponentiator ** and the modulus % work on NumPy arrays in Python just as they work for vectors or matrices in R (note that the ^ operator is not used for exponentiation in Python; it is the logical or operator, for which we used | in R). Python has the operator // (absent in R) which is called floor divide and gives the integer part of a quotient:

a = np.array([3,4,5])
c = 2
print("a + c =",a + c)
print("a ** c =",a**c) # note that '^' is NOT exponentiation in Python
print("a / c =",a/c) 
print("a % c =",a % c) 
print("a // c =",a // c) 
a + c = [5 6 7]
a ** c = [ 9 16 25]
a / c = [1.5 2.  2.5]
a % c = [1 0 1]
a // c = [1 2 2]

For two arrays, these operators function entrywise.

b = np.array([6,7,8])
print("a + b =", a + b) 
a + b = [ 9 11 13]

Note that there is no recycling of smaller arrays to match the length of larger arrays as there is in R! The following code gives an error.

# d = np.array([6,7,8,9,10,11])
# a + d

Many functions which we might think a programming language should be equipped with are not available in Python unless we import a library. For example, trigonometric functions like the sine and cosine are not included in “base” Python. The NumPy package, however, has these (and many other) functions, optimized for calculation across arrays:

print("2**a =",np.power(2,a)) # raise 2 to the powers given in a
print("b**a =",np.power(a,b)) # raise a to the powers given in b
print("e^a =",np.exp(a)) 
print("log(a) =",np.log(a)) 
print("arctan(a) =",np.arctan(a))
2**a = [ 8 16 32]
b**a = [   729  16384 390625]
e^a = [ 20.08553692  54.59815003 148.4131591 ]
log(a) = [1.09861229 1.38629436 1.60943791]
arctan(a) = [1.24904577 1.32581766 1.37340077]

Computing summary statistics on NumPy arrays

Suppose we want to take the sum of all the entries in a NumPy array. We can use the built-in sum function, or we can use the NumPy version of the sum function, accessed as np.sum. It turns out it is much faster to use the NumPy sum function, as it is optimized for take the sum across a NumPy array. Putting %timeit in front of a command will cause Python do an experiment to measure how long the line of code will take to execute. The below shows that np.sum() is faster than sum() if taking the sum of the values in a NumPy array.

D = rng.random((20,3)) # Uniform(0,1) distribution
%timeit sum(D)
6.18 μs ± 8.09 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
%timeit np.sum(D)
1.19 μs ± 12.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

There are also functions like np.min and np.max. A full list of functions is given here.

print("min = ",np.min(D),", max = ",np.max(D),sep="")
min = 0.010654129937187284, max = 0.9925221884123225

One can also perform these operations by appending the functions to a NumPy array, like this:

D.sum()
np.float64(31.99968237737704)
D.min()
np.float64(0.010654129937187284)
D.max()
np.float64(0.9925221884123225)

We can compute summary statistics for every row or for every column of a two-dimensional array by adding the axis= option:

D.sum(axis=0) # column sums
array([10.95568267,  8.54239108, 12.50160862])
np.sum(D,axis = 0) # same thing
array([10.95568267,  8.54239108, 12.50160862])
D.max(axis=1) # row maxima
array([0.05803244, 0.98790399, 0.83324261, 0.94442496, 0.91574237,
       0.93007899, 0.99252219, 0.69191915, 0.70987442, 0.89266305,
       0.71372798, 0.63208994, 0.82985533, 0.76899318, 0.80671202,
       0.92655494, 0.93946752, 0.85529326, 0.41358431, 0.81356512])
np.min(D,axis = 0) # column minima
array([0.04114672, 0.01065413, 0.05803244])

Missing values

If there are missing values, most summary statistic functions in the NumPy package have a version which can handle them. For example, change np.sum to np.nansum and the function will ignore missing values. You can make a missing value by typing None in Python.

D[rng.binomial(n = 1,p=0.1,size=D.shape) == 1] = None # randomly generate a boolean mask to make about 10% of the values missing
print(D)
[[0.04114672 0.05700311 0.05803244]
 [0.98790399        nan 0.91482303]
 [0.83324261 0.76669244 0.7869745 ]
 [0.83511747 0.38405645 0.94442496]
 [0.38214448 0.57359118 0.91574237]
 [0.45652672 0.93007899        nan]
 [0.99252219 0.32322096        nan]
 [0.69191915 0.65673432 0.24210835]
 [0.12087722 0.60121342        nan]
 [0.47059104 0.04578127 0.89266305]
 [0.65210242        nan 0.71372798]
 [0.46664996 0.2098609         nan]
 [0.48878455 0.09438201 0.82985533]
 [0.10570043 0.59780411        nan]
 [0.36561455 0.49456523 0.80671202]
 [0.39782895 0.92655494        nan]
 [0.93946752 0.12367211 0.89255202]
 [0.85529326 0.50936805 0.10276447]
 [0.05868433 0.01065413 0.41358431]
 [0.81356512 0.26102066        nan]]
print(np.nanmax(D))
0.9925221884123225
print(np.nanmin(D,axis=0)) # column minima, excluding missing values.
[0.04114672 0.01065413 0.05803244]

Practice

Practice writing code and reading code with the following exercises.

Write code

  1. Write code to create this list:
[0, 3, 6, 9, 12, 0, 3, 6, 9, 12]
  1. Create a NumPy array like this one:
array([[0, 0, 0, 0],
       [1, 1, 1, 1],
       [2, 2, 2, 2],
       [3, 3, 3, 3],
       [4, 4, 4, 4],
       [5, 5, 5, 5],
       [6, 6, 6, 6],
       [7, 7, 7, 7]])
  1. Write code which can produce a NumPy array like the one below, but of dimension \((n-1)\times n\) for any \(n\) (If a vector is premultiplied by this matrix, the result gives the successive differences in the entries of the vector).
[[-1.  1.  0.  0.  0.  0.  0.  0.]
 [ 0. -1.  1.  0.  0.  0.  0.  0.]
 [ 0.  0. -1.  1.  0.  0.  0.  0.]
 [ 0.  0.  0. -1.  1.  0.  0.  0.]
 [ 0.  0.  0.  0. -1.  1.  0.  0.]
 [ 0.  0.  0.  0.  0. -1.  1.  0.]
 [ 0.  0.  0.  0.  0.  0. -1.  1.]]
  1. The code below simulates 10000 rolls of a pair of 6-sided dice. Add to the code to obtain the proportion of times the sum of the rolls was an odd number.
R = rng.integers(low = 1,high = 7, size = (10000,2))
  1. The code below generates 2000 realizations of a Poisson random variable, say \(X\), and stores them in a NumPy array. Add to the code so that it returns a Monte Carlo approximation to the probability \(P( 0 < X \leq 6)\).
P = rng.poisson(3,size=2000)

Read code

Anticipate the output of the following code chunks:

ch = "Why hello."
ch[2:]
x = list(range(0,15,3))
x*2
a, b, c = [4,5], ['cat','cow'], [True, False]
d = [ a, b, c ]
d[1][1]
a = 8
b = 4
la = list(range(0,a))
lb = list(range(0,b))
np.array(lb*a).reshape(a,b) + np.array(la*b).reshape(b,a).transpose()
np.outer(np.array([1,-1]*2),np.array([1,-1]*2))