# Upgrading the Backdoor Algorithm

We have two objectives in the notebook.  First is to better understand the mechanics of the existing back door identification algorithm at work in DoWhy.  The second is to plan out and execute and improvement which gives the expected answers to the five games below.  A tertiary goal may be to create a method for generating fake data based on a given graph.  The existing fake data generator is not very robust.

```python
import pandas as pd
import numpy as np
import daft
import dowhy
from dowhy.do_why import CausalModel
from dowhy.causal_graph import CausalGraph
from dowhy.causal_identifier import CausalIdentifier
from dowhy.datasets import stochastically_convert_to_binary
import networkx as nx
import statsmodels.api as sm

from copy import deepcopy

import sympy as sp
import sympy.stats as spstats
```

```python
# Helper functions adapted to work with numpy arrays.
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def stochastically_convert_to_binary(x):
    ps = sigmoid(x)
    return np.hstack([np.random.choice([0, 1], 1, p=[1-p, p]) for p in ps])

def stachastically_combine(parents):
    # We define this in this way so that we can work with an arbitrary
    # number of parents.
    # This intercept centers the mean of the values around 0.5
    size = len(parents[0])
    #output = np.ones(size)*-0.25*(len(parents)-1)
    #for p in parents:
    #    output += 0.5*p
    output = np.mean(parents, axis=0)
    return stochastically_convert_to_binary(output)
```

```python
def binary_graph_data(treatment_name='X', outcome_name='Y', observed_node_names=['X', 'Y'], ate=1, graph=None, n_obs=1000):
    """Generates data from a provided causal graph.

    For simplicity, all variables are binary.

    Args:
        treatment_name (string): name of the treatment variable in the provided graph
        outcome_name (string): name of the outcome variable in the provided graph
        observed_node_names (list): list of all nodes in the graph for which we will generate data
        ate (float): the average treatment effect.  Currently doesn't work. I'm not sure how I would implement it.
        graph (string): the graph provided in either GML or DOT format
    Returns:
        ret_dict (dictionary): a dictionary of the desired dataframe and some of the input
                               parameters to match the other dataset generators
    """

    def generate_data(observed_node_names, parents, children, data={}, var=None):
        """Recursive algorithm to generating data

        Get a random var from the list and check it's parents and children.
        If the node has no parents
            binary data is generated at random and recursively runs on a child
        Else
            it checks if data has been generated for all parents, and
                if not, runs itself on the parent.
            If data has been generated for all parents, then
                it creates a linear column of data dependant on its parents.
        """
        if var is None:
            # Allows us to not specify a var to start with
            var = observed_node_names.pop()

        p, c = parents[var], children[var]

        if var in data.keys():
            pass
        elif p == set():
            # If it has no parents, then it's a root node. Randomly generate it.
            #print(f"Generating {var}")
            data[var] = np.random.choice(2, n_obs)
            try:
                # Remove it from the list of variables left to generate
                observed_node_names.remove(var)
            except:
                pass
        else:
            # If the selected node is not a root, then it needs to be checked
            # to ensure that all parents have been calculated
            for parent in list(p):
                if parent not in data.keys():
                    generate_data(observed_node_names, parents, children, data=data, var=parent)

            # Finally, if all parents have data, then we can generate data for this node
            parent_data = [data[parent] for parent in list(p)]
            #print(f"Generating {var}")
            data[var] = stachastically_combine(parent_data)

        if observed_node_names != []:
            child = observed_node_names.pop()
            generate_data(observed_node_names, parents, children, data=data, var=child)

        return pd.DataFrame(data)

    cgraph = CausalGraph(treatment_name=treatment_name, outcome_name=outcome_name, graph=graph, observed_node_names=observed_node_names)
    # We don't care about unobserved confounders in this dataset, so we generate the unobserved
    # subgraph and work with that.
    obs_graph = cgraph.get_unconfounded_observed_subgraph()
    # Saves us from calling .predecessors and descendants a bunch
    parents = {var: set(obs_graph.predecessors(var)) for var in observed_node_names}
    children = {var: set(nx.descendants(obs_graph, var)) for var in observed_node_names}

    data = generate_data(observed_node_names, parents, children)

    ret_dict = {
        "df": data,
        "treatment_name": treatment_name,
        "outcome_name": outcome_name,
        "graph": graph,
        "ate": ate
    }
    return ret_dict
```

## The Causal Identifier Class

In DoWhy, causal effects are identified through a combination of instrumental variables and backdoor adjustment. I had expected some form of algorithm to identify backdoors, but it appears that actually they rely on a function which identifies common causes.  I suspect that this is the root of why they over-identify deconfounders.... And confirmed.

```python
# The code is actually a method of the CausalGraph object
graph_dot = "digraph {A -> X;A -> B;B -> C;X -> E;D -> B;D -> E;E -> Y}"
observed_node_names = ['X', 'A', 'Y', 'B', 'C', 'D', 'E']
treatment = 'X'
outcome = 'Y'
graph = CausalGraph(treatment_name=treatment, outcome_name=outcome,
                    graph=graph_dot, observed_node_names=observed_node_names,
                    unobserved_common_cause=False)

# This will produced the "A" that we believe is unnecessary in the
# estimand
graph.get_common_causes("X", "Y")
```

    Error: Pygraphviz cannot be loaded. No module named 'pygraphviz'
    Trying pydot ...

    ['A']

```python
# We can identify the mechanics fairly easily from how .get_common_causes works.
# We start with sets of ancestors of both nodes.
print(f"Ancestors of X: {graph.get_ancestors('X')}")
print(f"Ancestors of Y: {graph.get_ancestors('Y')}")
# They then take the intersection of these sets to find the common nodes
print(f"The 'Common Causes': {list(graph.get_ancestors('X').intersection(graph.get_ancestors('Y')))}")
```

    Ancestors of X: {'A'}
    Ancestors of Y: {'X', 'A', 'E', 'D'}
    The 'Common Causes': ['A']

The primary issue with the current implementation in DoWhy is that is fails to account for the directions of the arrows.  The method doesn't consider any of the junctions nor whether the given nodes are *d-separated*.  As a result when it examines a graph like the one in game 2, used in the example above, it unnecessarily includes any node which is a parent of both X and Y, completely ignoring the colliders which prevent information from getting back to Y through the backdoor.

## Implementing a Proper Backdoor Identification Algorithm

So, the next question is can we implement a general algorithm for identifying backdoor paths given any causal graph.  For simplicity, lets assume that its a DAG. Within the structure of DoWhy, it might either be best implemented as a method of the Graph or within its own Causal Identifier class.

Outline of the general approach:
 1. Identify all nodes in backdoor paths of X to Y.
 2. Evaluate all junctions to see if X and Y are d separated
     * If they are: Stop
     * If they aren't, control from one junction

```python
graph_dot = "digraph {A -> X;A -> B;C -> B;C -> Y;X -> Y;B -> X;}"
observed_node_names = ['X', 'A', 'Y', 'B', 'C']
treatment = 'X'
outcome = 'Y'
graph = CausalGraph(treatment_name=treatment, outcome_name=outcome,
                    graph=graph_dot, observed_node_names=observed_node_names,
                    unobserved_common_cause=False)

graph.view_graph()
```

```python
# Identifying all of the parents of X, which tells us
# the nodes from which all backdoor paths flow.
backdoor_root = graph.get_parents('X')
# We will use the undirected version of this graph to get all backdoor paths
undag = graph._graph.to_undirected()

bdps = []
for backdoor in backdoor_root:
    for path in nx.algorithms.simple_paths.all_simple_paths(undag, backdoor, outcome):
        if treatment not in path:
            bdps.append(path)

print(bdps)
# At this point I think we want to induce a subgraph on just the appropriate nodes
bd_graph = graph._graph.subgraph(bdps[1])

# Doesn't that look like the prettiest junction you've ever seen
nx.draw(bd_graph, with_labels=True, font_weight='bold')

# So we can actually get a list of all junctions simply by moving a window over the list.
from collections import deque

def window(seq, n=2):
    """A nice solution stolen from https://stackoverflow.com/questions/6822725/rolling-or-sliding-window-iterator"""
    it = iter(seq)
    win = deque((next(it, None) for _ in range(n)), maxlen=n)
    yield win
    append = win.append
    for e in it:
        append(e)
        yield win

def get_junctions(bdp):
    return [list(junction) for junction in window(bdp, n=3)]

# This allows us to get a list of all junctions for a given backdoor path
junctions = get_junctions(bdps[1])
junctions

# We now need to be able to evaluate each junction and place it into one of three categories:
#    1. A chain: X -> A -> Y
#    2. A collider: X -> A <- Y
#    3. A confounder: X <- A -> Y
bdps
```

```python
# One way to test the status of the junction
test_junction = ['A', 'B', 'C']

def test_junction(graph, junction):
    jscore = 0
    l, m, r = junction
    # Induce the subgraph for this junction
    jgraph = graph.subgraph(junction)
    # And test it's edges
    for edge in list(jgraph.edges):
        if (edge == (m, l, 0)) | (edge == (m, r, 0)):
        # If we encounter an edge like this, we know it can't be a collider
            return 0
    # If we get here it's a collider
    return 1

test_junction(bd_graph, junction=['A', 'B', 'C'])
```

So at this point we can:
  *  Generate all backdoor paths
  *  Generate a list of junctions for each backdoor path
  *  Test each junction to see if it's open or closed

This just leaves the simultaneously closing all backdoor paths.  This problem takes the interesting form of a constraint satisfaction problem.  The real challenge will be in keeping track of how a change in one bdp will change the state of a different one.

Here's our general algorithm for identifying all deconfounding sets in addition to the smallest one:
```
Given a directed acyclic graph G, the backdoor paths from X to Y is given by:
  1. On the undirected equivalent of graph G, identify all paths from the parents of X to Y which don't pass through X. These are the backdoor paths.
  2. For each backdoor path, decompose the path into a list of junctions.
  3. Evaluate if each junction is open or closed by the d-separation rules.
  4. If for each backdoor path, at least one junction is closed. Then stop. X and Y and d-separated.
  5. Otherwise, we begin a breadth-first tree search.
      Each child is given by the action of controlling for one of the open nodes in the backdoor path that is entirely open.
          `controlling` for 'A' is equivalent to negating the state of all junctions where 'A' is in the middle.
      Each child is then evaluated to see if X and Y are d-separated given Z (the node we controlled for).
      If not, the tree continues
      We can stop once we've either identified the short set Z which cause X and Y to be d-separated or continue until we've identified

```

