# Tutorial 1b: Data frames

(c) 2017 Justin Bois. This work is licensed under a [Creative Commons Attribution License CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). All code contained herein is licensed under an [MIT license](https://opensource.org/licenses/MIT).

```python
import numpy as np
import pandas as pd

import bokeh.io
import bokeh.plotting

from bokeh.models import Legend
from bokeh.plotting import figure, show, output_file

bokeh.io.output_notebook()
```

    Loading BokehJS ...

In this tutorial, we will learn how to load data stored on disk into a Python data structure. We will use Pandas to read in CSV (comma separated value) files and store the results in the very handy Pandas DataFrame. This incredibly flexible and powerful data structure will be a centerpiece in the rest of this course and beyond. In this tutorial, we will learn about what a data frame is and how to use it.

The data set we will use comes from a fun paper about the adhesive properties of frog tongues. The reference is Kleinteich and Gorb, Tongue adhesion in the horned frog Ceratophrys sp., *Sci. Rep.*, 4, 5225, 2014.

## The data file
The data are contained in the file `frog_tongue_adhesion.csv`. Let's look at its contents:

```python
with open('data/frog_tongue_adhesion.csv', 'r') as f:
    for _ in range(20):
        print(next(f), end='')
```

    # These data are from the paper,
    #   Kleinteich and Gorb, Sci. Rep., 4, 5225, 2014.
    # It was featured in the New York Times.
    #    http://www.nytimes.com/2014/08/25/science/a-frog-thats-a-living-breathing-pac-man.html
    #
    # The authors included the data in their supplemental information.
    #
    # Importantly, the ID refers to the identifites of the frogs they tested.
    #   I:   adult, 63 mm snout-vent-length (SVL) and 63.1 g body weight,
    #        Ceratophrys cranwelli crossed with Ceratophrys cornuta
    #   II:  adult, 70 mm SVL and 72.7 g body weight,
    #        Ceratophrys cranwelli crossed with Ceratophrys cornuta
    #   III: juvenile, 28 mm SVL and 12.7 g body weight, Ceratophrys cranwelli
    #   IV:  juvenile, 31 mm SVL and 12.7 g body weight, Ceratophrys cranwelli
    date,ID,trial number,impact force (mN),impact time (ms),impact force / body weight,adhesive force (mN),time frog pulls on target (ms),adhesive force / body weight,adhesive impulse (N-s),total contact area (mm2),contact area without mucus (mm2),contact area with mucus / contact area without mucus,contact pressure (Pa),adhesive strength (Pa)
    2013_02_26,I,3,1205,46,1.95,-785,884,1.27,-0.290,387,70,0.82,3117,-2030
    2013_02_26,I,4,2527,44,4.08,-983,248,1.59,-0.181,101,94,0.07,24923,-9695
    2013_03_01,I,1,1745,34,2.82,-850,211,1.37,-0.157,83,79,0.05,21020,-10239
    2013_03_01,I,2,1556,41,2.51,-455,1025,0.74,-0.170,330,158,0.52,4718,-1381
    2013_03_01,I,3,493,36,0.80,-974,499,1.57,-0.423,245,216,0.12,2012,-3975

## Loading a data set
We use `pd.read_csv()` to load the data set. The data are stored in a **DataFrame**. Let's load a `DataFrame`

```python
df = pd.read_csv('data/frog_tongue_adhesion.csv', comment='#')

# Look at the DataFrame
df.head()
```

      date
      ID
      trial number
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      contact area with mucus / contact area without mucus
      contact pressure (Pa)
      adhesive strength (Pa)

      0
      2013_02_26
      I
      3
      1205
      46
      1.95
      -785
      884
      1.27
      -0.290
      387
      70
      0.82
      3117
      -2030

      1
      2013_02_26
      I
      4
      2527
      44
      4.08
      -983
      248
      1.59
      -0.181
      101
      94
      0.07
      24923
      -9695

      2
      2013_03_01
      I
      1
      1745
      34
      2.82
      -850
      211
      1.37
      -0.157
      83
      79
      0.05
      21020
      -10239

      3
      2013_03_01
      I
      2
      1556
      41
      2.51
      -455
      1025
      0.74
      -0.170
      330
      158
      0.52
      4718
      -1381

      4
      2013_03_01
      I
      3
      493
      36
      0.80
      -974
      499
      1.57
      -0.423
      245
      216
      0.12
      2012
      -3975

```python
# We can access a column of data (slice the data) using the column name
df.head()['impact force (mN)']
```

    0    1205
    1    2527
    2    1745
    3    1556
    4     493
    Name: impact force (mN), dtype: int64

Clearly, the indexing of the rows is preserved. The data were interpreted as integer type (`dtype = int64`), so we want to convert them into floats using `.astype()` method.

```python
# Use df.astype() method to convert it to a NumPy float 64 data type.
df['impact force (mN)'] = df['impact force (mN)'].astype(float)

# Let's check if it worked well
df['impact force (mN)'].dtype
```

    dtype('float64')

Now let's select only the specific impact forces, say, with the impact force above two Newtons. Pandas `DataFrame` can be conveniently sliced with the Booleans.

```python
# Generate True/False array of rows for indexing
inds = df['impact force (mN)'] >= 2000.0

# Take a look
inds.head()
```

    0    False
    1     True
    2    False
    3    False
    4    False
    Name: impact force (mN), dtype: bool

Now we have an array of Booleans that is of the same length as the `DataFrame` itself. Now, we can use the `.loc` featore of a `DataFrame` to slice what we want out of the `DataFrame`.

```python
# Slice out rows we want (with big force)
df_big_force = df.loc[inds,:]

# Let's look at the sliced rows;
# there will be just a couple of high-force valuea
df_big_force
```

      date
      ID
      trial number
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      contact area with mucus / contact area without mucus
      contact pressure (Pa)
      adhesive strength (Pa)

      1
      2013_02_26
      I
      4
      2527.0
      44
      4.08
      -983
      248
      1.59
      -0.181
      101
      94
      0.07
      24923
      -9695

      5
      2013_03_01
      I
      4
      2276.0
      31
      3.68
      -592
      969
      0.96
      -0.176
      341
      106
      0.69
      6676
      -1737

      8
      2013_03_05
      I
      3
      2641.0
      50
      4.27
      -690
      491
      1.12
      -0.239
      269
      224
      0.17
      9824
      -2568

      17
      2013_03_15
      I
      3
      2032.0
      60
      3.28
      -652
      486
      1.05
      -0.257
      147
      134
      0.09
      13784
      -4425

Using `.loc` allows us to index by row and column. We chose all columns (using `:`) and put an array of Booleans for the rows. We get back a `DataFrame` with the rows associated with `True` in the Booleans' array.

Now we only have the strikes of high force. It can be noted that the original indexing of rowas was retained. This is a good idea, as the indices do not have to be integers.

    To overcome this, we can use `iloc` attribute of a `DataFrame`, which give the indexing with sequential integers.

```python
# Indexing using iloc, which enables indexing by the corresponding
# integer sequence
df_big_force['impact force (mN)'].iloc[3]
```

    2032.0

## Tidy data
The data in our `DataFrame` are tidy. This concept comes from the development of databases, but has been generalized to data processing in the recent years. Tidy data refers to data in a tabular form with the following format:

1. Each variable forms a column,
2. Each observation forms a row.
3. Each type of observational unit forms a separate table.

## More data extraction
To extract a single observation (i. e. a single experiment), we can extract a row and see all of the measured quantities from a given strike using `.loc`.

```python
# Slice out experiment with index 42
df.loc[42,:]
```

    date                                                    2013_05_27
    ID                                                             III
    trial number                                                     3
    impact force (mN)                                              324
    impact time (ms)                                               105
    impact force / body weight                                    2.61
    adhesive force (mN)                                           -172
    time frog pulls on target (ms)                                 619
    adhesive force / body weight                                  1.38
    adhesive impulse (N-s)                                      -0.079
    total contact area (mm2)                                        55
    contact area without mucus (mm2)                                23
    contact area with mucus / contact area without mucus          0.37
    contact pressure (Pa)                                         5946
    adhesive strength (Pa)                                       -3149
    Name: 42, dtype: object

`DataFrame` conveniently arranges the indices describing the elements of the row as column headings. This is a Pandas `Series` object.

Slicing out a single index is not very meaningful, because the indices are arbitrary. We can instead look at our lab notebook and look at a trial number 3 on May 27, 2013, of frog III. This is a more common and indeed meaningful, use case.

```python
# Set up Boolean slicing
date = df['date'] == '2013_05_27'
trial = df['trial number'] == 3
ID = df['ID'] == 'III'

# Slice out the row
df.loc[date & trial & ID]
```

      date
      ID
      trial number
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      contact area with mucus / contact area without mucus
      contact pressure (Pa)
      adhesive strength (Pa)

      42
      2013_05_27
      III
      3
      324.0
      105
      2.61
      -172
      619
      1.38
      -0.079
      55
      23
      0.37
      5946
      -3149

The difference is that the returned object (slice) is a `DataFrame` instead of `Series`. We can easily make it a `Series` object using `iloc`. Note that in most use cases, this is jsut a matter of convenience for viewing and nothing more.

```python
df.loc[date & trial & ID, :].iloc[0]
```

    date                                                    2013_05_27
    ID                                                             III
    trial number                                                     3
    impact force (mN)                                              324
    impact time (ms)                                               105
    impact force / body weight                                    2.61
    adhesive force (mN)                                           -172
    time frog pulls on target (ms)                                 619
    adhesive force / body weight                                  1.38
    adhesive impulse (N-s)                                      -0.079
    total contact area (mm2)                                        55
    contact area without mucus (mm2)                                23
    contact area with mucus / contact area without mucus          0.37
    contact pressure (Pa)                                         5946
    adhesive strength (Pa)                                       -3149
    Name: 42, dtype: object

## Renaming columns
The lengthy syntax of access column names can be annoying, so it can be useful to change the names of the columns. For instance, now, to access the ratio of contact area with mucus to contact area without mucus, we would have to do following.

```python
# Set up criteria for our search in the DataFrame
date = df['date'] == '2013_05_27'
trial = df['trial number'] == 3
ID = df['ID'] == 'III'

# When indexind DataFrames, use & for Boolean and (and / for or; - for not)
df.loc[date & trial & ID, 'contact area with mucus / \
contact area without mucus']

```

    42    0.37
    Name: contact area with mucus / contact area without mucus, dtype: float64

The reason to use the verbose nature of the column headings is to avoid ambiguity. Also, many plotting packages, including *HoloViews* can automatically label axes based on headers in `DataFrame`s. To use shorter names, we do this.

```python
# Make a dictionary to rename columns
rename_dict = {'trial number' : 'trial',
               'contact area with mucus / contact area without mucus' :
               'ca_ratio'}

# Rename the columns
df = df.rename(columns=rename_dict)

# Try out the new column name
df.loc[date & trial & ID, 'ca_ratio']

# Indexing of dictionaries looks syntactically similar to cols in DataFrames
rename_dict['trial number']
```

    'trial'

## Computing with DataFrames and adding columns
The `DataFrame`s are convenient for the organization and selection of data, but how about computations and storing the results of computations in the new columns?

As a simple example, let's say we want to have a column with the impact force i nunits of Newtons instead of milliNewtons, so we can divide `impact force (mN)` columns elementwise by 1000, just as with Numpy arrays.

```python
# Add a new columns with impact force in units of Newtons
df['impact force (N)'] = df['impact force (mN)'] / 1000

# Take a look
df.head()
```

      date
      ID
      trial
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      ca_ratio
      contact pressure (Pa)
      adhesive strength (Pa)
      impact force (N)

      0
      2013_02_26
      I
      3
      1205.0
      46
      1.95
      -785
      884
      1.27
      -0.290
      387
      70
      0.82
      3117
      -2030
      1.205

      1
      2013_02_26
      I
      4
      2527.0
      44
      4.08
      -983
      248
      1.59
      -0.181
      101
      94
      0.07
      24923
      -9695
      2.527

      2
      2013_03_01
      I
      1
      1745.0
      34
      2.82
      -850
      211
      1.37
      -0.157
      83
      79
      0.05
      21020
      -10239
      1.745

      3
      2013_03_01
      I
      2
      1556.0
      41
      2.51
      -455
      1025
      0.74
      -0.170
      330
      158
      0.52
      4718
      -1381
      1.556

      4
      2013_03_01
      I
      3
      493.0
      36
      0.80
      -974
      499
      1.57
      -0.423
      245
      216
      0.12
      2012
      -3975
      0.493

The new column was created by assigning it to Pandas `Series` we calculated.

We can do other calculations on `DataFrame`s besides elementwise calculations. For example, if we wanted the mean impact force in units of milliNewtons, we can do this...

```python
df['impact force (mN)'].mean()
```

    801.6875

To compute all sorts of useful summary statistics all at once about the `DataFrame`, we can use `describe()` method, which gives the count, mean, standard deviation, minimum, 25th percentile, median, 75th percentiel, and maximum for each column.

```python
df.describe()
```

      trial
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      ca_ratio
      contact pressure (Pa)
      adhesive strength (Pa)
      impact force (N)

      count
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000
      80.000000

      mean
      2.400000
      801.687500
      39.062500
      2.920375
      -397.762500
      1132.450000
      1.444875
      -0.187462
      166.475000
      61.400000
      0.569000
      6073.162500
      -3005.875000
      0.801687

      std
      1.164887
      587.836143
      19.558639
      1.581092
      228.675562
      747.172695
      0.659858
      0.134058
      98.696052
      58.532821
      0.341862
      5515.265706
      2525.468421
      0.587836

      min
      1.000000
      22.000000
      6.000000
      0.170000
      -983.000000
      189.000000
      0.220000
      -0.768000
      19.000000
      0.000000
      0.010000
      397.000000
      -17652.000000
      0.022000

      25%
      1.000000
      456.000000
      29.750000
      1.470000
      -567.750000
      682.250000
      0.990000
      -0.277250
      104.750000
      16.750000
      0.280000
      2579.250000
      -3443.250000
      0.456000

      50%
      2.000000
      601.000000
      34.000000
      3.030000
      -335.000000
      927.000000
      1.320000
      -0.165000
      134.500000
      43.000000
      0.665000
      4678.000000
      -2185.500000
      0.601000

      75%
      3.000000
      1005.000000
      42.000000
      4.277500
      -224.500000
      1381.250000
      1.772500
      -0.081250
      238.250000
      92.500000
      0.885000
      7249.750000
      -1736.000000
      1.005000

      max
      5.000000
      2641.000000
      143.000000
      6.490000
      -92.000000
      4251.000000
      3.400000
      -0.001000
      455.000000
      260.000000
      1.000000
      28641.000000
      -678.000000
      2.641000

Now let's say we want to compute the mean impact force of each frog. We can combine Boolean indexing with looping to do that.

```python
for frog in df['ID'].unique():
    inds = df['ID'] == frog
    mean_impact_force = df.loc[inds, 'impact force (mN)'].mean()
    print('{0:s}: {1:.1f} mN'.format(frog, mean_impact_force))
```

    I: 1530.2 mN
    II: 707.4 mN
    III: 550.1 mN
    IV: 419.1 mN

We used the `.unique()` method to get the unique entries in the `ID` column. We can also use Panda's `.groupby()` function to do this much more cleanly and efficiently, but for now, it is good to appreciate the ability to pull out the data needed and do computations with it.

## Creating a DataFrame from scratch
Let's construct our own `DataFrame` from scratch, which would contain information about each frog.

One way to do this is to first create a dictionary with the respective fields, and then convert it into a `DataFrame` by instantiating a `pd.DataFrame` class with it.

```python
# Create a dictionary with the appropriate fields
data_dict = {'ID': ['I', 'II', 'III', 'IV'],
             'age': ['adult', 'adult', 'juvenile', 'juvenile'],
             ' SVL (mm)': [63, 70, 28, 31],
             'weight (g)': [63.1, 72.7, 12.7, 12.7],
             'species': ['cross', 'cross', 'cranwelli', 'cranwelli']}

# Make it into a DataFrame
df_frog_info = pd.DataFrame(data=data_dict)

# Take a look
df_frog_info

```

      ID
      age
      SVL (mm)
      weight (g)
      species

      0
      I
      adult
      63
      63.1
      cross

      1
      II
      adult
      70
      72.7
      cross

      2
      III
      juvenile
      28
      12.7
      cranwelli

      3
      IV
      juvenile
      31
      12.7
      cranwelli

In some instances, the data sets are not small enough to construct a dictionary by hand. Oftentimes, we have a two-dimensional array of data that we want to make into a `DataFrame`. For instance, we can have a Numpy array with two columns, one for snout-vent length and one for the weight.

```python
data = np.array([[63, 70, 28, 31], \
                 [63.1, 72.7, 12.7, 12.7]]).transpose()

# Let's verify this
data
```

    array([[63. , 63.1],
           [70. , 72.7],
           [28. , 12.7],
           [31. , 12.7]])

To turn the data above into `DataFrame`, we specify the `column` keyword argument too.

```python
df_demo = pd.DataFrame(data=data, columns=['SVL (mm)', 'weight (g)'])

# Take a look
df_demo
```

      SVL (mm)
      weight (g)

      0
      63.0
      63.1

      1
      70.0
      72.7

      2
      28.0
      12.7

      3
      31.0
      12.7

In general, any two-dimensional Numpy array can be converted into a `DataFrame` in this way. We only need to supply column names.

## Merging DataFrames
For each row in the `DataFrame` we can add the relevant value in each column. We will do these operations on a copy of `df` using the `copy()` method.

```python
# Make a copy of df
df_copy = df.copy()

# Build each column
for col in df_frog_info.columns[df_frog_info.columns != 'ID']:
    # Make a new column with empty values
    df_copy[col] = np.empty(len(df_copy))

    # Add in each entry, row by row
    for i, r in df_copy.iterrows():
        ind = df_frog_info['ID'] == r['ID']
        df_copy.loc[i, col] = df_frog_info.loc[ind, col].iloc[0]

# Take a look at the updated DataFrame
df_copy.head()
```

      date
      ID
      trial
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      ca_ratio
      contact pressure (Pa)
      adhesive strength (Pa)
      impact force (N)
      age
      SVL (mm)
      weight (g)
      species

      0
      2013_02_26
      I
      3
      1205.0
      46
      1.95
      -785
      884
      1.27
      -0.290
      387
      70
      0.82
      3117
      -2030
      1.205
      adult
      63.0
      63.1
      cross

      1
      2013_02_26
      I
      4
      2527.0
      44
      4.08
      -983
      248
      1.59
      -0.181
      101
      94
      0.07
      24923
      -9695
      2.527
      adult
      63.0
      63.1
      cross

      2
      2013_03_01
      I
      1
      1745.0
      34
      2.82
      -850
      211
      1.37
      -0.157
      83
      79
      0.05
      21020
      -10239
      1.745
      adult
      63.0
      63.1
      cross

      3
      2013_03_01
      I
      2
      1556.0
      41
      2.51
      -455
      1025
      0.74
      -0.170
      330
      158
      0.52
      4718
      -1381
      1.556
      adult
      63.0
      63.1
      cross

      4
      2013_03_01
      I
      3
      493.0
      36
      0.80
      -974
      499
      1.57
      -0.423
      245
      216
      0.12
      2012
      -3975
      0.493
      adult
      63.0
      63.1
      cross

We used the `iterrows()` method of the `df_copy` data frame. The iterator gives an index (which we called `i`) and a row of a `DataFrame` (which we called `r`). This methods, and the analogous one for iterating over columns, `iteritems()`, can be useful.

However, much better method is to use the Pandas's built-in `merge()` method. Called with all the default kwargs, this function finds a common columns between two `DataFramee`S (in this case, the `ID` column), and then uses those columns to merge them ,filling in values that amtch in the common column. This is exactly what we want.

```python
df = df.merge(df_frog_info)

# Check it
df.head()
```

      date
      ID
      trial
      impact force (mN)
      impact time (ms)
      impact force / body weight
      adhesive force (mN)
      time frog pulls on target (ms)
      adhesive force / body weight
      adhesive impulse (N-s)
      total contact area (mm2)
      contact area without mucus (mm2)
      ca_ratio
      contact pressure (Pa)
      adhesive strength (Pa)
      impact force (N)
      age
      SVL (mm)
      weight (g)
      species

      0
      2013_02_26
      I
      3
      1205.0
      46
      1.95
      -785
      884
      1.27
      -0.290
      387
      70
      0.82
      3117
      -2030
      1.205
      adult
      63
      63.1
      cross

      1
      2013_02_26
      I
      4
      2527.0
      44
      4.08
      -983
      248
      1.59
      -0.181
      101
      94
      0.07
      24923
      -9695
      2.527
      adult
      63
      63.1
      cross

      2
      2013_03_01
      I
      1
      1745.0
      34
      2.82
      -850
      211
      1.37
      -0.157
      83
      79
      0.05
      21020
      -10239
      1.745
      adult
      63
      63.1
      cross

      3
      2013_03_01
      I
      2
      1556.0
      41
      2.51
      -455
      1025
      0.74
      -0.170
      330
      158
      0.52
      4718
      -1381
      1.556
      adult
      63
      63.1
      cross

      4
      2013_03_01
      I
      3
      493.0
      36
      0.80
      -974
      499
      1.57
      -0.423
      245
      216
      0.12
      2012
      -3975
      0.493
      adult
      63
      63.1
      cross

## Plotting how impact forces correlate with other metrics
Let's say that we want to know how the impact forces correlate with other measures about the impact. Let's make a scatter plot of adhesion forces vs. impact forces.

```python
# Set up the plot
p = bokeh.plotting.figure(plot_height=300,
                          plot_width=500,
                          x_axis_label='impact force (mN)',
                          y_axis_label='adhesive force (mN)')

# Add a scatter plot
p.circle(df['impact force (mN)'], df['adhesive force (mN)'])

bokeh.io.show(p)
```

We can see some correlation between the adhesive forces and the impact forces. The stronger the impact force, the stronger the adhesive force, with some exceptions.

## Making subplots
Now let's learn how to make subplots in Bokeh. Let's say we want to plot the adhesive force, total contact area, impact time, and contact pressure against impact force. To lay these out as subplots, we make the respective `bokeh.plotting.figure.Figure` objects one by one and put them in a list. We then use the utilities from `bokeh.layouts` to make the subplots. Let's start by making the list of figure objects. It will help to wirte a quick function to generate a plot.

```python
def scatter(df, x, y, p=None, color='#30a2da',
            plot_height=300, plot_width=500):
    """Populate a figure with a scatter plot."""
    p = bokeh.plotting.figure(plot_height=plot_height,
                              plot_width=plot_width,
                              x_axis_label=x,
                              y_axis_label=y)

    p.circle(df[x], df[y], color=color)

    return p
```

Now we can build a list of plots

```python
# To be plotted
cols = ['impact time (ms)',
        'adhesive force (mN)',
        'total contact area (mm2)',
        'contact pressure (Pa)']

plots = []
for col in cols:
    plots.append(scatter(df,
                         'impact force (mN)',
                         col,
                         plot_height=200,
                         plot_width=400))

# Line up the plots in a column
bokeh.io.show(bokeh.layouts.column(plots))
```

There are some things that are to be cleared:

* Plots share the same x-axis, so we want to link the axes. This is accomplisehed by setting the `x_range` attribute of the respective `bokeh.plotting.figure.Figure` objects.
* All the x-axes are the same, so we do not need to label them all.

```python
for p in plots[:-1]:
    # Set each plot's x_range to be that of the last plot
    p.x_range = plots[-1].x_range

    # Only have x_axis label on bottom plot
    p.xaxis.axis_label = None

# To show only a single toolbar for a set of subplots, we should do this
bokeh.io.show(bokeh.layouts.gridplot(plots, ncols=1))
```

To arrange the plots in a 2x2 gird, we use ncols=2 as the kwarg. However, we have to supply an axis label for the plot in the lower left corner.

```python
plots[2].xaxis.axis_label = 'impact force (mN)'
bokeh.io.show(bokeh.layouts.gridplot(plots, ncols=2))
```

To have the blank spaces on the two-dimensional lsit of plots, we can use `bokeh.layouts.gridplot()` again. Let's leave out the contact pressure plot

```python
# Make 2D list of plots (put None for no plot)
plots = [[plots[0], plots[1]],
         [plots[2], None]]

# Show using gridplot without an ncols kwatg
bokeh.io.show(bokeh.layouts.gridplot(plots))
```

## Plotting the distribution of impact forces
To simply plot the distribution of impact forces, we can do a couple of things. Probably the most familiar thing is to plot them as a histogram. To do that with Bokeh, we can first compute the edges and heights of the bars of the histogram using Numpy, and then add them to the plot using the `quad()` method of Bokeh figures, which plots the filled rectangles.

```python
# Compute the histogram
heights, edges = np.histogram(df['impact force (mN)'])

# Set up the plot
p = bokeh.plotting.figure(plot_height=300,
                          plot_width=500,
                          x_axis_label='impact force (mN)',
                          y_axis_label='count')

# Generate the histogram
p.quad(top=heights, bottom=0, left=edges[:-1], right=edges[1:])

bokeh.io.show(p)
```

From looking at the histogram, there might be some bimodalidy. However, there is a better way to visualize the data - use of ECDF...
## Empirical cumulative distribution functions (ECDFs)
The primary reason why histogram might not be the best way to display the distributions of measurements is the **binning biais**. To create a histogram, we necessarily have to consider not the exact values of the measurements, but rather place them in bins. We do not plot all of the data, and the choice of bins can change what we can infer from the plot.

Instead, we should look at the **empirical cumulative distribution function** (or ECDF). The ECDF evaluated at $x$ for a set of measurements is defined as
\begin{align}
ECDF(x) = fraction \ of \ measurements \leq x
\end{align}
Let's write a function that will generate $x$ values of ECDFs and $y$ values of ECDFs.

```python
def ecdf(data):
    """Return the ECDF of the data."""
    x = np.sort(data)
    y = np.arange(1, len(data)+1) / len(data)

    return x, y
```

```python
# Get the values of the impact force for ECDF calculation
# For adult frogs
inds_ad = df['age'] == 'adult'
vals_ad = df.loc[inds_ad,'impact force (mN)']

# For juvenile frogs
inds_juv = df['age'] == 'juvenile'
vals_juv = df.loc[inds_juv,'impact force (mN)']

# Calculate the ECDFs for both adult and juvenile frogs
xad, yad = ecdf(vals_ad)
xjuv, yjuv = ecdf(vals_juv)

# Plot the ECDFs for the adult and juvenile frogs
# Set up the plot
p = bokeh.plotting.figure(plot_height=300,
                          plot_width=600,
                          x_axis_label='impact force (mN)',
                          y_axis_label='ECDF')

# Add a scatter plot
p = figure(toolbar_location="above")

p1 = p.circle(xad, yad, color='blue')
p2 = p.circle(xjuv, yjuv, color='red')

legend = Legend(items=[
    ('adult', [p1]),
    ('juvenile', [p2])
    ])
# Add legend
p.add_layout(legend)

p.xaxis.axis_label = 'impact force (mN)'
p.yaxis.axis_label = 'ECDF'

bokeh.io.show(p)
```

From the ECDF graph above, it is apparent that there is a difference in the impact forces between adult frogs and the juvenile frogs, explaining the slight bimodality of the histogram.
## Conclusions: what we learned?
* How to lead data from CSVfiles into Pandas `DataFrame`s.
* `DataFrame`s are useful objects for looking at data.


We will need to import some helper code, so we need to run this

```
import os
import sys
nb_dir = os.path.split(os.getcwd())[0]
if nb_dir not in sys.path:
    sys.path.append(nb_dir)
```

# Colab

We will need to download some data for this notebook, so if you are using [colab](https://colab.research.google.com), set the `using_colab` flag below to `True` in order to clone our [github repo](https://github.com/probabll/dgm4nlp).

```
using_colab = True
!ls
```

```
if using_colab:
  !rm -fr dgm4nlp sst
  !git clone https://github.com/probabll/dgm4nlp.git
  !cp -R dgm4nlp/notebooks/sst ./
  !ls
```

Now we can start our lab.

```
import torch
from torch import nn
# CPU should be fine for this lab
device = torch.device('cpu')
#device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
from collections import OrderedDict
import numpy as np
```

# Sentiment Classification

We are going to augment a sentiment classifier with a layer of discrete latent variables which will help us improve the model's interpretability. But first, let's quickly review the baseline task.

In sentiment classification, we have some text input $x = \langle x_1, \ldots, x_n \rangle$, e.g. a sentence or short paragraph, which expresses a certain sentiment $y$, i.e. one of $K$ classes, towards a subject (e.g. a film or a product).

We can learn a sentiment classifier by learning a categorical distribution over classes for a given input:

\begin{align}
Y|x &\sim \text{Cat}(f(x; \theta))
\end{align}

where the Categorical pmf is $\text{Cat}(y|\pi) = \pi_y$.

A categorical distribution over $K$ classes is parameterised by a $K$-dimensional probability vector, here we use a neural network $f$ to map from the input to this probability vector. Technically we say *a neural network parameterise our model*, that is, it computes the parameters of our categorical observation model. The figure below is a graphical depiction of the model: circled nodes are random variables (a shaded node is an observed variable), uncircled nodes are deterministic, a plate indicates multiple draws.

The neural network (NN) $f(\cdot; \theta)$ has parameters of its own, i.e. the weights of the various architecture blocks used, which we denoted generically by $\theta$.

Suppose we have a dataset $\mathcal D = \{(x^{(1)}, y^{(1)}), \ldots, (x^{(N)}, y^{(N)})\}$ containing $N$ i.i.d. observations. Then we can use the log-likelihood function
\begin{align}
\mathcal L(\theta|\mathcal D) &= \sum_{k=1}^{N} \log P(y^{(k)}|x^{(k)}, \theta) \\
&= \sum_{k=1}^{N} \log \text{Cat}(y^{(k)}|f(x^{(k)}; \theta))
\end{align}
 to estimate $\theta$ by maximisation:
 \begin{align}
 \theta^\star = \arg\max_{\theta \in \Theta} \mathcal L(\theta|\mathcal D) ~ .
 \end{align}

We can use stochastic gradient-ascent to find a local optimum of $\mathcal L(\theta|\mathcal D)$, which only requires a gradient estimate:

\begin{align}
\nabla_\theta \mathcal L(\theta|\mathcal D) &= \sum_{k=1}^{|\mathcal D|} \nabla_\theta  \log P(y^{(k)}|x^{(k)}, \theta) \\
&= \sum_{k=1}^{|\mathcal D|} \frac{1}{N} N \nabla_\theta  \log P(y^{(k)}|x^{(k)}, \theta)  \\
&= \mathbb E_{\mathcal U(1/N)} \left[ N \nabla_\theta  \log P(y^{(K)}|x^{(K)}, \theta) \right]  \\
&\overset{\text{MC}}{\approx} \frac{N}{M} \sum_{m=1}^M \nabla_\theta  \log P(y^{(k_m)}|x^{(k_m)}, \theta) \\
&\text{where }K_m \sim \mathcal U(1/N)
\end{align}

This is a Monte Carlo (MC) estimate of the gradient computed on $M$ data points selected uniformly at random from $\mathcal D$.

For as long as $f$ remains differentiable wrt to its inputs and parameters, we can rely on automatic differentiation to obtain gradient estimates.

In what follows we show how to design $f$ and how to extend this basic model to a latent-variable model.

## Data

We provide you some code to load the data (see `sst.sstutil.examplereader`). Play with the snippet below and inspect a few training instances:

```
from sst.sstutil import examplereader, Vocabulary, load_glove

# Let's load the data into memory.
print("Loading data")
train_data = list(examplereader('sst/data/sst/train.txt'))
dev_data = list(examplereader('sst/data/sst/dev.txt'))
test_data = list(examplereader('sst/data/sst/test.txt'))

print("train", len(train_data))
print("dev", len(dev_data))
print("test", len(test_data))

print('\n# Examples')
example = dev_data[0]
print("First dev example:", example)
print("First dev example tokens:", example.tokens)
print("First dev example label:", example.label)
```

## Architecture

The function $f$ conditions on a high-dimensional input (i.e. text), so we need to convert it to continuous real vectors. This is the job an *encoder*.

**Embedding Layer**

The first step is to convert the words in $x$ to vectors, which in this lab we will do with a pre-trained embedding layer (we will use GloVe).

We will denote the embedding of the $i$th word of the input by:

\begin{equation}
\mathbf x_i = \text{glove}(x_i)
\end{equation}

**Encoder Layer**

In this lab, an encoder takes a sequence of input vectors $\mathbf x_1^n$, each $I$-dimensional, and produces a sequence of output vectors $\mathbf t_1^n$, each $O$-dimensional and a summary vector $\mathbf h \in \mathbb R^O$:

\begin{equation}
    \mathbf t_1^n, \mathbf h = \text{encoder}(\mathbf x_1^n; \theta_{\text{enc}})
\end{equation}

where we use $\theta_{\text{enc}}$ to denote the subset of parameters in $\theta$ that are specific to this encoder block.

*Remark:* in practice for a correct batched implementation, our encoders also take a mask matrix and a vector of lengths.

Examples of encoding functions can be a feed-forward NN (with an aggregator based on sum or average/max pooling) or a recurrent NN (e.g. an LSTM/GRU). Other architectures are also possible.

**Output Layer**

From our summary vector $\mathbf h$, we need to parameterise a categorical distribution over $K$ classes, thus we use

\begin{align}
f(x; \theta) &= \text{softmax}(\text{dense}_K(\mathbf h; \theta_{\text{output}}))
\end{align}

where $\text{dense}_K$ is a dense layer with $K=5$ outputs and $\theta_{\text{output}}$ corresponds to its parameters (weight matrix and bias vector). Note that we need to use the softmax activation function in order to guarantee that the output of $f$ is a normalised probability vector.

## Implementation

To leave an indication of the shape of tensors in the code, we use the following convention

```python
[B, T, D]
```

where `B` stands for `batch_size`, `T` stands for `time` (or rather *maximum sequence length*), and `D` is the size of the representation.

Consider the following abstract Encoder class:

```
class Encoder(nn.Module):
    """
    An Encoder for us is a function that
      1. transforms a sequence of I-dimensional vectors into a sequence of O-dimensional vectors
      2. summarises a sequence of I-dimensional vectors into one O-dimensional vector

    """

    def __init__(self):
        super(Encoder, self).__init__()

    def forward(self, inputs, mask, lengths):
        """
        The input is a batch-first tensor of token ids. Here is an example:

        Example of inputs (though rather than words, we have word ids):
            INPUTS                     MASK       LENGTHS
            [the nice cat -PAD-]    -> [1 1 1 0]  [3]
            [the nice dog running]  -> [1 1 1 1]  [4]

        Note that:
              mask =  inputs == 1
              lengths = mask.sum(dim=-1)

        :param inputs: [B, T, I]
        :param mask: [B, T]
        :param lengths: [B]
        :returns: [B, T, O], [B, O]
            where the first tensor is the transformed input
            and the second tensor is a summary of all inputs
        """
        pass

```

Let's start easy, implement a *bag of words* encoder:

```
class BagOfWordsEncoder(Encoder):
    """
    This encoder does not transform the input sequence,
     and its summary output is just a sum.
    """

    pass
```

You can also consider implementing

* a feed-forward encoder with average pooling
* and a biLSTM encoder

but these are certainly optional.

```
class FFEncoder(Encoder):
    """
    A typical feed-forward NN with tanh hidden activations.
    """

    def __init__(self, input_size, output_size,
                 activation=None,
                 hidden_sizes=[],
                 aggregator='avg',
                 dropout=0.5):
        """
        :param input_size: int
        :param output_size: int
        :param hidden_sizes: list of integers (dimensionality of hidden layers)
        :param aggregator: 'sum' or 'avg'
        :param dropout: dropout rate
        """
        pass

    def forward(self, x, mask, lengths):
        """
        :param x: sequence of word embeddings, shape [B, T, I]
        :param mask: byte mask that is 0 for invalid positions, shape [B, T]
        :param lengths: the lengths of each input sequence [B]
        :return:
            outputs [B, T, O]
            sum/avg pooling [B, O]
        """
        pass
```

```
from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence

class LSTMEncoder(Encoder):
    """
    This module encodes a sequence using a bidirectional LSTM
     it returns the final state
     and the hidden states at each time step. Note: we concatenate representations
     from the two directions.
    """

    def __init__(self, in_features,
                 hidden_size: int = 200,
                 batch_first: bool = True,
                 bidirectional: bool = True):
        """
        :param in_features:
        :param hidden_size:
        :param batch_first:
        :param bidirectional:
        """
        pass

    def forward(self, x, mask, lengths):
        """
        Encode sentence x
        :param x: sequence of word embeddings, shape [B, T, I]
        :param mask: byte mask that is 0 for invalid positions, shape [B, T]
        :param lengths: the lengths of each input sequence [B]
        :return:
            outputs [B, T, O]
            final state [B, O]
        """
        pass
```

Here is some helper code to select and return an encoder:

```
def get_encoder(layer, in_features, hidden_size, bidirectional=True):
    """Returns the requested layer."""

    # TODO: make pass and average layers
    if layer == "bow":
        return BagOfWordsEncoder()
    elif layer == 'ff':
        return FFEncoder(
            in_features,
            2 * hidden_size,   # for convenience
            hidden_sizes=[hidden_size],
            aggregator='avg')
    elif layer == "lstm":
        return LSTMEncoder(
            in_features,
            hidden_size,
            bidirectional=bidirectional)
    else:
        raise ValueError("Unknown layer")
```

# Sentiment Classification with Latent Rationale

A latent rationale is a compact and informative fragment of the input based on which a NN classifier makes its decisions. [Lei et al (2016)](http://aclweb.org/anthology/D16-1011) proposed to induce such rationales along with a regression model for multi-aspect sentiment analsysis, their model is trained via REINFORCE on a dataset of beer reviews.

*Remark:* the model we will develop here can be seen as a probabilistic version of their model. The rest of this notebook focus on our own probabilitisc view of the model.

The picture below depicts our latent-variable model for rationale extraction:

where we augment the model with a collection of latent variables $z = \langle z_1, \ldots, z_n\rangle$ where $z_i$ is a binary latent variable. Each latent variable $z_i$ regulates whether or not the input $x_i$ is available to the classifier.  We use $x \odot z$ to denote the selected words, which, in the terminology of Lei et al, is a latent rationale.

Again the classifier parameterises a Categorical distribution over $K=5$ outcomes, though this time it can encode only a selection of the input:

\begin{align}
    Z_i & \sim \text{Bern}(p_1) \\
    Y|z,x &\sim \text{Cat}(f(x \odot z; \theta))
\end{align}

where we have a shared and fixed Bernoulli prior (with parameter $p_1$) for all $n$ latent variables.

Here is an example design for $f$:

\begin{align}
\mathbf x_i &= z_i \, \text{glove}(x_i) \\
\mathbf t_1^n, \mathbf h &= \text{encoder}(\mathbf x_1^n; \theta_{\text{enc}}) \\
f(x \odot z; \theta) &= \text{softmax}(\text{dense}_K(\mathbf h; \theta_{\text{output}}))
\end{align}

where:
* $z_i$ either leaves $\mathbf x_i$ unchanged or turns it into a vector of zeros;
* the encoder only sees features from selected inputs, i.e. $x_i$ for which $z_i = 1$;
* $\text{dense}_K$ is a linear layer with $K=5$ outputs.

## Prior

Our prior is a Bernoulli with fixed parameter $0 < p_1 < 1$:

\begin{align}
Z_i & \sim \text{Bern}(p_1)
\end{align}

whose pmf is $\text{Bern}(z_i|p_1) = p_1^{z_i}\times (1-p_1)^{1-z_i}$.

As we will be using Bernoulli priors and posteriors, it is a good idea to implement a Bernoulli class:

```
class Bernoulli:
    """
    This class encapsulates a collection of Bernoulli distributions.
    Each Bernoulli is uniquely specified by p_1, where
        Bernoulli(X=x|p_1) = pow(p_1, x) * pow(1 - p_1, 1 - x)
    is the Bernoulli probability mass function (pmf).
    """

    def __init__(self, logits=None, probs=None):
        """
        We can specify a Bernoulli distribution via a logit or a probability.
         You need to specify at least one, and if you specify both, beware that
         in this implementation logits will be used.

        Recall that: probs = sigmoid(logits).

        :param logits: a tensor of logits (a logit is defined as log (p_1/p_0))
            where p_0 = 1 - p_1
        :param probs: a tensor of probabilities, each in (0, 1)

        """
        pass

    def sample(self):
        """Returns a single sample with the same shape as the parameters"""
        pass

    def log_pmf(self, x):
        """
        Assess the log probability of a sample.

        :param x: either a single sample (0 or 1) or a tensor of samples with the same shape as the parameters.
        :returns: tensor with log probabilities with the same shape as parameters
            (if the input is a single sample we broadcast it to the shape of the parameters)
        """
        pass

    def kl(self, other: 'Bernoulli'):
        """
        Compute the KL divergence between two Bernoulli distributions (from self to other).

        :return: KL[self||other] with same shape parameters
        """
        pass

```

## Classifier

The classifier encodes only a selection of the input, which we denote $x \odot z$, and parameterises a Categorical distribution over $5$ outcomes (sentiment levels).

Thus let's implement a Categorical distribution (we will only need to be able to assess its lgo pmf):

```
class Categorical:

    def __init__(self, log_probs):
        # [B, K]: class probs
        self.log_probs = log_probs

    def log_pmf(self, x):
        """
        :param x: [B] integers (targets)
        :returns: [B] scalars (log probabilities)
        """
        pass
```

and a classifier architecture:

* implement the forward method

```
class Classifier(nn.Module):
    """
    The Encoder takes an input text (and rationale z) and computes p(y|x,z)
    """

    def __init__(self,
                 embed:        nn.Embedding = None,
                 hidden_size:  int = 200,
                 output_size:  int = 1,
                 dropout:      float = 0.1,
                 layer:        str = "pass",
                 ):

        super(Classifier, self).__init__()

        emb_size = embed.weight.shape[1]
        enc_size = hidden_size * 2
        # Here we embed the words
        self.embed_layer = nn.Sequential(
            embed
            # , nn.Dropout(p=dropout)
        )

        self.enc_layer = get_encoder(layer, emb_size, hidden_size)

        # and here we predict categorical parameters
        self.output_layer = nn.Sequential(
            nn.Dropout(p=dropout),
            nn.Linear(enc_size, output_size),
            nn.LogSoftmax(dim=-1)
        )

        self.report_params()

    def report_params(self):
        count = 0
        for name, p in self.named_parameters():
            if p.requires_grad and "embed" not in name:
                count += np.prod(list(p.shape))
        print("{} #params: {}".format(self.__class__.__name__, count))

    def forward(self, x, mask, z) -> Categorical:
        """
        :params x: [B, T, I] word representations
        :params mask: [B, T] indicates valid positions
        :params z: [B, T] binary selectors
        :returns: one Categorical distribution per instance in the batch
          each conditioning only on x_i for which z_i = 1
        """
        pass
```

## Inference

Computing the log-likelihood of an observation requires marginalising over assignments of $z$:

\begin{align}
P(y|x,\theta,p_1) &= \sum_{z_1 = 0}^1 \cdots \sum_{z_n=0}^1 P(z|p_1)\times P(y|x,z, \theta) \\
&= \sum_{z_1 = 0}^1 \cdots \sum_{z_n=0}^1 \left( \prod_{i=1}^n \text{Bern}(z_i|p_1)\right) \times \text{Cat}(y|f(x \odot z; \theta))
\end{align}

This is clearly intractable: there are $2^n$ possible assignments to $z$ and because the classifier conditions on all latent selectors, there's no way to simplify the expression.

We will avoid computing this intractable marginal by instead employing an independently parameterised inference model.
This inference model $Q(z|x, y, \lambda)$ is an approximation to the true postrerior $P(z|x, y, \theta, p_1)$, and we use $\lambda$ to denote its parameters.

We make a *mean field* assumption, whereby we model latent variables independently given the input:
\begin{align}
Q(z|x, y, \lambda)
    &= \prod_{i=1}^{n} Q(z_i|x; \lambda) \\
    &= \prod_{i=1}^{n} \text{Bern}(z_i|g_i(x; \lambda))
\end{align}

where $g(x; \lambda)$ is a NN that maps from $x = \langle x_1, \ldots, x_n\rangle$ to $n$ Bernoulli parameters, each of which, is a probability value (thus $0 < g_i(x; \lambda) < 1$).

Note that though we could condition on $y$ for approximate posterior inference, we are opportunistically leaving it out. This way, $Q$ is directly available at test time for making predictions. The figure below is a graphical depiction of the inference model (we show a dashed arrow from $y$ to $z$ to remind you that in principle the label is also available).

Here is an example design for $g$:
\begin{align}
\mathbf x_i &= \text{glove}(x_i) \\
\mathbf t_1^n, \mathbf h &= \text{encoder}(\mathbf x_1^n; \lambda_{\text{enc}}) \\
g_i(x; \lambda) &= \sigma(\text{dense}_1(\mathbf t_i; \lambda_{\text{output}}))
\end{align}
where
* $\text{glove}$ is a pre-trained embedding function;
* $\text{dense}_1$ is a dense layer with a single output;
* and $\sigma(\cdot)$ is the sigmoid function, necessary to parameterise a Bernoulli distribution.

From now on we will write $Q(z|x, \lambda)$, that is, without $y.

Here we implement this product of Bernoulli distributions:

* implement $g$ in the constructor
* and the forward pass

```
class ProductOfBernoullis(nn.Module):
    """
    This is an inference network that parameterises independent Bernoulli distributions.
    """

    def __init__(self,
                 embed:       nn.Embedding,
                 hidden_size: int = 200,
                 layer:       str = "bow"
                 ):
        """
        :param embed: an embedding layer
        :param hidden_suze: hidden size for transformed inputs
        :param layer: 'bow' for BoW encoding
          you may alternatively implement and 'lstm' option
          which uses a biLSTM to transform the inputs
        """
        super(ProductOfBernoullis, self).__init__()
        # 1. we should have an embedding layer
        # 2. we may transform the representations
        # 3. and we should compute parameters for Bernoulli distributions
        pass

    def report_params(self):
        count = 0
        for name, p in self.named_parameters():
            if p.requires_grad and "embed" not in name:
                count += np.prod(list(p.shape))
        print("{} #params: {}".format(self.__class__.__name__, count))

    def forward(self, x, mask) -> Bernoulli:
        """
        It takes a tensor of tokens (integers)
         and predicts a Bernoulli distribution for each position.

        :param x: [B, T]
        :param mask: [B, T]
        :returns: Bernoulli
        """
        pass
```

## Parameter Estimation

In variational inference, our objective is to maximise the *evidence lowerbound* (ELBO):

\begin{align}
\log P(y|x) &\ge \mathbb E_{Q(z|x, y, \lambda)}\left[ \log P(y|x, z, \theta, p_1) \right] - \text{KL}(Q(z|x, y, \lambda) || P(z|p_1)) \\
\text{ELBO}&\overset{\text{MF}}{=}\mathbb E_{Q(z|x, y, \lambda)}\left[ \log P(y|x, z, \theta, p_1) \right] - \sum_{i=1}^n \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))
\end{align}

where the *mean field* assumption we made implies that the KL term is simply a sum of KL divergences from a Bernoulli posterior to a Bernoulli prior.

Note that the ELBO remains intractable, namely, solving the expectation in closed form still requires $2^n$ evaluations of the classifier network. Though unlike the true posterior $P(z|x,y, \lambda)$, the approximation $Q(z|x,\lambda)$ is tractable (it does not require an intractable normalisation) and can be used to obtain gradient estimates based on samples.

### Gradient of the classifier network

For the classifier, we encounter no problem:

\begin{align}
\nabla_\theta \text{ELBO} &=\nabla_\theta\sum_{z} Q(z|x, \lambda)\log P(y|x,z,\theta) - \underbrace{\nabla_\theta \sum_{i=1}^n \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))}_{\color{blue}{0}}  \\
&=\sum_{z} Q(z|x, \lambda)\nabla_\theta\log P(y|x,z,\theta) \\
&= \mathbb E_{Q(z|x, \lambda)}\left[\nabla_\theta\log P(y|x,z,\theta) \right] \\
&\overset{\text{MC}}{\approx} \frac{1}{S} \sum_{s=1}^S \nabla_\theta \log P(y|x, z^{(s)}, \theta)
\end{align}
where $z^{(s)} \sim Q(z|x,\lambda)$.

