03. Map, Function, Collection, and Probability

Inference Theory 1

©2018 Raazesh Sainudiin. Attribution 4.0 International (CC BY 4.0)

  1. Maps and Functions
  2. Collections in Sage
    • List
    • Set
    • Tuple
    • Dictionary
  3. Probability

Maps and Functions

In the last notebook we understood sets. We can now understand functions which can be thought intuitively as a "black box" named $f$ that takes an input $x$ and returns an output $y=f(x)$.

wikipedia image 440px-Function_machine2.svg.png wikipedia image 440px-Function_color_example_3.svg.png

More formally, a function is a relation between a set of inputs and a set of permissible outputs with the property that each input is related to exactly one output.

The two sets for inputs and outputs are traditionally called domain and range or codomain, respectively.

The map or function associates each element in the domain with exactly one element in the range.

So, more than one distinct element in the domain can be associated with the same element in the range, and not every element in the range needs to be mapped (i.e, not everything in the range needs to have something in the domain associated with it).

Here is a map for some family relationships:

  • $Raaz$ has two daughters named $Anu$ and $Ashu$,
  • $Jenny$ has a sister called $Cathy$ and the father of $Jenny$ and $Cathy$ is $Jonathan$, and
  • $Fred$ is a man who has no daughter.

We can map these daughters to their fathers with the $FindFathersMap$ shown below: The daughters are in the domain and the fathers are in the range made up of men. Each daughter maps to a father (there is no immaculate conceptions here!). More than one daughter can map to the same father (some fathers have more daughters!). But there can be men in the range who are not fathers (this is also natural, may be they only have sons or have no children at all).

AFunction.png

The notation for this mapping would be:

$$FindFathersMap: Daughters \rightarrow Men$$

The domain is the set:

$$Daughters = \{Anu, Ashu, Jenny, Cathy\}$$

and the range or codomain is the set:

$$Men = \{Raaz, Jonathan, Fred\}.$$

The element in the range that an element in the domain maps to is called the image of that domain element.
For example, $Raaz$ is the image of $Anu$ in the $FindFathersMap$. The notation for this is:

$$FindFathersMap(Anu) = Raaz \ .$$

Note that what we have just written is a function, just like the more familiar format of

$$f(x) = y \ .$$

In computer science lingo each element in the domain is called a key and the images in the range are called values. The map or function associates each key with a value.

The keys for the map are unique since the domain is a set, i.e., a collection of distinct elements. Lots of keys can map to the same image value ($Jenny$ and $Cathy$ have the same father, $Anu$ and $Ashu$ have the same father), but the idea is that we can uniquely identify the value we want if we know the key. This means that we can't have multiple identical keys. This makes sense when you look at how the map works. We use the map to find values from the key, as we did when we found $Anu$'s father above. If we had more than one $Anu$ in the set of keys, we could not uniquely identify the value that maps to the key $Anu$.

We do not allow maps (functions) where one element in the domain maps to more than one element in the range. In computer science lingo, each key can have only one value associated with it.

NotAFunction.png

Formalising this, a function $f: \mathbb{X} \rightarrow \mathbb{Y}$ that maps each element $x \in \mathbb{X}$ to eaxctly one element $f(x) \in \mathbb{Y}$ is equivalent to the corresponding set of ordered pairs: $$\left\{(x, f(x)): x \in \mathbb{X}, f(x) \in \mathbb{Y}\right\} \ .$$

Here, $\left(x, f(x)\right)$ is an ordered pair (the familiar key-value pair in computer science).

The pre-image or inverse image of a function $f: \mathbb{X} \rightarrow \mathbb{Y}$ is $f^{[-1]}$.

The inverse image takes subsets in $\mathbb{Y}$ and returns subsets of $\mathbb{X}$.

The pre-image or inverse image of $y$ is $f^{[-1]}(y) = \left\{x \in \mathbb{X} : f(x) = y \right\} \subset \mathbb{X}$.

More generally, for any $B \subset \mathbb{Y}$:

$f^{[-1]}(B) = \{x \in \mathbb{X}: f(x) \in B \}$

For example, for our FindFathersMap,

  • $FindFathersMap^{[-1]}(Raaz) = \{Anu, Ashu\}$ and
  • $FindFathersMap^{[-1]}(Jonathan) = \{Jenny, Cathy\}$.

Now lets take a more mathematical looking function $f(x) = x^2$.

Version 1 of this function is going to have a finite domain (only five elements).

The domain is the set $\{-2, -1, 0, 1, 2\}$ and the range is the set $\{0, 1, 2, 3, 4\}$.

The mapping for version 1 is $$f(x) = x^2\,:\,\{-2, -1, 0, 1, 2\} \rightarrow \{0, 1, 2, 3, 4\} \ .$$

xSquaredMap.png

We can also represent this mapping as the set of ordered pairs $\{(-2,4), (-1,1), (0,0), (1,1), (2,4)\}$.

Note that the values 2 and 3 in the range have no pre-image in the domain. This is okay because not every element in the range needs to be mapped.

Having a domain with only five elements in it is a bit restrictive: how about an infinite domain?

Version 2
$$f(x) = x^2\,:\,\{\ldots, -2, -1, 0, 1, 2, \ldots\} \rightarrow \{0, 1, 2, 3, 4, \ldots\}$$

As ordered pairs, we now have $\{\ldots, (-2,4), (-1,1), (0,0), (1,1), (2,4), \ldots\}$, but it is impossible to write them all down since there are infinitely many elements in the domain.

What if we wanted to use the function for the whole of $\mathbb{R}$, the real line?

Version 3
$$f(x) = x^2 : \mathbb{R} \rightarrow \mathbb{R} \ . $$

In [1]:
def showURL(url, ht=500):
    """Return an IFrame of the url to show in notebook with height ht"""
    from IPython.display import IFrame
    return IFrame(url, width='95%', height=ht) 
showURL('https://en.wikipedia.org/wiki/Function_(mathematics)#Introduction_and_examples',400)
Out[1]:

Mathematical notation for functions

There are many notations for functions and it is a good idea to familiarize ourselves with some of them via examples. Scroll down the url function notation to become familiar with the following notations:

  • $f: \mathbb{X} \to \mathbb{Y}$ or $\mathbb{X} \overset{f}{\to} \mathbb{Y}$ means $f$ is a function from $\mathbb{X}$ to $\mathbb{Y}$
  • and $y=f(x)$ means $y \in \mathbb{Y}$ is equal to $f(x)$, the image of the function evaluated at $x \in \mathbb{X}$

Another convenient notation for a function is maps to denoted by $\mapsto$. For example, we can know the domain and range of a function and how it maps an argument to its image as follows:

  • $f: \mathbb{N} \to \mathbb{Z}$ can be read as
    • "$f$ is a function from $\mathbb{N}$ (the set of natural numbers) to $\mathbb{Z}$ (the set of integers)" or
    • "$f$ is a $\mathbb{Z}$-valued function of an $\mathbb{N}$-valued variable"
  • $x \mapsto 4-x$ can be read as "$x$ maps to $4-x$"
In [2]:
showURL("https://en.wikipedia.org/wiki/Function_(mathematics)#Notation",300)
Out[2]:

In a computer, maps or functions are encoded as (i) sets of ordered pairs or as (ii) procedures.

Example 1: Encoding a function or map as a set of ordered pairs

In SageMath we can encode a map as a set of ordered pairs as follows.