```python
def search_for_deconfounders(jstates, bdjunctions, paths, depth=1, deconfounders=[], proposal=[]):
    if (depth >= 10) or ((paths==0).sum() == 0):
        # We'll stop if we've reached 10 variables [arbitrary stopping point] or if this set has successfully deconfounded the graph
        return proposal
    # Determine which path is open
    pathix = np.argmin(paths)
    # And which nodes are in the middle of each junction in that path
    nodes = [node[1] for node in bdjunctions[pathix]]
    # Determine which of those nodes have already been controlled for in the proposal
    newnodes = list(set(nodes).difference(proposal))
    # We will consider the consequences of controlling for every junction which isn't already in the proposal.
    for node in newnodes:
        # We are going to create deepcopies of our input variables off the bat because we don't
        # want cross contamination between different sets of deconfounders.
        newbdstates, newjstates, newproposal = deepcopy(bdjunctions), deepcopy(jstates),deepcopy(proposal)
        # extract the middle value from that junction.  We will control this node.
        newproposal.append(node)
        print(f"Currently considering: {newproposal}")
        # The effect of which is that for every junction where that node is the middle, we should
        # flip the state of that junction in a new copy of jstates
        for key, item in newjstates.items():
            if key[1] == node:
                newjstates[key] = int(not(newjstates[key]))
        # We can then update the junction states in their path context as well
        for ix, backdoor in enumerate(newbdstates):
            for jx, junction in enumerate(newbdstates[ix]):
                newbdstates[ix][jx] = newjstates[tuple(junction)]
         # Calculate new path scores
        newpaths = np.array([sum(path) for path in newbdstates])
        deconfounder = search_for_deconfounders(jstates=newjstates,bdjunctions=bdjunctions, paths=newpaths,
                                                depth=depth+1, deconfounders=deconfounders, proposal=newproposal)
        #print(f"State of deconfounders: {deconfounder}")
        deconfounders.append(deconfounder)
    return deconfounders

def identify_deconfounders(treatment_name, outcome_name, graph):
    """Identifies the minimal set of deconfounders from a given treatment and outcome node

    :param treatment_name: name of the treatment variable
    :param outcome_name: name of the outcome variable
    :param graph: a CausalGraph instance
    :returns: a list of deconfounders for all backdoor paths from treatment to outcome
    """
    # Identifying all of the parents of X, which tells us
    # the nodes from which all backdoor paths flow.
    backdoor_root = graph.get_parents(treatment_name)
    # We will use the undirected version of this graph to get all backdoor paths
    undag = graph._graph.to_undirected()

    bdpaths = []
    for backdoor in backdoor_root:
        for path in nx.algorithms.simple_paths.all_simple_paths(undag, backdoor, outcome_name):
            if treatment_name not in path:
                bdpaths.append([treatment_name]+path)

    # Induce subgraphs on each set of backdoor nodes
    bdgraphs = [graph._graph.subgraph(bdp) for bdp in bdpaths]

    # This allows us to get a list of all junctions for each backdoor path
    bdjunctions = [get_junctions(bdp) for bdp in bdpaths]
    # We will store the state of all junctions in the same dictionary.  This
    # is how we'll allow closing a junction for one backdoor path influence
    # the other backdoor paths.
    jstates = {}
    # Iterating through each backdoor path and junction, we deterine its state
    # by determining if its a collider or not.
    bdstates = deepcopy(bdjunctions)
    for ix, backdoor in enumerate(bdgraphs):
        for jx, junction in enumerate(bdjunctions[ix]):
            # tuple is required because it's hashable, unlike lists...
            state = test_junction(bdgraphs[ix], junction=junction)
            jstates[tuple(junction)] = state
            bdstates[ix][jx] = state

    # Time to close the backdoors. If the sum of a backdoor paths's states is 0
    # then it's entirely open (no colliders at the beginning). We pull in numpy
    # here so we can more naturally work with the arrays.
    paths = np.array([sum(path) for path in bdstates])
    # We'll store the identified sets of deconfounders as we find them. We will put
    # an artificial cap based on the number of layers that we'll search in the tree.
    # It will collect as muany sets as it can. We just need to be mindful of ending
    # when there are no more possibilities.
    return search_for_deconfounders(jstates=jstates, bdjunctions=bdjunctions, paths=paths, depth=1, deconfounders=[], proposal=[])
```

```python
graph_dot = "digraph {A -> X;A -> B;C -> B;C -> Y;X -> Y;B -> X;}"
observed_node_names = ['X', 'A', 'Y', 'B', 'C']
treatment = 'X'
outcome = 'Y'
graph = CausalGraph(treatment_name=treatment, outcome_name=outcome,
                    graph=graph_dot, observed_node_names=observed_node_names,
                    unobserved_common_cause=False)

deconfounders = identify_deconfounders(treatment, outcome, graph)
deconfounders
```

    ERROR:dowhy.causal_graph:Error: Pygraphviz cannot be loaded. No module named 'pygraphviz'
    Trying pydot ...

    Currently considering: ['C']
    Currently considering: ['B']
    Currently considering: ['B', 'C']
    Currently considering: ['B', 'A']

    [['C'], ['B', 'C'], ['B', 'A'], [...]]

```python
deconfounders[3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3][3]
```

    [['C'], ['B', 'C'], ['B', 'A'], [...]]

## Example 1: Book of Why Backdoor Games

These games all come from Chapter 4 of the Book of Why.  They are devoid of context, but the graphical model is given and our only job is to identify the set of backdoor paths from X to Y.  I will use these games as unit tests of the behavior of DoWhy's backdoor algorithm.

### Game 1

This relatively trivial game has no backdoor paths.

```python

```

```python
graph_dot = "digraph {X -> A;A -> Y;A -> B;}"
observed_node_names = ["B", "X", "Y", "A"]

data = binary_graph_data(observed_node_names=observed_node_names, graph=graph_dot)
```

    ERROR:dowhy.causal_graph:Error: Pygraphviz cannot be loaded. No module named 'pygraphviz'
    Trying pydot ...

```python
graph_dot = "digraph {X -> A;A -> Y;A -> B;}"
observed_node_names = ["B", "X", "Y", "A"]

data = binary_graph_data(observed_node_names=observed_node_names, graph=graph_dot)

game_one = CausalModel(
                data=data['df'],
                treatment=data['treatment_name'],
                outcome=data['outcome_name'],
                graph=data['graph'])

game_one.view_model()
```

```python
identified_estimand = game_one.identify_effect()
print(identified_estimand)
```

    INFO:dowhy.causal_identifier:Common causes of treatment and outcome:['U']
    WARNING:dowhy.causal_identifier:There are unobserved common causes. Causal effect cannot be identified.

    WARN: Do you want to continue by ignoring these unobserved confounders? [y/n]  y

    INFO:dowhy.causal_identifier:Instrumental variables for treatment and outcome:[]

    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,,U) = P(Y|X,)
    ### Estimand : 2
    Estimand name: iv
    No such variable found!

```python
identify_deconfounders('X', 'Y', game_one._graph)
```

    Currently considering: ['U']
    State of deconfounders: ['U']

    [['U']]

```python
# Unfortunate that this generates an error.
lr_estimate = game_one.estimate_effect(identified_estimand,
        method_name="backdoor.linear_regression")
print(lr_estimate)
print("Causal Estimate is " + str(lr_estimate.value))
```

```python
# We can confirm this estimate against what we could produce without DoWhy
lm = sm.OLS(endog=data['df']['Y'], exog=sm.tools.add_constant(data['df']['X']))
res = lm.fit()
print(res.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      Y   R-squared:                       0.004
    Model:                            OLS   Adj. R-squared:                  0.003
    Method:                 Least Squares   F-statistic:                     3.835
    Date:                Fri, 15 Mar 2019   Prob (F-statistic):             0.0505
    Time:                        17:50:02   Log-Likelihood:                -696.70
    No. Observations:                1000   AIC:                             1397.
    Df Residuals:                     998   BIC:                             1407.
    Df Model:                           1
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const          0.5839      0.022     26.394      0.000       0.540       0.627
    X              0.0602      0.031      1.958      0.050      -0.000       0.121
    ==============================================================================
    Omnibus:                       33.901   Durbin-Watson:                   2.047
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):              166.303
    Skew:                          -0.470   Prob(JB):                     7.72e-37
    Kurtosis:                       1.237   Cond. No.                         2.66
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

### Game 2
Introduces a backdoor path, but it's already blocked!

```python
graph_dot = "digraph {A -> X;A -> B;B -> C;X -> E;D -> B;D -> E;E -> Y}"
observed_node_names = ['X', 'A', 'Y', 'B', 'C', 'D', 'E']

data = binary_graph_data(observed_node_names=observed_node_names, graph=graph_dot, treatment_name='X', n_obs=5000)

game_two = CausalModel(
                data=data['df'],
                treatment=data['treatment_name'],
                outcome=data['outcome_name'],
                graph=data['graph'])

game_two.view_model()
```

```python
identified_estimand = game_two.identify_effect()
print(identified_estimand)
```

    INFO:dowhy.causal_identifier:Common causes of treatment and outcome:['U', 'A']
    WARNING:dowhy.causal_identifier:There are unobserved common causes. Causal effect cannot be identified.

    WARN: Do you want to continue by ignoring these unobserved confounders? [y/n]  y

    INFO:dowhy.causal_identifier:Instrumental variables for treatment and outcome:['A']

    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|A))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,A,U) = P(Y|X,A)
    ### Estimand : 2
    Estimand name: iv
    Estimand expression:
    Expectation(Derivative(Y, A)/Derivative(X, A))
    Estimand assumption 1, As-if-random: If U→→Y then ¬(U →→A)
    Estimand assumption 2, Exclusion: If we remove {A}→X, then ¬(A→Y)

```python
identify_deconfounders('X', 'Y', game_two._graph)
```

    Currently considering: ['U']
    State of deconfounders: [['U']]

    [['U']]

```python
lr_estimate = game_two.estimate_effect(identified_estimand,
        method_name="backdoor.linear_regression")