### Gradient of the inference network

For the inference model, we have to use the *score function estimator* (a.k.a. REINFORCE):

\begin{align}
\nabla_\lambda \text{ELBO} &=\nabla_\lambda\sum_{z} Q(z|x, \lambda)\log P(y|x,z,\theta) - \nabla_\lambda \underbrace{\sum_{i=1}^n \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))}_{ \color{blue}{\text{tractable} }}  \\
&=\sum_{z} \nabla_\lambda Q(z|x, \lambda)\log P(y|x,z,\theta) - \sum_{i=1}^n \nabla_\lambda \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))   \\
&=\sum_{z}  \underbrace{Q(z|x, \lambda) \nabla_\lambda \log Q(z|x, \lambda)}_{\nabla_\lambda Q(z|x, \lambda)} \log P(y|x,z,\theta) - \sum_{i=1}^n \nabla_\lambda \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))   \\
&= \mathbb E_{Q(z|x, \lambda)}\left[ \log P(y|x,z,\theta) \nabla_\lambda \log Q(z|x, \lambda) \right] - \sum_{i=1}^n \nabla_\lambda \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))   \\
&\overset{\text{MC}}{\approx} \left(\frac{1}{S} \sum_{s=1}^S  \log P(y|x, z^{(s)}, \theta) \nabla_\lambda \log Q(z^{(s)}|x, \lambda)  \right) - \sum_{i=1}^n \nabla_\lambda \text{KL}(Q(z_i|x, \lambda) || P(z_i|p_1))
\end{align}