In [3]:
findFathersMap = {'Jenny': 'Jonathan', 'Cathy': 'Jonathan', 'Anu': 'Raaz', 'Ashu': 'Raaz'}
findFathersMap
Out[3]:
{'Anu': 'Raaz', 'Ashu': 'Raaz', 'Cathy': 'Jonathan', 'Jenny': 'Jonathan'}
In [4]:
type(findFathersMap)     # find the type of findFathersMap
Out[4]:
<type 'dict'>

findFathersMap is a Python/SageMath built-in datatype called dict which is short for dictionary. We will see dictionaries in more detail soon (or if you are imaptient see dict Python docs). For now, we are taking a mathematical view and learning to implement functions or maps as a set of ordered pairs.

Given a key (child) we can use the map to find the father.

In [5]:
findFathersMap['Anu']
Out[5]:
'Raaz'
In [6]:
print "The father of Anu is", findFathersMap['Anu']    # we can use a print statement to make it flow
The father of Anu is Raaz

We could also encode our inverse map of the findFathersMap as findDaughtersMap. We map from one father key (e.g. Raaz) to multiple values for daughters (for Raaz, it is Anu and Ashu).

In [7]:
findDaughtersMap = {'Jonathan': ['Jenny', 'Cathy'], 'Raaz': ['Anu', 'Ashu']}

# don't worry about the way we have done the printing below, just check the output makes sense to you!
for fatherKey in findDaughtersMap:
    print fatherKey, "has daughters:"
    for child in findDaughtersMap[fatherKey]:
        print '\t',child
Raaz has daughters:
	Anu
	Ashu
Jonathan has daughters:
	Jenny
	Cathy

We can also use a map for a numerical function $$y(x) = x^2 + 2: \{-2 ,-1 ,0, 1 ,2\} \to \{2, 3, 4, 5, 6\}$$ by encoding this function as a set of ordered pairs $$\{ (-2,6), (-1,3), (0,2), (1,3), (2,6)\}$$ in SageMath as follows.

In [8]:
myFunctionMap = {-2: 6, -1: 3,  0: 2, 1: 3, 2: 6}
myFunctionMap
Out[8]:
{-2: 6, -1: 3, 0: 2, 1: 3, 2: 6}

Looking up the value or image of the keys in our domain $\{-2,-1,0,1,2\}$ works. Note the KeyError message when the key is outside the domain.

In [9]:
myFunctionMap[-2]    # this return the value or image of the argument or key -2
Out[9]:
6
In [10]:
myFunctionMap[-20] # KeyError: -20 since -20 is not in {-2,-1,0,1,2}
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-10-bb93d5a98ee4> in <module>()
----> 1 myFunctionMap[-Integer(20)] # KeyError: -20 since -20 is not in {-2,-1,0,1,2}

KeyError: -20

Example 2: Encoding function as a procedure

But it is clearly not a good way to try to specify a function mapping that can deal with lots of different $x$'s: in fact it would be impossible to try to specify a mapping like this if we want the domain of the function to have infinitely many elements (for example, if the domain is the set of all integers, or all rational numbers, or a segment of the real line, for example $f(x) = x^2 + 2 : \mathbb{R} \rightarrow \mathbb{R}$).

Instead, in SAGE we can just define our own function as a procedure that can evaluate our image value "on-the-fly" for any $x$ we want. We'll be doing more on functions in later labs so for the moment just think about how defining the function can be seen as a much more flexible way of specifying a mapping from the arguments in the domain of the function to values or images in its range.

The basics steps in encoding a function as a procedure in SageMath/Python:

  1. The function named myFunc we want to define for $x \mapsto x^2+2$ is preceeded by the keyword def for definition.
  2. The function name myFunc is succeeded by any input argument(s) to it within a pair of parentheses, for e.g., (x) in this case since there is only one argument, namely x.
  3. After we have defined the function by its name myFunc followed by its input argument (x) we end the line with a colon : before continuing to the next line.
  4. Now, we are ready to write the body of the function. It is a customary to leave 4 white spaces. The number of spaces before a line or the indentation is used to delineate a block of code in SageMath/Python.
  5. It is a matter of courteous programming practice to enclose the Docstring, i.e., comments on what the function does, inside triple quotes. The Docstring is returned when we ask SageMath for help on the function.
  6. Finally, we output the image of our function with the keyword return and the expression x^2+2.
In [11]:
def myFunc(x):                                 # starting the definition
    '''A function to return x^2 + 2'''         # the docstring
    return x^2+2                               # the function body (only one line in this example
In [13]:
myFunc?

When you evaluate the above cell you should see something like this:

Signature:      myFunc(x)
Docstring:      A function to return x^2 + 2
Init docstring: x.__init__(...) initializes x; see help(type(x)) for signature
File:           ~/all/git/scalable-data-science/_360-in-525/2018/04/jp/<ipython-input-18-ac2a17246c36>
Type:           function
In [12]:
myFunc(0.1)   # use the function to calculate 0.1^2 + 2 with argument as a mpfr_real Literal
Out[12]:
2.01000000000000

YouTry

When you evaluate the cell below, SageMath will complain about an IndentationError that you can easily fix.

In [13]:
def myFunc(x):
    '''A function to return x^2 + 2'''
     return x^2+2
  File "<ipython-input-13-e9bade3af0c8>", line 3
    return x**Integer(2)+Integer(2)
    ^
IndentationError: unexpected indent

The command to plot is pretty simple. The four arguments to plot are the function to be plotted, the input argument that is varying along the x-axis, the lower-bound and upper-bound of the input argument.

In [ ]:
plot(myFunc(x),x, -20, 20) 
In [19]:
?plot  # for help hit the DocString of the function

We can get a bit more control of the figure size (and other aspects) by first assigning the plot to a variable and using the show method as follows.

In [24]:
myPlot = plot(myFunc(x),x, -20, 20)
myPlot.show(figsize=[6,3])
In [25]:
type(myPlot)
Out[25]:
<class 'sage.plot.graphics.Graphics'>
In [21]:
myPlot.show?

The simple plot command hides what is going on under the hood. Before we understand the fundamentals of plotting, let us get a better appreciation for the ordered pairs $(x,f(x))$ that make up the curve in this plot.

We can use SageMath to plot functions and add a way for you to interact with the plot. When you have evaluated this cell you'll see a plot of our function between $x=-20$ and $x=20$. The point on the curve where $x=3$ is indicated in red. You can alter the position of this point by putting a new value into the box at the top of the display (you can put in real number, eg 4.45, but if your number is outside the range -20 to 20 it won't be shown. Just try changing the value of x to plot as a point on the curve and don't worry about the way the code looks - you aren't expected to do this yourselves (at least not yet).

In [26]:
# Don't worry about this code and just interact with its output cell by changing the value in the box labelled x
@interact
def _(my_x=input_box(3, width=10, label="$x$")):
    myPt = (my_x, myFunc(my_x))
    myLabel = "(%.1f, %.1f)" % myPt
    p = plot(myFunc, (x,-20,20))
    if (my_x >= -20 and my_x <= 20):
        p += point(myPt,rgbcolor='red', pointsize=20)
        p += text(myLabel, (my_x+4,myFunc(my_x)), rgbcolor='red')
    p.show(figsize=[6,3])

A bit under plot's hood

The function you are viewing above from the plot command is actually just interpolated by a whole lot of points connected by lines!