print(lr_estimate)
print("Causal Estimate is " + str(lr_estimate.value))
```

    INFO:dowhy.causal_estimator:INFO: Using Linear Regression Estimator
    INFO:dowhy.causal_estimator:b: Y~X+A

    *** Causal Estimate ***

    ## Target estimand
    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|A))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,A,U) = P(Y|X,A)
    ### Estimand : 2
    Estimand name: iv
    Estimand expression:
    Expectation(Derivative(Y, A)/Derivative(X, A))
    Estimand assumption 1, As-if-random: If U→→Y then ¬(U →→A)
    Estimand assumption 2, Exclusion: If we remove {A}→X, then ¬(A→Y)

    ## Realized estimand
    b: Y~X+A
    ## Estimate
    Value: 0.009653642291781901

    Causal Estimate is 0.009653642291781901

The identified estimand, $Y \sim X+A$, is again not what we expect.  It is treating A as an instrumental variable... I'm not sure that this is totally correct, though the back door paths from A to Y are blocked and thus we aren't actually introducing bias by controlling for A.  However, since the only backdoor path from X to Y is blocked by the collider at B we don't need to control for A in the first place, which was the answer that I expected from the algorithm.

```python
# We can confirm this estimate against what we could produce without DoWhy
lm = sm.OLS(endog=data['df']['Y'], exog=sm.tools.add_constant(data['df'][['E']]))
res = lm.fit()
print(res.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      Y   R-squared:                       0.000
    Model:                            OLS   Adj. R-squared:                  0.000
    Method:                 Least Squares   F-statistic:                     1.631
    Date:                Fri, 15 Mar 2019   Prob (F-statistic):              0.202
    Time:                        17:50:34   Log-Likelihood:                -3369.3
    No. Observations:                5000   AIC:                             6743.
    Df Residuals:                    4998   BIC:                             6756.
    Df Model:                           1
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const          0.6459      0.011     59.437      0.000       0.625       0.667
    E              0.0177      0.014      1.277      0.202      -0.009       0.045
    ==============================================================================
    Omnibus:                      304.804   Durbin-Watson:                   2.013
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):              871.967
    Skew:                          -0.660   Prob(JB):                    4.52e-190
    Kurtosis:                       1.437   Cond. No.                         3.00
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

### Game 3

This game finally introduces a backdoor path which requires us to control for a variable.

```python
graph_dot = "digraph {X -> Y;B -> X;B -> Y;X -> A;B -> A;}"

observed_node_names = ['X', 'A', 'Y', 'B']

data = binary_graph_data(observed_node_names=observed_node_names, graph=graph_dot)

game_three = CausalModel(
                data=data['df'],
                treatment=data['treatment_name'],
                outcome=data['outcome_name'],
                graph=data['graph'])

game_three.view_model()
```

```python
identified_estimand = game_three.identify_effect()
print(identified_estimand)
```

    INFO:dowhy.causal_identifier:Common causes of treatment and outcome:['U', 'B']
    WARNING:dowhy.causal_identifier:There are unobserved common causes. Causal effect cannot be identified.

    WARN: Do you want to continue by ignoring these unobserved confounders? [y/n]  y

    INFO:dowhy.causal_identifier:Instrumental variables for treatment and outcome:[]

    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|B))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,B,U) = P(Y|X,B)
    ### Estimand : 2
    Estimand name: iv
    No such variable found!

```python
identify_deconfounders('X', 'Y', game_three._graph)
```

    Currently considering: ['U']
    Currently considering: ['U', 'B']
    State of deconfounders: [['U', 'B']]
    State of deconfounders: [['U', 'B'], [...]]

    [['U', 'B'], [...]]

```python
lr_estimate = game_three.estimate_effect(identified_estimand,
        method_name="backdoor.linear_regression")
print(lr_estimate)
print("Causal Estimate is " + str(lr_estimate.value))
```

    INFO:dowhy.causal_estimator:INFO: Using Linear Regression Estimator
    INFO:dowhy.causal_estimator:b: Y~X+B

    *** Causal Estimate ***

    ## Target estimand
    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|B))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,B,U) = P(Y|X,B)
    ### Estimand : 2
    Estimand name: iv
    No such variable found!

    ## Realized estimand
    b: Y~X+B
    ## Estimate
    Value: 0.08506994214927611

    Causal Estimate is 0.08506994214927611

```python
# We can confirm this estimate against what we could produce without DoWhy
lm = sm.OLS(endog=data['df']['Y'], exog=sm.tools.add_constant(data['df'][['X', 'B']]))
res = lm.fit()
print(res.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      Y   R-squared:                       0.047
    Model:                            OLS   Adj. R-squared:                  0.045
    Method:                 Least Squares   F-statistic:                     24.33
    Date:                Fri, 15 Mar 2019   Prob (F-statistic):           4.84e-11
    Time:                        17:50:55   Log-Likelihood:                -659.30
    No. Observations:                1000   AIC:                             1325.
    Df Residuals:                     997   BIC:                             1339.
    Df Model:                           2
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const          0.5037      0.027     18.782      0.000       0.451       0.556
    X              0.0851      0.031      2.740      0.006       0.024       0.146
    B              0.1768      0.030      5.884      0.000       0.118       0.236
    ==============================================================================
    Omnibus:                       45.234   Durbin-Watson:                   1.918
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):              143.071
    Skew:                          -0.552   Prob(JB):                     8.56e-32
    Kurtosis:                       1.511   Cond. No.                         3.35
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

This one worked great.  Appropriately controls for B.

### Game 4

This graph is designed specifically as a trap called "M-bias".  Many statisticians would control for B.  What would DoWhy do?

```python
graph_dot = "digraph {A -> X;A -> B;C -> B;C -> Y;}"

observed_node_names = ['X', 'A', 'Y', 'B', 'C']

data = binary_graph_data(observed_node_names=observed_node_names, graph=graph_dot)

game_four = CausalModel(
                data=data['df'],
                treatment=data['treatment_name'],
                outcome=data['outcome_name'],
                graph=data['graph'])

game_four.view_model()
```

```python
identified_estimand = game_four.identify_effect()
print(identified_estimand)
```

    INFO:dowhy.causal_identifier:Common causes of treatment and outcome:['U']
    WARNING:dowhy.causal_identifier:There are unobserved common causes. Causal effect cannot be identified.

    WARN: Do you want to continue by ignoring these unobserved confounders? [y/n]  y

    INFO:dowhy.causal_identifier:Instrumental variables for treatment and outcome:['A']

    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,,U) = P(Y|X,)
    ### Estimand : 2
    Estimand name: iv
    Estimand expression:
    Expectation(Derivative(Y, A)/Derivative(X, A))
    Estimand assumption 1, As-if-random: If U→→Y then ¬(U →→A)
    Estimand assumption 2, Exclusion: If we remove {A}→X, then ¬(A→Y)

```python
identify_deconfounders('X', 'Y', game_four._graph)
```

    Currently considering: ['U']
    State of deconfounders: [['U']]

    [['U']]

```python
lr_estimate = game_four.estimate_effect(identified_estimand,
        method_name="backdoor.linear_regression")
print(lr_estimate)
print("Causal Estimate is " + str(lr_estimate.value))
```

```python
# We can confirm this estimate against what we could produce without DoWhy
lm = sm.OLS(endog=data['df']['Y'], exog=sm.tools.add_constant(data['df'][['X']]))
res = lm.fit()
print(res.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      Y   R-squared:                       0.001
    Model:                            OLS   Adj. R-squared:                 -0.000
    Method:                 Least Squares   F-statistic:                    0.7938
    Date:                Fri, 15 Mar 2019   Prob (F-statistic):              0.373
    Time:                        17:51:21   Log-Likelihood:                -710.02
    No. Observations:                1000   AIC:                             1424.
    Df Residuals:                     998   BIC:                             1434.
    Df Model:                           1
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const          0.5700      0.025     23.139      0.000       0.522       0.618
    X              0.0283      0.032      0.891      0.373      -0.034       0.091
    ==============================================================================
    Omnibus:                       19.902   Durbin-Watson:                   2.188
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):              166.800
    Skew:                          -0.353   Prob(JB):                     6.02e-37
    Kurtosis:                       1.128   Cond. No.                         2.92
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

DoWhy get's this one solidly.  No need to control for anything, and if the model is working right then we should detect no effect as there is no causal line from X to Y.

### Game 5

Our final, and most challenging game.  There are two possible solutions, either C alone can be closed or A and B. Let's see what it determines.

```python
graph_dot = "digraph {A -> X;A -> B;C -> B;C -> Y;B -> X; X -> Y}"

observed_node_names = ['X', 'A', 'Y', 'B', 'C']

data = binary_graph_data(observed_node_names=observed_node_names, graph=graph_dot, n_obs=1000)

game_five = CausalModel(
                data=data['df'],
                treatment=data['treatment_name'],
                outcome=data['outcome_name'],
                graph=data['graph'])

game_five.view_model()
```

```python
identified_estimand = game_five.identify_effect()
print(identified_estimand)
```

    INFO:dowhy.causal_identifier:Common causes of treatment and outcome:['C', 'U', 'A', 'B']
    WARNING:dowhy.causal_identifier:There are unobserved common causes. Causal effect cannot be identified.

    WARN: Do you want to continue by ignoring these unobserved confounders? [y/n]  y

    INFO:dowhy.causal_identifier:Instrumental variables for treatment and outcome:['A']

    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|C,A,B))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,C,A,B,U) = P(Y|X,C,A,B)
    ### Estimand : 2
    Estimand name: iv
    Estimand expression:
    Expectation(Derivative(Y, A)/Derivative(X, A))
    Estimand assumption 1, As-if-random: If U→→Y then ¬(U →→A)
    Estimand assumption 2, Exclusion: If we remove {A}→X, then ¬(A→Y)

```python
deconfounders = identify_deconfounders('X', 'Y', game_five._graph)
```

    Currently considering: ['U']
    Currently considering: ['U', 'C']
    State of deconfounders: [['U', 'C']]
    Currently considering: ['U', 'B']
    Currently considering: ['U', 'B', 'C']
    State of deconfounders: [['U', 'C'], ['U', 'B', 'C']]
    Currently considering: ['U', 'B', 'A']
    State of deconfounders: [['U', 'C'], ['U', 'B', 'C'], ['U', 'B', 'A']]
    State of deconfounders: [['U', 'C'], ['U', 'B', 'C'], ['U', 'B', 'A'], [...]]
    State of deconfounders: [['U', 'C'], ['U', 'B', 'C'], ['U', 'B', 'A'], [...], [...]]