where $z^{(s)} \sim Q(z|x,\lambda)$.

## Implementation

Let's implement the model and the loss (negative ELBO). We work with the notion of a *surrogate loss*, that is, a computation node whose gradients wrt to parameters are equivalent to the gradients we need.

For a given sample $z \sim Q(z|x, \lambda)$, the following is a single-sample surrogate loss:

\begin{align}
\mathcal S(\theta, \lambda|x, y) = \log P(y|x, z, \theta) + \color{red}{\text{detach}(\log P(y|x, z, \theta) )}\log Q(z|x, \lambda) - \sum_{i=1}^n \text{KL}(Q(z_i|x, \lambda) || P(z_i|\phi))
\end{align}
where we introduce an auxiliary function such that
\begin{align}
\text{detach}(f(\alpha))  &= h(\alpha) \\
\nabla_\beta \text{detach}(h(\alpha))  &= 0
\end{align}
or in words, *detach* does not alter the forward call of its argument function $h$, but it alters $h$'s backward call by setting gradients to zero.

Show that it's gradients wrt $\theta$ and $\lambda$ are exactly what we need:

\begin{align}
\nabla_\theta \mathcal S(\theta, \lambda|x, y) = \color{red}{?}
\end{align}

\begin{align}
\nabla_\lambda \mathcal S(\theta, \lambda|x, y) = \color{red}{?}
\end{align}