To keep things more real let's expose the plot for what it really is next by only plotting a few points.

In [27]:
# plot 5 randomized points and leave them hanging
plot(myFunc,(-2,2), plot_points=5,marker='.',linestyle='', randomize=True, adaptive_recursion=0, figsize=[6,3])
Out[27]:
In [28]:
# plot 5 randomized points and join them
plot(myFunc,(-2,2), plot_points=5,marker='.',linestyle='-', randomize=True, adaptive_recursion=0, figsize=[6,3])
Out[28]:
In [29]:
# plot 100 randomized points and join them - now it looks continuous, doesn't it?
plot(myFunc,(-2,2), plot_points=100, marker='.',linestyle='-', randomize=True, adaptive_recursion=0, figsize=[6,3])
Out[29]:
In [30]:
# plot 100 randomized points and join them without marker for points - now it looks like default plot, doesn't it?
plot(myFunc,(-2,2), plot_points=100, marker='',linestyle='-', randomize=True, adaptive_recursion=0, figsize=[6,3])
Out[30]:

We will play with point, lines and other such objects in the sequel. For now, just remember that what you see is sometimes not what is under the hood.

You try

Define a more complicated function with four input arguments next. The rules for defining such a function are as before with the additional caveat of declaring all four input arguments inside the pair of parenthesis following the name of the function. In the cell below you will have to uncomment one line and then evaluate the cell to define the function (remember that comments begin with the # character).

In [31]:
# Here is a quadratic function of x with three additional parameters a,b,c
def myQuadFunc(a,b,c,x):
    '''A function to return a*x^2 + b*x + c'''
    return a*x^2 + b*x + c

Now try writing an expression to find out what myQuadFunc is for some values of x and coefficients a=1, b=0, c=2.

We have put the expression that uses these coefficients and x=10 into the cell below for you.

Can you see how Sage interprets the expression using the order in which we specified a, b, c, x in the definition above?

Try changing the expression to evaluate the function with same coefficients (a, b, c) but different values of x.

In [32]:
myQuadFunc(1, 0, 2, 10) # a = 1, b = 0, c = 2, x = 10
Out[32]:
102
In [ ]:
 
In [ ]:
 
In [33]:
# we can make the same plot as before by letting a=1, b=0, c=2
plot(myQuadFunc(1,0,2,x),x, -20, 20, figsize=[6,3])
Out[33]:
In [ ]:
 

Example 3: Polymorphism and Type Errors

You can call the same function myFunc with different types and the operations in the body of the function, such a s power (^) and addition (+), will be automatically evaluated for the input type. This can be quite convenient!

When code is written without mention of any specific type and thus can be used transparently with any number of new types we are experiencing the concept of polymorphism (parametric plymorphism or generic programming).

In [34]:
myFunc(float(0.1))   # use the function to calculate 0.1^2 + 2 with argument as a Python float
Out[34]:
2.01
In [35]:
myFunc(1/10)         # use the function to calculate 0.1^2 + 2 with argument as a Sage Rational
Out[35]:
201/100
In [36]:
myFunc(2)            # use the function to calculate 0.1^2 + 2 with argument as a Sage Integer
Out[36]:
6
In [37]:
myFunc(int(2))       # use the function to calculate 0.1^2 + 2 with argument as a Sage Rational
Out[37]:
6
In [38]:
myFunc('hello')      # calling myFunc on an input string argument results in a TypeError
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-38-34c295a46f9a> in <module>()
----> 1 myFunc('hello')      # calling myFunc on an input string argument results in a TypeError

<ipython-input-11-ac2a17246c36> in myFunc(x)
      1 def myFunc(x):                                 # starting the definition
      2     '''A function to return x^2 + 2'''         # the docstring
----> 3     return x**Integer(2)+Integer(2)                               # the function body (only one line in this example

/home/raazesh/all/software/sage/SageMath/src/sage/rings/integer.pyx in sage.rings.integer.Integer.__add__ (build/cythonized/sage/rings/integer.c:11091)()
   1607             return y
   1608 
-> 1609         return coercion_model.bin_op(left, right, operator.add)
   1610 
   1611     cpdef _add_(self, right):

/home/raazesh/all/software/sage/SageMath/src/sage/structure/coerce.pyx in sage.structure.coerce.CoercionModel_cache_maps.bin_op (build/cythonized/sage/structure/coerce.c:10736)()
   1137         # We should really include the underlying error.
   1138         # This causes so much headache.
-> 1139         raise bin_op_exception(op, x, y)
   1140 
   1141     cpdef canonical_coercion(self, x, y):

TypeError: unsupported operand parent(s) for +: '<type 'str'>' and 'Integer Ring'

We should see something like:

TypeError: unsupported operand parent(s) for +: '<type 'str'>' and 'Integer Ring'

This is because in the body of myFunc(x) we have x^2+2 where x^2 is added to 2 using thr + operator, where 2 is a SageMath type Integer. And when we pass in the string hello for x by evaluating myFunc('hello') we are running into the mentioned TypeError of unsupported operand parents for +.

Interestingly, integer power of a string using the ^ operator is well defined as illustrated for hello below!

In [39]:
'hello'^2  # or 'hello'^2 = 'hellohello', i.e., 'hello' concatenated with itself two times
Out[39]:
'hellohello'
In [40]:
'hi'^3   # or 'hi'^3 = 'hihihi', i.e., 'hi' concatenated with itself three times
Out[40]:
'hihihi'

Also, addition of two string is also defined as concatenation:

In [41]:
'hello'+'hey'
Out[41]:
'hellohey'

Just to illustrate this, we next write a function named myFunc2 that returns x^2+x and will work for any input argument for which the operations of ^2 and + are well-defined for their operand parent types.

In [42]:
def myFunc2(x):
    '''square x and add x to it'''
    return x^2+x
In [43]:
myFunc2(0.1)
Out[43]:
0.110000000000000
In [44]:
myFunc2('hi')             # 'hi'^2 + 'hi' = 'hihi'+'hi' = 'hihihi'
Out[44]:
'hihihi'

Remark: such parametric polymorphism can be convenient but it can also have unintended consequences when you run the program. So be cautious!

Example 4: Mathematical functions

Many familiar mathematical functions such as $\sin$, $\cos$, $\log$, $\exp$ are also available directly in SageMath as built-in functions. They can be evaluated using parenthesis (brackets) as follows:

In [45]:
cos(0)
Out[45]:
1
In [46]:
sin(0)
Out[46]:
0
In [47]:
sin(pi)
Out[47]:
0
In [48]:
print 'sin(pi) prints as', sin(pi)
sin(pi) prints as 0
In [49]:
# understand the output of each line and the symbolic/numeric expressions being evaluated and printed
print 'cos(1/2) prints as', cos(1/2)
print 'cos(1/2).n(digits=5) prints as', cos(1/2).n(digits=5)
print 'float(cos(1/2)) prints as', float(cos(1/2))
print 'cos(0.5) prints as', cos(0.5)

print 'exp(2*pi*e) prints as', exp(2*pi*e)

print 'log(10) prints as', log(10)
print 'log(10).n(digits=5) prints as', log(10).n(digits=5)
print 'float(log(10)) prints as', float(log(10))
print 'log(10.0) prints as', log(10.0)
cos(1/2) prints as cos(1/2)
cos(1/2).n(digits=5) prints as 0.87758
float(cos(1/2)) prints as 0.87758256189
cos(0.5) prints as 0.877582561890373
exp(2*pi*e) prints as e^(2*pi*e)
log(10) prints as log(10)
log(10).n(digits=5) prints as 2.3026
float(log(10)) prints as 2.30258509299
log(10.0) prints as 2.30258509299405

You try

You can find out what built-in functions are available for a particular variable by typing the variable name followed by a . and then pressing the TAB key.

In [50]:
x=-2
In [ ]:
x.  # place the cursor after the . and press Tab

Try the abs function that evaluates the absolute value of x that is one of the methods available for x.

Here are two ways of calling abs method for x.

In [51]:
x.abs() # 
Out[51]:
2
In [52]:
abs(x)
Out[52]:
2

Remember to ask for help!

If you want to know what a built-in function does, type the function name prepended by a '?'.

In [ ]:
?abs

Collections in Sage

We have already talked about the SageMath and Python number types and a little about the string type. You have also met sets in SageMath with a brief mention of lists. A set and list in Sage are, loosely speaking, examples of collection types. Collections are a useful idea: grouping or collecting together some data (or variables) so that we can refer to it and use it collectively.

SageMath provides quite a few collections. One that we will meet very often is the list, a built-in sequence type in Python like the strings we have already seen. See Python tutorial for built-in data structures.

Example 1: Lists

Technically, a list in SageMath/Python is a sequence type. This basically means that the order of the things in the list is imporant and we can use concepts related to ordering when we work with a list. More specifically, a list is a mutable sequence type. This just means that you can change or mutate the contents of the list.

Don't worry about these details (unless you are interested in them of course and follow links below), but for now just look at the worksheet cells below to see how useful and flexible lists are.

When you want a list in SageMath you put the things in the list inside [ ] called square brackets. You can put almost anything in a list (including having lists of lists, as we'll see later).

In [53]:
L1 = []     # an empty list
L1          # display L1
Out[53]:
[]
In [54]:
L2 = ['orange', 'apple', 'lemon']   # a list of strings - remember that strings are within quote marks
L2                                  # display L2
Out[54]:
['orange', 'apple', 'lemon']
In [55]:
L2.append('banana')                 # append something to the end of a list
L2                                  # display L2
Out[55]:
['orange', 'apple', 'lemon', 'banana']
In [56]:
L3 = [10, 11, 12 ,13]               # a list of integers
In [57]:
type(L3)                            # type of L3 is
Out[57]:
<type 'list'>

There are various functions and methods we can use with lists. For a more exhaustive dive see Python standard library docs specifically for the methods on lists that are:

In the interest of time, we are going to only familiarize ourselves with the immediately useful methods and learn new ones as we need them.

A very useful one is len, which gives us the length of the list, i.e., the number of elements in the list.

In [58]:
len(L3)
Out[58]:
4

What about getting at the elements in a list once we have put them in? This is done by indexing into the list, or list indexing . Slightly confusingly, once you have a list, you index into it by using the [ ] brackets again.

In [59]:
L3[0]                               # the first position in the list is
Out[59]:
10

Note that in SageMath the first position in the list is at position 0, or index [0] not at index [1]. In the list L3, which has 4 elements (len(L3) = 4), the indices are [0], [1], [2] and [3]. In the cell below you can check what you get when you ask for the element at the fourth position in the list (i.e., index [3] since the indexes start from [0]).

In [60]:
L3[3]
Out[60]:
13

You will get an error message if the index you use is out of range (which means that you are trying to refer to something outside the range of the list). SageMath/Python "knows" that the list only has 4 elements, so asking for the element in the fifth position (index [4]) makes no sense and you get an IndexError: list out of range.

In [61]:
L3[4]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-61-e2b78c9ce53d> in <module>()
----> 1 L3[Integer(4)]

IndexError: list index out of range

We can also get at more than one element in the list with the indexing operator [ ], by using : to indicate all elements from a position to another position. This slicing of a list is hard to explain in words but easy to see in action.

In [62]:
L3[0:2]                        # elements in positions 0 to 2 in list L3 are
Out[62]:
[10, 11]

If you leave out the starting and ending positions and just use [:] you'll get the whole list. This is useful for making copies of whole lists.

In [63]:
L4 = L3[:]
L4             # disclose L4
Out[63]:
[10, 11, 12, 13]

SageMath also provides some helpful ways to make lists quickly. The one you'll use most often is range. Used in its most simple form, range(n) gives you a list of n integers from 0 to n-1.

In [64]:
L5 = range(10)                      # a quick way to make a list of 10 numbers starting from 0
L5
Out[64]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Note that the numbers you get start from 0 and the last one is 9.

You'll see that we can get even cleverer and use range to get a list that starts and stops at specified numbers with a specified step size between the numbers. Let's try this to get numbers in steps of 5 from 100 to 145, in the cell below.

In [65]:
L6 = range(100, 150, 5)   # get a list with a specified start, stop and step 
L6
Out[65]:
[100, 105, 110, 115, 120, 125, 130, 135, 140, 145]

Notice that again we don't go right up to the "stop" number (200) but to the last one below it taking into account our step size (5), ie 145.

When we just asked for range(10) SageMath assumed a default start of 0 and a default step of 1. Thus, range(10) is equivalent to range(0, 10, 1).

YouTry

Start of this YouTry

Find out more about list and range by evaluating the cells below and looking at the Python docs (see links above) or just via DocStrings.

In [65]:
?range
In [89]:
help(list.append)   # or use help for brief DocStrings
Help on method_descriptor:

append(...)
    L.append(object) -- append object to end

Make yourself a list with some elements in it; you can choose how many elements to have and what type they are.
Assign the list to a variable named myList.

In [ ]:
myList = [ REPLACE_THIS_WITH_ELEMENTS_OF_LIST ]

Add a new element to myList using the .append method.

In [ ]:
 

Use the nice way of copying we showed you above (remember, [:]) to copy everything in myList to a new list called myNewList. Use the len function to check the length of your new list.

In [ ]:
myNewList = myList[ REPLACE_THIS_WITH_THE_RIGHT_CHARACTER ]
In [ ]:
len(myNewList) 

Use the indexing operator [ ] to find out what the first element in myList is (remember that the index of the first element will be 0).

In [ ]:
myList[ FILL_IN_HERE ]

Use the indexing opertor [ ] to change the first element in myNewList to some different value.

In [ ]:
myNewList[0] = FILL_IN_HERE 

Disclose the original list, myList, to check that nothing in that has changed.

In [ ]:
print myList
print myNewList

Use range to make a list of the integer numbers between 4 and 16, going up in steps of 4. Assign this list to a variable named rangeList.

In [ ]:
rangeList = range( FILL_IN_HERE )

Disclose your list rangeList to check the contents. You should have the values 4, 8, 12, 16. If you don't, check what you asked for in the cell above and fix it up to give the values that you wanted.

In [ ]:
rangeList

END of YouTry

Example 2: Tuples

A tuple is another built-in sequence type in Python like lists and strings. The values in a tuple are enclosed in curved parentheses ( ) and the values are separated by commas.

In [67]:
myTuple1 = (1, 2) # assign the tuple (1,2) to variable nemed muTuple1
myTuple1
Out[67]:
(1, 2)
In [68]:
type(myTuple1)
Out[68]:
<type 'tuple'>
In [69]:
myTuple2 = (10, 11, 13)
myTuple2
Out[69]:
(10, 11, 13)

Tuples are immutable. In programming, an 'immutable' object is an object whose state cannot be modified after it has been created (Etymology: 'mutable comes from the Latin verb mutare, or 'to change' -- the same root we get 'mutate' from. So in-mutable, or immutable, means not capable of or susceptible to change). This means that although we can access the element at a particular position in a tuple by indexing ...

In [70]:
myTuple1[0] # disclose what is in the first position in the tuple
Out[70]:
1

... we can't change what is in that particular position in the tuple.

In [71]:
myTuple1[0] = 10     # try to assign a different value to the first position in the tuple
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-71-f499764cf49e> in <module>()
----> 1 myTuple1[Integer(0)] = Integer(10)     # try to assign a different value to the first position in the tuple

TypeError: 'tuple' object does not support item assignment
In [72]:
myTuple1[0]    # the first element in the tuple is immutably 1
Out[72]:
1

Useful things you can do with tuples

Sage has a very useful zip function. Zip can be used to 'zip' sequences together, making a list of tuples out of the values at corresponding index positions in each list. Consider a simple example: Note that in general zip works with sequences, so it can be used to zip tuples as well as lists.

In [73]:
zip(['x', 'y', 'z'], [1, 2, 3])
Out[73]:
[('x', 1), ('y', 2), ('z', 3)]

Example 3: Sets

We already created and assigned sets and operated with them. Set/set are another SageMath/Python collection. Remember that in a set, each element has to be unique. We can specify a set directly or make one out of a list.

In [74]:
L6 = range(100, 150, 5)  # make sure we have a list L6
S = set(L6)     # make the set S from the list L6
S             # display the set s
Out[74]:
{100, 105, 110, 115, 120, 125, 130, 135, 140, 145}

Sets are unordered collections. This makes sense when we think about what we know about sets: what matters about a set is the unique elements in it.

The set $\{1, 2, 3\}$ is the same as the set $\{1, 3, 2\}$ is the same as the set $\{2, 3, 1\}$, etc.

This means that it makes no sense to ask SageMath what's at some particular position in a set as we could with lists. Lists are sequnces and order matters. Sets are unordered - order makes no sense for a set.

We cannot use the indexing operator [ ] with a set.

In [75]:
S[0] # will give an error message 'TypeError: 'set' object does not support indexing'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-75-457b444eb103> in <module>()
----> 1 S[Integer(0)] # will give an error message 'TypeError: 'set' object does not support indexing'

TypeError: 'set' object does not support indexing
In [ ]:
S. # put the cursor after the . and hit Tab to see all the methods available on Python set
In [76]:
SS = Set(L6) # this is a SageMath set with upper-case Set
In [ ]:
SS. # put the cursor after the . and hit Tab to see all the extra methods available on SageMath Set

Example 4: Dictionaries

When we created our maps or functions at the start of this worksheet, we actually used dictionaries: A SageMath/Python dictionary gives you a way of mapping from a key to a value. As we said earlier, the keys have to be unique (only one of each key) but more than one key can map to the same value. Remember the FindFathersMap? It is actually a Python dictionary or simply dict.

Although, we used the syntax for dictionaries to conceptually reinforce functions and maps, we revisit them here and contrast them with the other collections we have already seen like lists and sets.

In [77]:
findFathersMap = {'Jenny': 'Jonathan', 'Cathy': 'Jonathan', 'Anu': 'Raaz', 'Ashu': 'Raaz'}
findFathersMap
Out[77]:
{'Anu': 'Raaz', 'Ashu': 'Raaz', 'Cathy': 'Jonathan', 'Jenny': 'Jonathan'}
In [78]:
type(findFathersMap)
Out[78]:
<type 'dict'>

When we make a dictionary, we tell SageMath that it is a dictionary by using the curly brackets { } and by giving each key value pair in the format key: value.

A dictionary has something like the indexing operator we used for lists, but instead of specifying the position we want (like [0]) we specify the key we want, and SageMath returns the value that key maps to.

In [79]:
findFathersMap['Anu']           # who is Anu's father
Out[79]:
'Raaz'

Manipulating a dict

In the cell below we have the start of a simple dictionary for phone numbers.

In [80]:
myPhoneDict = {'Ben': 8888, 'Raaz': 3333}
myPhoneDict     # disclose the contents of the dictionary
Out[80]:
{'Ben': 8888, 'Raaz': 3333}

In the cell below let us add susy with phone number 78987 to our dictionary.

In [81]:
myPhoneDict['susy']=78987
In [82]:
myPhoneDict           # disclose the current contents of our dict
Out[82]:
{'Ben': 8888, 'Raaz': 3333, 'susy': 78987}

zip-ping of tuples gives us a quick way to make a dictionary if we have separate lists or tuples which contain our keys and values. Note that the ordering in the key and value sequences has to be consistent -- the first key will be mapped to the first value, etc., etc.

In [83]:
myKeys = ('Ben', 'Raaz', 'susy')
myValues = (888, 333, 78987)
myPhoneDictByZip = dict(zip(myKeys, myValues))
myPhoneDictByZip
Out[83]:
{'Ben': 888, 'Raaz': 333, 'susy': 78987}

You try

Try adding to what we have to put in two more people, Fred, whose phone number is 1234, and Mary whose phone number is 7777.
Remember that for SageMath, the names Fred and Mary are strings and you must put them in quote marks, like the names that are already there.

In [84]:
myPhoneDictByZip['Fred']=1234
myPhoneDictByZip['Mary']=7777
In [85]:
myPhoneDictByZip
Out[85]:
{'Ben': 888, 'Fred': 1234, 'Mary': 7777, 'Raaz': 333, 'susy': 78987}

Now try asking SageMath for Ben's phone number (ie, the phone number value associated with the key Ben).

In [86]:
myPhoneDictByZip['Ben']
Out[86]:
888
In [ ]:
 

There are also some useful methods of dictionaries that allow you to 'dissect' the dictionary and extract just the keys or just the values.

In [87]:
myPhoneDictByZip.keys()
Out[87]:
['susy', 'Raaz', 'Ben', 'Mary', 'Fred']
In [88]:
myPhoneDictByZip.values()
Out[88]:
[78987, 333, 888, 7777, 1234]

And there is also an .items() method that gives you back your your (key, value) pairs again. You will see that is it a list of tuples.

In [89]:
myPhoneDictByZip.items()
Out[89]:
[('susy', 78987), ('Raaz', 333), ('Ben', 888), ('Mary', 7777), ('Fred', 1234)]
In [ ]:
 

Probability

(This is a partial type-setting of the notes scribed on the black board.)

The origins of probability can be traced back to the 17th century. It arose out of the study of gambling and games of chance. Many well-known names associated with probability worked on problems to do with gambling: people like Bernoulli and Pascal did quite a lot of work in this area ... even Newton was persuaded to set down some of his thoughts about a game involving dice (in a letter to a Samuel Pepys).

Dive into the main wikipedia article here for more details. Here, we will take the shortest mathemtical path to understanding probability.

Probability has a language of its own. We are going to introduce you to some of the essential terms:

An experiment is an activity or procedure that produces distinct or well-defined outcomes. The set of such outcomes is called the sample space of the experiment. The sample space is usually denoted with the symbol $\Omega$. Lets look at some examples of experiments.

Roll a dice experiment

If our experiment is to roll a dice wth faces painted $red$, $green$, $yellow$, $pink$, $blue$ and $black$ and find what colour the top face is at the end of each roll, then the sample space $\Omega = \{red, green, yellow, pink, blue, black\}$.

Flip a coin experiment

If our experiment is to flip a coin where the two faces of the coin can be identified as 'heads' ($H$) and 'tails' ($T$), and we are interested in what face lands uppermost, then the sample space is $\Omega = \{H, T\}$.

Draw a fruit from a fruit bowl

Suppose we have a well-mixed fruit bowl that contains:

  • 2 oranges
  • 3 apples
  • 1 lemon

If our experiment is to take a single fruit from the bowl and the outcome is the type of fruit we take then what is the sample space for this experiment?

Recall that the sample space is the set of all possible outcomes of the experiment. If we take a single fruit we could get only one of the three fruits in each draw: an orange, or an apple or a lemon. The sample space $\Omega = \{orange, apple, lemon\}$.

An event is a subset of the sample space. For example, we could take the event $\{orange, lemon\} \subset \Omega$ in the fruit bowl experiment.

Probability maps a set of events to a set of numbers in a certain axiomatic manner. Abstractly, probability is a function that assigns numbers in the range 0 to 1 to events

$$P : \text{set of events } \rightarrow [0,1]$$

which satisfies the following axioms:

  1. For any event $A$, $ 0 \le P(A) \le 1$.
  2. If $\Omega$ is the sample space, $P(\Omega) = 1$.
  3. If $A$ and $B$ are disjoint (i.e., $A \cap B = \emptyset$), then $P(A \cup B) = P(A) + P(B)$.
  4. If $A_1, A_2, \ldots$ is an infinite sequence of pair-wise disjoint events (i.e., $A_i \cap A_j = \emptyset$ when $i \ne j$), then $$ \begin{array}{lcl} \underbrace{P\left(\bigcup_{i=1}^{\infty}A_i\right)} &=& \underbrace{\sum_{i=1}^{\infty}P\left(A_i\right)} \\ A_1 \cup A_2 \cup A_3 \dots &=& P(A_1) + P(A_2) + P(A_3) + \ldots \end{array} $$

These axioms or assumptions are motivated by the frequency interpretation of probability. The frequency interpretation of probability says that if we repeat an experiment a very large number of times then the fraction of times that the event $A$ occurs will be close to $P(A)$.

More precisely,

$$ \begin{array}{llcl} \mbox{let } & N(A, n) & = & \mbox{ the number of times } A \mbox{ occurs in the first } n \mbox{ trials,} \\ \mbox{then } & P(A) & = & \lim_{n \rightarrow \infty} \frac{N(A, n)}{n} \end{array} $$

To think about this, consider what $\lim_{n \rightarrow \infty} \frac{N(A, n)}{n}$ is:

$$ \begin{array}{c} \frac{N(A, 1)}{1},\, \frac{N(A, 2)}{2},\,\frac{N(A, 3)}{3},\,\ldots \mbox{where is this fraction going?} \end{array} $$

Let's look at axioms 1, 2, and 3 above more closely.

For any event $A$, $ 0 \le P(A) \le 1$. Well, clearly $0 \le \frac{N(A, n)}{n} \le 1$.

If $\Omega$ is the sample space, $P(\Omega) = 1$. This essentially says "something must happen". $P(\Omega) = \frac{N(\Omega, n)}{n} = \frac{n}{n} = 1$.

If $A$ and $B$ are disjoint (i.e., $A \cap B = \emptyset$), then $N(A \cup B, n) = N(A, n) + N(B, n)$ since $A \cup B$ occurs if either $A$ or $B$ occurs but we know that it is impossible for both $A$ and $B$ to happen (the intersection is the empty set). This extends to infinitely many disjoint events.

Axiom 4 is a bit more controversial, but here we assume it as part of our axiomatic definition of probability (without it the maths is much harder!).

Lets do some probability examples.

Example 1: Tossing a fair coin

The sample space and probabilties of this experiment are:

$$\Omega = \{H, T\}, \ \text{and} \ P(\{H\}) = P(\{T\}) = \frac{1}{2} \ .$$

We can represent our probability as the following function:

  • $P : \{ \{H\} , \{T\}, \{H,T\}, \{\} \} \to \{0,\frac{1}{2},1\}$,
  • with $P(\{H\})=\frac{1}{2}$, $P(\{T\})=\frac{1}{2}$, $P(\{H,T\})=1$ and $P(\{\})=0$.

notational convenience: The outcomes which are the elements in the the sample space are denoted without set brackets, for example: $P(\{H\})$ is denoted by $P(H)$ for brevity.

Check that all our axioms are satisfied:

  1. yes! because: $0 \le P(H) = P(T) = \frac{1}{2} \le 1$ and $0 \le P(\Omega) = 1 \le 1$.
  2. yes! becasue: $P(\Omega) = P(\{H, T\} = P(\{H\}) + P(\{T\}) = \frac{1}{2} + \frac{1}{2} = 1$.
  3. yes!, because: $P(\{H, T\} = P(\{H\}) + P(\{T\})$.

Example 2: Tossing an unfair coin

$\Omega = \{H, T\}$, $P(H) =\frac{3}{4}$, $P(T) = \frac{1}{4}$. So the coin lands heads 3 out of 4 times and lands tails only 1 out of 4 times.

Check that all our axioms are satisfied:

  1. $0 \le P(H) = \frac{3}{4} \le 1$, $0 \le P(T) = \frac{1}{4} \le 1$ and $0 \le P(\Omega) = 1 \le 1$.
  2. $P(\Omega) = P(\{H, T\} = P(\{H\}) + P(\{T\}) = \frac{3}{4} + \frac{1}{4} = 1$.
  3. $P(\{H, T\} = P(\{H\}) + P(\{T\})$.

Yes, all three axioms are satisfied by the probabiliy for this unfair coin experiment too.

Example 2': Tossing an unfair coin $m$ times to construct a random graph

From Wikipedia:

In the mathematical field of graph theory, the Erdős–Rényi model is either of two closely related models for generating random graphs. They are named after mathematicians Paul Erdős and Alfréd Rényi, who first introduced one of the models in 1959,12 while Edgar Gilbert introduced the other model contemporaneously and independently of Erdős and Rényi.3

In the model of Erdős and Rényi, all graphs on a fixed vertex set with a fixed number of edges are equally likely; in the model introduced by Gilbert, each edge has a fixed probability of being present or absent, independently of the other edges.

Draw the construction on a graph with $n$ vertices and $m$ edges.

What is exactly a Graph?

<br><br><br ><br >

In [90]:
showURL("https://en.wikipedia.org/wiki/Erd%C5%91s%E2%80%93R%C3%A9nyi_model",400)
Out[90]:

Example 3: New Zealand Lotto

In New Zealand Lotto, the balls drawn are numbered 1 to 40. The number on a ball is an outcome.

$\Omega = \{1, 2, \ldots,40\}$, $P(\omega) = \frac{1}{40}$ for each $\omega \in \Omega$ (i.e., $P(1) = P(2) = \ldots = P(40) = \frac{1}{40}$)

Now, consider the event that the first ball is even? What is the probability of this event, $P(\{2, 4, 6, \ldots, 38, 40\})$?

$$ \begin{array}{lcll} P(\{2, 4, \ldots, 38, 40\}) & = & P \left( \{2\} \cup \{4\} \cup \cdots \cup \{38\} \cup \{40\} \right) & \mbox{(defn. of set union)}\\ & = & P( \{2\}) +P( \{4\}) + \cdots + P(\{38\}) + P( \{40\}) & \mbox{(extn. Axiom 3)} \\ & = & \sum_{i \in \{2, 4, \ldots, 40\}} P(\{i\}) & \\ & = & 20 \times \frac{1}{40} & \\ & = & \frac{1}{2} & \end{array} $$

Similarly for the probability of an odd ball:

$$ \begin{array}{lcl} P(\{1, 3, 5, \ldots, 37, 39\}) & = & P \left( \{1\} \cup \{3\} \cup \cdots \cup \{37\} \cup \{39\} \right)\\ & = & P(\{1\}) +P( \{3\}) + \cdots + P(\{37\}) + P( \{39\})\\ & = & \sum_{i \in \{1, 3, \ldots, 37, 39\}} P(\{i\}) \\ & = & 20 \times \frac{1}{40} \\ & = & \frac{1}{2} \end{array} $$

Aside: The set of all possible subsets of $\Omega$ is called the power set and is denoted by $2^{\Omega}$. The power set contains all events of a sample space and is a natural domain for the probability function defined on a sample space with finitely many outcomes. We will see more on this later but if you are impatient see here.

Now, having introduced a number of definitions, we will derive some basic logical consequences, (i.e., properties) of probabilities (axiomatically defined).

Property 1

$P(A) = 1 - P(A^c)$, where $A^c = \Omega \setminus A$ is the complement of $A$.

Proof

$A \cap A^c = \emptyset$ and $A \cup A^c = \Omega$

Recall that axiom 3 says that if $A_1 \cap A_2 = \emptyset$ then $P(A_1 \cup A_2) = P(A_1) + P(A_2)$

So this implies that $P(A) + P(A^c) = P(\Omega) = 1$, by axiom 2 which says that $P(\Omega) = 1$

Subtracting $P(A^c)$ from both sides, we get $P(A) + P(A^c) - P(A^c) = 1 - P(A^c)$

Cancelling out the two $P(A^c)$ terms on the right hand side, we get $P(A) = 1 - P(A^c)$ .

For example, in the coin tossing experiment, $\Omega = \{H, T\}$

$$P(H) = 1 - P(H^c) = 1 - P(\Omega \setminus H) = 1 - P(T)$$

Property 2

For any two events $A$, $B$,

$P(A \cup B) = P(A) + P(B) - P(A \cap B)$

Proof:

AIntersectionB.png

This is an informal proof using the picture above. If we just add the probabilities of $A$ and $B$ we will double count the probabilties of the outcomes which are in both $A$ and $B$. We adjust for this double counting by subtracting $P(A \cap B)$.

Note that if $A \cap B = \emptyset$ then $P(A \cap B) = 0$ and $P(A \cup B) = P(A) + P(B)$.

Computer Representations of Mathematical Concepts and Objects

So we have probabilities associated with events satisfying some axioms -- that sounds like a good use for a dictionary? That's basically what are going to use, but we 'wrap' up the dictionary in some extra code that does things like check that the probabilities add to 1, and that each element in the list of outcomes we have given is unique. If you are interested in programming, we have coded our own type, or class, called ProbyMap (it's given in the next cell, and you can simply evaluate it and ignore all the details completely!). Once the class is coded, we create a ProbyMap by providing the sample space and the probabilities. You don't have to worry about how the class is implemented, but note that you may often want to use the computer to create a computerised representation of a mathematical concept. Once the concept (a discrete probability map in our case) is implemenetd, then we can use the computer to automate the mundane tasks of large-scale computations.

SageMath already has some such implementations that are more sophisticated and general:

Here we will roll our own for pedagogical reasons.

In [91]:
# create a class for a probability map - if you are new to SageMath/Python just evaluate and skip this cell
# This was coded by Jenny Harlow

import copy
class ProbyMap(object):                # class definition
    'Probability map class'
    def __init__(self, sspace, probs): # constructor
        self.__probmap = {}  # default probmap is empty
        # make checks on the objects given as sspace and probs
        try:                           
            sspace_set = set(sspace) # check that we can make the sample space into a set
            assert len(sspace_set) == len(sspace) # and not lose any elements
            prob_list = list(probs) # and we can make the probs into a list
            probsum = sum(prob_list) # and we can sum the probs
            assert probsum == 1 # and the probs sum to 1
            assert len(prob_list) == len(sspace_set) # and there is proby for each event
            
            self.__probmap = dict(zip(list(sspace),prob_list))    # map from sspace to probs
                    
        except TypeError, diag: # if there any problems with types
            init_error = 1
            print str(diag)
            
        except AssertionError:
            init_error = 1
            print "Check sample space and probabilities"
            
                  
    def P(self, events):
        '''Return the probability of an event or set of events.
        
        events is set of events in the sample space to calculate the probability for.'''
        
        retvalue = 0
        try:                              
            events_set = set(events) # check we can make a set out of the events
            assert len(events_set) == len(events) # and not lose any events
            assert events_set <= set(self.__probmap.keys()) # events subset of sample space
        
            for ev in events:     # add each mapped probability to the return value
                retvalue += self.__probmap[ev]
                
        except TypeError, diag:
            print str(diag)      
        
        except AssertionError:
            print "Check your events"
        
        return retvalue
        
    def __str__(self):                            # redefine printable string rep
        'Printable representation of the object.'
        num_keys = len(self.__probmap.keys())
        counter = 0
        retval = '{'
        for each_key in self.__probmap:
            counter += 1
            retval += str(each_key)
            retval += ': '
            retval += "%.3f" % self.__probmap[each_key]
            if counter < num_keys:
                retval += ', '
        retval += '}'        
                
        return retval
        
    __repr__ = __str__
        
    def get_probmap(self):                        # get a deep copy of the proby map
        return copy.deepcopy(self.__probmap)      # getter cannot alter object's map
    
    probmap = property(get_probmap)               # allow read access via .probmap
    
    def get_ref_probmap(self):                    # get a reference to the real probmap
        return self.__probmap                     # getter can alter the object's map
        
    
    ref_probmap = property(get_ref_probmap)       # allow access via .ref_probmap
    
    @staticmethod
    def dictExp(big_map, small_map):
        '''Internal helper function for __pow__(...).
        
        Takes two proby map dictionaries and returns one mult by other.'''
        new_bl = {}
        for sle in small_map:
            for ble in big_map:
                new_key = str(ble) + ' ' + str (sle)
                new_bl[new_key] = big_map[ble]*small_map[sle]
        return new_bl
        
    def __pow__(self, x):
        '''probability map exponentiated.'''
        try:
            assert isinstance(x, Integer)
            pmap = copy.deepcopy(self.__probmap) # copy the probability map dictionary
            new_pmap = copy.deepcopy(self.__probmap) # and another copy
            for i in range(x-1):
                new_pmap = self.dictExp(new_pmap, pmap)
            
            return ProbyMap(new_pmap.keys(), new_pmap.values())       
            
        except AssertionError:
            print "cannot raise to non-integer power"
            return None

Example 4: Experiments, outcomes, sample spaces, events, and the probability of events

Let's go back to the well-mixed fruit bowl experiment. The fruit bowl contains:

  • 2 oranges
  • 3 apples
  • 1 lemon

The experiment is to take one piece of fruit from the bowl and the outcome is the type of fruit we get.

The sample space is $\Omega = \{orange, apple, lemon\}$

We can use the Sage list to create this sample space (a list is a bit easier to use than a set, but using a list means that we are responsible for making sure that each element contained in it is unique).

In [92]:
# sample space is the set of distinct type of fruits in the bowl
samplespace = ['orange', 'apple', 'lemon']

We can also use a list to specify what the probability of each outcome in the sample space is. The probabilities can be calculated by knowing how many fruits of each kind are there in the bowl. We say that the fruit bowl is 'well-stirred', which essentially means that when we pick a fruit it really is a 'random sample' from the bowl (for example, we have not carefully put all the apples on the top so that someone will almost certainly get an apple when they take a fruit). More on this later in the course! Note that the probabilities encoded by a list named probabilities are in the same order as the outcomes in the samplespace list.

In [93]:
# probabilities take into account the number of each type of fruit in the "well-stirred" fruit bowl
probabilities = [2/6, 3/6, 1/6]
In [94]:
probMapFruitbowl = ProbyMap(sspace = samplespace, probs=probabilities) # make our probability map
probMapFruitbowl         # disclose our probability map
Out[94]:
{orange: 0.333, lemon: 0.167, apple: 0.500}

We can use our probability map to find the probability of a single outcome like this:

In [95]:
# Find the probability of outcome 'lemon'
probMapFruitbowl.P(['lemon'])
Out[95]:
1/6

We can also use our probability map to find the probability of an event (set of outcomes).

In [96]:
# Find the probability of the event {lemon, orange}
probMapFruitbowl.P(['lemon','orange'])
Out[96]:
1/2

Basically, the probability map implemenetd by ProbyMap is essentially a map or dictionary with some additional bells and whistles.

Basically, the probability map is essentially a map or dictionary.

Next we will obtain the set of all events (the largest $\sigma$-algebra or $\sigma$-field in math lingo) from the outcomes in our sample space via the Subset function and find the probability of each event using our ProbyMap in a for loop.

In [97]:
# make the set of all possible events from the set of outcomes 
setOfAllEvents = Subsets(samplespace)  # Subsets(A) returns the set of all subsets of A
list(setOfAllEvents)    # disclose the set of all events
Out[97]:
[{},
 {'orange'},
 {'apple'},
 {'lemon'},
 {'orange', 'apple'},
 {'orange', 'lemon'},
 {'lemon', 'apple'},
 {'orange', 'lemon', 'apple'}]

We have not done loops yet, but we will soon. Just as a foretaste, here we use a for loop to print out the computed probabilities for each event in the setOfAllEvents.

In [98]:
# loop through the set of all events and print the computed probability
for event in setOfAllEvents:
    print "P(", event, ") = ", probMapFruitbowl.P(event)
P( {} ) =  0
P( {'orange'} ) =  1/3
P( {'apple'} ) =  1/2
P( {'lemon'} ) =  1/6
P( {'orange', 'apple'} ) =  5/6
P( {'orange', 'lemon'} ) =  1/2
P( {'lemon', 'apple'} ) =  2/3
P( {'orange', 'lemon', 'apple'} ) =  1

YouTry

Try working through Example 6 below for yourself in the tutorial.

Example 5: Experiments with the English language

In English language text there are 26 letters in the alphabet. The relative frequencies with which each letter appears is tabulated below:

E 13.0% H 3.5% W 1.6%
T 9.3% L 3.5% V 1.3%
N 7.8% C 3.0% B 0.9%
R 7.7% F 2.8% X 0.5%
O 7.4% P 2.7% K 0.3%
I 7.4% U 2.7% Q 0.3%
A 7.3% M 2.5% J 0.2%
S 6.3% Y 1.9% Z 0.1%
D 4.4% G 1.6%    

Using these relative frequencies as probabilities we can create a probability map for the letters in the English alphabet. We start by defining the sample space and the probabilities.

In [99]:
alphaspace = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q',
              'R','S','T','U','V','W','X','Y','Z']
alphaRelFreqs = [73/1000,9/1000,30/1000,44/1000,130/1000,28/1000,16/1000,35/1000,74/1000,
                 2/1000,3/1000,35/1000, 25/1000,78/1000,74/1000,27/1000,3/1000,77/1000,63/1000,
                 93/1000,27/1000,13/1000,16/1000,5/1000,19/1000,1/1000]

Then we create the probability map, represented by a ProbyMap object.

In [100]:
probMapLetters = ProbyMap(sspace = alphaspace, probs=alphaRelFreqs) # make our probability map
probMapLetters         # disclose our probability map
Out[100]:
{A: 0.073, C: 0.030, B: 0.009, E: 0.130, D: 0.044, G: 0.016, F: 0.028, I: 0.074, H: 0.035, K: 0.003, J: 0.002, M: 0.025, L: 0.035, O: 0.074, N: 0.078, Q: 0.003, P: 0.027, S: 0.063, R: 0.077, U: 0.027, T: 0.093, W: 0.016, V: 0.013, Y: 0.019, X: 0.005, Z: 0.001}

Please do NOT try to list the set of all events of the 26 alphabet set: there are over 67 million events and the computer will probably crash! You can see how large a number we are talking about by evaluating the next cell which calculates $2^{26}$ for you.

In [101]:
2^26
Out[101]:
67108864

Instead of asking for the probability of each event (over 67 million of them to exhaustively march through!) we define some events of interest, say the vowels in the alphabet or the set of letters that make up a name.

In [102]:
vowels = ['A', 'E', 'I', 'O', 'U']

And we can get the probability that a letter drawn from a 'well-stirred' jumble of English letters is a vowel.

In [103]:
probMapLetters.P(vowels)
Out[103]:
189/500

We can make ourselves another set of letters and find probabilities for that too. In the cell below, we go straight from a string to a set. The reason that we can do this is that a string or str is in fact another collection; str is a sequence type just like list.

In [104]:
NameOfRaaz=set("RAAZESHSAINUDIIN")        # make a set from a string
NameOfRaaz                                # disclose the set NameOfRaaz you have built
Out[104]:
{'A', 'D', 'E', 'H', 'I', 'N', 'R', 'S', 'U', 'Z'}
In [105]:
probMapLetters.P(NameOfRaaz)
Out[105]:
301/500

Try either adapting what we have above, or doing the same thing in some of cells below, to find the probabilities of other sets of letters yourself. For example, what is the probability of your name in the above sense?

In [ ]:
 
In [ ]:
 

The crucial point of the above exercise with our own implementation of a Python class for probability maps is to show how computers and mathematics can go hand in hand to quickly generalize operations over specific instances of a large class of mathematical notions (Example 4 and 5 for probability models on finite sample spaces in our case above).

A Coming Attraction!

Recall how we downloaded Pride and Prejudice and processed it as a String and break it by Chapters. This is at our disposal - all we need to do is copy-paste the right set of cells from earlier here to have the string from that Book by fetching live from the Guthenberg Project again.

Think about what algorithmic constructs and methods one will need to split each sentence by words it contains and count the number of each distinct word.

We will need to understand for loops, list comprehensions and anonymous functions, as well as methods on strings for splitting (which you can search by adding a . after a srt and hitting the Tab button to look through exixting methods), and more crucially the data-structure we just saw called the dictionary, as we will see shortly.