```python
deconfounders[-1][-1]
```

    [[['C', 'U']], [[['C', 'U']], ['B', 'C', 'U']], ['B', 'A', 'U']]

```python
lr_estimate = game_five.estimate_effect(identified_estimand,
        method_name="backdoor.linear_regression")
print(lr_estimate)
print("Causal Estimate is " + str(lr_estimate.value))
```

    INFO:dowhy.causal_estimator:INFO: Using Linear Regression Estimator
    INFO:dowhy.causal_estimator:b: Y~X+A+C+B

    *** Causal Estimate ***

    ## Target estimand
    Estimand type: ate
    ### Estimand : 1
    Estimand name: backdoor
    Estimand expression:
    d
    ──(Expectation(Y|A,C,B))
    dX
    Estimand assumption 1, Unconfoundedness: If U→X and U→Y then P(Y|X,A,C,B,U) = P(Y|X,A,C,B)
    ### Estimand : 2
    Estimand name: iv
    Estimand expression:
    Expectation(Derivative(Y, A)/Derivative(X, A))
    Estimand assumption 1, As-if-random: If U→→Y then ¬(U →→A)
    Estimand assumption 2, Exclusion: If we remove {A}→X, then ¬(A→Y)

    ## Realized estimand
    b: Y~X+A+C+B
    ## Estimate
    Value: 0.11190830206093398

    Causal Estimate is 0.11190830206093398

```python
# We can confirm this estimate against what we could produce without DoWhy
lm = sm.OLS(endog=data['df']['Y'], exog=sm.tools.add_constant(data['df'][['X', 'C']]))
res = lm.fit()
print(res.summary())
```

                                OLS Regression Results
    ==============================================================================
    Dep. Variable:                      Y   R-squared:                       0.030
    Model:                            OLS   Adj. R-squared:                  0.028
    Method:                 Least Squares   F-statistic:                     15.43
    Date:                Thu, 14 Mar 2019   Prob (F-statistic):           2.51e-07
    Time:                        16:20:18   Log-Likelihood:                -672.11
    No. Observations:                1000   AIC:                             1350.
    Df Residuals:                     997   BIC:                             1365.
    Df Model:                           2
    Covariance Type:            nonrobust
    ==============================================================================
                     coef    std err          t      P>|t|      [0.025      0.975]
    ------------------------------------------------------------------------------
    const          0.5054      0.028     17.957      0.000       0.450       0.561
    X              0.1095      0.031      3.555      0.000       0.049       0.170
    C              0.1225      0.030      4.072      0.000       0.063       0.182
    ==============================================================================
    Omnibus:                       44.163   Durbin-Watson:                   1.968
    Prob(Omnibus):                  0.000   Jarque-Bera (JB):              152.421
    Skew:                          -0.544   Prob(JB):                     7.98e-34
    Kurtosis:                       1.427   Cond. No.                         3.47
    ==============================================================================

    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

It gives a correct answer, but not nearly the best.  Controlling for C would have been minimally sufficient, and A + B would have been a second best.  It makes me wonder about the logic internally, and why it doesn't aim towards the simplest solutions.  I would like to update the implementation, good practice in anticipation of the front door adjustment.  I have already played with an algorithm that more or less matches what was outlined [in this presentation](https://www.ssc.wisc.edu/~felwert/causality/wp-content/uploads/2013/06/2-elwert_dags.pdf).  For the complete identifcation algorithm we look to the [Ph.D. thesis of Ilya Shpitser](https://ftp.cs.ucla.edu/pub/stat_ser/shpitser-thesis.pdf), which provides a complete algorithm for identifying of a causal estimate can be identified.  Studying it will probably be helpful, particular if we want to be able to include the front-door in our estimands.

```python

```


TCII Github

Trabajo Semanal 5

Filtro Pasa Altos Notch con GIC

Liaño, Lucas

## Consigna

>Se debe diseñar un filtro pasa-altos, que presente máxima planicidad en la banda de paso (frecuencia de corte = 300 Hz) y un cero de transmisión en 100 Hz. El prototipo pasabajos normalizado presenta la siguiente respuesta:
>
>
> - Determine la expresión de H(s) del filtro pasa-altos normalizado
> - Realizar el diagrama de polos y ceros de H(s)
> - Sintetice el circuito del filtro pedido. Se utilizará para la estructura de segundo orden el siguiente circuito:
>
>

# Resolución a Mano

    Se adjunto PDF con la resolución manual 😊

## Implementación Computarizada

#### Importamos las librerias a utilizar

```python
import numpy as np
import matplotlib.pyplot as plt
from splane import pzmap, grpDelay, bodePlot, convert2SOS
from scipy import signal

# Esta es una liberia tomada de la comunidad [https://stackoverflow.com/questions/35304245/multiply-scipy-lti-transfer-functions?newreg=b12c460c179042b09ad75c2fb4297bc9]
from ltisys import *

# Módulos para Jupyter (mejores graficos!)
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['figure.figsize'] = [12, 4]
plt.rcParams['figure.dpi'] = 150 # 200 e.g. is really fine, but slower
```

#### Inicializamos las variables utilizadas

### Transferencia Notch Pasa-bajos Normalizada ( Dato )
\begin{equation}
    T(s) = \dfrac{s^{2} + 3^{2}} {s^2 + s + 1}
\end{equation}

### Transferencia Notch Pasa-altos Normalizada (Despues de calcular)
\begin{equation}
    T(s) = \dfrac{s^{2} + (\frac{1}{3})^{2}} {s^2 + s + 1} \cdot \dfrac{s}{s+1}
\end{equation}

```python
# Coeficientes Transferencias LP
wp = 1
Qp = 1
k = 3
wz = k * wp
```

## Definimos la transferencia a partir de la expresión hallada

```python
# Genero la función transferencia T1 en S
den_1 = [1, (wp / Qp), wp**2]
num_1 = [1, 0, wz**2]
T1 = ltimul(num_1, den_1);
```

## Diagrama de polos y ceros

### Transferencia $T_{1}(s)$

```python
pzmap(T1, 1);
```

## Diagrama de Bode

### Transferencia $H_{LP}(s)$

```python
fig, ax = bodePlot(T1.to_ss(), 2);
```

# Ahora aplicamos la transformación

### Transferencia $H_{HP}(s)$

```python
# Coeficientes Transferencias HP
wp = 1
Qp = 1
k = 1/3
wz = k * wp

sigma = 1
```

```python
# Genero la función transferencia T1 en S
den_1 = [1, (wp / Qp), wp**2]
num_1 = [1, 0, wz**2]
T1 = ltimul(num_1, den_1);

den_2 = [1, sigma]
num_2 = [1,0]
T2 = ltimul(num_2, den_2);

H = T1 * T2
```

```python
pzmap(H, 1);
```

```python
fig, ax = bodePlot(H.to_ss(), 2);
```

---
# Simulación Circuital


## 6.2 Subjective vs Objective Priors
Bayesian priors can be classified into two classes: objective priors, which aim to allow the data to influence the posterior the most, and subjective priors, which allow the practitioner to express his or her views into the prior.

What is an example of an objective prior? We have seen some already, including the flat prior, which is a uniform distribution over the entire possible range of the unknown. Using a flat prior implies that we give each possible value an equal weighting. Choosing this type of prior is invoking what is called "The Principle of Indifference", literally we have no prior reason to favor one value over another. Calling a flat prior over a restricted space an objective prior is not correct, though it seems similar. If we know $p$ in a Binomial model is greater than 0.5, then $\text{Uniform}(0.5,1)$ is not an objective prior (since we have used prior knowledge) even though it is "flat" over [0.5, 1]. The flat prior must be flat along the entire range of possibilities.

Aside from the flat prior, other examples of objective priors are less obvious, but they contain important characteristics that reflect objectivity. For now, it should be said that rarely is a objective prior truly objective. We will see this later.

### 6.2.2 Subjective Priors
On the other hand, if we added more probability mass to certain areas of the prior, and less elsewhere, we are biasing our inference towards the unknowns existing in the former area. This is known as a subjective, or informative prior. In the figure below, the subjective prior reflects a belief that the unknown likely lives around 0.5, and not around the extremes. The objective prior is insensitive to this.

```python
%matplotlib inline
import numpy as np
import scipy.stats as stats
from IPython.core.pylabtools import figsize
import matplotlib.pyplot as plt

figsize(12.5,3)
colors = ["#348ABD", "#A60628", "#7A68A6", "#467821"]

x = np.linspace(0,1)
y1, y2 = stats.beta.pdf(x, 1,1), stats.beta.pdf(x, 10,10)

p = plt.plot(x, y1,
    label='An objective prior \n(uninformative, \n"Principle of Indifference")')
plt.fill_between(x, 0, y1, color = p[0].get_color(), alpha = 0.3)

p = plt.plot(x,y2 ,
     label = "A subjective prior \n(informative)")
plt.fill_between(x, 0, y2, color = p[0].get_color(), alpha = 0.3)

p = plt.plot(x[25:], 2*np.ones(25), label = "another subjective prior")
plt.fill_between(x[25:], 0, 2, color = p[0].get_color(), alpha = 0.3)

plt.ylim(0,4)

plt.ylim(0, 4)
leg = plt.legend(loc = "upper left")
leg.get_frame().set_alpha(0.4)
plt.title("Comparing objective vs. subjective priors for an unknown probability");
```

The choice of a subjective prior does not always imply that we are using the practitioner's subjective opinion: more often the subjective prior was once a posterior to a previous problem, and now the practitioner is updating this posterior with new data. A subjective prior can also be used to inject domain knowledge of the problem into the model. We will see examples of these two situations later.

### 6.2.4 Empirical Bayes
While not a true Bayesian method, empirical Bayes is a trick that combines frequentist and Bayesian inference. As mentioned previously, for (almost) every inference problem there is a Bayesian method and a frequentist method. The significant difference between the two is that Bayesian methods have a prior distribution, with hyperparameters $\alpha$, while empirical methods do not have any notion of a prior. Empirical Bayes combines the two methods by using frequentist methods to select $\alpha$, and then proceeds with Bayesian methods on the original problem.