Implement the forward pass and loss below:

```
class Model(nn.Module):
    """

    Classifier model:
        Z_i ~ Bern(p_1) for i in 1..n
        Y|x,z ~ Cat(f([x_i if z_i 1 else 0 for i in 1..n ]))

    Inference model:
        Z_i|x ~ Bern(b_i) for i in 1..n
            where b_i = g_i(x)

    Objective:
        Single-sample MC estimate of ELBO

    Loss:
        Surrogate loss

    Consists of:
        - a product of Bernoulli distributions inference network
        - a classifier network
    """

    def __init__(self,
                 vocab:       object = None,
                 vocab_size:  int = 0,
                 emb_size:    int = 200,
                 hidden_size: int = 200,
                 num_classes: int = 5,
                 prior_p1:    float = 0.3,
                 det_prior: bool = True,
                 beta_shape:  list = [0.6, 0.6],
                 dropout:     float = 0.1,
                 layer_cls:   str = 'bow',
                 layer_inf:   str = 'bow',
                 ):
        """
        :param vocab: Vocabulary
        :param vocab_size: necessary for embedding layer
        :param emb_size: dimensionality of embedding layer
        :param hidden_size: dimensionality of hidden layers
        :param num_classes: number of classes
        :param prior_p1: (scalar) prior Bernoulli parameter
        :param det_prior: (boolean) whether the prior parameter is deterministic
        :param beta_shape: (pair of positive scalars)
            when the prior parameter is stochastic
            it is sampled from a Beta distribution (ignore this at first)
        :param dropout: (scalar) dropout rate
        :param layer_cls: type of encoder for classification
        :param layer_inf: type of encoder for inference
        """
        super(Model, self).__init__()

        self.vocab = vocab
        self.embed = embed = nn.Embedding(vocab_size, emb_size, padding_idx=1)

        self.cls_net = Classifier(
            embed=embed,
            hidden_size=hidden_size,
            output_size=num_classes,
            dropout=dropout,
            layer=layer_cls)

        self.inference_net = ProductOfBernoullis(
            embed=embed,
            hidden_size=hidden_size,
            layer=layer_inf)

        self._prior_p1 = prior_p1
        self._det_prior = det_prior
        self._beta_shape = beta_shape

    def get_prior_p1(self, p_min=0.001, p_max=0.999):
        """Return the prior Bernoulli parameter"""
        if self._det_prior:
            return self._prior_p1
        else:
            a, b = self._beta_shape
            prior_p1 = np.random.beta(a, b)
            prior_p1 = max(prior_p1, p_min)
            prior_p1 = min(prior_p1, p_max)
        return prior_p1

    def predict(self, py: Categorical, **kwargs):
        """
        Predict deterministically using argmax.
        :param py: B Categorical distributions (one per instance in batch)
        :return: predictions
            [B] sentiment levels
        """
        assert not self.training, "should be in eval mode for prediction"
        return py.log_probs.argmax(-1)

    def forward(self, x, mask):
        """
        Generate a sequence z with inference model,
         then predict with rationale xz, that is, x masked by z.

        :param x: [B, T] documents
        :param mask: [B, T] indicates valid positions vs padded positions
        :return:
            Categorical distributions P(y|x, z)
            Bernoulli distributions Q(z|x)
            Single sample z ~ Q(z|x) used for the conditional P(y|x, z)
        """
        pass

    def get_loss(self,
                 y,
                 py: Categorical,
                 qz: Bernoulli,
                 z,
                 mask,
                 iter_i=0,
                 # you may ignore the rest of the arguments for the time being
                 #  leave them as they are
                 kl_weight=1.0,
                 min_kl=0.0,
                 ll_mean=0.,
                 ll_std=1.,
                 **kwargs):
        """
        This computes the loss for the whole model.

        :param y: target labels [B]
        :param py: conditionals P(y|x, z)
        :param qz: approximate posteriors Q(z|x)
        :param z: sample of binary selectors [B, T]
        :param mask: indicates valid positions [B, T]
        :param iter_i: indicates the iteration
        :param kl_weight: (scalar) multiplies the KL term
        :param min_kl: (scalar) sets a minimum for the KL (aka free bits)
        :param ll_mean: (scalar) running average of reward
        :param ll_std: (scalar) running standard deviation of reward
        :return: loss (torch node), terms (dict)

            terms is an OrderedDict that holds the scalar items involved in the loss
            e.g. `terms['ll'] = ll.item()` is the log-likelihood term

            Consider tracking the following:
            Single-sample ELBO: terms['elbo']
            Log-Likelihood log P(y|x,z): terms['ll']
            KL: terms['kl']
            Score function surrogate log P(y|z, x) log Q(z|x): terms['sf']
            Rate of selected words: terms['selected']
        """

        pass
```

