# Calculating Thermodynamics Observables with a quantum computer

```python
# imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from functools import partial

from qiskit.utils import QuantumInstance
from qiskit import Aer
from qiskit.algorithms import NumPyMinimumEigensolver, VQE

from qiskit_nature.drivers import PySCFDriver, UnitsType, Molecule
from qiskit_nature.problems.second_quantization import ElectronicStructureProblem
from qiskit_nature.converters.second_quantization import QubitConverter
from qiskit_nature.mappers.second_quantization import JordanWignerMapper
from qiskit_nature.algorithms import GroundStateEigensolver
import qiskit_nature.constants as const
from qiskit_nature.algorithms.pes_samplers import BOPESSampler, EnergySurface1DSpline
from thermodynamics_utils.thermodynamics import constant_volume_heat_capacity
from thermodynamics_utils.vibrational_structure_fd import VibrationalStructure1DFD
from thermodynamics_utils.partition_function import DiatomicPartitionFunction
from thermodynamics_utils.thermodynamics import Thermodynamics

import warnings
warnings.simplefilter('ignore', np.RankWarning)
```

A preliminary draft with more information related to this tutorial can be found in preprint: Stober et al, arXiv 2003.02303 (2020)

### Calculation of the Born Oppenheimer Potential Energy Surface (BOPES)

To compute thermodynamic observables we begin with single point energy calculation which calculates the wavefunction and charge density and therefore  the energy of a particular arrangement of nuclei. Here we  compute the Born-Oppenheimer potential energy surface of a hydrogen molecule, as an example, which is simply the electronic energy as a function of bond length.

```python
qubit_converter = QubitConverter(mapper = JordanWignerMapper())
quantum_instance = QuantumInstance(backend=Aer.get_backend('statevector_simulator'))
solver = VQE(quantum_instance=quantum_instance)

me_gss = GroundStateEigensolver(qubit_converter, solver)
```

```python
stretch1 = partial(Molecule.absolute_distance, atom_pair=(1, 0))
mol = Molecule(geometry=[('H', [0., 0., 0.]),
                        ('H', [0., 0., 0.2])],
                       degrees_of_freedom=[stretch1],
                       masses=[1.6735328E-27, 1.6735328E-27]
                       )

# pass molecule to PSYCF driver
driver = PySCFDriver(molecule=mol)

es_problem = ElectronicStructureProblem(driver)
```

```python
# BOPES sampler testing
bs = BOPESSampler(gss=me_gss,
                  bootstrap=True)
points = np.linspace(0.45, 5, 50)
res = bs.sample(es_problem,points)
```

```python
energies = []
bs_res_full = res.raw_results
for point in points:
    energy = bs_res_full[point].computed_energies + bs_res_full[point].nuclear_repulsion_energy
    energies.append(energy)
```

```python
fig = plt.figure()
plt.plot(points,energies)
plt.title('Dissociation profile')
plt.xlabel('Interatomic distance')
plt.ylabel('Energy')
```

```python
energy_surface = EnergySurface1DSpline()

xdata = res.points
ydata = res.energies
energy_surface.fit(xdata=xdata, ydata=ydata)
```

```python
plt.plot(xdata, ydata, 'kx')
x = np.arange(min(xdata) - 0.25, max(xdata) + 0.25, 0.05)
plt.plot(x, energy_surface.eval(x), 'r-')
plt.xlabel(r'distance, $\AA$')
plt.ylabel('energy, Hartree')
dist = max(ydata) - min(ydata)
plt.ylim(min(ydata) - 0.1 * dist, max(ydata) + 0.1 * dist)
```

### Calculation of the molecular Vibrational Energy levels

The Born-Oppeheimer approximation removes internuclear vibrations from the molecular Hamiltonian and the energy computed from quantum mechanical ground-state energy calculations using this approximation contain only the electronic energy. Since even at absolute zero internuclear vibrations still occur, a correction is required to obtain the true zero-temperature energy of a molecule. This correction is called the zero-point vibrational energy (ZPE), which is computed by summing the contribution from internuclear vibrational modes. Therefore, the next step in computing thermodynamic observables is determining the vibrational energy levels. This can be done by constructing the Hessian matrix based on computed single point energies close to the equilibrium bond length. The eigenvalues of the Hessian matrix can then be used to determine the vibrational energy levels and the zero-point vibrational energy
\begin{equation}
{\rm ZPE} = \frac{1}{2}\, \sum_i ^M \nu_i \, ,
\end{equation}
with $\nu_i$ being the vibrational frequencies, $M = 3N − 6$ or $M = 3N − 5$ for non-linear or linear molecules, respectively, and $N$ is number of the particles.

Here we fit a "full" energy surface using a 1D spline potential and use it to evaluate molecular vibrational energy levels.

```python
vibrational_structure = VibrationalStructure1DFD(mol, energy_surface)

plt.plot(xdata, ydata, 'kx')
x = np.arange(min(xdata) - 0.25, max(xdata) + 0.25, 0.05)
plt.plot(x, energy_surface.eval(x), 'r-')
plt.xlabel(r'distance, $\AA$')
plt.ylabel('energy, Hartree')
dist = max(ydata) - min(ydata)
plt.ylim(min(ydata) - 0.1 * dist, max(ydata) + 0.1 * dist)
for N in range(15):
    on = np.ones(x.shape)
    on *= (energy_surface.eval(energy_surface.get_equilibrium_geometry()) +
           vibrational_structure.vibrational_energy_level(N))
    plt.plot(x, on, 'g:')
on = np.ones(x.shape)

plt.show()

```

### Create a partition function for the calculation of heat capacity