A very simple example follows: suppose we wish to estimate the parameter $\mu$ of a Normal distribution, with $\sigma = 5$. Since $\mu$ could range over the whole real line, we can use a Normal distribution as a prior for $\mu$. How to select the prior's hyperparameters, denoted ($\mu_p, \sigma_p^2$)? The $\sigma_p^2$ parameter can be chosen to reflect the uncertainty we have. For $\mu_p$, we have two options:

1 - Empirical Bayes suggests using the empirical sample mean, which will center the prior around the observed empirical mean:
$$ \mu_p = \frac{1}{N} \sum_{i=0}^N X_i $$

2 - Traditional Bayesian inference suggests using prior knowledge, or a more objective prior (zero mean and fat standard deviation).

Empirical Bayes can be argued as being semi-objective, since while the choice of prior model is ours (hence subjective), the parameters are solely determined by the data.

Personally, I feel that Empirical Bayes is double-counting the data. That is, we are using the data twice: once in the prior, which will influence our results towards the observed data, and again in the inferential engine of MCMC. This double-counting will understate our true uncertainty. To minimize this double-counting, I would only suggest using Empirical Bayes when you have lots of observations, else the prior will have too strong of an influence. I would also recommend, if possible, to maintain high uncertainty (either by setting a large $\sigma_p^2$ or equivalent.)

Empirical Bayes also violates a theoretical axiom in Bayesian inference. The textbook Bayesian algorithm of:

prior $\Rightarrow$ observed data $\Rightarrow$ posterior

is violated by Empirical Bayes, which instead uses

observed data $\Rightarrow$ prior $\Rightarrow$ observed data $\Rightarrow$ posterior

Ideally, all priors should be specified before we observe the data, so that the data does not influence our prior opinions.

## 6.3 Useful priors to know about
### 6.3.1 The Gamma distribution
A Gamma random variable, denoted $X \sim \text{Gamma}(\alpha, \beta)$, is a random variable over the positive real numbers. It is in fact a generalization of the Exponential random variable, that is:

$$ \text{Exp}(\beta) \sim \text{Gamma}(1, \beta) $$
This additional parameter allows the probability density function to have more flexibility, hence allowing the practitioner to express his or her subjective priors more accurately. The density function for a $\text{Gamma}(\alpha, \beta)$ random variable is:

$$ f(x \mid \alpha, \beta) = \frac{\beta^{\alpha}x^{\alpha-1}e^{-\beta x}}{\Gamma(\alpha)} $$
where $\Gamma(\alpha)$ is the Gamma function, and for differing values of $(\alpha, \beta)$ looks like:

```python
figsize(12.5, 5)
gamma = stats.gamma

parameters = [(1, 0.5), (9, 2), (3, 0.5), (7, 0.5)]
x = np.linspace(0.001 ,20, 150)
for alpha, beta in parameters:
    y = gamma.pdf(x, alpha, scale=1./beta)
    lines = plt.plot(x, y, label = "(%.1f,%.1f)"%(alpha,beta), lw = 3)
    plt.fill_between(x, 0, y, alpha = 0.2, color = lines[0].get_color())
    plt.autoscale(tight=True)

plt.legend(title=r"$\alpha, \beta$ - parameters");
```

### 6.3.2 The Wishart distribution
Until now, we have only seen random variables that are scalars. Of course, we can also have random matrices! Specifically, the Wishart distribution is a distribution over all positive semi-definite matrices. Why is this useful to have in our arsenal? (Proper) covariance matrices are positive-definite, hence the Wishart is an appropriate prior for covariance matrices. We can't really visualize a distribution of matrices, so I'll plot some realizations from the $5 \times 5$ (above) and $20 \times 20$ (below) Wishart distribution:

```python
n = 4
for i in range(10):
    ax = plt.subplot(2, 5, i+1)
    if i >= 5:
        n = 15
    plt.imshow(stats.wishart.rvs(n+1, np.eye(n)), interpolation="none",
                cmap = "hot")
    ax.axis("off")

plt.suptitle("Random matrices from a Wishart Distribution");
```

One thing to notice is the symmetry of these matrices. The Wishart distribution can be a little troubling to deal with, but we will use it in an example later.

### 6.3.3 The Beta distribution
You may have seen the term beta in previous code in this book. Often, I was implementing a Beta distribution. The Beta distribution is very useful in Bayesian statistics. A random variable $X$ has a $\text{Beta}$ distribution, with parameters $(\alpha, \beta)$, if its density function is:

$$f_X(x | \; \alpha, \beta ) = \frac{ x^{(\alpha - 1)}(1-x)^{ (\beta - 1) } }{B(\alpha, \beta) }$$
where $B$ is the Beta function (hence the name). The random variable $X$ is only allowed in [0,1], making the Beta distribution a popular distribution for decimal values, probabilities and proportions. The values of $\alpha$ and $\beta$, both positive values, provide great flexibility in the shape of the distribution. Below we plot some distributions:

```python
figsize(12.5, 5)

params = [(2, 5), (1, 1), (0.5, 0.5), (5, 5), (20, 4), (5, 1)]

x = np.linspace(0.01, .99, 100)
beta = stats.beta
for a, b in params:
    y = beta.pdf(x, a, b)
    lines = plt.plot(x, y, label = "(%.1f,%.1f)"%(a,b), lw = 3)
    plt.fill_between(x, 0, y, alpha = 0.2, color = lines[0].get_color())
    plt.autoscale(tight=True)
plt.ylim(0)
plt.legend(loc = 'upper left', title="(a,b)-parameters");
```

One thing I'd like the reader to notice is the presence of the flat distribution above, specified by parameters $(1,1)$. This is the Uniform distribution. Hence the Beta distribution is a generalization of the Uniform distribution, something we will revisit many times.

There is an interesting connection between the Beta distribution and the Binomial distribution. Suppose we are interested in some unknown proportion or probability $p$. We assign a $\text{Beta}(\alpha, \beta)$ prior to $p$. We observe some data generated by a Binomial process, say $X \sim \text{Binomial}(N, p)$, with $p$ still unknown. Then our posterior is again a Beta distribution, i.e. $p | X \sim \text{Beta}( \alpha + X, \beta + N -X )$. Succinctly, one can relate the two by "a Beta prior with Binomial observations creates a Beta posterior". This is a very useful property, both computationally and heuristically.

In light of the above two paragraphs, if we start with a $\text{Beta}(1,1)$ prior on $p$ (which is a Uniform), observe data $X \sim \text{Binomial}(N, p)$, then our posterior is $\text{Beta}(1 + X, 1 + N - X)$.

## 6.4 Example: Bayesian Multi-Armed Bandits
Of course, if we knew the bandit with the largest probability, then always picking this bandit would yield the maximum winnings. So our task can be phrased as "Find the best bandit, and as quickly as possible".

The task is complicated by the stochastic nature of the bandits. A suboptimal bandit can return many winnings, purely by chance, which would make us believe that it is a very profitable bandit. Similarly, the best bandit can return many duds. Should we keep trying losers then, or give up?

A more troublesome problem is, if we have found a bandit that returns pretty good results, do we keep drawing from it to maintain our pretty good score, or do we try other bandits in hopes of finding an even-better bandit? This is the exploration vs. exploitation dilemma.

### 6.4.1 Applications
The Multi-Armed Bandit problem at first seems very artificial, something only a mathematician would love, but that is only before we address some applications:
* Internet display advertising: companies have a suite of potential ads they can display to visitors, but the company is not sure which ad strategy to follow to maximize sales. This is similar to A/B testing, but has the added advantage of naturally minimizing strategies that do not work (and generalizes to A/B/C/D... strategies)
* Ecology: animals have a finite amount of energy to expend, and following certain behaviours has uncertain rewards. How does the animal maximize its fitness?
* Finance: which stock option gives the highest return, under time-varying return profiles.
* Clinical trials: a researcher would like to find the best treatment, out of many possible treatment, while minimizing losses.
* Psychology: how does punishment and reward affect our behaviour? How do humans learn?

Many of these questions above are fundamental to the application's field.

It turns out the optimal solution is incredibly difficult, and it took decades for an overall solution to develop. There are also many approximately-optimal solutions which are quite good. The one I wish to discuss is one of the few solutions that can scale incredibly well. The solution is known as Bayesian Bandits.

### 6.4.2 A Proposed Solution
Any proposed strategy is called an online algorithm (not in the internet sense, but in the continuously-being-updated sense), and more specifically a reinforcement learning algorithm. The algorithm starts in an ignorant state, where it knows nothing, and begins to acquire data by testing the system. As it acquires data and results, it learns what the best and worst behaviours are (in this case, it learns which bandit is the best). With this in mind, perhaps we can add an additional application of the Multi-Armed Bandit problem:

* Psychology: how does punishment and reward affect our behaviour? How do humans learn?

The Bayesian solution begins by assuming priors on the probability of winning for each bandit. In our vignette we assumed complete ignorance of these probabilities. So a very natural prior is the flat prior over 0 to 1. The algorithm proceeds as follows:

For each round:
1. Sample a random variable $X_b$ from the prior of bandit $b$, for all $b$.
2. Select the bandit with largest sample, i.e. select $B = \text{argmax}\;\; X_b$.
3. Observe the result of pulling bandit $B$, and update your prior on bandit $B$.
4. Return to 1.

That's it. Computationally, the algorithm involves sampling from $N$ distributions. Since the initial priors are $\text{Beta}(\alpha=1,\beta=1)$ (a uniform distribution), and the observed result $X$ (a win or loss, encoded 1 and 0 respectfully) is Binomial, the posterior is a $\text{Beta}(\alpha=1+X,\beta=1+1−X)$.

To answer our question from before, this algorithm suggests that we should not discard losers, but we should pick them at a decreasing rate as we gather confidence that there exist better bandits. This follows because there is always a non-zero chance that a loser will achieve the status of $B$, but the probability of this event decreases as we play more rounds (see figure below).

Below we implement Bayesian Bandits using two classes, Bandits that defines the slot machines, and BayesianStrategy which implements the above learning strategy.