```
# This will be used later for maintaining runnin averages of quantites like
#  terms in the ELBO
from collections import deque

class MovingStats:

    def __init__(self, memory=-1):
        self.data = deque([])
        self.memory = memory

    def append(self, value):
        if self.memory != 0:
            if self.memory > 0 and len(self.data) == self.memory:
                self.data.popleft()
            self.data.append(value)

    def mean(self):
        if len(self.data):
            return np.mean([x for x in self.data])
        else:
            return 0.

    def std(self):
        return np.std(self.data) if len(self.data) > 1 else 1.

```

# Training loop

```
# some helper code for mini batching
#  this will take care of annoying things such as
#  sorting training instances by length (necessary for pytorch's LSTM, for example)
from sst.util import make_kv_string, get_minibatch, prepare_minibatch, print_parameters
```

```
import torch.optim
# We will use Adam
from torch.optim import Adam
# and a couple of tricks to reduce learning rate on plateau
from torch.optim.lr_scheduler import ReduceLROnPlateau
# here is some helper code to evaluate your model
from sst.evaluate import evaluate

cfg = dict()

# Data
cfg['training_path'] = "sst/data/sst/train.txt"
cfg['dev_path'] = "sst/data/sst/dev.txt"
cfg['test_path'] = "sst/data/sst/test.txt"
cfg['word_vectors'] = 'sst/data/sst/glove.840B.300d.filtered.txt'
# Model
cfg['prior_p1'] = 0.3
cfg['beta_a'] = 0.6
cfg['beta_b'] = 0.6
cfg['det_prior'] = True
# Architecture
cfg['num_epochs'] = 50
cfg['print_every'] = 100
cfg['eval_every'] = -1
cfg['batch_size'] = 25
cfg['eval_batch_size'] = 25
cfg['subphrases'] = False
cfg['min_phrase_length'] = 2
cfg['lowercase'] = True
cfg['fix_emb'] = True
cfg['embed_size'] = 300
cfg['hidden_size'] = 150
cfg['num_layers'] = 1
cfg['dropout'] = 0.5
cfg['layer_inf'] = 'bow'
cfg['layer_cls'] = 'bow'
cfg['save_path'] = 'data/results'
cfg['baseline_memory'] = 1000
cfg['min_kl'] = 0.  # use more than 0 to enable free bits
cfg['kl_weight'] = 1.  # start from zero to enable annealing
cfg['kl_inc'] = 0.00001
# Optimiser (leave as is)
cfg['lr'] = 0.0002
cfg['weight_decay'] = 1e-5
cfg['lr_decay'] = 0.5
cfg['patience'] = 5
cfg['cooldown'] = 5
cfg['threshold'] = 1e-4
cfg['min_lr'] = 1e-5
cfg['max_grad_norm'] = 5.

print('# Configuration')
for k, v in cfg.items():
    print("{:20} : {:10}".format(k, v))

iters_per_epoch = len(train_data) // cfg["batch_size"]

if cfg["eval_every"] == -1:
    eval_every = iters_per_epoch
    print("Set eval_every to {}".format(iters_per_epoch))

# Let's load the data into memory.
print("Loading data")
train_data = list(examplereader(
    cfg['training_path'],
    lower=cfg['lowercase'],
    subphrases=cfg['subphrases'],
    min_length=cfg['min_phrase_length']))
dev_data = list(examplereader(cfg['dev_path'], lower=cfg['lowercase']))
test_data = list(examplereader(cfg['test_path'], lower=cfg['lowercase']))

print("train", len(train_data))
print("dev", len(dev_data))
print("test", len(test_data))

print('\n# Example')
example = dev_data[0]
print("First dev example:", example)
print("First dev example tokens:", example.tokens)
print("First dev example label:", example.label)

```