The partition function for a molecule is the product of contributions from translational, rotational, vibrational, electronic, and nuclear degrees of freedom. Having the vibrational frequencies, now we can obtain the vibrational partition function  $q_{\rm vibration}$ to compute the whole molecular partition function
\begin{equation}
q_{\rm vibration} = \prod_{i=1} ^M \frac{\exp\,(-\Theta_{\nu_i}/2T)}{1-\exp\,(-\Theta_{\nu_i}/2T} \, .
\end{equation}
Here $\Theta_{\nu_i}= h\nu_i/k_B$, $T$ is the temperature and $k_B$ is the Boltzmann constant.

The single-point energy calculations and the resulting partition function can be used to calculate the (constant volume or constant pressure) heat capacity of the molecules. The constant volume heat capacity, for example, is given by

\begin{equation}
C_v = \left.\frac{\partial U}{\partial T}\right|_{N,V}\, ,
\qquad
{\rm with} \quad
U=k_B T^2 \left.\frac{\partial {\rm ln} Q}{\partial T}\right|_{N,V} .
\end{equation}

$U$ is the internal energy, $V$ is the volume and $Q$ is the partition function.

Here we illustrate the simplest usage of the partition function, namely creating a Thermodynamics object to compute properties like the constant pressure heat capacity defined above.

```python
Q = DiatomicPartitionFunction(mol, energy_surface, vibrational_structure)

P = 101350  # Pa
temps = np.arange(10, 1050, 5)  # K

mol.spins = [1/2,1/2]

td = Thermodynamics(Q, pressure = 101350)
td.set_pressure(101350)
temps = np.arange(10, 1500, 5)
ymin = 5
ymax = 11

plt.plot(temps,
         td.constant_pressure_heat_capacity(temps) / const.CAL_TO_J)
plt.xlim(0, 1025)
plt.ylim(ymin, ymax)
plt.xlabel('Temperature, K')
plt.ylabel('Cp, cal mol$^{-1}$ K$^{-1}$')

plt.show()
```

Here we demonstrate how to access particular components (the rotational part) of the partition function, which in the H2 case we can further split to para-hydrogen and ortho-hydrogen   components.

```python
eq = Q.get_partition(part="rot", split="eq")
para = Q.get_partition(part="rot", split="para")
ortho = Q.get_partition(part="rot", split="ortho")
```

We will now plot the constant volume heat capacity (of the rotational part) demonstrating how we can call directly the functions in the 'thermodynamics' module, providing a callable object for the partition function (or in this case its rotational component). Note that in the plot we normalize the plot dividing by the universal gas constant R (Avogadro's number times Boltzman's constant) and we use crossed to compare with experimental data found in literature.

```python
# REFERENCE DATA from literature
df_brink_T = [80.913535,135.240157,176.633783,219.808499,246.226899]
df_brink_Cv = [0.118605,0.469925,0.711510,0.833597,0.895701]

df_eucken_T = [25.120525, 30.162485, 36.048121, 41.920364, 56.195875, 62.484934, 72.148692, 73.805910, 73.804236, 92.214423,180.031917,230.300866]
df_eucken_Cv  = [0.012287,0.012354,0.008448,0.020478,0.032620,0.048640,0.048768,0.076678,0.078670,0.170548,0.667731,0.847681]

df_gia_T = [190.919338,195.951254,202.652107,204.292585,209.322828,225.300754,234.514217,243.747768]
df_gia_Cv = [0.711700,0.723719,0.749704,0.797535,0.811546,0.797814,0.833793,0.845868]

df_parting_T = [80.101665, 86.358919,185.914204,239.927797]
df_parting_Cv = [0.084730,0.138598,0.667809,0.891634]

df_ce_T = [80.669344,135.550569,145.464190,165.301153,182.144856,203.372528,237.993108,268.696642,294.095771,308.872014]
df_ce_Cv = [0.103048,0.467344,0.541364,0.647315,0.714078,0.798258,0.891147,0.944848,0.966618,0.985486]
```

```python
HeatCapacity = constant_volume_heat_capacity

R = const.N_A * const.KB_J_PER_K
plt.plot(temps,
         HeatCapacity(eq, temps) / R, '-k',
         label='Cv_rot Equilibrium')
plt.plot(temps, HeatCapacity(para, temps) / R, '-b',
         label='Cv_rot Para')
plt.plot(temps, HeatCapacity(ortho, temps) / R, '-r',
         label='Cv_rot Ortho')
plt.plot(temps, 0.25 * HeatCapacity(para, temps) / R
         + 0.75 * HeatCapacity(ortho, temps) / R, '-g',
         label='Cv_rot 1:3 para:ortho')
plt.plot(df_brink_T, df_brink_Cv, '+g')
plt.plot(df_eucken_T, df_eucken_Cv, '+g')
plt.plot(df_gia_T, df_gia_Cv, '+g')
plt.plot(df_parting_T, df_parting_Cv, '+g')
plt.plot(df_ce_T, df_ce_Cv, '+g', label = 'experimental data')
plt.legend(loc='upper right', frameon=False)
plt.xlim(10, 400)
plt.ylim(-0.1, 2.8)
plt.xlabel('Temperature, K')
plt.ylabel('Cv (rotational)/R')
plt.tight_layout()
plt.show()

```

```python
import qiskit.tools.jupyter
%qiskit_version_table
%qiskit_copyright
```

Version Information

Qiskit Software	Version

Qiskit	None

Terra	0.17.0.dev0+7dde867

Aer	0.8.0

Ignis	0.6.0.dev0+1537c75

Aqua	0.9.0.dev0+f7e1440

IBM Q Provider	0.12.0.dev0+8f3168b

System information

Python	3.7.9 (default, Aug 31 2020, 07:22:35)
[Clang 10.0.0 ]

OS	Darwin

CPUs	2

Memory (Gb)	12.0

Thu Apr 01 09:50:07 2021 EDT

This code is a part of Qiskit

© Copyright IBM 2017, 2021.

This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
 of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.


Probabilistic Programming
=====
and Bayesian Methods for Hackers
========

##### Version 0.1

`Original content created by Cam Davidson-Pilon`

`Ported to Python 3 and PyMC3 by Max Margenot (@clean_utensils) and Thomas Wiecki (@twiecki) at Quantopian (@quantopian)`
___

Welcome to *Bayesian Methods for Hackers*. The full Github repository is available at [github/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers](https://github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers). The other chapters can be found on the project's [homepage](https://camdavidsonpilon.github.io/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/). We hope you enjoy the book, and we encourage any contributions!

Chapter 1
======
***

The Philosophy of Bayesian Inference
------

> You are a skilled programmer, but bugs still slip into your code. After a particularly difficult implementation of an algorithm, you decide to test your code on a trivial example. It passes. You test the code on a harder problem. It passes once again. And it passes the next, *even more difficult*, test too! You are starting to believe that there may be no bugs in this code...

If you think this way, then congratulations, you already are thinking Bayesian! Bayesian inference is simply updating your beliefs after considering new evidence. A Bayesian can rarely be certain about a result, but he or she can be very confident. Just like in the example above, we can never be 100% sure that our code is bug-free unless we test it on every possible problem; something rarely possible in practice. Instead, we can test it on a large number of problems, and if it succeeds we can feel more *confident* about our code, but still not certain.  Bayesian inference works identically: we update our beliefs about an outcome; rarely can we be absolutely sure unless we rule out all other alternatives.

### The Bayesian state of mind

Bayesian inference differs from more traditional statistical inference by preserving *uncertainty*. At first, this sounds like a bad statistical technique. Isn't statistics all about deriving *certainty* from randomness? To reconcile this, we need to start thinking like Bayesians.

The Bayesian world-view interprets probability as measure of *believability in an event*, that is, how confident we are in an event occurring. In fact, we will see in a moment that this is the natural interpretation of probability.

For this to be clearer, we consider an alternative interpretation of probability: *Frequentist*, known as the more *classical* version of statistics, assume that probability is the long-run frequency of events (hence the bestowed title). For example, the *probability of plane accidents* under a frequentist philosophy is interpreted as the *long-term frequency of plane accidents*. This makes logical sense for many probabilities of events, but becomes more difficult to understand when events have no long-term frequency of occurrences. Consider: we often assign probabilities to outcomes of presidential elections, but the election itself only happens once! Frequentists get around this by invoking alternative realities and saying across all these realities, the frequency of occurrences defines the probability.

Bayesians, on the other hand, have a more intuitive approach. Bayesians interpret a probability as measure of *belief*, or confidence, of an event occurring. Simply, a probability is a summary of an opinion. An individual who assigns a belief of 0 to an event has no confidence that the event will occur; conversely, assigning a belief of 1 implies that the individual is absolutely certain of an event occurring. Beliefs between 0 and 1 allow for weightings of other outcomes. This definition agrees with the probability of a plane accident example, for having observed the frequency of plane accidents, an individual's belief should be equal to that frequency, excluding any outside information. Similarly, under this definition of probability being equal to beliefs, it is meaningful to speak about probabilities (beliefs) of presidential election outcomes: how confident are you candidate *A* will win?

Notice in the paragraph above, I assigned the belief (probability) measure to an *individual*, not to Nature. This is very interesting, as this definition leaves room for conflicting beliefs between individuals. Again, this is appropriate for what naturally occurs: different individuals have different beliefs of events occurring, because they possess different *information* about the world. The existence of different beliefs does not imply that anyone is wrong. Consider the following examples demonstrating the relationship between individual beliefs and probabilities:

- I flip a coin, and we both guess the result. We would both agree, assuming the coin is fair, that the probability of Heads is 1/2. Assume, then, that I peek at the coin. Now I know for certain what the result is: I assign probability 1.0 to either Heads or Tails (whichever it is). Now what is *your* belief that the coin is Heads? My knowledge of the outcome has not changed the coin's results. Thus we assign different probabilities to the result.

-  Your code either has a bug in it or not, but we do not know for certain which is true, though we have a belief about the presence or absence of a bug.

-  A medical patient is exhibiting symptoms $x$, $y$ and $z$. There are a number of diseases that could be causing all of them, but only a single disease is present. A doctor has beliefs about which disease, but a second doctor may have slightly different beliefs.

This philosophy of treating beliefs as probability is natural to humans. We employ it constantly as we interact with the world and only see partial truths, but gather evidence to form beliefs. Alternatively, you have to be *trained* to think like a frequentist.

To align ourselves with traditional probability notation, we denote our belief about event $A$ as $P(A)$. We call this quantity the *prior probability*.

John Maynard Keynes, a great economist and thinker, said "When the facts change, I change my mind. What do you do, sir?" This quote reflects the way a Bayesian updates his or her beliefs after seeing evidence. Even — especially — if the evidence is counter to what was initially believed, the evidence cannot be ignored. We denote our updated belief as $P(A |X )$, interpreted as the probability of $A$ given the evidence $X$. We call the updated belief the *posterior probability* so as to contrast it with the prior probability. For example, consider the posterior probabilities (read: posterior beliefs) of the above examples, after observing some evidence $X$:

1\. $P(A): \;\;$ the coin has a 50 percent chance of being Heads. $P(A | X):\;\;$ You look at the coin, observe a Heads has landed, denote this information $X$, and trivially assign probability 1.0 to Heads and 0.0 to Tails.

2\.   $P(A): \;\;$  This big, complex code likely has a bug in it. $P(A | X): \;\;$ The code passed all $X$ tests; there still might be a bug, but its presence is less likely now.

3\.  $P(A):\;\;$ The patient could have any number of diseases. $P(A | X):\;\;$ Performing a blood test generated evidence $X$, ruling out some of the possible diseases from consideration.

It's clear that in each example we did not completely discard the prior belief after seeing new evidence $X$, but we *re-weighted the prior* to incorporate the new evidence (i.e. we put more weight, or confidence, on some beliefs versus others).

By introducing prior uncertainty about events, we are already admitting that any guess we make is potentially very wrong. After observing data, evidence, or other information, we update our beliefs, and our guess becomes *less wrong*. This is the alternative side of the prediction coin, where typically we try to be *more right*.

### Bayesian Inference in Practice

 If frequentist and Bayesian inference were programming functions, with inputs being statistical problems, then the two would be different in what they return to the user. The frequentist inference function would return a number, representing an estimate (typically a summary statistic like the sample average etc.), whereas the Bayesian function would return *probabilities*.

For example, in our debugging problem above, calling the frequentist function with the argument "My code passed all $X$ tests; is my code bug-free?" would return a *YES*. On the other hand, asking our Bayesian function "Often my code has bugs. My code passed all $X$ tests; is my code bug-free?" would return something very different: probabilities of *YES* and *NO*. The function might return:

>    *YES*, with probability 0.8; *NO*, with probability 0.2

This is very different from the answer the frequentist function returned. Notice that the Bayesian function accepted an additional argument:  *"Often my code has bugs"*. This parameter is the *prior*. By including the prior parameter, we are telling the Bayesian function to include our belief about the situation. Technically this parameter in the Bayesian function is optional, but we will see excluding it has its own consequences.

#### Incorporating evidence

As we acquire more and more instances of evidence, our prior belief is *washed out* by the new evidence. This is to be expected. For example, if your prior belief is something ridiculous, like "I expect the sun to explode today", and each day you are proved wrong, you would hope that any inference would correct you, or at least align your beliefs better. Bayesian inference will correct this belief.

Denote $N$ as the number of instances of evidence we possess. As we gather an *infinite* amount of evidence, say as $N \rightarrow \infty$, our Bayesian results (often) align with frequentist results. Hence for large $N$, statistical inference is more or less objective. On the other hand, for small $N$, inference is much more *unstable*: frequentist estimates have more variance and larger confidence intervals. This is where Bayesian analysis excels. By introducing a prior, and returning probabilities (instead of a scalar estimate), we *preserve the uncertainty* that reflects the instability of statistical inference of a small $N$ dataset.

One may think that for large $N$, one can be indifferent between the two techniques since they offer similar inference, and might lean towards the computationally-simpler, frequentist methods. An individual in this position should consider the following quote by Andrew Gelman (2005)[1], before making such a decision:

> Sample sizes are never large. If $N$ is too small to get a sufficiently-precise estimate, you need to get more data (or make more assumptions). But once $N$ is "large enough," you can start subdividing the data to learn more (for example, in a public opinion poll, once you have a good estimate for the entire country, you can estimate among men and women, northerners and southerners, different age groups, etc.). $N$ is never enough because if it were "enough" you'd already be on to the next problem for which you need more data.

### Are frequentist methods incorrect then?

**No.**

Frequentist methods are still useful or state-of-the-art in many areas. Tools such as least squares linear regression, LASSO regression, and expectation-maximization algorithms are all powerful and fast. Bayesian methods complement these techniques by solving problems that these approaches cannot, or by illuminating the underlying system with more flexible modeling.

#### A note on *Big Data*
Paradoxically, big data's predictive analytic problems are actually solved by relatively simple algorithms [2][4]. Thus we can argue that big data's prediction difficulty does not lie in the algorithm used, but instead on the computational difficulties of storage and execution on big data. (One should also consider Gelman's quote from above and ask "Do I really have big data?")

The much more difficult analytic problems involve *medium data* and, especially troublesome, *really small data*. Using a similar argument as  Gelman's above, if big data problems are *big enough* to be readily solved, then we should be more interested in the *not-quite-big enough* datasets.

### Our Bayesian framework

We are interested in beliefs, which can be interpreted as probabilities by thinking Bayesian. We have a *prior* belief in event $A$, beliefs formed by previous information, e.g., our prior belief about bugs being in our code before performing tests.

Secondly, we observe our evidence. To continue our buggy-code example: if our code passes $X$ tests, we want to update our belief to incorporate this. We call this new belief the *posterior* probability. Updating our belief is done via the following equation, known as Bayes' Theorem, after its discoverer Thomas Bayes:

\begin{align}
 P( A | X ) = & \frac{ P(X | A) P(A) } {P(X) } \\\\[5pt]
& \propto P(X | A) P(A)\;\; (\propto \text{is proportional to })
\end{align}

The above formula is not unique to Bayesian inference: it is a mathematical fact with uses outside Bayesian inference. Bayesian inference merely uses it to connect prior probabilities $P(A)$ with an updated posterior probabilities $P(A | X )$.

##### Example: Mandatory coin-flip example

Every statistics text must contain a coin-flipping example, I'll use it here to get it out of the way. Suppose, naively, that you are unsure about the probability of heads in a coin flip (spoiler alert: it's 50%). You believe there is some true underlying ratio, call it $p$, but have no prior opinion on what $p$ might be.

We begin to flip a coin, and record the observations: either $H$ or $T$. This is our observed data. An interesting question to ask is how our inference changes as we observe more and more data? More specifically, what do our posterior probabilities look like when we have little data, versus when we have lots of data.

Below we plot a sequence of updating posterior probabilities as we observe increasing amounts of data (coin flips).

```python
"""
The book uses a custom matplotlibrc file, which provides the unique styles for
matplotlib plots. If executing this book, and you wish to use the book's
styling, provided are two options:
    1. Overwrite your own matplotlibrc file with the rc-file provided in the
       book's styles/ dir. See http://matplotlib.org/users/customizing.html
    2. Also in the styles is  bmh_matplotlibrc.json file. This can be used to
       update the styles in only this notebook. Try running the following code:

        import json
        s = json.load(open("../styles/bmh_matplotlibrc.json"))
        matplotlib.rcParams.update(s)

"""

# The code below can be passed over, as it is currently not important, plus it
# uses advanced topics we have not covered yet. LOOK AT PICTURE, MICHAEL!
%matplotlib inline
from IPython.core.pylabtools import figsize
import numpy as np
from matplotlib import pyplot as plt
figsize(11, 9)

import scipy.stats as stats

dist = stats.beta
n_trials = [0, 1, 2, 3, 4, 5, 8, 15, 50, 500]
data = stats.bernoulli.rvs(0.5, size=n_trials[-1])
x = np.linspace(0, 1, 100)

# For the already prepared, I'm using Binomial's conj. prior.
for k, N in enumerate(n_trials):
    sx = plt.subplot(int(len(n_trials)/2), 2, k+1)
    plt.xlabel("$p$, probability of heads") \
        if k in [0, len(n_trials)-1] else None
    plt.setp(sx.get_yticklabels(), visible=False)
    heads = data[:N].sum()
    y = dist.pdf(x, 1 + heads, 1 + N - heads)
    plt.plot(x, y, label="observe %d tosses,\n %d heads" % (N, heads))
    plt.fill_between(x, 0, y, color="#348ABD", alpha=0.4)
    plt.vlines(0.5, 0, 4, color="k", linestyles="--", lw=1)

    leg = plt.legend()
    leg.get_frame().set_alpha(0.4)
    plt.autoscale(tight=True)

plt.suptitle("Bayesian updating of posterior probabilities",
             y=1.02,
             fontsize=14)

plt.tight_layout()
```

The posterior probabilities are represented by the curves, and our uncertainty is proportional to the width of the curve. As the plot above shows, as we start to observe data our posterior probabilities start to shift and move around. Eventually, as we observe more and more data (coin-flips), our probabilities will tighten closer and closer around the true value of $p=0.5$ (marked by a dashed line).

Notice that the plots are not always *peaked* at 0.5. There is no reason it should be: recall we assumed we did not have a prior opinion of what $p$ is. In fact, if we observe quite extreme data, say 8 flips and only 1 observed heads, our distribution would look very biased *away* from lumping around 0.5 (with no prior opinion, how confident would you feel betting on a fair coin after observing 8 tails and 1 head?). As more data accumulates, we would see more and more probability being assigned at $p=0.5$, though never all of it.

The next example is a simple demonstration of the mathematics of Bayesian inference.

##### Example: Bug, or just sweet, unintended feature?

Let $A$ denote the event that our code has **no bugs** in it. Let $X$ denote the event that the code passes all debugging tests. For now, we will leave the prior probability of no bugs as a variable, i.e. $P(A) = p$.

We are interested in $P(A|X)$, i.e. the probability of no bugs, given our debugging tests $X$. To use the formula above, we need to compute some quantities.

What is $P(X | A)$, i.e., the probability that the code passes $X$ tests *given* there are no bugs? Well, it is equal to 1, for a code with no bugs will pass all tests.

$P(X)$ is a little bit trickier: The event $X$ can be divided into two possibilities, event $X$ occurring even though our code *indeed has* bugs (denoted $\sim A\;$, spoken *not $A$*), or event $X$ without bugs ($A$). $P(X)$ can be represented as:

\begin{align}
P(X ) & = P(X \text{ and } A) + P(X \text{ and } \sim A) \\\\[5pt]
 & = P(X|A)P(A) + P(X | \sim A)P(\sim A)\\\\[5pt]
& = P(X|A)p + P(X | \sim A)(1-p)
\end{align}

We have already computed $P(X|A)$ above. On the other hand, $P(X | \sim A)$ is subjective: our code can pass tests but still have a bug in it, though the probability there is a bug present is reduced. Note this is dependent on the number of tests performed, the degree of complication in the tests, etc. Let's be conservative and assign $P(X|\sim A) = 0.5$. Then

\begin{align}
P(A | X) & = \frac{1\cdot p}{ 1\cdot p +0.5 (1-p) } \\\\
& = \frac{ 2 p}{1+p}
\end{align}
This is the posterior probability. What does it look like as a function of our prior, $p \in [0,1]$?

```python
figsize(12.5, 4)
p = np.linspace(0, 1, 50)
plt.plot(p, 2*p/(1+p), color="#348ABD", lw=3)
#plt.fill_between(p, 2*p/(1+p), alpha=.5, facecolor=["#A60628"])
plt.scatter(0.2, 2*(0.2)/1.2, s=140, c="#348ABD")
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.xlabel("Prior, $P(A) = p$")
plt.ylabel("Posterior, $P(A|X)$, with $P(A) = p$")
plt.title("Are there bugs in my code?");
```

We can see the biggest gains if we observe the $X$ tests passed when the prior probability, $p$, is low. Let's settle on a specific value for the prior. I'm a strong programmer (I think), so I'm going to give myself a realistic prior of 0.20, that is, there is a 20% chance that I write code bug-free. To be more realistic, this prior should be a function of how complicated and large the code is, but let's pin it at 0.20. Then my updated belief that my code is bug-free is 0.33.

Recall that the prior is a probability: $p$ is the prior probability that there *are no bugs*, so $1-p$ is the prior probability that there *are bugs*.

Similarly, our posterior is also a probability, with $P(A | X)$ the probability there is no bug *given we saw all tests pass*, hence $1-P(A|X)$ is the probability there is a bug *given all tests passed*. What does our posterior probability look like? Below is a chart of both the prior and the posterior probabilities.

```python
figsize(12.5, 4)
colours = ["#348ABD", "#A60628"]

prior = [0.20, 0.80]
posterior = [1./3, 2./3]
plt.bar([0, .7], prior, alpha=0.70, width=0.25,
        color=colours[0], label="prior distribution",
        lw="3", edgecolor=colours[0])

plt.bar([0+0.25, .7+0.25], posterior, alpha=0.7,
        width=0.25, color=colours[1],
        label="posterior distribution",
        lw="3", edgecolor=colours[1])

plt.xticks([0.20, .95], ["Bugs Absent", "Bugs Present"])
plt.title("Prior and Posterior probability of bugs present")
plt.ylabel("Probability")
plt.legend(loc="upper left");
```

Notice that after we observed $X$ occur, the probability of bugs being absent increased. By increasing the number of tests, we can approach confidence (probability 1) that there are no bugs present.

This was a very simple example of Bayesian inference and Bayes rule. Unfortunately, the mathematics necessary to perform more complicated Bayesian inference only becomes more difficult, except for artificially constructed cases. We will later see that this type of mathematical analysis is actually unnecessary. First we must broaden our modeling tools. The next section deals with *probability distributions*. If you are already familiar, feel free to skip (or at least skim), but for the less familiar the next section is essential.

_______

## Probability Distributions

**Let's quickly recall what a probability distribution is:** Let $Z$ be some random variable. Then associated with $Z$ is a *probability distribution function* that assigns probabilities to the different outcomes $Z$ can take. Graphically, a probability distribution is a curve where the probability of an outcome is proportional to the height of the curve. You can see examples in the first figure of this chapter.

We can divide random variables into three classifications:

-   **$Z$ is discrete**: Discrete random variables may only assume values on a specified list. Things like populations, movie ratings, and number of votes are all discrete random variables. Discrete random variables become more clear when we contrast them with...

-   **$Z$ is continuous**: Continuous random variable can take on arbitrarily exact values. For example, temperature, speed, time, color are all modeled as continuous variables because you can progressively make the values more and more precise.

- **$Z$ is mixed**: Mixed random variables assign probabilities to both discrete and continuous random variables, i.e. it is a combination of the above two categories.

### Discrete Case
If $Z$ is discrete, then its distribution is called a *probability mass function*, which measures the probability $Z$ takes on the value $k$, denoted $P(Z=k)$. Note that the probability mass function completely describes the random variable $Z$, that is, if we know the mass function, we know how $Z$ should behave. There are popular probability mass functions that consistently appear: we will introduce them as needed, but let's introduce the first very useful probability mass function. We say $Z$ is *Poisson*-distributed if:

$$P(Z = k) =\frac{ \lambda^k e^{-\lambda} }{k!}, \; \; k=0,1,2, \dots $$

$\lambda$ is called a parameter of the distribution, and it controls the distribution's shape. For the Poisson distribution, $\lambda$ can be any positive number. By increasing $\lambda$, we add more probability to larger values, and conversely by decreasing $\lambda$ we add more probability to smaller values. One can describe $\lambda$ as the *intensity* of the Poisson distribution.

Unlike $\lambda$, which can be any positive number, the value $k$ in the above formula must be a non-negative integer, i.e., $k$ must take on values 0,1,2, and so on. This is very important, because if you wanted to model a population you could not make sense of populations with 4.25 or 5.612 members.

If a random variable $Z$ has a Poisson mass distribution, we denote this by writing

$$Z \sim \text{Poi}(\lambda) $$

One useful property of the Poisson distribution is that its expected value is equal to its parameter, i.e.:

$$E\large[ \;Z\; | \; \lambda \;\large] = \lambda $$

We will use this property often, so it's useful to remember. Below, we plot the probability mass distribution for different $\lambda$ values. The first thing to notice is that by increasing $\lambda$, we add more probability of larger values occurring. Second, notice that although the graph ends at 15, the distributions do not. They assign positive probability to every non-negative integer.

```python
figsize(12.5, 4)

import scipy.stats as stats
a = np.arange(16)
poi = stats.poisson
lambda_ = [1.5, 4.25]
colours = ["#348ABD", "#A60628"]

plt.bar(a, poi.pmf(a, lambda_[0]), color=colours[0],
        label="$\lambda = %.1f$" % lambda_[0], alpha=0.60,
        edgecolor=colours[0], lw="3")

plt.bar(a, poi.pmf(a, lambda_[1]), color=colours[1],
        label="$\lambda = %.1f$" % lambda_[1], alpha=0.60,
        edgecolor=colours[1], lw="3")

plt.xticks(a + 0.4, a)
plt.legend()
plt.ylabel("probability of $k$")
plt.xlabel("$k$")
plt.title("Probability mass function of a Poisson random variable; differing \
$\lambda$ values");
```

### Continuous Case
Instead of a probability mass function, a continuous random variable has a *probability density function*. This might seem like unnecessary nomenclature, but the density function and the mass function are very different creatures. An example of continuous random variable is a random variable with *exponential density*. The density function for an exponential random variable looks like this:

$$f_Z(z | \lambda) = \lambda e^{-\lambda z }, \;\; z\ge 0$$

Like a Poisson random variable, an exponential random variable can take on only non-negative values. But unlike a Poisson variable, the exponential can take on *any* non-negative values, including non-integral values such as 4.25 or 5.612401. This property makes it a poor choice for count data, which must be an integer, but a great choice for time data, temperature data (measured in Kelvins, of course), or any other precise *and positive* variable. The graph below shows two probability density functions with different $\lambda$ values.

When a random variable $Z$ has an exponential distribution with parameter $\lambda$, we say *$Z$ is exponential* and write

$$Z \sim \text{Exp}(\lambda)$$

Given a specific $\lambda$, the expected value of an exponential random variable is equal to the inverse of $\lambda$, that is:

$$E[\; Z \;|\; \lambda \;] = \frac{1}{\lambda}$$

```python
a = np.linspace(0, 4, 100)
expo = stats.expon
lambda_ = [0.5, 1]

for l, c in zip(lambda_, colours):
    plt.plot(a, expo.pdf(a, scale=1./l), lw=3,
             color=c, label="$\lambda = %.1f$" % l)
    plt.fill_between(a, expo.pdf(a, scale=1./l), color=c, alpha=.33)

plt.legend()
plt.ylabel("PDF at $z$")
plt.xlabel("$z$")
plt.ylim(0,1.2)
plt.title("Probability density function of an Exponential random variable;\
 differing $\lambda$");
```

### But what is $\lambda \;$?

**This question is what motivates statistics**. In the real world, $\lambda$ is hidden from us. We see only $Z$, and must go backwards to try and determine $\lambda$. The problem is difficult because there is no one-to-one mapping from $Z$ to $\lambda$. Many different methods have been created to solve the problem of estimating $\lambda$, but since $\lambda$ is never actually observed, no one can say for certain which method is best!

Bayesian inference is concerned with *beliefs* about what $\lambda$ might be. Rather than try to guess $\lambda$ exactly, we can only talk about what $\lambda$ is likely to be by assigning a probability distribution to $\lambda$.

This might seem odd at first. After all, $\lambda$ is fixed; it is not (necessarily) random! How can we assign probabilities to values of a non-random variable? Ah, we have fallen for our old, frequentist way of thinking. Recall that under Bayesian philosophy, we *can* assign probabilities if we interpret them as beliefs. And it is entirely acceptable to have *beliefs* about the parameter $\lambda$.

##### Example: Inferring behaviour from text-message data

Let's try to model a more interesting example, one that concerns the rate at which a user sends and receives text messages:

>  You are given a series of daily text-message counts from a user of your system. The data, plotted over time, appears in the chart below. You are curious to know if the user's text-messaging habits have changed over time, either gradually or suddenly. How can you model this? (This is in fact my own text-message data. Judge my popularity as you wish.)

```python
figsize(12.5, 3.5)
count_data = np.loadtxt("data/txtdata.csv")
n_count_data = len(count_data)
plt.bar(np.arange(n_count_data), count_data, color="#348ABD")
plt.xlabel("Time (days)")
plt.ylabel("count of text-msgs received")
plt.title("Did the user's texting habits change over time?")
plt.xlim(0, n_count_data);
```

Before we start modeling, see what you can figure out just by looking at the chart above. Would you say there was a change in behaviour during this time period?

How can we start to model this? Well, as we have conveniently already seen, a Poisson random variable is a very appropriate model for this type of *count* data. Denoting day $i$'s text-message count by $C_i$,

$$ C_i \sim \text{Poisson}(\lambda)  $$

We are not sure what the value of the $\lambda$ parameter really is, however. Looking at the chart above, it appears that the rate might become higher late in the observation period, which is equivalent to saying that $\lambda$ increases at some point during the observations. (Recall that a higher value of $\lambda$ assigns more probability to larger outcomes. That is, there is a higher probability of many text messages having been sent on a given day.)

How can we represent this observation mathematically? Let's assume that on some day during the observation period (call it $\tau$), the parameter $\lambda$ suddenly jumps to a higher value. So we really have two $\lambda$ parameters: one for the period before $\tau$, and one for the rest of the observation period. In the literature, a sudden transition like this would be called a *switchpoint*:

$$
\lambda =
\begin{cases}
\lambda_1  & \text{if } t \lt \tau \cr
\lambda_2 & \text{if } t \ge \tau
\end{cases}
$$

If, in reality, no sudden change occurred and indeed $\lambda_1 = \lambda_2$, then the $\lambda$s posterior distributions should look about equal.

We are interested in inferring the unknown $\lambda$s. To use Bayesian inference, we need to assign prior probabilities to the different possible values of $\lambda$. What would be good prior probability distributions for $\lambda_1$ and $\lambda_2$? Recall that $\lambda$ can be any positive number. As we saw earlier, the *exponential* distribution provides a continuous density function for positive numbers, so it might be a good choice for modeling $\lambda_i$. But recall that the exponential distribution takes a parameter of its own, so we'll need to include that parameter in our model. Let's call that parameter $\alpha$.

\begin{align}
&\lambda_1 \sim \text{Exp}( \alpha ) \\\
&\lambda_2 \sim \text{Exp}( \alpha )
\end{align}

$\alpha$ is called a *hyper-parameter* or *parent variable*. In literal terms, it is a parameter that influences other parameters. Our initial guess at $\alpha$ does not influence the model too strongly, so we have some flexibility in our choice.  **A good rule of thumb is to set the exponential parameter equal to the inverse of the average of the count data.** Since we're modeling $\lambda$ using an exponential distribution, we can use the expected value identity shown earlier to get:

$$\frac{1}{N}\sum_{i=0}^N \;C_i \approx E[\; \lambda \; |\; \alpha ] = \frac{1}{\alpha}$$

An alternative, and something I encourage the reader to try, would be to have two priors: one for each $\lambda_i$. Creating two exponential distributions with different $\alpha$ values reflects our prior belief that the rate changed at some point during the observations.

What about $\tau$? Because of the noisiness of the data, it's difficult to pick out a priori when $\tau$ might have occurred. Instead, we can assign a *uniform prior belief* to every possible day. This is equivalent to saying

\begin{align}
& \tau \sim \text{DiscreteUniform(1,70) }\\\\
& \Rightarrow P( \tau = k ) = \frac{1}{70}
\end{align}

So after all this, what does our overall prior distribution for the unknown variables look like? Frankly, *it doesn't matter*. What we should understand is that it's an ugly, complicated mess involving symbols only a mathematician could love. And things will only get uglier the more complicated our models become. Regardless, all we really care about is the posterior distribution.

We next turn to PyMC3, a Python library for performing Bayesian analysis that is undaunted by the mathematical monster we have created.

Introducing our first hammer: PyMC3
-----

PyMC3 is a Python library for programming Bayesian analysis [3]. It is a fast, well-maintained library. The only unfortunate part is that its documentation is lacking in certain areas, especially those that bridge the gap between beginner and hacker. One of this book's main goals is to solve that problem, and also to demonstrate why PyMC3 is so cool.

We will model the problem above using PyMC3. This type of programming is called *probabilistic programming*, an unfortunate misnomer that invokes ideas of randomly-generated code and has likely confused and frightened users away from this field. The code is not random; it is probabilistic in the sense that we create probability models using programming variables as the model's components. Model components are first-class primitives within the PyMC3 framework.

B. Cronin [5] has a very motivating description of probabilistic programming:

>   Another way of thinking about this: unlike a traditional program, which only runs in the forward directions, a probabilistic program is run in both the forward and backward direction. It runs forward to compute the consequences of the assumptions it contains about the world (i.e., the model space it represents), but it also runs backward from the data to constrain the possible explanations. In practice, many probabilistic programming systems will cleverly interleave these forward and backward operations to efficiently home in on the best explanations.

Because of the confusion engendered by the term *probabilistic programming*, I'll refrain from using it. Instead, I'll simply say *programming*, since that's what it really is.

PyMC3 code is easy to read. The only novel thing should be the syntax. Simply remember that we are representing the model's components ($\tau, \lambda_1, \lambda_2$ ) as variables.

```python
import pymc3 as pm
import theano.tensor as tt

with pm.Model() as model:
    alpha = 1.0/count_data.mean()  # Recall count_data is the
                                   # variable that holds our txt counts
    lambda_1 = pm.Exponential("lambda_1", alpha)
    lambda_2 = pm.Exponential("lambda_2", alpha)

    tau = pm.DiscreteUniform("tau", lower=0, upper=n_count_data - 1)
```

In the code above, we create the PyMC3 variables corresponding to $\lambda_1$ and $\lambda_2$. We assign them to PyMC3's *stochastic variables*, so-called because they are treated by the back end as random number generators.

```python
with model:
    idx = np.arange(n_count_data) # Index
    lambda_ = pm.math.switch(tau > idx, lambda_1, lambda_2)
```

This code creates a new function `lambda_`, but really we can think of it as a random variable: the random variable $\lambda$ from above. The `switch()` function assigns `lambda_1` or `lambda_2` as the value of `lambda_`, depending on what side of `tau` we are on. The values of `lambda_` up until `tau` are `lambda_1` and the values afterwards are `lambda_2`.

Note that because `lambda_1`, `lambda_2` and `tau` are random, `lambda_` will be random. We are **not** fixing any variables yet.

```python
with model:
    observation = pm.Poisson("obs", lambda_, observed=count_data)
```

The variable `observation` combines our data, `count_data`, with our proposed data-generation scheme, given by the variable `lambda_`, through the `observed` keyword.

The code below will be explained in Chapter 3, but I show it here so you can see where our results come from. One can think of it as a *learning* step. The machinery being employed is called *Markov Chain Monte Carlo* (MCMC), which I also delay explaining until Chapter 3. This technique returns thousands of random variables from the posterior distributions of $\lambda_1, \lambda_2$ and $\tau$. We can plot a histogram of the random variables to see what the posterior distributions look like. Below, we collect the samples (called *traces* in the MCMC literature) into histograms.

```python
### Mysterious code to be explained in Chapter 3.
with model:
    step = pm.Metropolis()
    trace = pm.sample(10000, tune=5000,step=step, return_inferencedata=False)
```

    Multiprocess sampling (4 chains in 4 jobs)
    CompoundStep
    >Metropolis: [tau]
    >Metropolis: [lambda_2]
    >Metropolis: [lambda_1]

<div>
    <style>
        /* Turns off some styling */
        progress {
            /* gets rid of default border in Firefox and Opera. */
            border: none;
            /* Needs to be in here for Safari polyfill so background images work as expected. */
            background-size: auto;
        }
        .progress-bar-interrupted, .progress-bar-interrupted::-webkit-progress-bar {
            background: #F44336;
        }
    </style>
  <progress value='60000' class='' max='60000' style='width:300px; height:20px; vertical-align: middle;'></progress>
  100.00% [60000/60000 00:07<00:00 Sampling 4 chains, 0 divergences]
</div>

    Sampling 4 chains for 5_000 tune and 10_000 draw iterations (20_000 + 40_000 draws total) took 8 seconds.
    The rhat statistic is larger than 1.4 for some parameters. The sampler did not converge.
    The estimated number of effective samples is smaller than 200 for some parameters.

```python
lambda_1_samples = trace['lambda_1']
lambda_2_samples = trace['lambda_2']
tau_samples = trace['tau']
```

```python
figsize(12.5, 10)
#histogram of the samples:

ax = plt.subplot(311)
ax.set_autoscaley_on(False)

plt.hist(lambda_1_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_1$", color="#A60628", density=True)
plt.legend(loc="upper left")
plt.title(r"""Posterior distributions of the variables
    $\lambda_1,\;\lambda_2,\;\tau$""")
plt.xlim([15, 30])
plt.xlabel("$\lambda_1$ value")

ax = plt.subplot(312)
ax.set_autoscaley_on(False)
plt.hist(lambda_2_samples, histtype='stepfilled', bins=30, alpha=0.85,
         label="posterior of $\lambda_2$", color="#7A68A6", density=True)
plt.legend(loc="upper left")
plt.xlim([15, 30])
plt.xlabel("$\lambda_2$ value")

plt.subplot(313)
w = 1.0 / tau_samples.shape[0] * np.ones_like(tau_samples)
plt.hist(tau_samples, bins=n_count_data, alpha=1,
         label=r"posterior of $\tau$",
         color="#467821", weights=w, rwidth=2.)
plt.xticks(np.arange(n_count_data))

plt.legend(loc="upper left")
plt.ylim([0, .75])
plt.xlim([35, len(count_data)-20])
plt.xlabel(r"$\tau$ (in days)")
plt.ylabel("probability");
```

### Interpretation

Recall that Bayesian methodology returns a *distribution*. Hence we now have distributions to describe the unknown $\lambda$s and $\tau$. What have we gained? Immediately, we can see the uncertainty in our estimates: the wider the distribution, the less certain our posterior belief should be. We can also see what the plausible values for the parameters are: $\lambda_1$ is around 18 and $\lambda_2$ is around 23. The posterior distributions of the two $\lambda$s are clearly distinct, indicating that it is indeed likely that there was a change in the user's text-message behaviour.

What other observations can you make? If you look at the original data again, do these results seem reasonable?

Notice also that the posterior distributions for the $\lambda$s do not look like exponential distributions, even though our priors for these variables were exponential. In fact, the posterior distributions are not really of any form that we recognize from the original model. But that's OK! This is one of the benefits of taking a computational point of view. If we had instead done this analysis using mathematical approaches, we would have been stuck with an analytically intractable (and messy) distribution. Our use of a computational approach makes us indifferent to mathematical tractability.

Our analysis also returned a distribution for $\tau$. Its posterior distribution looks a little different from the other two because it is a discrete random variable, so it doesn't assign probabilities to intervals. We can see that near day 45, there was a 50% chance that the user's behaviour changed. Had no change occurred, or had the change been gradual over time, the posterior distribution of $\tau$ would have been more spread out, reflecting that many days were plausible candidates for $\tau$. By contrast, in the actual results we see that only three or four days make any sense as potential transition points.

### Why would I want samples from the posterior, anyways?

We will deal with this question for the remainder of the book, and it is an understatement to say that it will lead us to some amazing results. For now, let's end this chapter with one more example.

We'll use the posterior samples to answer the following question: what is the expected number of texts at day $t, \; 0 \le t \le 70$ ? Recall that the expected value of a Poisson variable is equal to its parameter $\lambda$. Therefore, the question is equivalent to *what is the expected value of $\lambda$ at time $t$*?

In the code below, let $i$ index samples from the posterior distributions. Given a day $t$, we average over all possible $\lambda_i$ for that day $t$, using $\lambda_i = \lambda_{1,i}$ if $t \lt \tau_i$ (that is, if the behaviour change has not yet occurred), else we use $\lambda_i = \lambda_{2,i}$.

```python
figsize(12.5, 5)
# tau_samples, lambda_1_samples, lambda_2_samples contain
# N samples from the corresponding posterior distribution
N = tau_samples.shape[0]
expected_texts_per_day = np.zeros(n_count_data)
for day in range(0, n_count_data):
    # ix is a bool index of all tau samples corresponding to
    # the switchpoint occurring prior to value of 'day'
    ix = day < tau_samples
    # Each posterior sample corresponds to a value for tau.
    # for each day, that value of tau indicates whether we're "before"
    # (in the lambda1 "regime") or
    #  "after" (in the lambda2 "regime") the switchpoint.
    # by taking the posterior sample of lambda1/2 accordingly, we can average
    # over all samples to get an expected value for lambda on that day.
    # As explained, the "message count" random variable is Poisson distributed,
    # and therefore lambda (the poisson parameter) is the expected value of
    # "message count".
    expected_texts_per_day[day] = (lambda_1_samples[ix].sum()
                                   + lambda_2_samples[~ix].sum()) / N

plt.plot(range(n_count_data), expected_texts_per_day, lw=4, color="#E24A33",
         label="expected number of text-messages received")
plt.xlim(0, n_count_data)
plt.xlabel("Day")
plt.ylabel("Expected # text-messages")
plt.title("Expected number of text-messages received")
plt.ylim(0, 60)
plt.bar(np.arange(len(count_data)), count_data, color="#348ABD", alpha=0.65,
        label="observed texts per day")

plt.legend(loc="upper left");
```

Our analysis shows strong support for believing the user's behavior did change ($\lambda_1$ would have been close in value to $\lambda_2$ had this not been true), and that the change was sudden rather than gradual (as demonstrated by $\tau$'s strongly peaked posterior distribution). We can speculate what might have caused this: a cheaper text-message rate, a recent weather-to-text subscription, or perhaps a new relationship. (In fact, the 45th day corresponds to Christmas, and I moved away to Toronto the next month, leaving a girlfriend behind.)

##### Exercises

1\.  Using `lambda_1_samples` and `lambda_2_samples`, what is the mean of the posterior distributions of $\lambda_1$ and $\lambda_2$?

```python
#type your code here.
print(lambda_1_samples.mean())
print(lambda_2_samples.mean())
```

    17.633934560236177
    22.400857652609584

2\.  What is the expected percentage increase in text-message rates? `hint:` compute the mean of `lambda_1_samples/lambda_2_samples`. Note that this quantity is very different from `lambda_1_samples.mean()/lambda_2_samples.mean()`.

```python
#type your code here.
(lambda_2_samples/lambda_1_samples).mean()
```

    1.2742173991420649

3\. What is the mean of $\lambda_1$ **given** that we know $\tau$ is less than 45.  That is, suppose we have been given new information that the change in behaviour occurred prior to day 45. What is the expected value of $\lambda_1$ now? (You do not need to redo the PyMC3 part. Just consider all instances where `tau_samples < 45`.)

```python
#type your code here.
lambda_1_samples[tau_samples < 45].mean()
```

    17.53620648248265

### References

-  [1] Gelman, Andrew. N.p.. Web. 22 Jan 2013. [N is never large enough](http://andrewgelman.com/2005/07/31/n_is_never_larg).
-  [2] Norvig, Peter. 2009. [The Unreasonable Effectiveness of Data](http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35179.pdf).
- [3] Salvatier, J, Wiecki TV, and Fonnesbeck C. (2016) Probabilistic programming in Python using PyMC3. *PeerJ Computer Science* 2:e55 <https://doi.org/10.7717/peerj-cs.55>
- [4] Jimmy Lin and Alek Kolcz. Large-Scale Machine Learning at Twitter. Proceedings of the 2012 ACM SIGMOD International Conference on Management of Data (SIGMOD 2012), pages 793-804, May 2012, Scottsdale, Arizona.
- [5] Cronin, Beau. "Why Probabilistic Programming Matters." 24 Mar 2013. Google, Online Posting to Google . Web. 24 Mar. 2013. <https://plus.google.com/u/0/107971134877020469960/posts/KpeRdJKR6Z1>.

```python
from IPython.core.display import HTML
def css_styling():
    styles = open("../styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```

<style>
    @font-face {
        font-family: "Computer Modern";
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunss.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-weight: bold;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunsx.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-style: oblique;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunsi.otf');
    }
    @font-face {
        font-family: "Computer Modern";
        font-weight: bold;
        font-style: oblique;
        src: url('http://9dbb143991406a7c655e-aa5fcb0a5a4ec34cff238a2d56ca4144.r56.cf5.rackcdn.com/cmunso.otf');
    }
    div.cell{
        width:800px;
        margin-left:16% !important;
        margin-right:auto;
    }
    h1 {
        font-family: Helvetica, serif;
    }
    h4{
        margin-top:12px;
        margin-bottom: 3px;
       }
    div.text_cell_render{
        font-family: Computer Modern, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
        line-height: 145%;
        font-size: 130%;
        width:800px;
        margin-left:auto;
        margin-right:auto;
    }
    .CodeMirror{
            font-family: "Source Code Pro", source-code-pro,Consolas, monospace;
    }
    .prompt{
        display: None;
    }
    .text_cell_render h5 {
        font-weight: 300;
        font-size: 22pt;
        color: #4057A1;
        font-style: italic;
        margin-bottom: .5em;
        margin-top: 0.5em;
        display: block;
    }

    .warning{
        color: rgb( 240, 20, 20 )
        }
</style>


Applied Seismology

Lab: Instrument Response

Seismo-Live: http://seismo-live.org

##### Authors:
* Carl Tape ([@carltape](https://github.com/carltape))
* Lion Krischer ([@krischer](https://github.com/krischer))
---

based on *GEOS 626: Applied Seismology from Carl Tape*

---

```python
# This cell does two things:
# (1) make plots appear in the notebook and (2) creates prettier plots.
# Make sure to execute it at least once!
%matplotlib inline
import matplotlib.pyplot as plt
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = 10, 5
```

### L1 Instructions

This is a Python and ObsPy version of the above mentioned lab. We will perform the same calculations and create similar plots but we will be using different services, data formats, and tools.

It aims to teach students how to calculate, visualize and use instruments responses.

### L2 Computing and plotting instrument response, $I(ω)$

##### Acquiring Data

The first step when working with instrument data is to acquire it. Nowadays this is mostly done using the *station* web service defined by the [FDSN](http://www.fdsn.org/) (International Federation of Digital Seismograph Networks). Once you know how to use this you can use it to download data from data centers across the globe including IRIS, ORFEUS, ETH, RESIF, GEONET, and many more. Notable exceptions are most Asian countries and countries that don't freely share their data.

With ObsPy this works by first importing the FDSN `Client` class and then initializing it with the data center you want to download from. For this lab we will use data from the Geoscope project, which can be downloaded from IRIS (its "home" data center is the IPGP in Paris which you can also use - just replace `"IRIS"` with `"IPGP"` in the following code box - but most of you will be more familiar with IRIS).

See the [documentation of obspy.clients.fdsn](http://docs.obspy.org/packages/obspy.clients.fdsn.html) for more details.

```python
from obspy.clients.fdsn import Client
# ObsPy knows which website to connect to for many data center.
# For others you can also pass the full URL.
c = Client("IRIS")
```

Station information is time dependent due to for example changed sensors, new calibrations, or other changes. It is thus important to specify the time frame for which to request response information.

ObsPy handles absolute times with the `UTCDateTime` class. See the [documentation](http://docs.obspy.org/packages/autogen/obspy.core.utcdatetime.UTCDateTime.html) and the [tutorial](http://docs.obspy.org/tutorial/code_snippets/utc_date_time.html) for more information.

```python
import obspy
# Create two times which we will later use to download time series data.
starttime = obspy.UTCDateTime(2004, 12, 26, 0, 58, 50)
# Create a new time object by adding 5 days to the previous ones. Time
# differences are in seconds.
endtime = starttime + 86400 * 5
```

We will use the `CAN` station which is located in Canberra, Australia, and featured in [*Park et al.* (2005, Figure 1)](http://dx.doi.org/10.1126/science.1112305). Please note that in many cases the station code is not unique and in practice the network code is also required. See the ObsPy tutorials for more details about these concepts. The `get_station()` method of the FDSN `Client` object returns station/instrument information. Once again see its [documentation](http://docs.obspy.org/packages/autogen/obspy.clients.fdsn.client.Client.get_stations.html) for more information.

```python
# This will get all stations that satisfy all constraints simultaneosly.
inv = c.get_stations(network="G", station="CAN", channel="LHZ",
                     level="response",
                     starttime=starttime, endtime=endtime)
print(inv)
```

    Inventory created at 2019-11-25T09:22:54.000000Z
    	Created by: IRIS WEB SERVICE: fdsnws-station | version: 1.1.37
    		    http://service.iris.edu/fdsnws/station/1/query?starttime=2004-12-26...
    	Sending institution: IRIS-DMC (IRIS-DMC)
    	Contains:
    		Networks (1):
    			G
    		Stations (1):
    			G.CAN (Canberra, Australia)
    		Channels (1):
    			G.CAN..LHZ

The `station` services return `StationXML` files which are the most modern and future proof data format for station and instrument information. **If possible at all: Always try to use StationXML files!**

##### Plotting the Instrument Response

We can now plot the instrument response with the `plot_response()` method. The `min_freq` argument determines the lowest plotted frequency - the maximum plotted frequency is always the Nyquist frequency.

```python
inv.plot_response(min_freq=0.001);
```

These are [Bode plots](https://en.wikipedia.org/wiki/Bode_plot) and show the amplitude $A(\omega)$ and the phase $\phi(\omega)$ response where

$$
I(\omega) = A(\omega) e^{i \phi(\omega)}
$$

Note that the formulas use $\omega$ but the plots show the frequency $f$ with $\omega = 2 \pi f$. The first variant usually simplifies notation whereas the latter one facilitates physical interpretation.

This plot does not just show the instrument response - it shows the response of the whole recording chain from observed ground motion to actually stored data. This includes the analoge response of the actual seismometer, the analog-to-digitial converter (DAC), and possible digital filter stages.

The sampling rate of this channel is 1 sample per second ($\Delta T$ = 1s), so the **Nyquist frequency**, which is defined as

$$
f_{Nyq} = 1 / (2 \Delta t),
$$

is $f_{Nyq}$ = 0.5 Hz. The above plot does not show frequencies higher than the Nyquist frequency (the vertical dashed line) as these frequencies do not exist in the data and thus have no physical meaning.

Have a look at the details of the response information of this channel:

```python
inv[0][0][0].response
```

    Channel Response
    	From M/S (VELOCITY in Meters Per Second) to COUNTS (DIGITAL UNIT in Counts)
    	Overall Sensitivity: 1.84484e+09 defined at 0.010 Hz
    	5 stages:
    		Stage 1: PolesZerosResponseStage from M/S to V, gain: 2252
    		Stage 2: PolesZerosResponseStage from V to V, gain: 1
    		Stage 3: CoefficientsTypeResponseStage from V to COUNTS, gain: 819200
    		Stage 4: CoefficientsTypeResponseStage from COUNTS to COUNTS, gain: 1
    		Stage 5: CoefficientsTypeResponseStage from COUNTS to COUNTS, gain: 1

Here we see that it consists of multiple stages that convert from one unit to another. The final instrument response is the multipication (in the frequency domain) of all of these.

**QUESTION:** What are the input and output units of the whole instrument? What does that mean physically?

**QUESTION:** How does one conceptually remove the instrument response from recorded data?

##### Incomplete Responses

You can also only plot the response of a range of stages. The plotted sensitivity in Obspy 1.0.1 is the overall sensitivity of all stages and not only the chosen ones - thus the plot looks a bit strange. This will be rectified [soon](https://github.com/obspy/obspy/issues/1368).

```python
inv[0][0][0].plot(0.001, start_stage=1, end_stage=1);
```

Note that the major differences (except a constant factor) are close to the Nyquist frequency - these are due to the here not plotted decimation and FIR filter stages. In many cases it is sufficient to only use Poles & Zeros information but there are stations where this is not true. As it is tedious to manually check everytime: **There is no reason to not use the full chain instrument response if it is available!**

##### Output Units

Modern seismometers usually record the velocity of ground motion, strong motions sensor record acceleration.

**QUESTION:** Why is that?

The instrument response describes how incoming data (in whatever the instrument measures) it transformed into what is finally stored in a file. Restoring original ground motion requires the inversion of that process. The deconvolution of the instrument response as written for example in a StationXML file will convert data back to its original units.

If your analysis requires other units it is best to perform the integration or differentiation during the instrument deconvolution. That deconvolution is usually performed in the frequency domain - once there integration/differentiation is almost free and the most accurate it gets.

The following plots show the instrument response $I_d(\omega)$ from displacement, velocity $I_v(\omega)$, and acceleration $I_a(\omega)$.

```python
print("Displacement")
inv.plot_response(0.001, output="DISP")
print("Velocity")
inv.plot_response(0.001, output="VEL")
print("Acceleration")
inv.plot_response(0.001, output="ACC");
```

Basic properties of the Fourier transform result in

$$
\begin{align}
X_v(\omega) &= (i \omega) X_d(\omega) \\
X_a(\omega) &= (i \omega) X_v(\omega)
\end{align}
$$

where $X_d$, $X_v$, and $X_a$ are the Fourier transforms of displacement $x_d(t)$, velocity $x_v(t)$, and acceleration $x_a(t)$.

We also have the relationship between ground motion, instrument response, and the output form the seismometer:

$$
\begin{align}
x_a(t) * i_a(t) &= c(t) \\
X_a(\omega) I_a (\omega) &= C(\omega)
\end{align}
$$

Here we have assumed that the instrument response is described with respect to acceleration. But we could alternatively consider the instrument response with respect to velocity or displacement; **the key point is that what comes out of the seismometer,** $C(\omega)$**, is fixed.** But we can descibe the input ground motion as displacement, velocity, or acceleration. Showing all three together and omitting explicit $\omega$ dependence, we have

$$
\begin{align}
C &= X_a I_a = X_v I_v = X_d I_d\\
  &= (i\omega) X_v I_a = (i \omega) X_d I_V = X_d I_d \\
  &= (i\omega)^2 X_d I_a = (i \omega) X_d I_V = X_d I_d \\
I_v &= I_d \ (i\omega)\\
I_a&= I_v \ (i\omega)\\
\end{align}
$$

It turns out that the effect of differentiation in the time domain leads to an *increase by a factor of one* in the slope of the amplitude spectrum, $|H(\omega)|$, in log-log space, for example, by changing from $X_d(\omega)$ to $X_v(\omega)$. But when we are looking at the *instrument response*, the slope will *decrease by a factor of one* when changing from, say, $I_d(\omega)$ to $I_v(\omega)$.

We see this in the above figure. Consider the flat segment in the velocity correction spectrum: the displacement shows a corresponding slope increase whereas the acceleration show a slope decrease.

**QUESTION:** What is meant by a "broadband" seismometer?

### L3 Deconvolve instrument response for a seismogram

We will download some data and remove its instrument response.

The first step is to get some data - we will once again use the FDSN web services but this time also to download waveform data.

```python
otime = obspy.UTCDateTime(2004, 12, 26, 0, 58, 50)
duration = 5 * 86400
starttime = otime
endtime = starttime + duration

st = c.get_waveforms(network="G", station="CAN", location="", channel="LH*",
                     starttime=starttime, endtime=endtime)

# Do it once again to also get the response information of the vertical channels.
inv = c.get_stations(network="G", station="CAN", location="", channel="LH*",
                     starttime=starttime, endtime=endtime, level="response")
```

We will now show what the main arrival looks like at bandpass 50-500s with and without the instrument deconvolution.

```python
# Plot the first hour - slice() will leave the original
# data stream intact and just returns a new view on the
# data.
st.slice(endtime=otime + 3600).copy().taper(0.05).filter(
    "bandpass", freqmin=1.0 / 500.0, freqmax=1.0 / 50.0).plot()
```

```python
# Now plot the same but with the instrument removed.
# Note the copy() here as remove_response() would otherwise
# remove the contents of the original data stream.
st.slice(endtime=otime + 3600).copy().remove_response(
    inventory=inv, output="VEL").taper(0.05).filter(
    "bandpass", freqmin=1.0 / 500.0, freqmax=1.0 / 50.0).plot()
```

What are the differences? What is their cause?

We will now plot the amplitude spectrum over 0.2 - 1.0 mHz to show the gravest normal mode peaks. We will only use the vertical component to simplify things.

We will first do it without instrument correction

```python
import numpy as np

tr = st.select(component="Z")[0].copy().taper(0.05)

# rfft() is a varient of the FFT for real valued input.
# The negative frequencies of a DFT for purely real valued
# input are just the complex conjugates of the corresponding
# positive frequencies and thus redundant.
D = np.fft.rfft(tr.data)
# Return the corresponding frequencies.
freqs = np.fft.rfftfreq(tr.stats.npts, d=tr.stats.delta)

plt.plot(freqs, np.abs(D))
plt.xlim(0.2E-3, 1.0E-3)
plt.ylim(0, 1E7)
plt.xlabel("Frequency [mHz]")
plt.show()
```

Instrument removal is, in practice, a tricky and unstable operation. Have a look at the [documentation of the remove_response() method](http://docs.obspy.org/packages/autogen/obspy.core.trace.Trace.remove_response.html) and try to understand the meaning of the various parameters and when you would want to use each.

```python
# Remember the copy()!
tr = st.select(component="Z").copy().remove_response(inventory=inv,
                                                     output="DISP")[0]

D = np.fft.rfft(tr.data)
freqs = np.fft.rfftfreq(tr.stats.npts, d=tr.stats.delta)

plt.plot(freqs, np.abs(D))
plt.xlim(0.2E-3, 1.0E-3)
plt.ylim(0, 5.0)
plt.xlabel("Frequency [mHz]")
plt.show()
```

Are there any difference to the previous versions? If yes - what are they? Keep in mind that the previous plot is based on velocity data, whereas this one has been corrected to displacement.

We are interested in the spectrum of the data so we don't really need to use instrument corrected data to calculate the spectrum - we can just calculate the spectrum and divide by the response.

```python
tr = st.select(component="Z")[0].copy().taper(0.05)

D = np.fft.rfft(tr.data)
freqs = np.fft.rfftfreq(tr.stats.npts, d=tr.stats.delta)
# Advanced ObsPy-foo to directly get the complex response.
freq_response, _ = \
    inv.select(channel="LHZ")[0][0][0].response.get_evalresp_response(
        tr.stats.delta, tr.stats.npts, output="DISP")

plt.plot(freqs, np.abs(D)/ np.abs(freq_response))
plt.xlim(0.2E-3, 1.0E-3)
plt.ylim(0, 35.0)
plt.xlabel("Frequency [mHz]")
plt.show()
```

Note that this is quite different then the previous spectrum. **Why?** Keep in mind that we are operating far from the passband of the instrument. The following two plots as well as the
[documentation of the remove_response() method](http://docs.obspy.org/packages/autogen/obspy.core.trace.Trace.remove_response.html) should help you answer that question. Which alternative yields the more correct answer?

```python
inv.select(channel="LHZ").plot_response(min_freq=0.0001);
```

```python
tr = st.select(component="Z").copy().remove_response(
    inventory=inv, output="DISP", plot=True)[0]
```


#  Impact Semaine 1 : Word Embeddings systems
####   Robin Quillivic
####    23 novembre 2018

# Introduction :
Ce premier notebook a pour but d'éclaicir la notion de "representation continues des mots" (Word embeddings en anglais). En effet, cette notion est centrale pour mon projet impact qui vise à identidier les biais présents dans certaines de ces representations. Nous allons donc nous attacher dans un premier temps à comprendre (de manière non exhautive) l'état de l'art et implémenter de manière simplifier quelque uns de ces systemes de représentation.

Ce travail se base principalement sur ces sources:
- __Thèse de Sahar Ghannay (30 premières pages)__: "Etude sur les representations continues de mots appliquees à la detection automatique des erreurs dereconnaissance de la parole.
- __Serie d'article en Anglais:__ https://textminingonline.com/dive-into-nltk-part-xi-from-word2vec-to-wordnet
- __Les ressources disponibles à cette adresse__: https://textprocessing.org/getting-started-with-word2vec
- __Le papier scientifique à l'origine de Word2Vect: __ Tomas Mikolov, Kai Chen, Greg Corrado, and Jeffrey Dean. Efficient Estimation of Word Representations in Vector Space. In Proceedings of Workshop at ICLR, 2013

## 1) La represention "One-hot":
Celle ci consiste à représenter les mots d'un corpus comme des vecteurs composés de 0 et de 1. Il y a un 1 pour indiquer la position du mot dans le corpus.

**Exemple simple:**

Si notre corpus **C=["Je suis un étudiant centralien en dernière année]**

Alors la représentation one-hot de notre copus est la suivante:
- "Je" $\Longrightarrow$ $(1\space 0\space  0\space  0\space  0\space   0\space  0\space  0\space )$
- "suis" $\Longrightarrow$ $(0\space 1\space  0\space  0\space  0\space   0\space  0\space  0\space )$
- etc.

On remarque que tout les vecteurs ont une  longeur de 8 car il y a 8 mots (taille de notre corpus)
Cette representation vectorielle des mot bien que très simple possède de nombreux desavantage:
- Taille des vecteurs (imaginons un corpus de la taille de wikipédia
- Absence de sens et de relation entre les mots

## 2)  L'hyposthèse de distribution [Harris, 1954],

En se basant sur l'hypothèse suivante:  **"les mots dans des contextes similaires ont des signiﬁcations semblables.**, de nombreuses nouvelle méthode sont apparues:
- **Méthode de Clustering:** On regroupe les mots en foction de leur contexte.

- **Representation distributionelle:** On se base sur des matrice de co-occurences de chaque mots.

- **Representation distribuée: Word embeddings** Les word embeddings constituent une projection des mots du vocabulaire dans un espace de faible dimension de manière à préserver les similarités sémantiques et syntaxiques. Chaque dimension représente une caractéristique latente du mot, qui peut capter des propriétés syntaxiques et sémantiques. Ces méthods plus récentes répose principalement sur les avancées du Deep Leanring et sur la comprehension des reseaux neuronaux. Ce sont actuelement les méthodes les plus performantes.

## 3) Word2Vec
Word2Vect est l'une des boites à outils les plus performante et les plus connues en terme de representation continue des mots. Nous allons essayer de comprendre plus précisement les  algorythmes présents dans cette boite à outils.
Il y a principalement deux archutectures neuronales présentent dans Word2Vec :

- **L'architecture CBOW** : prédit le mot courant en fonction du contexte. Ainsi dans cette algorithme l'idée est de maximiser la fonction de cout suivante:
$$
\begin{equation}
S=\frac{1}{N}\sum_{i=1}^{N}log p(m_i|m_{i-n}..mm_{i-1}m_{i+1}..m_{i+n})
\end{equation}
$$

- **Remarques:**
    - Avec N = Nombre de mot dans le corpus et n= la taille de la fenêtre ( nombre de mot utilisé pour définir le contexte)
    - Il est important de remarquer que dans ce modèle l'ordre des mots n'a pas d'importance.

- **L'architecure Skip Ngram** : prédit les mots en contexte en fonction du mot courant. Autrement dit c'est l'inverse de la fonction precedente. Cette fois ci nous voulons maximiser cette focntion:

$$
\begin{equation}
S=\frac{1}{N}\sum_{i=1}^{N}\space \sum_{-n<j<n \ne 0} log p(m_i|m_{i+j})
\end{equation}
$$

- **Il existe un troisième modèle Word2Vec-deps**: qui prend en compte la nature des relations entre les mots. Aisni, le corpus d'entrainement est labélisé avec  les relations syntaxiques entre les mots. Nous detaillrons ce modèle plus tard si nous l'utilions

## 4) une implementation simple
Dans cette partie nous nous proposons d'impleter simplement chacune des deux methodes.

##### Corpus et importation

```python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
```

```python
import tensorflow as tf
```

    C:\Users\Robin\Anaconda3\lib\site-packages\h5py\__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
      from ._conv import register_converters as _register_converters

```python

corpus_en = ['king is a strong man',
          'queen is a wise woman',
          'boy is a young man',
          'girl is a young woman',
          'prince is a young king',
          'princess is a young queen',
          'man is strong',
          'woman is pretty',
          'prince is a boy will be king',
          'princess is a girl will be queen']

corpus_fr = ['le roi est un homme fort',
            'la reine est une personne pleine de sagesse'
            'un garçon est un jeune homme'
            'une fille est une jeune femme'
            'homme est fort'
            'la femme est jolie'
            'le prince est un garçon qui deviendra roi'
            'la princesse est une jeune fille qui deviendra renne']
```

##### Focntion preliminaire

```python
# Fonction qui élimines les mots inutiles ou bien trop frequent ou bien avec peu de signification
def remove_stop_words(corpus,langue='en'):
    if langue == 'en':
        stop_words = ['is', 'a', 'will', 'be']
    else :
        stop_words = ['le', 'la', 'un', 'une','est','qui','de']
    results = []
    for text in corpus:
        tmp = text.split(' ')
        for stop_word in stop_words:
            if stop_word in tmp:
                tmp.remove(stop_word)
        results.append(" ".join(tmp))

    return results

# Focntion qui créer un vecteur avec tout les mots du corpus

def create_words(corpus):
    words = []
    for text in corpus:
        for word in text.split(' '):
            words.append(word)

    words = set(words)
    return(words)

def neighbors(window_size,corpus):
    word2int = {}
    words = create_words(corpus)
    # On associe un nombre à chaque mots
    for i,word in enumerate(words):
        word2int[word] = i

    # On découpe notre corpus par phrase
    sentences = []
    for sentence in corpus:
        sentences.append(sentence.split())

    #Pour un mot donné nous créons sa liste de voisin
    data = []
    for sentence in sentences:
        for id, word in enumerate(sentence):
            for neighbor in sentence[max(id - window_size, 0) : min(id + window_size, len(sentence)) + 1] :
                if neighbor != word:
                    data.append([word, neighbor])

    df = pd.DataFrame(data, columns = ['entree', 'voisin'])
    return(word2int,df)
```

```python
clean_corpus = remove_stop_words(corpus_en)
word2int, df = neighbors(2,clean_corpus)

```

```python
df.head()
```

      entree
      voisin

      0
      king
      strong

      1
      king
      man

      2
      strong
      king

      3
      strong
      man

      4
      man
      king

```python
word2int
```

    {'wise': 0,
     'queen': 1,
     'strong': 2,
     'young': 3,
     'boy': 4,
     'prince': 5,
     'woman': 6,
     'girl': 7,
     'princess': 8,
     'pretty': 9,
     'king': 10,
     'man': 11}

### 4-1 CBOW

```python

```

### 4-2 Skip Gram

```python
# FOcntion pour transformer les mot en One-hot vecteurs

def to_one_hot_encoding(data_point_index,D):
    """
    data_point_index =  La representation des mots en entiers
    D = la dimension des vecteur en one-hot
    """
    one_hot_encoding = np.zeros(D)
    one_hot_encoding[data_point_index] = 1
    return one_hot_encoding

 :
    # Nous créons un tablea avec tout les mots
    words = create_words(corpus)

    # On definit la domension pour le codage one-hot
    D=len(words)

    # On associe un entier a chaque mot
    # On creer la liste de tuple à partir de la taille de la fenetre
    word2int, df = neighbors(window,corpus)

    # On creer les vecteurs encodé en one hot pour chaque mot et chaque voisin

    X = [] # Vecteurs des mots en entrée
    Y = [] # vecteurs des mots voisins

    for x, y in zip(df['entree'], df['voisin']):
        X.append(to_one_hot_encoding(word2int[ x ],D))
        Y.append(to_one_hot_encoding(word2int[ y ],D))

    # on les convertie en numpy array

    X_train = np.asarray(X)
    Y_train = np.asarray(Y)

    # nous creons les place_holder
    x = tf.placeholder(tf.float32, shape=(None, D))
    y_label = tf.placeholder(tf.float32, shape=(None, D))

    # On chosie la dimension dans lequel sera projecté nos mots
    D_final  = 2

    #
    # Couche cachée : qui representera les mots finalements
    # C'est en somme un produit mtricielle
    W1 = tf.Variable(tf.random_normal([D, D_final]))
    b1 = tf.Variable(tf.random_normal([1])) #bias
    hidden_layer = tf.add(tf.matmul(x,W1), b1)

    # Ccouche de sortie
    W2 = tf.Variable(tf.random_normal([D_final,D]))
    b2 = tf.Variable(tf.random_normal([1]))

    #Focntion softmax qui nous preojecte dans [0,1]
    prediction = tf.nn.softmax(tf.add( tf.matmul(hidden_layer, W2), b2))

    # On definit notre founction de cout comme l'entropie croisée
    cost = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(prediction), axis=[1]))

    # On définit l'operation d'entrainement qui est une descent de gradient optimisé
    # sur la fonction cout

    # training operation
    train_op = tf.train.GradientDescentOptimizer(0.05).minimize(cost)

    return(words,x,y_label,W1,b1,W2,b2,prediction,cost,train_op,X_train,Y_train)

```

```python
def training(nbIteration,clean_corpus):
    words,x,y_label,W1,b1,W2,b2,prediction,cost,train_op,X_train,Y_train= tensor_graph(clean_corpus,2)
    sess = tf.Session()
    init = tf.global_variables_initializer()
    sess.run(init)

    iteration = 1000
    for i in range(iteration):
        # Entrée :  X_train en encodage one-hot
        # Etiquettes : Les vosins de chaque mot en
        sess.run(train_op, feed_dict={x: X_train, y_label: Y_train})
        if i % 300 == 0:
            print('Iteration '+str(i)+""" l'erreur est de :""", sess.run(cost, feed_dict={x: X_train, y_label: Y_train}))

    vectors_1_en = sess.run(W1 + b1)
    vectors_2_en = sess.run(W2 + b2)
    return(words,vectors_1_en)

```

```python
clean_corpus_en = remove_stop_words(corpus_en)
words_en, vectors_1_en = training(10000,clean_corpus_en)

print("###########################################")

clean_corpus_en = remove_stop_words(corpus_fr,langue='fr')
words_fr,vectors_1_fr = training(10000,clean_corpus_en)
```

    Iteration 0 l'erreur est de : 4.3602915
    Iteration 300 l'erreur est de : 2.230297
    Iteration 600 l'erreur est de : 2.0680375
    Iteration 900 l'erreur est de : 1.9835137
    ###########################################
    Iteration 0 l'erreur est de : 3.7725234
    Iteration 300 l'erreur est de : 3.1615095
    Iteration 600 l'erreur est de : 2.9786558
    Iteration 900 l'erreur est de : 2.885068

```python
def data_frame(vectors_1_en,words):
    df_W2V_en = pd.DataFrame(vectors_1_en, columns = ['x1', 'x2'])
    df_W2V_en['mots'] = words
    df_W2V_en = df_W2V_en[['mots', 'x1', 'x2']]
    return(df_W2V_en)
```

```python
df_W2V_en =  data_frame(vectors_1_en,words_en)
df_W2V_fr = data_frame(vectors_1_fr,words_fr)

```

```python

def plotGraphWord(df_W2V_en,vectors_1_en):

    plt.figure(figsize =(10,10))
    fig, ax = plt.subplots()

    for word, x1, x2 in zip(df_W2V_en['mots'], df_W2V_en['x1'], df_W2V_en['x2']):
        ax.annotate(word, (x1,x2 ))

    PADDING = 1.0
    x_axis_min = np.amin(vectors_1_en, axis=0)[0] - PADDING
    y_axis_min = np.amin(vectors_1_en, axis=0)[1] - PADDING
    x_axis_max = np.amax(vectors_1_en, axis=0)[0] + PADDING
    y_axis_max = np.amax(vectors_1_en, axis=0)[1] + PADDING

    plt.xlim(x_axis_min,x_axis_max)
    plt.ylim(y_axis_min,y_axis_max)
    plt.rcParams["figure.figsize"] = (10,10)

    plt.show()

```

```python
plotGraphWord(df_W2V_fr,vectors_1_fr)
plotGraphWord(df_W2V_en,vectors_1_en)
```

```python

```


# Liver analysis

**Author**: Alma Andersson

**Description**:

Joint spatial analysis of the entire dataset. The analysis is separated into three parts

- Data Loading and processing
- Feature by distance analysis
- Classification of structures

### Data loading and processing
A brief section loading and processing the data.

### Feature by distance analysis
Here, expression as a function of distance (_feature by distance_) is exemplified, as well as the slightly modified variant where the log-ratio between distances to two different structure classes (here portal and central veins) is examined.

### Classification
This section illustrates how the type of a vein can be predicted based on its neighborhood expression profile (NEP). Included in the analysis is the creation of NEPs, a two step procedure where a neighborhood is first identified, from which a weighted gene expression (by distance) is then assembled. Once the NEPs are formed, a _logistic regression_ model is trained to predict vein type based on the NEP. Cross validation is also implemented to assess performance.

### Load libraries

Load data the necessary packages and data for the analysis, also specify certain constants which will be used throughout the analysis.

```python
%load_ext autoreload
%autoreload 2

```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

```python
import hepaquery as hq
import pandas as pd
import numpy as np
import anndata as ad

from skmisc.loess import loess

import os
import os.path as osp
from os import listdir

import matplotlib.pyplot as plt
from matplotlib import rcParams

from PIL import Image

from functools import reduce

from scipy.spatial.distance import cdist

from hepaquery.structures import VeinData, Model
import hepaquery.visual as viz
import hepaquery.utils as ut

import re
import datetime

from typing import Tuple

```

```python
print(hq.__path__)
```

    ['/Users/franziskahildebrandt/.local/lib/python3.8/site-packages/hepaquery-0.0.1-py3.8.egg/hepaquery']

```python
rcParams["figure.facecolor"] = "white"
```

### Global Variables

Here certain global variables are set, which will be used to save output and load data. A brief explanation is provided below:

- TAG : Unique identifier for the analysis. All results will be saved in a folder called "TAG-analysis" within the res-folder.
- REPO\_DIR : Path to the repository directory, only change this if files are moved around.
- DATA_DIR : Path to the data being loaded.
- GENE_LIST_DIR : Directory to lists of genes
- SCALE_FACTOR : Scaling factor between pixels to $\mu m$

```python

TAG = re.sub(":| |-|\\.|","",str(datetime.datetime.today()))
TAG = TAG + "-analysis-spp"

REPO_DIR = osp.dirname(osp.abspath(os.getcwd()))
DATA_DIR = osp.join("/Users/franziskahildebrandt/Desktop/PhDprojectFranziskaHildebrandt/ST-liver/uninfectedLiver-sequenced/ankarliver/data/h5ad-cca")
RESULTS_DIR = osp.join(REPO_DIR,"res/bivariate_res/ans_4",TAG)

if not osp.exists(RESULTS_DIR):
    os.mkdir(RESULTS_DIR)

GENE_LIST_DIR = osp.join(REPO_DIR,"data/gene-lists/ans_4")

SCALE_FACTOR = 2.8

SAVE_RESULTS = True

```

```python
# Get data paths
PTHS = list(filter(lambda x: x.endswith("h5ad"),os.listdir(DATA_DIR)))
PTHS = {p:osp.join(DATA_DIR,p) for p in PTHS }

data_set = {n:ad.read_h5ad(p) for n,p in PTHS.items()}
new_names = {k:d.uns["sample"] + "-" + d.uns["replicate"] for k,d in data_set.items()}
data_set = dict((new_names[k],d) for k,d in data_set.items())

data_set
```

    {'CN65-E1': AnnData object with n_obs × n_vars = 590 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN65-C1': AnnData object with n_obs × n_vars = 647 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN16-E1': AnnData object with n_obs × n_vars = 1348 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN73-E2': AnnData object with n_obs × n_vars = 650 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN65-D1': AnnData object with n_obs × n_vars = 663 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN16-D1': AnnData object with n_obs × n_vars = 1262 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN73-C1': AnnData object with n_obs × n_vars = 673 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN16-D2': AnnData object with n_obs × n_vars = 487 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN65-D2': AnnData object with n_obs × n_vars = 629 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN16-E2': AnnData object with n_obs × n_vars = 487 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN65-C2': AnnData object with n_obs × n_vars = 626 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN73-D1': AnnData object with n_obs × n_vars = 684 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances'}

```python
exclude = ["CN65-C1","CN65-C2","CN16-D1", "CN16-E1"]

for ex in exclude:
    data_set.pop(ex)

data_set
```

    {'CN65-E1': AnnData object with n_obs × n_vars = 590 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN73-E2': AnnData object with n_obs × n_vars = 650 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN65-D1': AnnData object with n_obs × n_vars = 663 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN73-C1': AnnData object with n_obs × n_vars = 673 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN16-D2': AnnData object with n_obs × n_vars = 487 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN65-D2': AnnData object with n_obs × n_vars = 629 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN16-E2': AnnData object with n_obs × n_vars = 487 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances',
     'CN73-D1': AnnData object with n_obs × n_vars = 684 × 9655
         obs: '_x', '_y', 'x', 'y', 'sample', 'replicate'
         var: 'gene'
         uns: 'img', 'mask', 'replicate', 'sample'
         obsm: 'vein_distances'}

```python
genes = ut.load_genelist(GENE_LIST_DIR,
                         filter_tag = "Spp",
                        )
```

```python
genes
```

    {'Spp1': ['Spp1'], 'all': ['Spp1']}

Visualize the data to make sure everything looks as expected

```python
n_genes = len(genes["all"])

fig,ax  = viz.get_figure(n_elements=n_genes,n_cols = 5,side_size=5)

ax = ax.flatten()
alpha = 0.05

likelihoods = dict()

plt_idx = 0
for ii,gene in enumerate(genes["all"]):
    ys = []
    xs = []
    THRS = 143

    for k,data in enumerate(data_set.values()):
        if not gene in data.var.index.values:
            continue
        _y = data.obs_vector(gene)
        _x = data.obsm["vein_distances"][["dist_type_central","dist_type_portal"]].values
        keep = np.max(_x,axis=1) < THRS
        _x = _x[keep,:]
        _y = _y[keep]
        ys.append(_y)
        xs.append(_x)

    if len(xs)> 0:
        ys = np.concatenate(ys)
        xs = np.concatenate(xs,axis=0)
        xs = np.hstack((np.ones((xs.shape[0],1)),xs))

        ll = viz.bivariate_expression_plot(ax[plt_idx],
                                           data = [xs,ys],
                                           feature = gene,
                                           feature_name = "Gene",
                                           cmap = plt.cm.magma,
                                           distance_scale_factor = 2.8,
                                          )

        likelihoods[gene] = ll
        plt_idx += 1

fig.tight_layout()
if SAVE_RESULTS:
    fig.savefig(osp.join(RESULTS_DIR,"feature-by-distance-ratio.svg"),
                facecolor = "white", dpi = 600)
```

## Likelihood ratio test

If we have a _full model_ and one _reduced model_, where the second is nested in the former we can conduct a so called likelihood ratio test (LRT) to determine whether the full model significantly improves the model or not (if not we use the simpler model).

The LRT is executed as follows:

Let $\mathcal{L}_{reduced}$ and $\mathcal{L}_{full}$ be the likelihood values of each model, and let:

\begin{equation}
    D = -2\times \log(\frac{\mathcal{L}_{reduced}}{\mathcal{L}_{full}})
\end{equation}

then $D \sim \mathcal{X}^2(\delta)$ where $\delta$ is the number the difference in number of parameters between the full and reduced model.

By computing the CDF for a given value $D$ and subtracting this from 1, we get the probability of observing a value equally large or more extreme (p-value). If the p-value is below a certain significance threshold $\alpha$ we use the full model does a better job explaining the data (accounting for the extra parameters).

In this example we test two reduced models, one where the portal respectively central distance is removed. Whenever we get a p-value larger than $\alpha$ this means that the covariate (distance) that we removed did not add to the reduced model.

Source : https://www.itl.nist.gov/div898/handbook/apr/section2/apr233.htm

```python
likelihoods

```

    {'Hsd11b1': array([-168.83181243, -178.38193909, -177.31436719]),
     'Ly6e': array([-578.23977685, -583.80882075, -579.20622452]),
     'Fgfr2': array([-254.88037274, -255.64625152, -254.9045114 ]),
     'Rspo3': array([-70.36631966, -71.32776032, -80.11368227]),
     'Lifr': array([-483.59842281, -487.43952699, -486.11744982]),
     'Dcn': array([-551.29545849, -552.13056255, -552.84158347])}

```python
model_eval = ut.likelihood_ratio_test(likelihoods,
                                      dofs = [1,1,2],
                                      included_covariates=["central","portal"."intercept"],
                                     )

if SAVE_RESULTS:
    model_eval.to_csv(osp.join(RESULTS_DIR,"likelihood-ratio-test-results.tsv"),
                      sep = "\t",
                     )

```

```python
model_eval
```

      covariates_central
      covariates_portal
      [full_model]_superior_to_[central_model]
      [full_model]_superior_to_[portal_model]

      Hsd11b1
      0.000012
      0.000038
      True
      True

      Ly6e
      0.000846
      0.164442
      True
      False

      Fgfr2
      0.215849
      0.826088
      False
      False

      Rspo3
      0.165539
      0.000010
      False
      True

      Lifr
      0.005577
      0.024796
      True
      True

      Dcn
      0.196231
      0.078666
      False
      False

```python

```