```python
rand = np.random.rand

class Bandits(object):
    """
    This class represents N bandits machines.

    parameters:
        p_array: a (n,) Numpy array of probabilities >0, <1.

    methods:
        pull( i ): return the results, 0 or 1, of pulling
                   the ith bandit.
    """
    def __init__(self, p_array):
        self.p = p_array
        self.optimal = np.argmax(p_array)

    def pull(self, i):
        #i is which arm to pull
        return np.random.rand() < self.p[i]

    def __len__(self):
        return len(self.p)

class BayesianStrategy(object):
    """
    Implements a online, learning strategy to solve
    the Multi-Armed Bandit problem.

    parameters:
        bandits: a Bandit class with .pull method

    methods:
        sample_bandits(n): sample and train on n pulls.

    attributes:
        N: the cumulative number of samples
        choices: the historical choices as a (N,) array
        bb_score: the historical score as a (N,) array
    """

    def __init__(self, bandits):

        self.bandits = bandits
        n_bandits = len(self.bandits)
        self.wins = np.zeros(n_bandits)
        self.trials = np.zeros(n_bandits)
        self.N = 0
        self.choices = []
        self.bb_score = []

    def sample_bandits(self, n=1):

        bb_score = np.zeros(n)
        choices = np.zeros(n)

        for k in range(n):
            #sample from the bandits's priors, and select the largest sample
            choice = np.argmax(np.random.beta(1 + self.wins, 1 + self.trials - self.wins))

            #sample the chosen bandit
            result = self.bandits.pull(choice)

            #update priors and score
            self.wins[choice] += result
            self.trials[choice] += 1
            bb_score[k] = result
            self.N += 1
            choices[k] = choice

        self.bb_score = np.r_[self.bb_score, bb_score]
        self.choices = np.r_[self.choices, choices]
        return
```

Below we visualize the learning of the Bayesian Bandit solution.

```python
figsize(11.0, 10)

beta = stats.beta
x = np.linspace(0.001,.999,200)

def plot_priors(bayesian_strategy, prob, lw = 3, alpha = 0.2, plt_vlines = True):
    ## plotting function
    wins = bayesian_strategy.wins
    trials = bayesian_strategy.trials
    for i in range(prob.shape[0]):
        y = beta(1+wins[i], 1 + trials[i] - wins[i])
        p = plt.plot(x, y.pdf(x), lw = lw)
        c = p[0].get_markeredgecolor()
        plt.fill_between(x,y.pdf(x),0, color = c, alpha = alpha,
                         label="underlying probability: %.2f" % prob[i])
        if plt_vlines:
            plt.vlines(prob[i], 0, y.pdf(prob[i]) ,
                       colors = c, linestyles = "--", lw = 2)
        plt.autoscale(tight = "True")
        plt.title("Posteriors After %d pull" % bayesian_strategy.N +\
                    "s"*(bayesian_strategy.N > 1))
        plt.autoscale(tight=True)
    return
```

```python
hidden_prob = np.array([0.85, 0.60, 0.75])
bandits = Bandits(hidden_prob)
bayesian_strat = BayesianStrategy(bandits)

draw_samples = [1, 1, 3, 10, 10, 25, 50, 100, 200, 600]

for j,i in enumerate(draw_samples):
    plt.subplot(5, 2, j+1)
    bayesian_strat.sample_bandits(i)
    plot_priors(bayesian_strat, hidden_prob)
    #plt.legend()
    plt.autoscale(tight = True)
plt.tight_layout()
```

### 6.4.3 A Measure of Good
We need a metric to calculate how well we are doing. Recall the absolute best we can do is to always pick the bandit with the largest probability of winning. Denote this best bandit's probability by $w_{opt}$. Our score should be relative to how well we would have done had we chosen the best bandit from the beginning. This motivates the total regret of a strategy, defined:

$$\begin{align}
R_T = \sum_{i=1}^{T} \left( w_{opt} - w_{B(i)} \right)\\\\
= Tw^* - \sum_{i=1}^{T} \;  w_{B(i)}
\end{align}$$

where $w_{B(i)}$ is the probability of a prize of the chosen bandit in the $i$ round. A total regret of 0 means the strategy is matching the best possible score. This is likely not possible, as initially our algorithm will often make the wrong choice. Ideally, a strategy's total regret should flatten as it learns the best bandit. (Mathematically, we achieve $w_{B(i)}=w_{opt}$ often)

Below we plot the total regret of this simulation, including the scores of some other strategies:

1. Random: randomly choose a bandit to pull. If you can't beat this, just stop.
2. Largest Bayesian credible bound: pick the bandit with the largest upper bound in its 95% credible region of the underlying probability.
3. Bayes-UCB algorithm: pick the bandit with the largest score, where score is a dynamic quantile of the posterior.
4. Mean of posterior: choose the bandit with the largest posterior mean. This is what a human player (sans computer) would likely do.
5. Largest proportion: pick the bandit with the current largest observed proportion of winning.

The code for these are in the other_strats.py, where you can implement your own very easily.

```python
figsize(12.5, 5)
from other_strats import *

#define a harder problem
hidden_prob = np.array([0.15, 0.2, 0.1, 0.05])
bandits = Bandits(hidden_prob)

#define regret
def regret(probabilities, choices):
    w_opt = probabilities.max()
    return (w_opt - probabilities[choices.astype(int)]).cumsum()

#create new strategies
strategies= [upper_credible_choice,
            bayesian_bandit_choice,
            ucb_bayes ,
            max_mean,
            random_choice]
algos = []
for strat in strategies:
    algos.append(GeneralBanditStrat(bandits, strat))
```

```python
#train 10000 times
for strat in algos:
    strat.sample_bandits(10000)

#test and plot
for i,strat in enumerate(algos):
    _regret = regret(hidden_prob, strat.choices)
    plt.plot(_regret, label = strategies[i].__name__, lw = 3)

plt.title("Total Regret of Bayesian Bandits Strategy vs. Random guessing")
plt.xlabel("Number of pulls")
plt.ylabel("Regret after $n$ pulls");
plt.legend(loc = "upper left");
```

Like we wanted, Bayesian bandits and other strategies have decreasing rates of regret, representing we are achieving optimal choices. To be more scientific so as to remove any possible luck in the above simulation, we should instead look at the expected total regret:

$$\bar{R}_T = E[ R_T ] $$

It can be shown that any sub-optimal strategy's expected total regret is bounded below logarithmically. Formally,

$$ E[R_T] = \Omega \left( \;\log(T)\; \right) $$

Thus, any strategy that matches logarithmic-growing regret is said to "solve" the Multi-Armed Bandit problem.

### 6.4.4 Extending the Algorithm
Because of the Bayesian Bandits algorithm's simplicity, it is easy to extend. Some possibilities:

* If interested in the minimum probability (eg: where prizes are a bad thing), simply choose $B = \text{argmin} \; X_b$ and proceed.

* Adding learning rates: Suppose the underlying environment may change over time. Technically the standard Bayesian Bandit algorithm would self-update itself (awesome) by noting that what it thought was the best is starting to fail more often. We can motivate the algorithm to learn changing environments quicker by simply adding a rate term upon updating:

*self.wins[choice] = rate*self.wins[choice] + result
self.trials[choice] = rate*self.trials[choice] + 1*

If rate < 1, the algorithm will forget its previous wins quicker and there will be a downward pressure towards ignorance. Conversely, setting rate > 1 implies your algorithm will act more risky, and bet on earlier winners more often and be more resistant to changing environments.

* Hierarchical algorithms: We can setup a Bayesian Bandit algorithm on top of smaller bandit algorithms. Suppose we have $N$ Bayesian Bandit models, each varying in some behavior (for example different rate parameters, representing varying sensitivity to changing environments). On top of these $N$ models is another Bayesian Bandit learner that will select a sub-Bayesian Bandit. This chosen Bayesian Bandit will then make an internal choice as to which machine to pull. The super-Bayesian Bandit updates itself depending on whether the sub-Bayesian Bandit was correct or not.

* Extending the rewards, denoted $y_a$ for bandit $a$, to random variables from a distribution $f_{y_a}(y)$ is straightforward. More generally, this problem can be rephrased as "Find the bandit with the largest expected value", as playing the bandit with the largest expected value is optimal. In the case above, $f_{y_a}$ was Bernoulli with probability $p_a$, hence the expected value for a bandit is equal to $p_a$, which is why it looks like we are aiming to maximize the probability of winning. If $f$ is not Bernoulli, and it is non-negative, which can be accomplished apriori by shifting the distribution (we assume we know $f$), then the algorithm behaves as before:

For each round,
1. Sample a random variable $X_b$ from the prior of bandit $b$, for all $b$.
2. Select the bandit with largest sample, i.e. select bandit $B = \text{argmax}\;\; X_b$.
3. Observe the result,$R \sim f_{y_a}$, of pulling bandit $B$, and update your prior on bandit $B$.
4. Return to 1

The issue is in the sampling of $X_b$ drawing phase. With Beta priors and Bernoulli observations, we have a Beta posterior — this is easy to sample from. But now, with arbitrary distributions $f$, we have a non-trivial posterior. Sampling from these can be difficult.

* There has been some interest in extending the Bayesian Bandit algorithm to commenting systems. Recall in Chapter 4, we developed a ranking algorithm based on the Bayesian lower-bound of the proportion of upvotes to total votes. One problem with this approach is that it will bias the top rankings towards older comments, since older comments naturally have more votes (and hence the lower-bound is tighter to the true proportion). This creates a positive feedback cycle where older comments gain more votes, hence are displayed more often, hence gain more votes, etc. This pushes any new, potentially better comments, towards the bottom. J. Neufeld proposes a system to remedy this that uses a Bayesian Bandit solution.

His proposal is to consider each comment as a Bandit, with the number of pulls equal to the number of votes cast, and number of rewards as the number of upvotes, hence creating a $\text{Beta}(1+U,1+D)$ posterior. As visitors visit the page, samples are drawn from each bandit/comment, but instead of displaying the comment with the $\max$ sample, the comments are ranked according to the ranking of their respective samples.

Just for fun, though the colors explode, we watch the Bayesian Bandit algorithm learn 15 different options.

```python
figsize(12.0, 8)
beta = stats.beta
hidden_prob = beta.rvs(1,13, size = 35)
print(hidden_prob)
bandits = Bandits(hidden_prob)
bayesian_strat = BayesianStrategy(bandits)

for j,i in enumerate([100, 200, 500, 1300]):
    plt.subplot(2, 2, j+1)
    bayesian_strat.sample_bandits(i)
    plot_priors(bayesian_strat, hidden_prob, lw = 2, alpha = 0.0, plt_vlines=False)
    #plt.legend()
    plt.xlim(0, 0.5)
```