```
def train():

    # Create a vocabulary object to map str <-> int
    vocab = Vocabulary()  # populated by load_glove
    glove_path = cfg["word_vectors"]
    vectors = load_glove(glove_path, vocab)

    # You may consider using tensorboardX
    # writer = SummaryWriter(log_dir=cfg["save_path"])

    # Map the sentiment labels 0-4 to a more readable form (and the opposite)
    i2t = ["very negative", "negative", "neutral", "positive", "very positive"]
    t2i = OrderedDict({p: i for p, i in zip(i2t, range(len(i2t)))})

    print('\n# Constructing model')
    model = Model(
        vocab_size=len(vocab.w2i),
        emb_size=cfg["embed_size"],
        hidden_size=cfg["hidden_size"],
        num_classes=len(t2i),
        prior_p1=cfg['prior_p1'],
        det_prior=cfg['det_prior'],
        beta_shape=[cfg['beta_a'], cfg['beta_b']],
        vocab=vocab,
        dropout=cfg["dropout"],
        layer_cls=cfg["layer_cls"],
        layer_inf=cfg["layer_inf"])

    print('\n# Loading embeddings')
    with torch.no_grad():
        model.embed.weight.data.copy_(torch.from_numpy(vectors))
        if cfg["fix_emb"]:
            print("fixed word embeddings")
            model.embed.weight.requires_grad = False
        model.embed.weight[1] = 0.  # padding zero

    # Congigure optimiser
    optimizer = Adam(model.parameters(), lr=cfg["lr"],
                     weight_decay=cfg["weight_decay"])
    # and learning rate scheduler
    scheduler = ReduceLROnPlateau(
        optimizer, mode="min", factor=cfg["lr_decay"], patience=cfg["patience"],
        verbose=True, cooldown=cfg["cooldown"], threshold=cfg["threshold"],
        min_lr=cfg["min_lr"])

    # Prepare a few auxiliary variables
    iter_i = 0
    train_loss = 0.
    print_num = 0
    losses = []
    accuracies = []
    best_eval = 1.0e9
    best_iter = 0

    model = model.to(device)

    # Some debugging info
    print(model)
    print_parameters(model)

    batch_size = cfg['batch_size']
    eval_batch_size = cfg['eval_batch_size']
    print_every = cfg['print_every']

    # Parameters of tricks to better optimise the ELBO
    kl_inc = cfg['kl_inc']
    kl_weight = cfg['kl_weight']
    min_kl = cfg['min_kl']
    # Running estimates for baselines
    ll_moving_stats = MovingStats(cfg['baseline_memory'])

    while True:  # when we run out of examples, shuffle and continue
        for batch in get_minibatch(train_data, batch_size=batch_size, shuffle=True):

            epoch = iter_i // iters_per_epoch
            if epoch > cfg['num_epochs']:
                break

            # forward pass
            model.train()
            x, y, _ = prepare_minibatch(batch, model.vocab, device=device)

            mask = (x != 1)
            py, qz, z = model(x, mask)

            # "KL annealing"
            kl_weight += kl_inc
            if kl_weight > 1.:
                kl_weight = 1.0

            loss, terms = model.get_loss(
                y,
                py=py,
                qz=qz,
                z=z,
                mask=mask,
                kl_weight=kl_weight,
                min_kl=min_kl,
                ll_mean=ll_moving_stats.mean(),
                ll_std=ll_moving_stats.std(),
                iter_i=iter_i)

            train_loss += loss.item()

            # keep an running estimate of the reward (log P(y|x,z))
            ll_moving_stats.append(terms['ll'])

            # backward pass
            model.zero_grad()  # erase previous gradients

            loss.backward()  # compute new gradients

            # gradient clipping generally helps
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=cfg['max_grad_norm'])

            # update weights
            optimizer.step()

            print_num += 1
            iter_i += 1

            # print info
            if iter_i % print_every == 0:

                train_loss = train_loss / print_every

                print_str = make_kv_string(terms)
                print("Epoch %r Iter %r loss=%.4f %s" %
                      (epoch, iter_i, train_loss, print_str))
                losses.append(train_loss)
                print_num = 0
                train_loss = 0.

            # evaluate
            if iter_i % eval_every == 0:

                dev_eval, rationales = evaluate(
                    model, dev_data,
                    batch_size=eval_batch_size,
                    device=device,
                    cfg=cfg, iter_i=iter_i)
                accuracies.append(dev_eval["acc"])

                print("\n# epoch %r iter %r: dev %s" % (
                    epoch, iter_i, make_kv_string(dev_eval)))

                for exid in range(3):
                    print(' dev%d [gold=%d,pred=%d]:' % (exid, dev_data[exid].label, rationales[exid][1]),
                          ' '.join(rationales[exid][0]))
                print()

                # adjust learning rate
                scheduler.step(dev_eval["loss"])
```

```
train()
```

# Variance reduction

**This is an extra**

We can use a *control variate* to reduce the variance of our gradient estimates.

Let's recap the idea in general terms. We are looking to solve some expectation
\begin{align}
\mu_f = \mathbb E[f(Z)]
\end{align}
but unfortunatelly, realising the full sum (or integral for continuous variables) is intractable. Thus we employ MC estimation
\begin{align}
\hat \mu_f &\overset{\text{MC}}{\approx} \frac{1}{S} \sum_{s=1}^S f(z_s) & \text{where }z_s \sim Q(z|x)
\end{align}
Note that the variance of this estimate is
\begin{align}
\text{Var}(\hat \mu_f) &=  \frac{1}{S}\text{Var}(f(Z)) \\
&= \frac{1}{S} \mathbb E[( f(Z) - \mathbb E[f(Z)])^2]
\end{align}
Note that this variance is such that it goes down as we sample more, in a rate $\mathcal O(S^{-1})$.
See that if we sample $10$ times more, we will only obtain an decrease in variance in the order of $10^{-1}$. This means that sampling more is generally not the most convenient way to decrease variance.

*Digression* we can estimate the variance itself via MC, an unbiased estimate looks like
\begin{align}
\hat \sigma^2_f = \frac{1}{S(S-1)} \sum_{s=1}^S (f(z_s) - \hat \mu_f)^2
\end{align}
but not that this estimate is even hard to improve since it decreases with $\mathcal O(S^{-2})$.

Back to out main problem: let's try and improve the variance of our estimator to $\mu_f$.

It's a fact, and it can be shown trivially, that
\begin{align}
\mu_f &=  \mathbb E[f(Z) - \psi(Z)] + \underbrace{\mathbb E[\psi(Z)]}_{\mu_\psi} \\
 &\overset{\text{MC}}{\approx} \underbrace{\left(\frac{1}{S} \sum_{s=1}^S f(z_s) - \psi(z_s) \right) + \mu_\psi}_{\hat c}
\end{align}
where we assume the existence of some function $\psi(z)$ for which the expected value $\mu_\psi$ is known and we estimate the expected difference $\mathbb E[f(Z) - \psi(Z)]$ via MC. We used this axuxiliary function, also known as a *control variate*, to derive a new estimator, which we will denote by $\hat c$.

The variance of this new estimator is show below:

\begin{align}
\text{Var}( \hat c ) &= \text{Var}(\hat \mu_{f-\psi}) + 2\underbrace{\text{Cov}(\hat \mu_{f-\psi}, \mu_\psi)}_{\mathbb E[\hat \mu_{f-\psi}  \mu_\psi] - \mathbb E[\hat \mu_{f-\psi}] \mathbb E[\mu_\psi]} + \underbrace{\text{Var}(\mu_\psi)}_{\color{blue}{0} } \\
&= \frac{1}{S}\text{Var}(f- \psi)  + 2 \underbrace{\left( \mu_\psi \mu_{f-\psi} - \mu_{f-\psi} \mu_\psi \right)}_{\color{blue}{0}}
\end{align}
where the variance of $\mu_\psi$ is 0 because we know it in closed form (no need for MC estimation), and the covariance is $0$ as shown in the second row.

That is, the variance of $\hat c$ is essentially the variance of estimating $\mathbb E[f(Z) - \psi(Z)]$, which in turn depends on the variance

\begin{align}
\text{Var}(f-\psi) &= \text{Var}(f) - 2\text{Cov}(f, \psi) + \text{Var}(\psi)
\end{align}
where we can see that if $\text{Cov}(f, \psi) > \frac{\text{Var}(\psi)}{2}$ we achieve variance reduction as then $\text{Var}(f-\psi)$ would be smaller than $\text{Var(f)}$.

## Baselines

Baslines are control variates of a very simple form:
\begin{align}
\mathbb E[f(Z)] = \mathbb E[f(Z) - C] + \mathbb E[C]
\end{align}
where $C$ is a constant with respect to $z$.

In the context of the score function estimator, a baseline looks like a quantity $C(x; \omega)$, this may be
* just a constant;
* or a function of the input (but not of the latent variable), which could be itself implemented as a neural network;
* a combination of the two.

Let's focus on the first term of the ELBO (so I'm omitting the KL term here). The gradient with respect to parameters of the inference model becomes:

\begin{align}
&\mathbb E_{Q(z|x, \lambda)}\left[ \log P(x|z, \theta) \nabla_\lambda \log Q(z|x, \lambda)\right]\\
&=\mathbb E_{Q(z|x, \lambda)}\left[\log P(x|z, \theta) \nabla_\lambda \log Q(z|x, \lambda) - \color{red}{C(x; \omega)\nabla_\lambda \log Q(z|x, \lambda) }  \right] + \underbrace{\mathbb E_{Q(z|x, \lambda)}\left[\color{red}{C(x; \omega)\nabla_\lambda \log Q(z|x, \lambda) }  \right] }_{=0} \\
&= \mathbb E_{Q(z|x, \lambda)}\left[ \color{blue}{\left(\log P(x|z, \theta) - C(x; \omega) \right)}\nabla_\lambda \log Q(z|x, \lambda)\right] \\
&
\end{align}
We can show that the last term is $0$