## 6.5 Eliciting expert prior
Specifying a subjective prior is how practitioners incorporate domain knowledge about the problem into our mathematical framework. Allowing domain knowledge is useful for many reasons:

* Aids speeds of MCMC convergence. For example, if we know the unknown parameter is strictly positive, then we can restrict our attention there, hence saving time that would otherwise be spent exploring negative values.
* More accurate inference. By weighing prior values near the true unknown value higher, we are narrowing our eventual inference (by making the posterior tighter around the unknown)
* Express our uncertainty better. See the Price is Right problem in Chapter 5.

Of course, practitioners of Bayesian methods are not experts in every field, so we must turn to domain experts to craft our priors. We must be careful with how we elicit these priors though. Some things to consider:

1. From experience, I would avoid introducing Betas, Gammas, etc. to non-Bayesian practitioners. Furthermore, non-statisticians can get tripped up by how a continuous probability function can have a value exceeding one.

2. Individuals often neglect the rare tail-events and put too much weight around the mean of distribution.

3. Related to above is that almost always individuals will under-emphasize the uncertainty in their guesses.

Eliciting priors from non-technical experts is especially difficult. Rather than introduce the notion of probability distributions, priors, etc. that may scare an expert, there is a much simpler solution.

### 6.5.2 Example: Stock Returns
Take note stock brokers: you're doing it wrong. When choosing which stocks to pick, an analyst will often look at the daily return of the stock. Suppose $S_t$ is the price of the stock on day $t$, then the daily return on day $t$ is :

$$r_t = \frac{ S_t - S_{t-1} }{ S_{t-1} } $$

The expected daily return of a stock is denoted $\mu = E[ r_t ]$. Obviously, stocks with high expected returns are desirable. Unfortunately, stock returns are so filled with noise that it is very hard to estimate this parameter. Furthermore, the parameter might change over time (consider the rises and falls of AAPL stock), hence it is unwise to use a large historical dataset.

Historically, the expected return has been estimated by using the sample mean. This is a bad idea. As mentioned, the sample mean of a small sized dataset has enormous potential to be very wrong (again, see Chapter 4 for full details). Thus Bayesian inference is the correct procedure here, since we are able to see our uncertainty along with probable values.

For this exercise, we will be examining the daily returns of the AAPL, GOOG, MSFT and AMZN. Before we pull in the data, suppose we ask our a stock fund manager: "What do you think the return profile looks like for each of these companies?"

Our stock broker, without needing to know the language of Normal distributions, or priors, or variances, etc. creates four distributions using the trial roulette method above. Suppose they look enough like Normals, so we fit Normals to them. They may look like:

```python
figsize(11., 5)
colors = ["#348ABD", "#A60628", "#7A68A6", "#467821"]

normal = stats.norm
x = np.linspace(-0.15, 0.15, 100)

expert_prior_params = {"AAPL":(0.05, 0.03),
                 "GOOG":(-0.03, 0.04),
                 "TSLA": (-0.02, 0.01),
                 "AMZN": (0.03, 0.02),
                 }

for i, (name, params) in enumerate(expert_prior_params.items()):
    plt.subplot(2, 2, i+1)
    y = normal.pdf(x, params[0], scale = params[1])
    #plt.plot( x, y, c = colors[i] )
    plt.fill_between(x, 0, y, color = colors[i], linewidth=2,
                     edgecolor = colors[i], alpha = 0.6)
    plt.title(name + " prior")
    plt.vlines(0, 0, y.max(), "k","--", linewidth = 0.5)
    plt.xlim(-0.15, 0.15)
plt.tight_layout()
```

Note that these are subjective priors: the expert has a personal opinion on the stock returns of each of these companies, and is expressing them in a distribution. He's not wishful thinking -- he's introducing domain knowledge.

## 6.6 Conjugate Priors
Recall that a $\text{Beta}$ prior with $\text{Binomial}$ data implies a $\text{Beta}$ posterior. Graphically:

$$ \underbrace{\text{Beta}}_{\text{prior}} \cdot \overbrace{\text{Binomial}}^{\text{data}} = \overbrace{\text{Beta}}^{\text{posterior} } $$

Notice the $\text{Beta}$ on both sides of this equation (no, you cannot cancel them, this is not a real equation). This is a really useful property. It allows us to avoid using MCMC, since the posterior is known in closed form. Hence inference and analytics are easy to derive. This shortcut was the heart of the Bayesian Bandit algorithm above. Fortunately, there is an entire family of distributions that have similar behaviour.

Suppose $X$ comes from, or is believed to come from, a well-known distribution, call it $f_{\alpha}$, where $\alpha$ are possibly unknown parameters of $f$. $f$ could be a Normal distribution, or Binomial distribution, etc. For particular distributions $f_{\alpha}$, there may exist a prior distribution $p_{\beta}$, such that:

$$ \overbrace{p_{\beta}}^{\text{prior}} \cdot \overbrace{f_{\alpha}(X)}^{\text{data}} = \overbrace{p_{\beta'}}^{\text{posterior} } $$

where $\beta'$ is a different set of parameters <em>but $p$ is the same distribution as the prior</em>. A prior $p$ that satisfies this relationship is called a conjugate prior. As I mentioned, they are useful computationally, as we can avoided approximate inference using MCMC and go directly to the posterior. This sounds great, right?

Unfortunately, not quite. There are a few issues with conjugate priors.

1. The conjugate prior is not objective. Hence only useful when a subjective prior is required. It is not guaranteed that the conjugate prior can accommodate the practitioner's subjective opinion.

2. There typically exist conjugate priors for simple, one dimensional problems. For larger problems, involving more complicated structures, hope is lost to find a conjugate prior. For smaller models, Wikipedia has a nice table of conjugate priors.

Really, conjugate priors are only useful for their mathematical convenience: it is simple to go from prior to posterior. I personally see conjugate priors as only a neat mathematical trick, and offer little insight into the problem at hand.

## 6.7 Jefferys Priors
Earlier, we talked about objective priors rarely being objective. Partly what we mean by this is that we want a prior that doesn't bias our posterior estimates. The flat prior seems like a reasonable choice as it assigns equal probability to all values.

But the flat prior is not transformation invariant. What does this mean? Suppose we have a random variable $\textbf X$ from Bernoulli($\theta$). We define the prior on $p(\theta) = 1$.

```python
figsize(12.5, 5)

x = np.linspace(0.000 ,1, 150)
y = np.linspace(1.0, 1.0, 150)
lines = plt.plot(x, y, color="#A60628", lw = 3)
plt.fill_between(x, 0, y, alpha = 0.2, color = lines[0].get_color())
plt.autoscale(tight=True)
plt.ylim(0, 2);
```

Now, let's transform $\theta$ with the function $\psi = log \frac{\theta}{1-\theta}$. This is just a function to stretch $\theta$ across the real line. Now how likely are different values of $\psi$ under our transformation.

```python
figsize(12.5, 5)

psi = np.linspace(-10 ,10, 150)
y = np.exp(psi) / (1 + np.exp(psi))**2
lines = plt.plot(psi, y, color="#A60628", lw = 3)
plt.fill_between(psi, 0, y, alpha = 0.2, color = lines[0].get_color())
plt.autoscale(tight=True)
plt.ylim(0, 1);
```

Oh no! Our function is no longer flat. It turns out flat priors do carry information in them after all. The point of Jeffreys Priors is to create priors that don't accidentally become informative when you transform the variables you placed them originally on.

Jeffreys Priors are defined as:

$$p_J(\theta) \propto \mathbf{I}(\theta)^\frac{1}{2}$$$$\mathbf{I}(\theta) = - \mathbb{E}\bigg[\frac{d^2 \text{ log } p(X|\theta)}{d\theta^2}\bigg]$$

$\mathbf{I}$ being the Fisher information

## 6.8 Effect of the Prior as N Increases
In the first chapter, I proposed that as the amount of our observations or data increases, the influence of the prior decreases. This is intuitive. After all, our prior is based on previous information, and eventually enough new information will shadow our previous information's value. The smothering of the prior by enough data is also helpful: if our prior is significantly wrong, then the self-correcting nature of the data will present to us a less wrong, and eventually correct, posterior.

We can see this mathematically. First, recall Bayes Theorem from Chapter 1 that relates the prior to the posterior.

The posterior distribution for a parameter $\theta$, given a data set ${\textbf X}$ can be written as

$$p(\theta | {\textbf X}) \propto \underbrace{p({\textbf X} | \theta)}_{{\textrm likelihood}}  \cdot  \overbrace{ p(\theta) }^{ {\textrm prior} }  $$

or, as is more commonly displayed on the log scale,

$$ \log( p(\theta | {\textbf X})  ) = c + L(\theta;{\textbf X}) + \log(p(\theta)) $$

The log-likelihood, $L(\theta;{\textbf X}) = \log \left( p({\textbf X}|\theta) \right)$, <strong>scales with the sample size</strong>, since it is a function of the data, while the prior density does not. Therefore, as the sample size increases, the absolute value of $L(\theta;{\textbf X})$ is getting larger while $\log(p(\theta))$ stays fixed (for a fixed value of $\theta$), thus the sum $L(\theta;{\textbf X}) + \log(p(\theta))$ becomes more heavily influenced by $L(\theta;{\textbf X})$ as the sample size increases.

There is an interesting consequence not immediately apparent. As the sample size increases, the chosen prior has less influence. Hence inference converges regardless of chosen prior, so long as the areas of non-zero probabilities are the same.

Below we visualize this. We examine the convergence of two posteriors of a Binomial's parameter $\theta$, one with a flat prior and the other with a biased prior towards 0. As the sample size increases, the posteriors, and hence the inference, converge.

```python
figsize(12.5, 15)

p = 0.6
beta1_params = np.array([1.,1.])
beta2_params = np.array([2,10])
beta = stats.beta

x = np.linspace(0.00, 1, 125)
data = stats.bernoulli.rvs(p, size=500)

plt.figure()
for i,N in enumerate([0,4,8, 32,64, 128, 500]):
    s = data[:N].sum()
    plt.subplot(8,1,i+1)
    params1 = beta1_params + np.array([s, N-s])
    params2 = beta2_params + np.array([s, N-s])
    y1,y2 = beta.pdf(x, *params1), beta.pdf( x, *params2)
    plt.plot(x,y1, label = r"flat prior", lw =3)
    plt.plot(x, y2, label = "biased prior", lw= 3)
    plt.fill_between(x, 0, y1, color ="#348ABD", alpha = 0.15)
    plt.fill_between(x, 0, y2, color ="#A60628", alpha = 0.15)
    plt.legend(title = "N=%d" % N)
    plt.vlines(p, 0.0, 7.5, linestyles = "--", linewidth=1)
    #plt.ylim( 0, 10)#
```

Keep in mind, not all posteriors will "forget" the prior this quickly. This example was just to show that eventually the prior is forgotten. The "forgetfulness" of the prior as we become awash in more and more data is the reason why Bayesian and Frequentist inference eventually converge as well.


```
from sympy import I, Matrix, symbols, Symbol, eye
from datetime import datetime
import numpy as np
import pandas as pd

```

```
# Rotas
R1 = Matrix([[0,"R1_p1",0],[0,0,"R1_v1"],[0,0,0]])
R2 = Matrix([[0,"R2_p1",0],[0,0,"R2_v1"],[0,0,0]])
```

```
# Seções (semáforos)
T1 = Matrix([[0, "p1"],["v1", 0]])
```

```
def kronSum(A,B):
  m = np.size(A,1)
  n = np.size(B,1)
  A = np.kron(A,np.eye(n))
  B = np.kron(np.eye(m),B)
  return A + B
```

```
momento_inicio = datetime.now()
'''
Algebra de rotas
'''
rotas = kronSum(R1,R2)

'''
Algebra de seções
secoes = kronSum(T1,T2)
secoes = kronSum(secoes,T3)
secoes = kronSum(secoes,T4)
secoes = kronSum(secoes,T5)
'''

'''
Algebra de sistema
'''
sistema = np.kron(rotas, T1) # lembrar de trocar para "secoes" se tiver vários semáforos

# calcula tempo de processamento
tempo_processamento = datetime.now() - momento_inicio
```

```
sistema = pd.DataFrame(data=sistema,index=list(range(1,np.size(sistema,0)+1)), columns=list(range(1,np.size(sistema,1)+1)))
```

```
sistema.shape
```

    (18, 18)

```
print(tempo_processamento)
```

    0:00:00.018771

```
sistema
```

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

      1
      0
      0
      0
      1.0*R2_p1*p1
      0
      0
      0
      1.0*R1_p1*p1
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      2
      0
      0
      1.0*R2_p1*v1
      0
      0
      0
      1.0*R1_p1*v1
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      3
      0
      0
      0
      0
      0
      1.0*R2_v1*p1
      0
      0
      0
      1.0*R1_p1*p1
      0
      0
      0
      0
      0
      0
      0
      0

      4
      0
      0
      0
      0
      1.0*R2_v1*v1
      0
      0
      0
      1.0*R1_p1*v1
      0
      0
      0
      0
      0
      0
      0
      0
      0

      5
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R1_p1*p1
      0
      0
      0
      0
      0
      0

      6
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R1_p1*v1
      0
      0
      0
      0
      0
      0
      0

      7
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_p1*p1
      0
      0
      0
      1.0*R1_v1*p1
      0
      0
      0
      0

      8
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_p1*v1
      0
      0
      0
      1.0*R1_v1*v1
      0
      0
      0
      0
      0

      9
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_v1*p1
      0
      0
      0
      1.0*R1_v1*p1
      0
      0

      10
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_v1*v1
      0
      0
      0
      1.0*R1_v1*v1
      0
      0
      0

      11
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R1_v1*p1

      12
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R1_v1*v1
      0

      13
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_p1*p1
      0
      0

      14
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_p1*v1
      0
      0
      0

      15
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_v1*p1

      16
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      1.0*R2_v1*v1
      0

      17
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      18
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

```
momento_inicio = datetime.now()
colunas = ['de_noh', 'para_noh', 'aresta']
grafo = pd.DataFrame(columns=colunas)
r = 1
c = 1
for j in range(np.size(sistema,0)):
  for i in range(np.size(sistema,0)):
    if sistema.loc[r,c]==0 and c < np.size(sistema,0):
      c += 1
    elif c < np.size(sistema,0):
      grafo.loc[len(grafo)+1] = (r, c, sistema.loc[r,c])
      c += 1
    else:
      c = 1
      r += 1
tempo_processamento = datetime.now() - momento_inicio
print(tempo_processamento)
```

    0:00:00.081615

```
grafo['aresta'] = grafo['aresta'].astype('str')
grafo
```

      de_noh
      para_noh
      aresta

      1
      1
      4
      1.0*R2_p1*p1

      2
      1
      8
      1.0*R1_p1*p1

      3
      2
      3
      1.0*R2_p1*v1

      4
      2
      7
      1.0*R1_p1*v1

      5
      3
      6
      1.0*R2_v1*p1

      6
      3
      10
      1.0*R1_p1*p1

      7
      4
      5
      1.0*R2_v1*v1

      8
      4
      9
      1.0*R1_p1*v1

      9
      5
      12
      1.0*R1_p1*p1

      10
      6
      11
      1.0*R1_p1*v1

      11
      7
      10
      1.0*R2_p1*p1

      12
      7
      14
      1.0*R1_v1*p1

      13
      8
      9
      1.0*R2_p1*v1

      14
      8
      13
      1.0*R1_v1*v1

      15
      9
      12
      1.0*R2_v1*p1

      16
      9
      16
      1.0*R1_v1*p1

      17
      10
      11
      1.0*R2_v1*v1

      18
      10
      15
      1.0*R1_v1*v1

      19
      12
      17
      1.0*R1_v1*v1

      20
      13
      16
      1.0*R2_p1*p1

      21
      14
      15
      1.0*R2_p1*v1

      22
      16
      17
      1.0*R2_v1*v1

```
new = grafo["aresta"].str.split("*", n = -1, expand = True)
grafo["aresta"]=new[1]
grafo["semaforo_secao"]=new[2]
new = grafo["aresta"].str.split("_", n = -1, expand = True)
grafo["semaforo_trem"]=new[1]
grafo['coincide'] = np.where(grafo['semaforo_secao']==grafo['semaforo_trem'], True, False)
grafo
```

      de_noh
      para_noh
      aresta
      semaforo_secao
      semaforo_trem
      coincide

      1
      1
      4
      R2_p1
      p1
      p1
      True

      2
      1
      8
      R1_p1
      p1
      p1
      True

      3
      2
      3
      R2_p1
      v1
      p1
      False

      4
      2
      7
      R1_p1
      v1
      p1
      False

      5
      3
      6
      R2_v1
      p1
      v1
      False

      6
      3
      10
      R1_p1
      p1
      p1
      True

      7
      4
      5
      R2_v1
      v1
      v1
      True

      8
      4
      9
      R1_p1
      v1
      p1
      False

      9
      5
      12
      R1_p1
      p1
      p1
      True

      10
      6
      11
      R1_p1
      v1
      p1
      False

      11
      7
      10
      R2_p1
      p1
      p1
      True

      12
      7
      14
      R1_v1
      p1
      v1
      False

      13
      8
      9
      R2_p1
      v1
      p1
      False

      14
      8
      13
      R1_v1
      v1
      v1
      True

      15
      9
      12
      R2_v1
      p1
      v1
      False

      16
      9
      16
      R1_v1
      p1
      v1
      False

      17
      10
      11
      R2_v1
      v1
      v1
      True

      18
      10
      15
      R1_v1
      v1
      v1
      True

      19
      12
      17
      R1_v1
      v1
      v1
      True

      20
      13
      16
      R2_p1
      p1
      p1
      True

      21
      14
      15
      R2_p1
      v1
      p1
      False

      22
      16
      17
      R2_v1
      v1
      v1
      True

```
grafo = pd.DataFrame(data=grafo)
```

```
# PASSO 1
alcancavel = [1]
N = np.size(grafo,0)
for i in range(N):
  de = grafo.loc[i+1]['de_noh']
  para = grafo.loc[i+1]['para_noh']
  if de in alcancavel:
    alcancavel.append(para)
  else:
    i += 1

  alcancavel.sort()
```

```
grafo01 = grafo[grafo.de_noh.isin(alcancavel)]
```

```
grafo01
```

      de_noh
      para_noh
      aresta
      semaforo_secao
      semaforo_trem
      coincide

      1
      1
      4
      R2_p1
      p1
      p1
      True

      2
      1
      8
      R1_p1
      p1
      p1
      True

      7
      4
      5
      R2_v1
      v1
      v1
      True

      8
      4
      9
      R1_p1
      v1
      p1
      False

      9
      5
      12
      R1_p1
      p1
      p1
      True

      13
      8
      9
      R2_p1
      v1
      p1
      False

      14
      8
      13
      R1_v1
      v1
      v1
      True

      15
      9
      12
      R2_v1
      p1
      v1
      False

      16
      9
      16
      R1_v1
      p1
      v1
      False

      19
      12
      17
      R1_v1
      v1
      v1
      True

      20
      13
      16
      R2_p1
      p1
      p1
      True

      22
      16
      17
      R2_v1
      v1
      v1
      True

```
grafo01.drop(grafo01[grafo01.coincide == False].index, inplace=True)
grafo01

```

    /usr/local/lib/python3.6/dist-packages/pandas/core/frame.py:3997: SettingWithCopyWarning:
    A value is trying to be set on a copy of a slice from a DataFrame

    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
      errors=errors,

      de_noh
      para_noh
      aresta
      semaforo_secao
      semaforo_trem
      coincide

      1
      1
      4
      R2_p1
      p1
      p1
      True

      2
      1
      8
      R1_p1
      p1
      p1
      True

      7
      4
      5
      R2_v1
      v1
      v1
      True

      9
      5
      12
      R1_p1
      p1
      p1
      True

      14
      8
      13
      R1_v1
      v1
      v1
      True

      19
      12
      17
      R1_v1
      v1
      v1
      True

      20
      13
      16
      R2_p1
      p1
      p1
      True

      22
      16
      17
      R2_v1
      v1
      v1
      True