\begin{align}
&\mathbb E_{Q(z|x, \lambda)}\left[C(x; \omega)\nabla_\lambda \log Q(z|x, \lambda)   \right]  \\&= C(x; \omega) \mathbb E_{Q(z|x, \lambda)}\left[\nabla_\lambda \log Q(z|x, \lambda)   \right]\\
&= C(x; \omega) \mathbb E_{Q(z|x, \lambda)}\left[\frac{1}{Q(z|x, \lambda)} \nabla_\lambda Q(z|x, \lambda)   \right] \\
&= C(x; \omega) \sum_z Q(z|x, \lambda) \frac{1}{Q(z|x, \lambda)} \nabla_\lambda Q(z|x, \lambda)   \\
&= C(x; \omega) \sum_z\nabla_\lambda Q(z|x, \lambda)  \\
&= C(x; \omega) \nabla_\lambda \underbrace{\sum_z Q(z|x, \lambda)  }_{=1}\\
&=0
\end{align}

Examples of useful baselines:

* a running average of the learning signal: at some iteration $t$ we can use a running average of $\log P(x|z, \theta)$ using parameter estimates $\theta$ from iterations $i < t$, this is a baseline that likely leads to high correlation between control variate and learning signal and can lead to variance reduction;
* another technique is to have an MLP with parameters $\omega$ predict a scalar and train this MLP to approximate the learning signal $\log P(x|z, \theta)$ via regression:
\begin{align}
\arg\max_\omega \left( C(x; \omega) - \log P(x|z, \theta) \right)^2
\end{align}
its left as an extra to implement these ideas.

One more note: we can also use something called a *multiplicative baseline* in the literature of reinforcement learning, whereby we incorporate a running estimate of the standard deviation of the learning signal computed based on the values attained on previous iterations:
\begin{align}
\mathbb E_{Q(z|x, \lambda)}\left[ \frac{1}{\hat\sigma_{\text{past}}}\left(\log P(x|z, \theta) - \hat \mu_{\text{past}}\right)\nabla_\lambda \log Q(z|x, \lambda)\right]
\end{align}
this form of contorl variate aim at promoting the learning signal (or reward in reinforcement learning literature) to be distributed by $\mathcal N(0, 1)$. Note that multiplying the reward by a constant does not bias the estimator, and in this case, may lead to variance reduction.

```

```

```

```

```

```


# Science, Data, Tools
or *'Tips and tricks for a your everyday workflow'*

## Matteo Guzzo

# Prologue

AKA *My Research*

## The cumulant expansion

The struggle has been too long, but we are starting to see the light...

## As you might recall...

We are trying to go beyond the GW approximation

and succeed in describing satellites.

## The spectral function

It looks like this:
\begin{equation}
 A(\omega) = \mathrm{Im}|G(\omega)|
\end{equation}

## GW vs Cumulant

Mathematically very different:

\begin{equation}
 G^{GW} (\omega) = \frac1{ \omega - \epsilon - \Sigma (\omega) }
\end{equation}

\begin{equation}
 G^C(t_1, t_2) = G^0(t_1, t_2) e^{ i \int_{t_1}^{t_2} \int_{t'}^{t_2} dt' dt'' W (t', t'') }
\end{equation}

BUT they connect through $\mathrm{Im} W (\omega) = \frac1\pi \mathrm{Im} \Sigma ( \epsilon - \omega )$.

[Guzzo et al., Phys. Rev. Lett. 107, 166401, 2011](http://journals.aps.org/prl/abstract/10.1103/PhysRevLett.107.166401)

## Implementation

Using a multi-pole representation for $\Sigma^{GW}$:

\begin{equation}
 \mathrm{Im} W (\omega) = \frac1\pi \mathrm{Im} \Sigma ( \epsilon - \omega )
\end{equation}
\begin{equation}
 W (\tau) = - i \lambda \bigl[ e^{ i \omega_p \tau } \theta ( - \tau ) + e^{ - i \omega_p \tau } \theta ( \tau ) \bigr]
\end{equation}

## GW vs Cumulant

- GW:

\begin{equation}
 A(\omega) = \frac1\pi \frac{\mathrm{Im}\Sigma (\omega)}
 { [ \omega - \epsilon - \mathrm{Re}\Sigma (\omega) ]^2 +
 [ \mathrm{Im}\Sigma (\omega) ]^2}
\end{equation}

- Cumulant (plasmon-pole):

\begin{equation}
 A(\omega) = \frac1\pi \sum_{n=0}^{\infty} \frac{a^n}{n!} \frac{\Gamma}{ (\omega - \epsilon + n \omega_p)^2 + \Gamma^2 }
\end{equation}

## Problems

\begin{equation}
 A(\omega) = \frac1\pi \sum_{n=0}^{\infty} \frac{a^n}{n!} \frac{\Gamma}{ (\omega - \epsilon + n \omega_p)^2 + \Gamma^2 }
\end{equation}

## Alternative: full numerical

With $ t = t_2 - t_1 $:

\begin{equation}
 G^C (t) = G^0(t) e^{ i \int d\omega
 \frac{W (\omega)} {\omega^2} \left( e^{i \omega t} -i \omega t - 1 \right) }
\end{equation}

[Kas *et al.*, Phys. Rev. B 90, 085112, 2014](http://journals.aps.org/prb/abstract/10.1103/PhysRevB.90.085112)

To be continued... --> https://github.com/teoguso/SF.git

# Acknowledgements

**Jiangqiang "Sky" Zhou** - Ecole Polytechnique, France

**Matteo Gatti** - Ecole Polytechnique, France

**Andris Gulans** - HU Berlin

# Let's talk about tools (My *actual* Outline)

### How do you go about your everyday work?

### What do you use to:

- Write and maintain code

- Review and analyse data

- Present data and results

# My answer

- Git version control system:
https://git-scm.com/

- Python + matplotlib plotting library:

  https://www.python.org/

  http://matplotlib.org/

- Jupyter notebooks:
http://jupyter.org/

# Time to take a breath

(Questions?)

# Git

**QUESTION:** Who knows what Git is?

State of the art for **distributed version control**

# Version control?

# Git in a nutshell

# Git in a nutshell

- Very powerful (*essential*) for collaborative work (e.g. software, articles)

- Very useful for personal work

# Ok, but what should I use it for?

- Source code

- Scripts

- Latex documents

In short:

# Any text-based (ASCII) project

Fortran, Python, Latex, Bash, ...

# Python + matplotlib

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
with plt.xkcd():
    plt.rcParams['figure.figsize'] = (6., 4.)
    x = np.linspace(-5, 5, 50)
    gauss = np.exp(-(x**2) / 2)/np.sqrt(2 * np.pi)
    ax = plt.subplot(111)
    ax.plot(x, gauss, label="Best curve ever")
    cdf = np.array([np.trapz(gauss[:i], x[:i]) for i, _ in enumerate(gauss)])
    plt.plot(x, cdf, label="Bestest curve ever")
    plt.xlim(-3, 3)
    ax.set_xticks([])
    ax.set_yticks([])
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    plt.xlabel('most exciting independent variable')
    plt.ylabel("I'm not trolling")
    plt.legend(loc='best')
    plt.annotate("I CAN ALSO\n DO MATH:\n"+
                 "      "+
                 r"$\frac{e^{-\frac{x^2}{2}}}{\sqrt{2\pi}}$",
        xy=(0.1, 0.4), arrowprops=dict(arrowstyle='->'), xytext=(2, 0.6))
    fname = "./graphics/xkcd1.png"
    plt.savefig(fname, dpi=300)
```

See https://www.xkcd.com

```python
"""
Simple demo of a scatter plot.
"""
# import numpy as np
# import matplotlib.pyplot as plt

N = 50
x = np.random.rand(N)
y = np.random.rand(N)
colors = np.random.rand(N)
area = np.pi * (15 * np.random.rand(N))**2  # 0 to 15 point radiuses

plt.scatter(x, y, s=area, c=colors, alpha=0.5)
plt.savefig("./graphics/scatter_demo.png", dpi=200)
```

http://matplotlib.org/gallery.html

```python
"""
This shows an example of the "fivethirtyeight" styling, which
tries to replicate the styles from FiveThirtyEight.com.
"""

from matplotlib import pyplot as plt
import numpy as np

x = np.linspace(0, 10)

with plt.style.context('fivethirtyeight'):
    plt.plot(x, np.sin(x) + x + np.random.randn(50))
    plt.plot(x, np.sin(x) + 0.5 * x + np.random.randn(50))
    plt.plot(x, np.sin(x) + 2 * x + np.random.randn(50))

plt.savefig("./graphics/fivethirtyeight_demo.png", dpi=200)
```

http://matplotlib.org/gallery.html

# Jupyter notebooks

www.jupyter.org

# Jupyter what?

- Interactive scientific computing environment

  - Data analysis and visualization
  - Interactive reports
  - Parallel computing!

- Originated from the IPython project in 2014

- Now supports a number of languages (Python, BASH, ...)

# Jupyter notebooks

- Interactive documents that combine

  - Executable code

  - Rich-text elements ([**Markdown**](https://en.wikipedia.org/wiki/Markdown) + HTML)

- They are **text-based** (Git anyone?)

# More cool stuff

LaTex support and syntax highlighting

Write
``` latex
\begin{equation}
 \Sigma (\omega) = \int \, d\omega' ...
\end{equation}
```

and get
\begin{equation}
 \Sigma (\omega) = \int \, d\omega' ...
\end{equation}

# TO DEMO OR NOT TO DEMO?

Also remember to convert to pdf!

# Conclusion

- __Git__: an essential tool for any workflow
  - tracking of personal work
  - easy collaborative work

- __Jupyter__ - Great for:

  - Data analysis

  - Reports

  - (Works best with Python)

# Resources

## Git
- http://rogerdudler.github.io/git-guide/
- https://git-scm.com/book/en/v2/Getting-Started-Git-Basics

## Jupyter notebooks

- [Beginner video tutorial](https://www.youtube.com/watch?v=HW29067qVWk)
- [Notebooks gallery](https://github.com/ipython/ipython/wiki/A-gallery-of-interesting-IPython-Notebooks)

## Markdown
- https://help.github.com/articles/getting-started-with-writing-and-formatting-on-github/
- https://guides.github.com/features/mastering-markdown/

# Bonus clip

How do you think these slides were made?

See it at <https://github.com/teoguso/sol_1116>