```python
# Installs kneed if you don't already have it installed.
import sys
!{sys.executable} -m pip install kneed
```

```python
# Imports modules which will be used throughout the code.
import pandas as pd
import numpy as np
import matplotlib
from matplotlib import pyplot as plt
import sklearn
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score
from scipy import stats, integrate
from kneed import KneeLocator
import math
from sympy.solvers import solve
from sympy import Symbol
import os
import ipywidgets as widgets
```

```python
# Creates a user interface using the widget module.
widget_width = '900px'
padding = '0px 0px 0px 4px'

text0 = widgets.Text(
    value="",
    description="Filepath or URL:",
    placeholder="Enter filepath or URL to file adding '?raw=true' to the end of any URL",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text0

text1 = widgets.Text(
    value="",
    description="Pad Voltage / V:",
    placeholder="Enter pad voltage",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text1

text2 = widgets.Text(
    value="",
    description="Resistance / $ \Omega $:",
    placeholder="Enter resistance in ohms",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text2

text3 = widgets.Text(
    value="",
    description="Thickness / $\mu$m:",
    placeholder="Enter thickness in micrometres",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text3

text6 = widgets.Text(
    value="",
    description="Filepath to save the data:",
    placeholder="eg. C:/Users/tom/dick/harry",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text6

text8 = widgets.Text(
    value="",
    description="Thickness error / $\mu$m:",
    placeholder="Enter thickness error in micrometres",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text8

# Sets the output of the user input to variables which can be used in the rest of the code.
toolbar_widget = widgets.VBox()
toolbar_widget.children = [
    text0,
    text1,
    text2,
    text3,
    text8,
    text6

]
#Displays the widget.
toolbar_widget
```

    VBox(children=(Text(value='', description='Filepath or URL:', layout=Layout(padding='0px 0px 0px 4px', width='…

```python
# Sets variable names for the output of the user interface.
filename = text0.value
Pad_Voltage = float(text1.value)
Resistance = float(text2.value)
Thickness = float(text3.value) * 10 ** (-6)
Thickness_error = float(text8.value) * 10 ** (-6)
filepath = text6.value
# Takes output from the user imput filename and adds extra to create new files and files.
savename = str(filepath) + "/electron_plate_data.csv"
graph_savename = str(filepath) + "/Graphs"

# If the path does not yet exist on the user's PC it will be created by the code.
if not os.path.exists(graph_savename):
    os.makedirs(graph_savename)

# If a path for files used in the code does not yet exist they will be created.
if not os.path.exists(str(filepath) + "/Intermediate_files"):
    os.makedirs(str(filepath) + "/Intermediate_files")

# By default this reads the csv format data frame (df) ready for analysis and assumes there are no headers in the data.
# If there are headers in the data you should delete "headers = None, names = ["Time", "Current"]".
df = pd.read_csv(filename, header=None, names=["Time", "Voltage"])

# A new dataframe is created removing the first 11 points to avoid the extra information often contained early
# in the csv files. If the code doesn't work here try adding higher numbers as there may be even more unwanted
# data at the beginning. Missing a few early datapoints makes a negligible difference to the overall results.
df1 = df.drop(labels = [0,1,2,3,4,5,6,7,8,9,10,11], axis = 0)
# Drops any further Not-A-Number values just in case.
df1.dropna()

# The index of the dataframe is reset to ensure the datapoints start at 0 not at 12, etc.
df1.reset_index(drop = True, inplace = True)

# Saves the cleaned data into an intermediate file for use later.
df1.to_csv(str(filepath) + "/Intermediate_files/corrected_testdata.csv", index=False)

# Sets the data to datatype 'float64' if it isn't already in this form
df1["Time"] = df1["Time"].astype("float")
df1["Voltage"] = df1["Voltage"].astype("float")
# Resets the index of the dataframe to avoid confusion
df1.reset_index(drop = True, inplace = True)

# Prints the types of data to ensure we definitely don't have any non float64 data remaining. This is mainly here for
# troubleshooting.
print(" ")
print("The datatypes of each column:")
print(df1.dtypes)

# Creates a column in the dataframe for the current by dividing each value of the pad voltage by the user provided
# resistance.
df1['Current'] = df1["Voltage"] / Resistance
# Removes any infinite values that may have been left in the code by replacing them with Not a Number (NaN) values
# and then deleting any NaN values.
df1.replace([np.inf, -np.inf], np.NaN, inplace=True)
df1.dropna()

# Creates new dataframes for data with times above and below the 0s mark. Works by reading the first corrected file and
# writing a new csv file containing no values where the first character is '-' thereby removing all negative values.
# This means we can still access the data with negative values whilst also having cleaned the initial data.
with open(str(filepath) + "/Intermediate_files/corrected_testdata.csv", 'r') as f:
    with open(str(filepath) + "/Intermediate_files/corrected_testdata_positive.csv", 'w') as g:
        with open(str(filepath) + "/Intermediate_files/correlated_testdata_negative.csv", 'w') as h:
            for row in f:
                if row[0] != '-':
                    g.write(row)
                else:
                    h.write(row)

# Reads the positive and negative dataframes and creates the current values for them both.
df2 = pd.read_csv(str(filepath) + "/Intermediate_files/corrected_testdata_positive.csv")
df_negative = pd.read_csv(str(filepath) + "/Intermediate_files/correlated_testdata_negative.csv", header=None, names=["Time", "Voltage"])
df2['Current'] = df2["Voltage"] / Resistance
df_negative["Current"] = df_negative["Voltage"] / Resistance

# Removes any infinite values and resets the index of the positive dataframe.
df2.replace([np.inf, -np.inf], np.NaN, inplace=True)
df2.dropna(inplace = True)
df2.reset_index(drop = True, inplace = True)

# The dark current is found by taking the mean value of the current from the negative data.
dark_current = df_negative["Current"].mean()
# Each value of the current in the positive dataframe has the dark current subtracted from its value.
df2['Current'] -= dark_current
# New columns are created in the dataframe for the logarithms of time and current.
df2['Log Time'] = np.log10(df2["Time"])
df2['Log Current'] = np.log10(df2["Current"])
# Infinite values are once again removed as subtracting the dark current from some noise in the positive region leads
# to negative values being put into a logarithm which leads to an error.
df2.replace([np.inf, -np.inf], np.NaN, inplace=True)
df2.dropna(inplace = True)

# A plot of the raw data for current against is produced including the slopes before and after the dark current
# has been subtracted and the plot is saved in the specified filepath.
plt.figure()
plt.plot(df1["Time"], df1["Current"], label = "With dark current")
plt.plot(df2["Time"], df2["Current"], label = "Without dark current")
#plt.title("Time of Flight Data")
plt.xlabel("Time / s")
plt.ylabel("Current / A")
plt.legend()
plt.savefig(str(graph_savename) + "/Raw_data" + str(Pad_Voltage) + "V.png", dpi = 300, bbox_inches = 'tight')
plt.show()

# A polynomial regression of the raw data is generated. You can change the number of free parameters after the
# 2nd comma.
a = np.polyfit(df2["Time"], df2["Current"], 20)
b = np.poly1d(a)

# The polynomial regression is printed out as a linear equation for completeness.
print("The generated polynomial regression:")
print(" ")
print(b)

# This linspace is used to plot the polynomial regression. We can choose how many points it plots after the
# 2nd comma.
myline = np.linspace(df2["Time"].min(), df2["Time"].max(), 1000)

# We plot the current v the time again with the polynomial regression displayed to check if the regression
# seems sensible.
plt.figure()
plt.plot(df2["Time"], df2["Current"])
plt.plot(myline, b(myline))
#plt.title("Current against Time")
plt.xlabel("Time / s")
plt.ylabel("Current / A")
plt.show()

# This function uses our polynomial regression to find an integral of the curve and finds the error in its
# measurement using Gaussian quadrature.
photocharge, photocharge_error = integrate.quad(b, df2["Time"].min(), df2["Time"].max())
print("Polynomial photocharge:", photocharge, "with an error of:", photocharge_error)
print(" ")
# This function performs a cumulative trapezoidal integral approximation on the raw data without the need
# for a polynomial.
y_int = integrate.cumtrapz(df2["Current"], df2["Time"], initial=0)
print("Cumulative trapezoidal photocharge:", y_int[-1])

# A polynomial regression of the double logarithmic data is created ready to be graphed.
f = np.polyfit(df2["Log Time"], df2["Log Current"], 10)
p = np.poly1d(f)
myline_1 = np.linspace(df2["Log Time"].min(), df2["Log Time"].max(), 1000)

# The double logarithmic plot is created.
plt.figure()
plt.plot(df2["Log Time"], df2["Log Current"])
plt.plot(myline_1, p(myline_1))
#plt.title("log(Current) against log(Time)")
plt.xlabel("log(Time / s)")
plt.ylabel("log(Current / A)")
plt.show()

# The polynomial regression is printed in the form of a linear equation.
print("The polynomial for the double logarithmic plot:")
print(" ")
print(p)
```

```python
# A 2nd user interface is created for selecting the cuts.
widget_width = '900px'
padding = '0px 0px 0px 4px'

text4 = widgets.Text(
    value="",
    description="Upper bound for the log time (highest magnitude):",
    placeholder="Insert a positive value and the code makes it negative",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text4

text5 = widgets.Text(
    value="",
    description="Lower bound for the log time (lowest magnitude):",
    placeholder="Insert a positive value and the code makes it negative",
    style={"description_width": "initial"},
    layout=widgets.Layout(width=widget_width, padding=padding),
)
text5

# The inputs are saved as variables to be used later in the code.
toolbar_widget = widgets.VBox()
toolbar_widget.children = [
    text5,
    text4

]
# Displays the widget.
toolbar_widget
```

    VBox(children=(Text(value='', description='Lower bound for the log time (lowest magnitude):', layout=Layout(pa…

```python
# Sets the outputs of the widget to be of float64 type.
cut_max = float(text4.value)
cut_min = float(text5.value)

print("--------------------------------------------------------------------------------------------------")
# A new dataframe is created where values outside of the cuts are disgarded.
df3 = df2[df2["Log Time"] < -cut_min]
df3 = df3[df3["Log Time"] > -cut_max]

# A polynomial regression of the line between the cuts is generated. You can edit the number of free
# parameters after the 2nd comma.
f1 = np.polyfit(df3["Log Time"], df3["Log Current"], 6)
p1 = np.poly1d(f1)

# A linspace is used to allow us to plot our polynomial regression. You can edit the number of points in the
# linspace after the 2nd comma.
myline_2 = np.linspace(df3["Log Time"].min(), df3["Log Time"].max(), 100)

# Plots a double logarithmic graph of the data and of the polynomial between the two cuts.
plt.figure()
plt.plot(df3["Log Time"], df3["Log Current"])
plt.plot(myline_2, p1(myline_2))
#plt.title("log(Current) against log(Time)")
plt.xlabel("log(Time / s)")
plt.ylabel("log(Current / A)")
plt.show()

# Prints the linear equation of the polynomial regression between the two cuts.
print("The zoomed-in polynomial for the double logarithmic plot:")
print(" ")
print(p1)

# The kneedle algorithm searches for a knee point on a concave decreasing curve. This can be editted
# to be used for a convex and an increasing curve but that is not needed here. The knee point is saved
# into a variable known as kneedle_2
kneedle_2 = KneeLocator(myline_2, p1(myline_2), S = 1.0, curve = "concave", direction = "decreasing")
# The different components of kneedle_2 are given variable names
knee_x = kneedle_2.knee
knee_y = kneedle_2.knee_y
# The values of the logarithmic time and current for the kneedle algorithm alone are printed.
print(" ")
print("Log(Time) value of knee point:", knee_x)
print(" ")

print("Log(Current) value of knee point:", knee_y)
print(kneedle_2.plot_knee())

# Values used as the nearest points to the knee on the graph are chosen arbitrarily by adding and
# subtracting +/- 0.5 * the time of flight found using the kneedle method.
t_min = knee_x + math.log10(0.5 * -knee_x)
t_max = knee_x - math.log10(0.5 * -knee_x)

# 2 dataframes are created with only the data between the cuts and t_min or t_max
df4 = df3[df3["Log Time"] > t_min]
df5 = df3[df3["Log Time"] < t_max]

# The logarithmic time and current values are saved in lists which are then reshaped ready for use in
# the linear regression algorithm.
v = np.array(df4["Log Time"].values.tolist())
w = df4["Log Current"].tolist()
x = v.reshape((-1, 1))
y = w

# A linear regression is found with the slope and intercept being printed for both of the lines used
# in the bisection.
model1 = LinearRegression().fit(x, y)
intercept_1 = model1.intercept_
slope_alpha_2 = model1.coef_
print('Intercept alpha 2:', intercept_1)
print('Slope alpha 2:', slope_alpha_2)

v = np.array(df5["Log Time"].values.tolist())
w = df5["Log Current"].tolist()
x = v.reshape((-1, 1))
y = w

model2 = LinearRegression().fit(x, y)
intercept_2 = model2.intercept_
slope_alpha_1 = model2.coef_
print('Intercept alpha 1:', intercept_2)
print('Slope alpha 1:', slope_alpha_1)

# This finds the logarithmic time and current of the bisection of the two lines and prints
# them out.
x = Symbol('x')
m = solve(intercept_1 -intercept_2 + (slope_alpha_2 - slope_alpha_1) * x, x)
print("The Log(Time) value of the intersection:", m[x])
n = intercept_1 + slope_alpha_2 * m[x]
o = n[0]
print("The Log(Current) value of the intersection:", o)

# The zoomed-in double logarithmic plot is displayed with the bisection lines.
linspace = np.linspace(df2["Log Time"].min(), df2["Log Time"].max(), num=50)
plt.figure()
plt.plot(df3["Log Time"], df3["Log Current"], label = "Raw Data")
plt.plot(myline_2, p1(myline_2), label = "Polynomial")
plt.plot(linspace, slope_alpha_2*linspace + intercept_1, label = "Fit 2")
plt.plot(linspace, slope_alpha_1*linspace + intercept_2, label = "Fit 1")
plt.vlines(knee_x, df3["Log Current"].min(), df3["Log Current"].max(), label = "Knee Point")
#plt.title("log(Current) against log(Time)")
plt.xlabel("log(Time / s)")
plt.ylabel("log(Current / A)")
plt.xlim([df3["Log Time"].min(), df3["Log Time"].max()])
plt.ylim([df3["Log Current"].min(), df3["Log Current"].max()])
plt.legend()
plt.show()

# # The double logarithmic plot is displayed with the cuts, kneedle point, and the bisection lines.
plt.figure()
plt.scatter(df2["Log Time"], df2["Log Current"], label = "Raw Data", s = 5)
plt.plot(linspace, slope_alpha_2*linspace + intercept_1, label = "Fit 2", color = "red")
plt.plot(linspace, slope_alpha_1*linspace + intercept_2, label = "Fit 1", color = "green")
plt.vlines(knee_x, df2["Log Current"].min(), df2["Log Current"].max(), label = "Knee Point")
plt.vlines(-cut_max, df2["Log Current"].min(), df2["Log Current"].max(), label = "Cut 1", linestyles = "dashed")
plt.vlines(-cut_min, df2["Log Current"].min(), df2["Log Current"].max(), label = "Cut 2", linestyles = "dashed")
#plt.title("log(Current) against log(Time)")
plt.xlabel("log(Time / s)")
plt.ylabel("log(Current / A)")
plt.xlim([df2["Log Time"].min(), df2["Log Time"].max()])
plt.ylim([df2["Log Current"].min(), df2["Log Current"].max()])
plt.legend()
plt.savefig(str(graph_savename) + "/Scatter_doublelog" + str(Pad_Voltage) + "V.png", dpi = 300, bbox_inches = 'tight')
plt.show()

print("Slope of line corresponding to Alpha 1:", slope_alpha_1[0])
print("Slope of line corresponding to Alpha 2:", slope_alpha_2[0])
```

```python
# A new dataframe is created including many different data columns as shown below.
data = {'Pad Voltage/V': [Pad_Voltage], 'Resistance/Ohms': [Resistance], 'Thickness/m': [Thickness], 'Time [Fit]/microsecs': [10**6 * 10**(m[x])], 'Time [Knee]/microsecs': [10**6 * 10**(knee_x)], 'Dark current/ micro A': [10**6 * dark_current], 'Photocharge [Raw]/C': [y_int[-1]], 'Photocharge [Fit]/C': [photocharge], 'Photocharge error [Fit]/C': [photocharge_error], 'Alpha1': [1 + slope_alpha_1[0]], 'Alpha2': [-1 - slope_alpha_2[0]], 'Electric Field / (V/m)': [Pad_Voltage / Thickness], 'Mobility [Fit] / cm^2 V^-1 s^-1': [(10000 * (Thickness)**2) / (Pad_Voltage * 10**((m[x])))], 'Mobility [Knee] / cm^2 V^-1 s^-1': [(10000 * (Thickness)**2) / (Pad_Voltage * 10**((knee_x)))], 'sqrt(Electric Field) / (V/m)^0.5': [math.sqrt(Pad_Voltage / Thickness)]}
main_dataframe = pd.DataFrame(data)

# The code checks if a prexisting file exists and appends the data to that dataframe if it does. If it does not
# exist a new csv file is created with generated headers.
if os.path.isfile(savename) == True:
    main_dataframe.to_csv(savename, mode = 'a', header = False, index = False)
else:
    main_dataframe.to_csv(savename, header = True, index = False)
```

```python
# A new dataframe for use in generating plots is created by reading the dataframe created above
# and a Poole-Frenkel plot is created and saved.
new_df = pd.read_csv(savename)

# The logarithm of the mobility is found and saved into the dataframe.
new_df["LogMobilityFit"] = np.log10(new_df["Mobility [Fit] / cm^2 V^-1 s^-1"])
new_df["LogMobilityKnee"] = np.log10(new_df["Mobility [Knee] / cm^2 V^-1 s^-1"])

# A Poole-Frenkel plot is created and saved.
plt.figure()
plt.scatter(new_df["sqrt(Electric Field) / (V/m)^0.5"], np.log10(new_df["Mobility [Fit] / cm^2 V^-1 s^-1"]), label = "Fit")
plt.scatter(new_df["sqrt(Electric Field) / (V/m)^0.5"], np.log10(new_df["Mobility [Knee] / cm^2 V^-1 s^-1"]), label = "Knee")
#plt.title("log(Mobility) against Electric Field^0.5")
plt.xlabel("sqrt(Electric Field) / (V/m)^0.5")
plt.ylabel("log(Mobility / cm^2 V^-1 s^-1)")

# A linear regression is used to generate a trendline.
z = np.polyfit(new_df["sqrt(Electric Field) / (V/m)^0.5"], new_df["LogMobilityFit"], 1)
print(z)
y_hat = np.poly1d(z)(new_df["sqrt(Electric Field) / (V/m)^0.5"])

# The equation of the two linear regressions (for the knee fit and the bisection fit)
# is printed in the corner of the graph.
text = f"$y={z[0]:0.3f}\;x{z[1]:+0.3f}$"
plt.gca().text(0.05, 0.95, text,transform=plt.gca().transAxes,
     fontsize=14, verticalalignment='top')
plt.plot(new_df["sqrt(Electric Field) / (V/m)^0.5"], y_hat, "b--", lw=1)
z = np.polyfit(new_df["sqrt(Electric Field) / (V/m)^0.5"], new_df["LogMobilityKnee"], 1)
print(z)
y_hat = np.poly1d(z)(new_df["sqrt(Electric Field) / (V/m)^0.5"])
text = f"$y={z[0]:0.3f}\;x{z[1]:+0.3f}$"
plt.gca().text(0.05, 0.95, text,transform=plt.gca().transAxes,
     fontsize=14, verticalalignment='bottom')
plt.plot(new_df["sqrt(Electric Field) / (V/m)^0.5"], y_hat, "r--", lw=1)
plt.legend()
plt.savefig(str(graph_savename) + "/mobility_field.png", dpi = 300, bbox_inches = 'tight')
plt.show()

# The photocharge is converted to nC from C and then a graph of photocharge against pad voltage
# is created.
new_df["Photocharge [Fit]/nC"] = new_df["Photocharge [Fit]/C"] * 1000000000
new_df["Photocharge [Raw]/nC"] = new_df["Photocharge [Raw]/C"] * 1000000000
plt.figure()
plt.scatter(new_df["Pad Voltage/V"], new_df["Photocharge [Fit]/nC"], label = "Fit")
plt.scatter(new_df["Pad Voltage/V"], new_df["Photocharge [Raw]/nC"], label = "Raw")
#plt.title("Photocharge against Pad Voltage")
plt.xlabel("Pad Voltage / V")
plt.ylabel("Photocharge / nC")
plt.ylim([0, new_df["Photocharge [Raw]/nC"].max()])
plt.xlim(0,)

# A trendline is plotted and its equation is put in the corner of the graph.
z = np.polyfit(new_df["Pad Voltage/V"], new_df["Photocharge [Raw]/nC"], 1)
print(z)
y_hat = np.poly1d(z)(new_df["Pad Voltage/V"])
text = f"$y={z[0]:0.3f}\;x{z[1]:+0.3f}$"
plt.gca().text(0.05, 0.95, text,transform=plt.gca().transAxes,
     fontsize=14, verticalalignment='top')
plt.plot(new_df["Pad Voltage/V"], y_hat, "r--", lw=1)
plt.legend(loc = "lower right")
plt.savefig(str(graph_savename) + "/photocharge_voltage.png", dpi = 300, bbox_inches = 'tight')
plt.show()

# A graph of knee time v bisection time is plotted with a trendline and its equation.
plt.figure()
plt.scatter(new_df["Time [Fit]/microsecs"], new_df["Time [Knee]/microsecs"])
#plt.title("TOF of Kneedle Method against TOF of Fit Method")
plt.xlabel("TOF for Fit Method / microseconds")
plt.ylabel("TOF for Kneedle Method / microseconds")
z = np.polyfit(new_df["Time [Fit]/microsecs"], new_df["Time [Knee]/microsecs"], 1)
y_hat = np.poly1d(z)(new_df["Time [Fit]/microsecs"])
text = f"$y={z[0]:0.3f}\;x{z[1]:+0.3f}$"
plt.gca().text(0.05, 0.95, text,transform=plt.gca().transAxes,
     fontsize=14, verticalalignment='top')
plt.plot(new_df["Time [Fit]/microsecs"], y_hat, "r--", lw=1)
plt.ylim(0,)
plt.xlim(0,)
plt.savefig(str(graph_savename) + "/kneedletime_fittime.png", dpi = 300, bbox_inches = 'tight')
plt.show()

# Creates a plot of alpha1 against pad voltage.
plt.figure()
plt.scatter(new_df["Pad Voltage/V"], new_df["Alpha1"])
#plt.title("Alpha 1 against Pad Voltage")
plt.xlabel("Pad Voltage / V")
plt.ylabel("Alpha 1")
z = np.polyfit(new_df["Pad Voltage/V"], new_df["Alpha1"], 1)
y_hat = np.poly1d(z)(new_df["Pad Voltage/V"])
text = f"$y={z[0]:0.3f}\;x{z[1]:+0.3f}$"
plt.gca().text(0.05, 0.95, text,transform=plt.gca().transAxes,
     fontsize=14, verticalalignment='top')
plt.plot(new_df["Pad Voltage/V"], y_hat, "r--", lw=1)
plt.savefig(str(graph_savename) + "/alpha1_voltage.png", dpi = 300, bbox_inches = 'tight')
plt.show()

# Creates a plot of alpha2 against pad voltage.
plt.figure()
plt.scatter(new_df["Pad Voltage/V"], new_df["Alpha2"])
#plt.title("Alpha 2 against Pad Voltage")
plt.xlabel("Pad Voltage / V")
plt.ylabel("Alpha 2")
z = np.polyfit(new_df["Pad Voltage/V"], new_df["Alpha2"], 1)
y_hat = np.poly1d(z)(new_df["Pad Voltage/V"])
text = f"$y={z[0]:0.3f}\;x{z[1]:+0.3f}$"
plt.gca().text(0.05, 0.95, text,transform=plt.gca().transAxes,
     fontsize=14, verticalalignment='top')
plt.plot(new_df["Pad Voltage/V"], y_hat, "r--", lw=1)
plt.savefig(str(graph_savename) + "/alpha2_voltage.png", dpi = 300, bbox_inches = 'tight')
plt.show()
```

```python

```

Tool for Analysing Dispersive Time of Flight Data.ipynb


# Supply Network Design 1

## Objective and Prerequisites

This model is an example of a supply network design problem. Given a set of factories, depots, and customers, the goal is to determine how to satisfy customer demand while minimizing shipping costs. This problem can be regarded as one of finding the minimum cost flow through a network.

This model is example 19 from the fifth edition of Model Building in Mathematical Programming, by H. Paul Williams on pages 273-275 and 330-332.

This example is of beginning difficulty; we assume that you know Python and have some knowledge of the Gurobi Python API and building mathematical optimization models.

**Download the Repository**

You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip).

**Gurobi License**

In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-LOG-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_supply-network-design_1_COM_EVAL_GITHUB_&utm_term=supply-network-design&utm_content=C_JPM) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=3PW&utm_medium=OT&utm_campaign=WW-MU-LOG-OR-O_LEA-PR_NO-Q3_FY20_WW_JPME_supply-network-design_1_ACADEMIC_EVAL_GITHUB_&utm_term=supply-network-design&utm_content=C_JPM) as an *academic user*.

---
## Problem Description

In this problem, we have six end customers, each with a known demand for a product.  Customer demand can be satisfied from a set of four depots, or directly from a set of two factories.  Each depot can support a maximum volume of product moving through it, and each factory can produce a maximum amount of product.  There are known costs associated with transporting the product, from a factory to a depot, from a depot to a customer, or from a factory directly to a customer.

Our supply network has two factories, in Liverpool and Brighton, that produce a product.  Each has a maximum production capacity:

| Factory | Supply (tons) |
| --- | --- |
| Liverpool | 150,000 |
| Brighton |  200,000 |

The product can be shipped from a factory to a set of four depots.  Each depot has a maximum throughput.  Depots don't produce or consume the product; they simply pass the product on to customers.

| Depot | Throughput (tons) |
| --- | --- |
| Newcastle | 70,000 |
| Birmingham | 50,000 |
| London | 100,000 |
| Exeter | 40,000 |

Our network has six customers, each with a given demand.

| Customer | Demand (tons) |
| --- | --- |
| C1 | 50,000 |
| C2 | 10,000 |
| C3 | 40,000 |
| C4 | 35,000 |
| C5 | 60,000 |
| C6 | 20,000 |

Shipping costs are given in the following table (in dollars per ton).  Columns are source cities and rows are destination cities.  Thus, for example, it costs $1 per ton to ship the product from Liverpool to London.  A '-' in the table indicates that that combination is not possible, so for example it is not possible to ship from the factory in Brighton to the depot in Newcastle.

| To | Liverpool | Brighton | Newcastle | Birmingham | London | Exeter |
| --- | --- | --- | --- | --- | --- | --- |
| Depots |
| Newcastle  | 0.5 |   - |
| Birmingham | 0.5 | 0.3 |
| London     | 1.0 | 0.5 |
| Exeter     | 0.2 | 0.2 |
| Customers |
| C1 | 1.0 | 2.0 |   - | 1.0 |   - |   - |
| C2 |   - |   - | 1.5 | 0.5 | 1.5 |   - |
| C3 | 1.5 |   - | 0.5 | 0.5 | 2.0 | 0.2 |
| C4 | 2.0 |   - | 1.5 | 1.0 |   - | 1.5 |
| C5 |   - |   - |   - | 0.5 | 0.5 | 0.5 |
| C6 | 1.0 |   - | 1.0 |   - | 1.5 | 1.5 |

The question to be answered is how to satisfy the demands of the end customers while minimizing shipping costs.

---
## Model Formulation

### Sets and Indices

$f \in \text{Factories}=\{\text{Liverpool}, \text{Brighton}\}$

$d \in \text{Depots}=\{\text{Newcastle}, \text{Birmingham}, \text{London}, \text{Exeter}\}$

$c \in \text{Customers}=\{\text{C1}, \text{C2}, \text{C3}, \text{C4}, \text{C5}, \text{C6}\}$

$\text{Cities} = \text{Factories} \cup \text{Depots} \cup \text{Customers}$

### Parameters

$\text{cost}_{s,t} \in \mathbb{R}^+$: Cost of shipping one ton from source $s$ to destination $t$.

$\text{supply}_f \in \mathbb{R}^+$: Maximum possible supply from factory $f$ (in tons).

$\text{through}_d \in \mathbb{R}^+$: Maximum possible flow through depot $d$ (in tons).

$\text{demand}_c \in \mathbb{R}^+$: Demand for goods at customer $c$ (in tons).

### Decision Variables

$\text{flow}_{s,t} \in \mathbb{N}^+$: Quantity of goods (in tons) that is shipped from source $s$ to destionation $t$.

### Objective Function

- **Cost**: Minimize total shipping costs.

\begin{equation}
\text{Minimize} \quad Z = \sum_{(s,t) \in \text{Cities} \times \text{Cities}}{\text{cost}_{s,t}*\text{flow}_{s,t}}
\end{equation}

### Constraints

- **Factory output**: Flow of goods from a factory must respect maximum capacity.

\begin{equation}
\sum_{t \in \text{Cities}}{\text{flow}_{f,t}} \leq \text{supply}_{f} \quad \forall f \in \text{Factories}
\end{equation}

- **Customer demand**: Flow of goods must meet customer demand.

\begin{equation}
\sum_{s \in \text{Cities}}{\text{flow}_{s,c}} = \text{demand}_{c} \quad \forall c \in \text{Customers}
\end{equation}

- **Depot flow**: Flow into a depot equals flow out of the depot.

\begin{equation}
\sum_{s \in \text{Cities}}{\text{flow}_{s,d}} =
\sum_{t \in \text{Cities}}{\text{flow}_{d,t}}
\quad \forall d \in \text{Depots}
\end{equation}

- **Depot capacity**: Flow into a depot must respect depot capacity.

\begin{equation}
\sum_{s \in \text{Cities}}{\text{flow}_{s,d}} \leq \text{through}_{d}
\quad \forall d \in \text{Depots}
\end{equation}

---
## Python Implementation

We import the Gurobi Python Module and other Python libraries.

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

import gurobipy as gp
from gurobipy import GRB

# tested with Python 3.7.0 & Gurobi 9.0
```

## Input Data
We define all the input data for the model.

```python
# Create dictionaries to capture factory supply limits, depot throughput limits, and customer demand.

supply = dict({'Liverpool': 150000,
               'Brighton': 200000})

through = dict({'Newcastle': 70000,
                'Birmingham': 50000,
                'London': 100000,
                'Exeter': 40000})

demand = dict({'C1': 50000,
               'C2': 10000,
               'C3': 40000,
               'C4': 35000,
               'C5': 60000,
               'C6': 20000})

# Create a dictionary to capture shipping costs.

arcs, cost = gp.multidict({
    ('Liverpool', 'Newcastle'): 0.5,
    ('Liverpool', 'Birmingham'): 0.5,
    ('Liverpool', 'London'): 1.0,
    ('Liverpool', 'Exeter'): 0.2,
    ('Liverpool', 'C1'): 1.0,
    ('Liverpool', 'C3'): 1.5,
    ('Liverpool', 'C4'): 2.0,
    ('Liverpool', 'C6'): 1.0,
    ('Brighton', 'Birmingham'): 0.3,
    ('Brighton', 'London'): 0.5,
    ('Brighton', 'Exeter'): 0.2,
    ('Brighton', 'C1'): 2.0,
    ('Newcastle', 'C2'): 1.5,
    ('Newcastle', 'C3'): 0.5,
    ('Newcastle', 'C5'): 1.5,
    ('Newcastle', 'C6'): 1.0,
    ('Birmingham', 'C1'): 1.0,
    ('Birmingham', 'C2'): 0.5,
    ('Birmingham', 'C3'): 0.5,
    ('Birmingham', 'C4'): 1.0,
    ('Birmingham', 'C5'): 0.5,
    ('London', 'C2'): 1.5,
    ('London', 'C3'): 2.0,
    ('London', 'C5'): 0.5,
    ('London', 'C6'): 1.5,
    ('Exeter', 'C3'): 0.2,
    ('Exeter', 'C4'): 1.5,
    ('Exeter', 'C5'): 0.5,
    ('Exeter', 'C6'): 1.5
})
```

## Model Deployment

We create a model and the variables. The variables simply capture the amount of product that flows along each allowed path between a source and destination.  Objective coefficients are provided here (in $\text{cost}$) , so we don't need to provide an optimization objective later.

```python
model = gp.Model('SupplyNetworkDesign')
flow = model.addVars(arcs, obj=cost, name="flow")
```

    Using license file c:\gurobi\gurobi.lic
    Set parameter TokenServer to value SANTOS-SURFACE-

Our first constraints require the total flow along arcs leaving a factory to be at most as large as the supply capacity of that factory.

```python
# Production capacity limits

factories = supply.keys()
factory_flow = model.addConstrs((gp.quicksum(flow.select(factory, '*')) <= supply[factory]
                                 for factory in factories), name="factory")
```

Our next constraints require the total flow along arcs entering a customer to be equal to the demand from that customer.

```python
# Customer demand

customers = demand.keys()
customer_flow = model.addConstrs((gp.quicksum(flow.select('*', customer)) == demand[customer]
                                  for customer in customers), name="customer")
```

Our final constraints relate to depots.  The first constraints require that the total amount of product entering the depot must equal the total amount leaving.

```python
# Depot flow conservation

depots = through.keys()
depot_flow = model.addConstrs((gp.quicksum(flow.select(depot, '*')) == gp.quicksum(flow.select('*', depot))
                               for depot in depots), name="depot")
```

The second set limits the product passing through the depot to be at most equal the throughput of that deport.

```python
# Depot throughput

depot_capacity = model.addConstrs((gp.quicksum(flow.select('*', depot)) <= through[depot]
                                   for depot in depots), name="depot_capacity")
```

We now optimize the model

```python
model.optimize()
```

    Gurobi Optimizer version 9.0.0 build v9.0.0rc2 (win64)
    Optimize a model with 16 rows, 29 columns and 65 nonzeros
    Model fingerprint: 0x3607c855
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      Objective range  [2e-01, 2e+00]
      Bounds range     [0e+00, 0e+00]
      RHS range        [1e+04, 2e+05]
    Presolve removed 1 rows and 0 columns
    Presolve time: 0.03s
    Presolved: 15 rows, 29 columns, 64 nonzeros

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    1.4800000e+05   1.812500e+04   0.000000e+00      0s
           7    1.9850000e+05   0.000000e+00   0.000000e+00      0s

    Solved in 7 iterations and 0.03 seconds
    Optimal objective  1.985000000e+05

---
## Analysis

Product demand from all of our customers can be satisfied for a total cost of $\$198,500$. The optimal plan is as follows.

```python
product_flow = pd.DataFrame(columns=["From", "To", "Flow"])
for arc in arcs:
    if flow[arc].x > 1e-6:
        product_flow = product_flow.append({"From": arc[0], "To": arc[1], "Flow": flow[arc].x}, ignore_index=True)
product_flow.index=[''] * len(product_flow)
product_flow
```

      From
      To
      Flow

      Liverpool
      C1
      50000.0

      Liverpool
      C6
      20000.0

      Brighton
      Birmingham
      50000.0

      Brighton
      London
      55000.0

      Brighton
      Exeter
      40000.0

      Birmingham
      C2
      10000.0

      Birmingham
      C4
      35000.0

      Birmingham
      C5
      5000.0

      London
      C5
      55000.0

      Exeter
      C3
      40000.0

---
## References

H. Paul Williams, Model Building in Mathematical Programming, fifth edition.

Copyright © 2020 Gurobi Optimization, LLC

```python

```


$\newcommand{\myvector}[2]{\left\{ \begin{array}{c}#1\\#2\end{array} \right\}}$
$\newcommand{\mymatrix}[4]{\left[ \begin{array}{c}#1&#2\\#3&#4\end{array} \right]}$
$\newcommand{\sM}[1]{\boldsymbol{s}_{\mathrm{M#1}}}$
$\newcommand{\rvector}[1]{\boldsymbol{r}_{\mathrm{#1}}}$

# APPENDIX D:  Bayesian and least-squares results lack of divergence

**Note:** This notebook was updated on 2021-05-18.  Previous versions of this notebook erroneously used a highly informative prior distribution for Bayesian IK calculations. This error has been corrected below in the [Systematic Approach Comparison](#systematic_approach_comparison) section. Corrected text has been highlighted in red. For additional details please refer to Pohl et al. (2021) and also to the new Appendix F.

Pohl AJ, MR Schofield, R Ferber (2021). Examination of a Bayesian approach to inverse kinematics. *J Biomech*, in press.

This Appendix originally described a minimal inverse kinematics model for which Bayesian and least-sqaures results diverged. However, this divergence was caused by overly informative prior distributions. In interest of consistency with the previous Appendix D, this notebook makes minimal adjustments. Please refer to Pohl et al. (2021) and the new Appendix F for detailed explanations.

### Model

The planar mechanism depicted below has two degrees of freedom (DOF): one translational and one rotational. The first (slider) segment moves to a position $y$ and the second segment rotates about point A to an angle $\theta$. Two markers (M1 and M2) are rigidly fixed to the rotating segment and have fixed local positions:  $\sM{1} = \myvector{35}{0}$ and $\sM{2} = \myvector{45}{0}$.

The three points' true global positions are:

\begin{align}
\rvector{A} &= \myvector{0}{y}\\ \\
\rvector{M1} &= \rvector{A} + \boldsymbol{R} \ \sM{1}\\ \\
\rvector{M2} &= \rvector{A} + \boldsymbol{R} \ \sM{2}
\end{align}

where $\boldsymbol{R}$ is the rotation matrix:

\begin{equation}
\boldsymbol{R} = \mymatrix{\cos\theta}{-\sin\theta}{\sin\theta}{\cos\theta}
\end{equation}

### Inverse kinematics (IK) problem

Imagine that we have measured the following marker positions:

\begin{align}
\rvector{M1}' &= \myvector{33.51}{12.11}\\ \\
\rvector{M2}' &= \myvector{42.63}{16.18}
\end{align}

The IK problem is to estimate $y$ and $\theta$ given these measurements.

For argument's sake let's say we also happen to know that the true values of the unknown variables are: $y$ = 0 and $\theta$ = 20 deg. In this case the true global marker positions are:

\begin{align}
\rvector{M1} &= \myvector{32.89}{11.97}\\ \\
\rvector{M2} &= \myvector{42.29}{15.39}
\end{align}

Let's assemble everything we know into Python for later use in our IK solutions.

```python
%matplotlib inline

from math import sin,cos,radians,degrees
import numpy as np
from matplotlib import pyplot

#(0) Define known values:
### local positions:
sM1        = np.array([35.0, 0.0])
sM2        = np.array([45.0, 0.0])
### measurements:
rpM1       = np.array([33.51, 12.11])
rpM2       = np.array([42.63, 16.18])
### true values: (not used in IK solutions, but useful for visualization)
y_true     = 0.0
theta_true = radians(20)

#(1) Compute global marker positions given y and theta:
def rotation_matrix(theta):
    '''Construct rotation matrix'''
    c,s    = cos(theta), sin(theta)
    R      = np.matrix(  [[c, -s], [s, c]] )
    return R

def rotate(R, r):
    '''Rotate a position vector r using rotation matrix R'''
    return np.asarray(  (R * np.mat(r).T)  ).flatten()

def get_positions(y, theta):
    '''Compute global marker positions given y and theta'''
    rA     = np.array([0, y])       #global A position
    R      = rotation_matrix(theta)
    rM1    = rA + rotate(R, sM1)    #global M1 position
    rM2    = rA + rotate(R, sM2)    #global M2 position
    return rM1,rM2

rM1,rM2    = get_positions(y_true, theta_true)

#(2) Plot:
pyplot.figure()
ax = pyplot.axes()
ax.plot(rM1[0],  rM1[1],  'o', markeredgecolor='k', markerfacecolor='w', ms=40)
ax.plot(rM2[0],  rM2[1],  'o', markeredgecolor='k', markerfacecolor='w', ms=40)
ax.plot(rpM1[0], rpM1[1], 'o', markeredgecolor='r', markerfacecolor='r', ms=20)
ax.plot(rpM2[0], rpM2[1], 'o', markeredgecolor='r', markerfacecolor='r', ms=20)
ax.text(33, 11, 'True position',     color='k', size=14)
ax.text(34, 12, 'Measured position', color='r', size=14)
ax.set_xlabel('Global X position  (mm)', size=18)
ax.set_ylabel('Global Y position  (mm)', size=18)
pyplot.axis('equal')
pyplot.show()
```

# Least-squares solution

This IK problem is relatively simple, so it could be solved using an analytical approach similar to the one presented in Appendix B. However, we'll be a bit lazy and instead solve the IK problem numerically, using the "get_positions" function that we've already written above.

Once we set values for $y$ and $\theta$ we know the global positions of $\rvector{M1}$ and $\rvector{M2}$, so we can compute the difference between those positions and our measurements as:

\begin{align}
\boldsymbol{\epsilon}_1 &= \rvector{M1}' - \rvector{M1}\\
\boldsymbol{\epsilon}_2 &= \rvector{M2}' - \rvector{M2}
\end{align}

We can then express total measurement error as follows:

\begin{equation}
f(y, \theta) =
\big| \ \boldsymbol{\epsilon}_1 \ \big| \ ^2 \
+ \
\big| \ \boldsymbol{\epsilon}_2 \ \big| \ ^2
\end{equation}

Although we have called $f(y, \theta)$ a "measurement error" function, the opposite perspective is equally apt: "guess error".  That is, if we know our measurement values ($\rvector{M1}'$, $\rvector{M2}'$) and then use $y$ and $\theta$ to guess the true marker positions ($\rvector{M1}$, $\rvector{M2}$), then $f(y, \theta)$ represents our guess error. We nevertheless use "measurement error" below because we presume there is no guess which can yield zero error.

For later purposes let's bundle our two unknown variables into a generalized "unknowns" vector $\boldsymbol{x}$ as follows:

\begin{align}
\boldsymbol{x} = \myvector{y}{\theta}
\end{align}

Now our error function is:

\begin{align}
f(\boldsymbol{x}) =
\big| \ \boldsymbol{\epsilon}_1 \ \big| \ ^2 \
+ \
\big| \ \boldsymbol{\epsilon}_2 \ \big| \ ^2
\end{align}

Let's implement this function in Python and then explore its output to make sure it's returning reasonable values.

```python
def measurement_error(x):
    y,theta = x
    rM1,rM2 = get_positions(y, theta)
    e1,e2   = rpM1 - rM1, rpM2 - rM2
    e1,e2   = np.linalg.norm(e1), np.linalg.norm(e2)
    f       = e1**2 + e2**2
    return f

x = [0, radians(20)]
print( measurement_error(x) )
```

    1.145633045191809

Here the total measurement error is 1.146 mm$^2$. By moving to a different vertical position (e.g. $y$=2) or a different angular position ($\theta$=10 deg) we see that measurement error increases:

```python
print(   measurement_error( [2, radians(20)] )   )
print(   measurement_error( [0, radians(10)] )   )
```

    5.4320789094058
    110.13801125048406

Since we can compute the error function $f(y,\theta)$ for arbitrary $y$ and $\theta$ values we can view it as a three-dimensional surface as follows:

```python
from matplotlib import cm
from mpl_toolkits.mplot3d import Axes3D

### Compute error for a range of y and theta values:
Y       = np.linspace(-2, 5, 41)
THETA   = np.radians( np.linspace(14, 23, 41) )
Y,THETA = np.meshgrid(Y, THETA)
F       = [measurement_error(x)   for x in zip(Y.flatten(), THETA.flatten())]
F       = np.reshape(F, Y.shape)
F[F>4]  = 4  #cap all values at 4 to more clearly see the function's minimum

### Plot:
fig     = pyplot.figure()
ax      = fig.gca(projection='3d')
ax.plot_surface(Y, np.degrees(THETA), F, cmap=cm.jet, rstride=1, cstride=1, linewidth=0.2)
ax.set_xlabel(r'$y$  (mm)', size=18)
ax.set_ylabel(r'$\theta$  (deg)', size=18)
ax.set_zlabel(r'$f(y, \theta)$', size=18)
pyplot.show()
```

In the figure above it appears that our error function's minimum value occurs for approximately $y$=1.5 and $\theta$=19, but let's find its minimum value algorithmically using **scipy.optimize.minimize** as follows:

```python
from scipy import optimize

x0       = [0, radians(20)]  #initial (y, theta) guess
results  = optimize.minimize(measurement_error, x0)

print('Least-squares estimates:')
print('   y     = %.3f' %results.x[0])
print('   theta = %.3f' %degrees(results.x[1]) )
```

    Least-squares estimates:
       y     = 1.321
       theta = 18.700

Due to measurement error our final estimates for $y$ and $\theta$ are slightly different from their true values ($y$=0, $\theta$=20).

# Bayesian approach

The figure below depicts a stochastic forward-kinematics model for this system.  Once the numerical values of $y$ and $\theta$ are known we know the values of our deterministic variables $\rvector{M1}$ and $\rvector{M2}$.  Similarly, once the numerical value of our measurement error $\epsilon$ is known we will also know the value of our generalized measurement vector $\boldsymbol{q}$, where $\boldsymbol{q}$ contains all four measured coordinates from $\rvector{M1}'$ and $\rvector{M2}'$. Let's implement this model in PyMC.

```python
import pymc

q_observed = np.asarray([rpM1, rpM2]).flatten() #measured positions
tau        = 20  #presumed measurement precision (we'll relax this later)
y          = pymc.Normal("y", 0, 1)  #prior for y
theta      = pymc.Uniform("theta", radians(-45), radians(45))  #prior for theta

@pymc.deterministic
def observations_model(y=y, theta=theta):
    rM1,rM2 = get_positions(y, theta)
    q       = np.asarray([rM1, rM2]).flatten()
    return q
q_model   = pymc.Normal("q", observations_model, tau, value=q_observed, observed=True)
```

Now that we have a model of our observations we can set values for $y$ and $\theta$ and then check what kinds of results our model produces.

```python
y.set_value(0)
theta.set_value( radians(20) )
print( q_model.random() )
print( q_model.random() )
print( q_model.random() )
```

    [32.99941182 12.00754189 42.07943539 15.2370373 ]
    [32.90132008 11.85785493 41.9218965  15.34236793]
    [32.65119733 12.20083499 42.50388501 15.61976264]

The first two columns represent $\rvector{M1}'$ and the last two columns represent $\rvector{M2}'$.  We can see that these values are similar to our actual measured values, so our model appears to have been specified correctly.

Variability exists in these values because we have not set the value of our last stochastic variable ($\epsilon$); this variability is built into the model above via the parameter "tau" which specifies our measurement precision.  If we raise tau to a very high value we will see that the model's results become much less variable.

```python
tau       = 1e9
q_model   = pymc.Normal("q", observations_model, tau, value=q_observed, observed=True)
print( q_model.random() )
print( q_model.random() )
print( q_model.random() )
print( q_model.random() )
print( q_model.random() )
```

    [32.88922646 11.97069963 42.28610841 15.39090322]
    [32.88926753 11.97069683 42.28612075 15.39091761]
    [32.88925811 11.97069876 42.28614937 15.39092467]
    [32.88921084 11.97069041 42.28619638 15.39091307]
    [32.88927265 11.97069497 42.28614831 15.39087162]

Now let's relax our assumptions regarding tau's true value, then use PyMC to run Markov-Chain Monte-Carlo simulations with the goal of finding posterior distributions for $y$ and $\theta$ (and tau) which are most consistent with the observed marker positions.

```python
tau        = pymc.Normal("tau", 20, 1)  #prior for measurement precision
y          = pymc.Normal("y", 0, 1)     #prior for y
theta      = pymc.Uniform("theta", radians(-45), radians(45))  #prior for theta

@pymc.deterministic
def observations_model(y=y, theta=theta):
    rM1,rM2 = get_positions(y, theta)
    q       = np.asarray([rM1, rM2]).flatten()
    return q
q_model   = pymc.Normal("q", observations_model, tau, value=q_observed, observed=True)

mcmc      = pymc.MCMC([q_model, y, theta, tau])
mcmc.sample(40000, 20000)
Y         = mcmc.trace('y')[:]
THETA     = np.degrees( mcmc.trace('theta')[:] )
TAU       = np.degrees( mcmc.trace('tau')[:] )

print('\n\nBayesian estimates:')
print('   y     = %.3f' %Y.mean() )
print('   theta = %.3f' %THETA.mean() )
```

     [-----------------100%-----------------] 40000 of 40000 complete in 7.7 sec

    Bayesian estimates:
       y     = 1.073
       theta = 19.029

Our Bayesian estimates for $y$ and $\theta$ are somewhat closer to their true values than are our least-squares estimates. But this is just for one case.  Let's now compare the two approaches' results more systematically.

# Systematic approach comparison

First let's pick some new true values for $y$ and $\theta$ and then generate ten random datasets based on those true values.

```python
y_true      = -0.1
theta_true  = radians(5)
rM1,rM2     = get_positions(y_true, theta_true)
q_true      = np.array([rM1, rM2]).flatten()

nIterations = 10
noise_amp   = 0.5
np.random.seed(0)
Q_obs       = q_true + noise_amp * np.random.randn(nIterations, 4)

print(Q_obs)
```

    [[35.74884061  3.1505296  45.31813041  4.94245502]
     [35.80059343  2.46181206 45.30380562  3.74632982]
     [34.81520501  3.15575025 44.9007832   4.54914518]
     [35.2473333   3.0112885  45.05069303  3.98884559]
     [35.61385397  2.84787186 44.98529526  3.39496055]
     [33.59031953  3.27726029 45.26097951  3.45092591]
     [36.00169175  2.22326816 44.85164067  3.7284165 ]
     [35.63320404  3.68513038 44.90623513  4.01108968]
     [34.42292156  1.96005276 44.65480534  3.90018291]
     [35.48195977  3.55164092 44.63509801  3.67085705]]

These are the ten measurements we'll test. Let's first compute the least-squares solutions for each set of measurements.

```python
def measurement_error(x, q_obs):
    y,theta = x
    rpM1    = q_obs[:2]
    rpM2    = q_obs[2:]
    rM1,rM2 = get_positions(y, theta)
    e1,e2   = rpM1 - rM1, rpM2 - rM2
    e1,e2   = np.linalg.norm(e1), np.linalg.norm(e2)
    f       = e1**2 + e2**2
    return f

def solution_ls(q_obs):
    x0      = [y_true, theta_true]  #initial (y, theta) guess
    results = optimize.minimize(measurement_error, x0, args=(q_obs,))
    y,theta = results.x
    return y, degrees(theta)

np.set_printoptions(precision=3, suppress=True)
RESULTS_LS  = np.array([solution_ls(q_obs)  for q_obs in Q_obs])
print('Least-squares results:')
print('   y     = %s' %RESULTS_LS[:,0])
print('   theta = %s' %RESULTS_LS[:,1])
```

    Least-squares results:
       y     = [ 0.568  0.501 -0.742  0.672  1.625  1.289 -0.372  2.901 -3.663  3.141]
       theta = [4.988 3.732 6.596 4.055 2.144 2.974 4.801 1.356 9.488 0.674]

Next let's get Bayesian estimates for each dataset.

**2021-05-14 update:**   Code in subsequent cells has been updated. While a variety of minor changes have been made, only one major change has been:  use of less-informative priors  in `solution_bayesian`;  this function now uses only the LS solution and not the true solution to inform the priors. For a more systematic comparison of priors see Appendix F.

```python
def solution_bayesian(q_obs):
    qls     = solution_ls( q_obs )  # LS solution (y,theta)
    yls     = qls[0]                # LS solution (y)
    thetals = radians( qls[1] )     # LS solution (theta)
    tau     = pymc.Normal("tau", 1/(noise_amp**2), 1)    # a highly informative prior for noise magnitude
    y       = pymc.Normal("y", yls, 1, value=yls)        # a moderately informative prior for y
    theta   = pymc.Uniform("theta", thetals-radians(45), thetals+radians(45), value=thetals)  # a moderately informative prior for y

    @pymc.deterministic
    def observations_model(y=y, theta=theta):
        rM1,rM2 = get_positions(y, theta)
        q       = np.asarray([rM1, rM2]).flatten()
        return q
    q_model   = pymc.Normal("q", observations_model, tau, value=q_obs, observed=True)

    mcmc      = pymc.MCMC([q_model, y, theta, tau])
    mcmc.sample(40000, 20000, progress_bar=False)
    Y         = mcmc.trace('y')[:]
    THETA     = np.degrees( mcmc.trace('theta')[:] )

    return Y.mean(), THETA.mean()

RESULTS_B = np.zeros((nIterations,2))
for i,q_obs in enumerate(Q_obs):
    print('Iteration %d of %d...' %(i+1, nIterations))
    y,theta = solution_bayesian(q_obs)
    RESULTS_B[i] = [y, theta]
    print('   y = %.3f,  theta = %.3f' %(y,theta))

```

    Iteration 1 of 10...
       y = 0.572,  theta = 4.991
    Iteration 2 of 10...
       y = 0.515,  theta = 3.716
    Iteration 3 of 10...
       y = -0.816,  theta = 6.715
    Iteration 4 of 10...
       y = 0.591,  theta = 4.171
    Iteration 5 of 10...
       y = 1.732,  theta = 1.978
    Iteration 6 of 10...
       y = 1.198,  theta = 3.120
    Iteration 7 of 10...
       y = -0.354,  theta = 4.773
    Iteration 8 of 10...
       y = 2.892,  theta = 1.368
    Iteration 9 of 10...
       y = -3.630,  theta = 9.439
    Iteration 10 of 10...
       y = 3.171,  theta = 0.640

Here is a summary of IK error for the two approaches:

```python
x_true   = [y_true, degrees(theta_true)]
error_LS = RESULTS_LS - x_true
error_B  = RESULTS_B - x_true

print('Average absolute error (least-squares):')
print('   y: %.3f,  theta: %.3f' %tuple( np.abs(error_LS).mean(axis=0) ) )
print('Average absolute error (Bayesian):')
print('   y: %.3f,  theta: %.3f' %tuple( np.abs(error_B).mean(axis=0) ) )

```

    Average absolute error (least-squares):
       y: 1.587,  theta: 2.136
    Average absolute error (Bayesian):
       y: 1.587,  theta: 2.140

On average there is little-to no improvement over the LS solutions.

Finally, let's check how the least-squares and Bayesian approaches compare on a case-by-case basis.

```python
error_difference = np.abs(error_LS) - np.abs(error_B)
print(error_difference)
```

    [[-0.004  0.002]
     [-0.014 -0.016]
     [-0.073 -0.119]
     [ 0.081  0.116]
     [-0.107 -0.166]
     [ 0.091  0.147]
     [ 0.018 -0.027]
     [ 0.01   0.012]
     [ 0.033  0.049]
     [-0.03  -0.034]]

Positive values indicate that the least-squares solution had greater error and negative values indicate the opposite.

This again shows that the Bayesian approach yields little improvement.

# Summary

This Appendix shows that Bayesian and least-squares (LS) inverse kinematics (IK) estimates are not expected to diverge for simple planar rotations. Refer to Appendix F for a consideration of how informative priors lead to different results like those reported in the original Appendix D.


# Table of Contents

1  Practice Problems

1.1  2E1.

1.2  2E2.

1.3  2E3.

1.4  2E4.

1.5  2M1.

1.6  2M2.

1.7  2M3.

1.8  2M4.

1.9  2M5.

1.10  2M6.

1.11  2M7.

1.12  2H1.

1.13  2H2.

1.14  2H3.

1.15  2H4.

## Import Statements

```python
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import pymc3 as pm
import scipy.stats as stats

%config InlineBackend.figure_format = 'retina'
plt.style.use(['seaborn-colorblind', 'seaborn-darkgrid'])
```

## Helper Functions

```python
def posterior_grid_approx(prior, success=6, tosses=9):
    """
        This function helps calculate a grid approximation of the posterior distribution.

        Parameters:
            prior: np.array
                A distribution representing our state of knowledge before seeing the data.
                Number of items should be the same as number of grid points.

            success: integer
                Number of successes.

            tosses: integer
                Number of tosses (i.e. successes + failures).

        Returns:
            p_grid: np.array
                Evenly-spaced out grid between 0 and 1.

            posterior: np.array
                The posterior distribution.

    """
    # define grid
    p_grid = np.linspace(0, 1, prior.shape[0])

    # compute likelihood at each point in the grid
    likelihood = stats.binom.pmf(success, tosses, p_grid)

    # compute product of likelihood and prior
    unstd_posterior = likelihood * prior

    # standardize the posterior, so it sums to 1
    posterior = unstd_posterior / unstd_posterior.sum()
    return p_grid, posterior

def grid_approx_plot(
    w=6,
    n=9,
    prior=np.repeat(5,100),
    x_label='probability of water'
):
    """
        This function helps calculate a grid approximation of the posterior distribution,
        and then plots it.

        Parameters:
            prior: np.array
                A distribution representing our state of knowledge before seeing the data.
                Number of items should be the same as number of grid points.

            w: integer
                Number of successes.

            n: integer
                Number of trials.

    """
    p_grid, posterior = posterior_grid_approx(
        success=w,
        tosses=n,
        prior=prior
    )
    plt.plot(p_grid, posterior, 'o-', label=f'success = {w}\ntosses = {n}')
    plt.xlabel(x_label)
    plt.ylabel('posterior probability')
#     plt.title('{} points'.format(points))
    plt.legend(loc=0)

    return p_grid, posterior
```

# Practice Problems

## 2E1.

Which of the expressions below correspond to the statement: _the probability of rain on Monday_?

(1) $Pr(rain)$
* Nope. This is the averaged probability of rain.

(2) $Pr(rain\mid Monday)$
* Yes. Given that it is Monday, what is the probability of rain?

(3) $Pr(Monday \mid rain)$
* No. This is the probability of being Monday given that it is raining.

(4) $Pr(rain, Monday) / Pr(Monday)$
* Yes. This is equivalent to the second option.

## 2E2.

Which of the expressions below correspond to the expression: $Pr(Monday \mid rain)$?

(1) The probability of rain on Monday.
* No. This is $Pr(rain \mid Monday)$.

(2) The probability of rain, given that it is Monday.
* No. This is $Pr(rain \mid Monday)$.

(3) The probability that it is Monday, given that it is raining.
* Yes. This is $Pr(Monday \mid rain)$.

(4) The probability that it is Monday and that it is raining.
* No. This is $Pr(Monday, rain)$.

## 2E3.

Which of the expressions below correspond to the statement:  _the probability that it is Monday, given that it is raining_?

(1) $Pr(Monday \mid rain)$
* Yes. By definition.

(2) $Pr(rain \mid Monday)$
* No. This is probability of rain given that it is a Monday.

(3) $Pr(rain\mid Monday)Pr(Monday)$
* No. This is equivalent to $Pr(rain, Monday)$, or the probability that it is raining and it is Monday.

(4) $Pr(rain\mid Monday)Pr(Monday) / Pr(rain)$
* This is equivalent to $Pr(rain, Monday) / Pr(rain)$, which then simplifies to $Pr(Monday \mid rain)$. So yes.

(5) $Pr(Monday \mid rain)Pr(rain) / Pr(Monday)$
* This simplifies to $Pr(Monday, rain) / Pr(Monday) = Pr(rain \mid Monday)$, so nope.

## 2E4.

The Bayesian statistician Bruno de Finetti (1906-1985) began his book on probability theory with the declaration: "PROBABILITY DOES NOT EXIST." The capitals appeared in the original, so I imagine de Finetti wanted us to shout this statement. What he meant is that probabilty is a device for describing uncertainty from the perspective of an observer with limited knowledge; it has no objective reality. Discuss the globe tossing example from the chapter, in light of this statement. What does it mean to say "the probability of water is 0.7?"

Ans:

I think he meant that probability only exists in the "small world." It represents a person's state of uncertainty on some aspect of the real world. For example, in the real world, water is objectively some proportion of the whole Earth. The true proportion of water in Earth is probably some precise number, for example: 0.712345678... However, when a Bayesian says that "the probability of water is 0.7," this implies that this person _believes_ the ratio is about 0.7. The person is betting that the ratio is close to 0.7.

## 2M1.

Recall the globe tossing model from the chapter. Compute and plot the grid approximate posterior distribution for each of the following sets of observations. In each case, assume a uniform prior _p_.

(1) W,W,W

```python
_, _ = grid_approx_plot(n=3,w=3, prior=np.repeat(5,int(1e3)) )
```

(2) W,W,W,L

```python
_,_ = grid_approx_plot(n=4, w=3, prior=np.repeat(5,int(1e3)) )
```

(3) L,W,W,L,W,W,W

```python
_, _ = grid_approx_plot(n=7, w=5, prior=np.repeat(5,int(1e3)))
```

## 2M2.

Now assume a prior for _p_ that is equal to zero when $p < 0.5$ and is a positive constant when $p >= 0.5$. Again, compute and plot the grid approximate posterior distribution for each of the sets of observations in the problem just above.

```python
_, _ = grid_approx_plot(
    n=7,
    w=5,
    prior=( np.linspace(0, 1, int(1e3)) >= 0.5).astype(int))
```

## 2M3.

Suppose there are two globes, one for Earth and one for Mars. The Earth globe is 70% covered in water. The Mars globe is 100% land. Further suppose that one of these globes--you don't know which--was tossed in the air and produced a "land" observation. Assume that each globe was equally likely to be tossed. Show that the posterior probability that the globe was the Earth, conditional on seeing "land" $Pr(\text{Earth}\mid\text{land})$ is 0.23.

$Pr(\text{water} \mid \text{Earth}) = 0.7$

$Pr(\text{land} \mid \text{Earth}) = 0.3$

$Pr(\text{Earth}) = 0.5$

$Pr(\text{land} \mid \text{Mars}) = 1.0$

$Pr(\text{Mars}) = 0.5$

\begin{equation}
\begin{aligned}
    Pr(\text{land}) &= Pr(\text{land} \mid \text{Earth})Pr(\text{Earth}) + Pr(\text{land} \mid \text{Mars})Pr(\text{Mars}) \\
    &= 0.3 * 0.5 + 1.0 * 0.5 \\
    &= 0.65
\end{aligned}
\end{equation}

\begin{equation}
\begin{aligned}
    Pr(\text{Earth} \mid \text{land}) &= \frac{0.3 * 0.5}{0.65} \\
    &= 0.23 \\
\end{aligned}
\end{equation}

## 2M4.

Suppose you have a deck with only three cards. Each card has two sides, and each side is either black or white. One card has two black sides. The second card has one black and one white side. The third card has two white sides. Now suppose all three cards are placed in a bag and shuffled. Someone reaches into the bag and pulls out a card and places it flat on a table. A black side is shown facing up, but you don't know the color of the side facing down. Show that the probability that the other side is also black is 2/3. Use the counting method (Section 2 of the chapter) to approach this problem. This means counting up the ways that each card could produce the observed data (a black side facing up on the table).

Note: $B$ stands for black and $W$ stands for white. $B_1$ means that side 1 of a card is black. The left side is the side facing up (e.g. $B_2 \mid B_1$ means that the second side is black and it is the one facing up, while the other side is also black).

Here are the combinations:

$B_1 \mid B_2$ Black side is facing up and other side is black

$B_2 \mid B_1$ Black side is facing up and other side is black

$B_1 \mid W_2$ Black side is facing up, but other side is white.

$W_2 \mid B_1$ Not relevant, since white side up.

$W_1 \mid W_2$ Not relevant, since white side up.

$W_2 \mid W_1$ Not relevant, since white side up.

There are three events consistent with "Black side facing up," and only two of them are consistent with the the event "other side is also black." Thus the probability that the other side is black, given that the known side is black is $2/3$.

## 2M5.

Now suppose there are four cards: B/B, B/W, W/W, and another B/B. Again suppose a card is drawn from the bag and a black side appears face up. Again calculate the probability that the other side is black.

Previously:

$B_1 \mid B_2$ Black side is facing up and other side is black

$B_2 \mid B_1$ Black side is facing up and other side is black

$B_1 \mid W_2$ Black side is facing up, but other side is white.

$W_2 \mid B_1$ Not relevant, since white side up.

$W_1 \mid W_2$ Not relevant, since white side up.

$W_2 \mid W_1$ Not relevant, since white side up.

New Data:

$B_1 \mid B_2$ Black side is facing up and other side is black

$B_2 \mid B_1$ Black side is facing up and other side is black

There are now five events consistent with "Black side facing up," and four of them are consistent with the the event "other side is also black," so the probability of the other side being black, given that the side being shown is black is $4/5$.

## 2M6.

Imagine that the black ink is heavy, and so cards with black sides are heavier than cards with white sides. As a result, it's less likely that a card with black sides is pulled from the bag. So again assume there are three cards: B/B, B/W, and W/W. After experimenting a number of times, you conclude that for every way to pull the B/B card from the bag there are 2 ways to pull the B/W card and 3 ways to pull the W/W card. Again suppose that a card is pulled and a black side appears face up. Show that the probability the other side is black is now 0.5. Use the counting method, as before.

A probabilistically equal statement to this is if we pretend to have the bag have one $B/B$ card, two $B/W$ and three $W/W$ card in the bag.

$B/B$ gives us two ways of picking a black side up: $B_1 / B_2$ and $B_2 / B_1$.

If we pick one of the $B/W$ cards, there is one way that a card has black side up: $B_1$. However, because picking up a $B/W$ is twice as likely as picking up a $B/B$, then there are $2\times 1 = 2$ ways to get a card where we know that one side is black.

There are $4$ total ways of picking a black card, but only half of them has black on the other side. Therefore the probability that the other side is black, given that a black side is face up, is 0.5.

## 2M7.

Assume again the original card problem, with a single card showing a black side face up. Before looking at the other side, we draw another card from the bag and lay it face up on the table. The face that is shown on the new card is white. Show that the probability that the first card, the one showing a black side, has black on its other side is now 0.75. Use the counting method, if you can. Hint: Treat this like the sequence of globe tosses, counting all the ways to see each observation, for each possible first card.

$B_1 \mid B_2$ -> $W_1 \mid B_2$ Black on the other side.

$B_1 \mid B_2$ -> $W_1 \mid W_2$ Black on the other side.

$B_1 \mid B_2$ -> $W_2 \mid W_1$ Black on the other side.

$B_2 \mid B_1$ -> $W_1 \mid B_2$ Black on the other side.

$B_2 \mid B_1$ -> $W_1 \mid W_2$ Black on the other side.

$B_2 \mid B_1$ -> $W_2 \mid W_1$ Black on the other side.

$B_1 \mid W_2$ -> $W_1 \mid W_2$ Not black on the other side.

$B_1 \mid W_2$ -> $W_2 \mid W_1$ Not black on the other side.

Given that the second card has white face-up and the first card has black face-up, the probability that the first card's other side is black is 0.75.

## 2H1.

Suppose there are two species of panda bear. Both are equally common in the wild and live in the same places. They look exactly alike and eat the same food, and there is yet no genetic assay capable of telling them apart. They differ however in their family sizes. Species A gives birth to twins 10% of the time, otherwise birthing a single infant. Species B births twins 20% of the time, otherwise birthing singleton infants. Assume these numbers are known with certainty, from many years of field research.

Now suppose you are managing a captive panda breeding program. You have a new female panda of unknown species, and she has just given birth to twins. What is the probability that her next birth will also be twins?

Note: $C_1=2$ stands for twins during the first birthing event, $C_2=1$ stands for a singleton infant during the second birthing event. $S=a$ means that the species which gave birth was from Species A.

$P(C_1=2 \mid S=a) = 0.1$

$P(C_1=1 \mid S=a) = 0.9$

$P(C_1=2 \mid S=b) = 0.2$

$P(C_1=1 \mid S=b) = 0.8$

$P(S=a) = 0.5$

$P(S=b) = 0.5$

$$
\begin{equation}
    \begin{aligned}
        P(S=a \mid C_1=2) &= \frac{P(C_1=2 \mid S=a)P(S=a)}{P(C_1=2)} \\
        &= \frac{0.05}{0.05+0.1} \\
        &= \frac{1}{3} \\
    \end{aligned}
\end{equation}
$$

$$
\begin{equation}
    \begin{aligned}
        P(S=b \mid C_1=2) &= 1 - P(S=a \mid C_1=2) \\
        &= \frac{2}{3}
    \end{aligned}
\end{equation}
$$

$$
\begin{equation}
    \begin{aligned}
        P(C_2=2 \mid C_1=2) &= \sum_S P(C_2=2, S, \mid C_1=2) \\
        &= P(C_2=2, S=a \mid C_1=2) + P(C_2=2, S=b \mid C_1=2) \\
        &= P(C_2=2 \mid S=a, C_1=2) \cdot P(S=a \mid C_1=2) \\
        &\quad+ P(C_2=2 \mid S=b, C_1=2) \cdot P(S=b \mid C_1=2) \\
        &= P(C_2=2 \mid S=a) \cdot P(S=a \mid C_1=2) \\
        &\quad+ P(C_2=2 \mid S=b) \cdot P(S=b \mid C_1=2) & \text{Conditional independence. See Note below}\\
        &= \frac{1}{10} \times \frac{1}{3} + \frac{2}{10} \times \frac{2}{3} \\
        &= \frac{1}{30} + \frac{4}{30} \\
        &= \frac{1}{6}
    \end{aligned}
\end{equation}
$$

_Note_: The causal diagram implied here is the following:

The causal diagram implies that if we already know the Species ($S$), then Birth at Time 1 ($C_1$) and Birth at Time 2 ($C_2$) are independent from each other. In mathematical notation:

$$ C_2 \mathrel{\unicode{x2AEB} C_1 \mid S}$$

In other words, if we know the species, knowing about the birth at time 1 does not give us any more information about birth at time 2. Therefore, we have the license to remove $C_1$ from the conditioning section:

$$
\begin{equation}
    \begin{aligned}
         P(C_2=2 \mid S=a, C_1=2) &= P(C_2=2 \mid S=a) \\
         P(C_2=2 \mid S=b, C_1=2) &= P(C_2=2 \mid S=b) \\
    \end{aligned}
\end{equation}
$$

## 2H2.

Recall all the facts from the problem above. Now compute the probability that the panda we have is from species A, assuming we have observed only the first birth and that it was twins.

$$
\begin{equation}
    \begin{aligned}
        P(S=a \mid C_1=2) &= \frac{P(C_1=2 \mid S=a)P(S=a)}{P(C_1=2)} \\
        &= \frac{0.05}{0.05+0.1} \\
        &= \frac{1}{3} \\
    \end{aligned}
\end{equation}
$$

## 2H3.

Continuing on from the previous problem, suppose the same panda mother has a second birth and that it is not twins, but a singleton infant. Compute the posterior probability that this panda is species A.

$$
\begin{equation}
    \begin{aligned}
        P(S=a \mid C_2=1, C_1=2) &= \frac{P(C_2=1, C_1=2 \mid S=a)P(S=a)}{P(C_2=1, C_1=2)} \\
        &= \frac{P(C_2=1 \mid S=a)P(C_1=2 \mid S=a)P(S=a)}{P(C_2=1, C_1=2)} \\
        &= \frac{P(C_2=1 \mid S=a)P(C_1=2 \mid S=a)P(S=a)}{P(C_2=1 \mid S=a)P(C_1=2 \mid S=a)P(S=a) + P(C_2=1 \mid S=b)P(C_1=2 \mid S=b)P(S=b)} \\
        &= \frac{0.9 \times 0.1 \times 0.5}{0.9 \times 0.1 \times 0.5 + 0.8 \times 0.2 \times 0.5} \\
        &= 0.36
    \end{aligned}
\end{equation}
$$

## 2H4.

A common boast of Bayesian statisticians is that Bayesian inference makes it easy to use all the data, even if the data are of different types.

So suppose now that a veterinarian comes along who has a new genetic test that she claims can identify the species of our mother panda. But the test, like all tests, is imperfect. This in the information you have about the test:

* The probability it correctly identifies a species A panda is 0.8.
* The probability it correctly identifies a species B panda is 0.65.

The vet administers the test to your panda and tells you that the test is positive for species A. First ignore your previous information from the births and compute the posterior probability that your panda is species A. Then redo your calculation now using the birth data as well.

Let $T$ stand for the test result.

$P(T=a \mid S=a) = 0.8$

$P(T=b \mid S=b) = 0.65$, therefore: $P(T=a \mid S=b) = 0.35$.

Without birth data:

$$
\begin{equation}
    \begin{aligned}
        P(S=a \mid T=a) &= \frac{P(T=a \mid S=a)P(S=a)}{P(T=a)} \\
        &= \frac{P(T=a \mid S=a)P(S=a)}{P(T=a \mid S=a)P(S=a) + P(T=a \mid S=b)P(S=b)} \\
        &= \frac{0.8 \times 0.5}{0.8 \times 0.5 + 0.35 \times 0.5} \\
        &= \frac{0.8}{1.15} \\
        &\approx 0.70
    \end{aligned}
\end{equation}
$$

With birth data:

We use the posterior of the previous answer $P(S=a \mid C_2=1, C_1=2) = 0.36$ as the new prior $P(S=a)$:

$$
\begin{equation}
    \begin{aligned}
        P(S=a \mid T=a) &= \frac{P(T=a \mid S=a)P(S=a)}{P(T=a)} \\
        &= \frac{P(T=a \mid S=a)P(S=a)}{P(T=a \mid S=a)P(S=a) + P(T=a \mid S=b)P(S=b)} \\
        &= \frac{0.8 \times 0.36}{0.8 \times 0.36 + 0.35 \times 0.64} \\
        &\approx 0.56
    \end{aligned}
\end{equation}
$$

```python

```


# 逻辑回归

作者：杨岱川

时间：2019年11月

github：https://github.com/DrDavidS/basic_Machine_Learning

开源协议：[MIT](https://github.com/DrDavidS/basic_Machine_Learning/blob/master/LICENSE)

## 逻辑回归简介

**逻辑回归**，或者叫**逻辑斯谛回归（logistic regression）**，是统计学中的经典分类方法，属于对数线性模型。

>严格来说，此逻辑（logistic）非彼逻辑（logic），叫“逻辑回归”不太准确，见[这里](https://www.zhihu.com/question/43602640/answer/847913754)。

在统计中，logistic 模型（又叫 logit 模型）一般用于对某些类别或者事件建模，比如确定一幅图像里面是不是包含了猫、狗，或者狮子之类。检测到的每个对象都被分配一个介于0到1之间的概率，而且它们的**总和为1**。

回归分析

>逻辑方程（logistic function）最早用于人口增长模型，是比利时数学家、医生 [Pierre François Verhulst](https://en.wikipedia.org/wiki/Pierre_Fran%C3%A7ois_Verhulst) 在比利时数学家、统计学家 [Adolphe Quetelet](https://en.wikipedia.org/wiki/Adolphe_Quetelet) 的指导下，在大约19世纪30年代发明的。而作为一般统计模型的逻辑回归，最初由美国物理学家和统计学家 [Joseph Berkson](https://en.wikipedia.org/wiki/Joseph_Berkson) 开发和推广于1944年，“logit”这个词也是他发明的。

## 逻辑回归模型

### 逻辑分布

首先我们定义一下什么叫逻辑分布（logistic distribution）：

>设 $X$ 是连续随机变量， $X$ 服从逻辑分布是指 $X$ 具有下列**分布函数**和**密度函数**：
>
>$$\large F(x)=P(X\le x)=\frac{1}{1+{\rm e}^{-(x-\mu)/\gamma}}$$
>
>$$\large f(x)=F'(x)=\frac{{\rm e}^{-(x-\mu)/\gamma}}{\gamma(1+{\rm e}^{-(x-\mu)/\gamma})^2}$$
>
>其中，$\mu$ 为位置参数，$\gamma > 0$ 为形状参数。

>考研数学中，概率论与数理统计里面学过概率密度函数，可以复习一下：
>
>在数学中，连续型随机变量的**概率密度函数**（简称为**密度函数**）是一个描述这个随机变量的输出值，在某个确定的取值点附近的可能性的函数。而随机变量的取值落在某个区域之内的概率则为概率密度函数在这个区域上的积分。当概率密度函数存在的时候，累积分布函数是概率密度函数的积分。概率密度函数一般以小写标记。
>
>如果随机变量 $X$ 的分布函数可以表示为
>
>$$\large F(x)=\int_{-\infty}^x f(t)dt,\quad x\in R$$
>
>其中 $f(x)$ 是非负可积函数，则称 $X$ 为连续性随机变量，称 $f(x)$ 为 $X$ 的概率密度函数。
>
>简而言之，概率密度反映了概率在点 $x$ 处的密集程度。对分布函数**求导**，就可以得到密度函数。

### 逻辑分布的图像

逻辑分布的分布函数 $F(x)$ 属于逻辑函数，其图形是一条 S 形曲线，即 Sigmoid curve，它也就是神经网络算法中常用的激活函数之一，Sigmoid函数：

$$\large F(x)=\frac{1}{1+e^{-x}}$$

分布函数 $F(x)$ 绘图如下：

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

def sigmoid(x):
    """定义sigmoid函数"""
    return 1.0/(1.0 + np.exp(-x))
```

```python
x = np.arange(-8, 8, 0.02)
plt.plot(x, sigmoid(x))

plt.axvline(0.0,color='k')
plt.axhline(y=0.0,ls='dotted',color='k')
plt.axhline(y=1.0,ls='dotted',color='k')
plt.axhline(y=0.5,ls='dotted',color='k')
plt.yticks([0.0,0.5,1.0])
plt.ylim(-0.1,1.1)
plt.xlabel('x')
plt.ylabel('$F(x)$')
plt.show()
```

实际上，这条 S 形曲线是以点 $\left(\mu=0，\large \frac{1}{2}\right)$ 中心对称的。

曲线在中心附近增长很快，在两端增长速度慢。形状参数 $\gamma$ 的值越小，曲线在中心附近增长得越快。编程中没有写入 $\gamma$，感兴趣可以自己改写试一试。

其密度函数 $f(x)=F'(x)=\large \frac{e^{-x}}{(1+e^{-x})^2}$ 绘图如下：

```python
def sigmoid_density_function(x):
    """定义sigmoid的密度函数"""
    numerator = np.exp(-x)
    denominator = (1.0 + np.exp(-x))**2
    return np.exp(-x)/denominator
```

```python
x = np.arange(-8, 8, 0.02)
plt.plot(x, sigmoid_density_function(x))

plt.axvline(0.0,color='k')
plt.axhline(y=0.0,ls='dotted',color='k')
plt.axhline(y=0.3,ls='dotted',color='k')
plt.axhline(y=0.25,ls='dotted',color='k')
plt.yticks([0.0, 0.25, 0.3])
plt.ylim(-0.1, 0.3)
plt.xlabel('x')
plt.ylabel('$f(x)$')
plt.show()
```

### 二项逻辑回归模型

二项逻辑回归模型（binomial logistic regression model）是一种分类模型，由条件概率分布 $P(Y|X)$ 表示，形式为参数化的逻辑分布。

其中，随机变量 $X$ 为实数，随机变量 $Y \in \{0,1\}$，即取值为 0或者1。我们采用监督学习的方法来估计模型参数。

给出定义：

>二项逻辑回归模型是如下的条件概率分布：
>
>$$\large P(Y=1|x)=\frac{\exp(w \cdot x + b)}{1+\exp(w \cdot x + b)}$$
>
>$$\large P(Y=0|x)=\frac{1}{1+\exp(w \cdot x + b)}$$
>
>其中，$x\in \mathbf R^n$ 是输入特征，$n$ 为特征维度，而 $Y \in \{0,1\}$ 是输出结果，$w \in \mathbf R^n$ 和 $b \in \mathbf R$ 是参数，$w$ 称为权值向量， $b$ 称为偏置，$w\cdot x$ 为 $w$ 和 $x$ 的内积。

给定一个实例 $x$，按照定义中的公式可以计算出其分到 0 和 1 的概率，然后比较两个条件概率值的大小，将 $x$ 分到概率值较大的那一类。

### 逻辑回归模型的特点

定义：

>一个事件的几率（odds）是指该事件发生与该事件不发生的概率之比。

如果事件发生的概率是 $p$，则事件的几率是 $\large \frac{p}{1-p}$，则该事件的对数几率（log odds），或者叫 logit 函数是

$$\large{\rm logit}(p)=\log\frac{p}{1-p}$$

代入二项逻辑回归的条件概率分布公式得到：

$$\large \log \frac{P(Y=1|x)}{1-P(Y=1|x)}=w\cdot x$$

故，在逻辑回归模型中，输出 $Y=1$ 的对数几率是输入 $x$ 的线性函数。

从之前的图像可以看出，线性函数 $w \cdot x$ 的值越接近正无穷，概率值就越接近1,；线性函数的值越接近负无穷，概率值就越接近0。

### 模型的参数估计

模型的参数估计，其实就是我们的训练过程。

和往常一样，首先我们给定一组训练数据集：

$$\large T=\left \{(x_1,y_1),(x_2,y_2),\cdots,(x_N,y_N) \right \} $$

其中：

$x_i \in \Bbb X \subseteq \mathbf R^n$ 为实例的特征向量，可以即数据的 features（特征），有 $n$ 个维度；

$ y_i \in \left \{ 1,0 \right \}$，即样本的标签。

我们可以使用极大似然估计法（见2.05节）来估计模型的参数，从而得到逻辑回归模型。

设：

$$\large P(Y=1|x)=\pi(x),\quad P(Y=0|x)=1-\pi(x)$$

根据上文的二项式逻辑回归的条件概率分布公式有：

$$\large \log \frac{P(Y=1|x)}{1-P(Y=1|x)}= \log \frac{\pi (x)}{1-\pi (x)} =w\cdot x$$

似然函数为

$$\large \prod_{i=1}^{N}{\left[ \pi(x_i) \right]}^{y_i}{\left[ 1-\pi(x_i) \right]^{1-y_i}}$$

>这里 $y_i\in \left \{ 1,0 \right \}$，故当第 $i$ 个实例的标签 $y_i=1$ 时，有 $\pi(x_i)$，而 $y_i=0$ 时，有 $1-\pi(x_i)$，共有 $N$ 个实例，连乘起来。

对数似然函数（即对上面的似然函数求对数）为

$$
\large
\begin{equation}\begin{split}
L(w)& = \sum_{i=1}^N \left[y_i\log \pi(x_i)+(1-y_i)\log(1-\pi(x_i))\right] \\
& = \sum_{i=1}^N \left[ y_i\log \frac{\pi(x_i)}{1-\pi(x_i)} + \log(1-\pi(x_i)) \right]\\
& = \sum_{i=1}^N \left[ y_i(w\cdot x_i)-\log(1+\exp(w \cdot x_i)) \right]
\end{split}\end{equation}
$$

>补充说明：对似然函数取对数，是因为对数函数单调递增，似然函数的极大值与取对数后的似然函数极大值一致，但是再计算上更简单了。参考说明：[似然函数与极大似然估计](https://zhuanlan.zhihu.com/p/32568242)。

>在《统计学习方法》（第二版）中发现一个小错误：
>
>2019年6月第2次印刷
>
>P93 最后的对数似然函数公式
>
>最后一行的 $\log(1 + \exp(w\cdot x_i))$ 少了一个后括号

对 $L(w)$ 求极大值，可以得到 $w$ 的估计值（**极大似然估计**就是对对数似然函数求极大值），就是求 $\max L(w)$ 而得到模型的参数 $w$。

现在问题就变成了以对数似然函数为目标函数的最优化问题，而逻辑回归的训练中经常采用的方法就是[梯度下降法（Gradient Descent）](https://en.wikipedia.org/wiki/Gradient_descent)以及[拟牛顿法（Quasi-Newton Methods）](https://en.wikipedia.org/wiki/Quasi-Newton_method)。

假设 $w$ 的极大似然估计值为 $\hat w$（可以念做hat w），则我们训练所得到的逻辑回归模型为：

$$\large P(Y=1|x)=\frac{\exp(\hat w \cdot x)}{1 + \exp(\hat w \cdot x)}$$

$$\large P(Y=0|x)=\frac{1}{1 + \exp(\hat w \cdot x)}$$

>从二分类延伸到多分类的逻辑回归模型叫做**多项逻辑回归模型**，可以用于多分类，可以简单地认为是做了多次二分类。

## 逻辑回归的SKlearn实现

首先说一下逻辑回归的损失函数和正则化，这部分是书上没有的内容。

### 损失函数

接上一节，由于参数估计的目标是求对数似然函数的极大值，而

$$\large \max L(w) = \sum_{i=1}^N \left[y_i\log \pi(x_i)+(1-y_i)\log(1-\pi(x_i))\right]$$

相当于求括号里面的每个

$$\large \left[y_i\log \pi(x_i)+(1-y_i)\log(1-\pi(x_i))\right] $$

的极大值，我们在前面加一个符号，相当于变成了求

$$\large -\left[y_i\log \pi(x_i)+(1-y_i)\log(1-\pi(x_i))\right]$$

的极小值，这就是逻辑回归的损失函数，也叫做**最大似然损失函数**，括号拆开整理一下就是：

$$\large  L(y_i, \pi(x_i)) = - y_i\log \pi(x_i)-(1-y_i)\log(1-\pi(x_i))$$

### 正则化

在 SKlearn 的线性模型页面，有对[逻辑回归](https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression)的详细说明。

SKlearn 的实现中，对于模型的实现额外增加了正则项，包括 $L1$ 正则和 $L2$ 正则。

在2.01节已简单讲过正则化了，这里再回顾一下p范数(p-norm)的定义：

$$\large ||x||^p := \left( \sum_{i=1}^n |x_i|^p \right)^{\frac{1}{p}}$$

对于 $L2$ 正则，有

$$\large ||x||^2 := \left( \sum_{i=1}^n |x_i|^2 \right)^{\frac{1}{2}}$$

所以在这里，逻辑回归的  $L2$ 正则定义为 $\large \frac{\lambda}{2}||w||^2$，其中 $\lambda$ 为系数。

故逻辑回归的结构风险最小化就是

$$\large R_{srm}(f) = \frac{1}{N}\sum^{N}_{i=1}L(y_i,\pi(x_i))+\frac{\lambda}{2}||w||^2 $$

具体计算这里不在深入了。

### 编程实现

在 `sklearn.linear_model.LogisticRegression` 的具体[文档](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html#sklearn.linear_model.LogisticRegression)中，已经有较为全面的实现了。

这里我们借用官方例子做演示，顺便讲解函数中的部分参数。

```python
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
```

```python
# 导入iris数据集
X, y = load_iris(return_X_y=True)
```

```python
# 二八划分
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
```

我们采用 `L-BFGS` 算法作为我们的优化器，它是一种改进的拟牛顿法，和SGD做的是同样的事情。

```python
clf = LogisticRegression(random_state=42,  # 随机种子
                         penalty='l2',  # 正则化
                         solver='lbfgs',  # 具体优化算法，lbfgs支持l2正则
                         multi_class='multinomial',  # 多项逻辑回归
                         max_iter=200,  # 最大迭代次数
                         n_jobs=2 # 进程数量
                         ).fit(X_train, y_train)
```

```python
y_pred = clf.predict(X_test)  # 预测
```

```python
y_pred
```

    array([2, 1, 1, 1, 0, 2, 1, 0, 2, 2, 1, 2, 1, 2, 1, 0, 2, 2, 0, 1, 1, 2,
           2, 1, 2, 1, 2, 2, 1, 1])

```python
y_test
```

    array([2, 1, 2, 1, 0, 2, 1, 0, 2, 2, 1, 2, 1, 2, 2, 0, 2, 2, 0, 1, 1, 2,
           2, 1, 2, 1, 2, 2, 1, 1])

```python
accuracy_score(y_test, y_pred)
```

    0.9333333333333333

```python

```

02机器学习基础/2.08 逻辑回归的原理与应用.ipynb

02机器学习基础/2.08 逻辑回归的原理与应用.ipynb

02机器学习基础/2.08 逻辑回归的原理与应用.ipynb


\begin{align}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\   \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{align}

\begin{align}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\   \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{align}


```python
# HIDDEN
from datascience import *
from prob140 import *
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
%matplotlib inline
import math
from scipy import stats
from scipy import misc
```

### Returns and First Passage Times ###
Throughout this section we will assume that $X_0, X_1, X_2, \ldots $ is an irreducible aperiodic Markov Chain with a finite state space. We will start by listing some facts about the number of times this chain visits each state. We won't prove the facts but will indicate why they are true. You should know the facts even if you don't study the sketched justifications.

- For large $n$, all elements of the $n$-step transition matrix are positive. That is, for sufficiently large $n$, $P_n(i, j) > 0$ for all $i$ and $j$.

To see why this is true, remember that for each pair of states $i$ and $j$, the sequence $P_n(i, j)$ converges to $\pi(j) > 0$ as $n$ gets large. Therefore the terms in the sequence are positive for large $n$. As there is a finite number of pairs of states, there is a finite number of sequences of transition probabilities, and so after some large enough $n$, all the $n$-step transition probabilities are positive.

- Each state is visited infinitely many times with probability 1. We say that each state is *recurrent*. As all the states are recurrent, we say for short that *the chain is recurrent*.

Observe that because the state space is finite, at least one state has to be visited infinitely often. It can be derived from this that since all states communicate with each other, all states must be visited infinitely often.

- Fix any states $i$ and $j$ (which could be the same). Given that the chain starts at $i$, the expected time till it reaches $j$ is finite. In particular, given that the chain starts at $i$, the expected return time to $i$ is finite. We say that all states are *positive recurrent*, and hence that the *chain is positive recurrent*.

To see why this is true, note from above that for any state $i$, there is some time $n^*$ for which $P_n^*(i, i)$ is positive. Start the chain at $i$ and watch the chain only at times that are multiples of $n^*$. At these times, the number of moves till the chain returns to $i$ is a geometric random variable with a positive "success" probability $P_{n^*}(i, i)$, and hence it has finite expectation. If you watch the chain at all times, not just at multiples of $n^*$, it will return to $i$ even faster.

#### Expected Long Run Occupation Proportions ####
We know that the chain has a stationary distribution $\pi$ that is unique and strictly positive.
We also know that for every state $j$, the expected long run proportion of time the chain spends at $j$ is $\pi(j)$. We call this the expected long run proportion of *times at which the chain occupies the state $j$*.

### First Passage Times ###
We will start with *hitting times* defined as follows. For any state $j$, the *first hitting time* or the *first passage time* of $j$ is

$$
W_j = \inf \{ n \ge 1: X_n = j \}
$$

That is, $W_j$ is the first time at which the chain reaches state $j$ once it has started running. We will be lazy and call $W_j$ a *hitting time* instead of a *first hitting time*, but we will make sure to use *first* in contexts where we are studying repeated hits on $j$.

We have seen these examples of these $W$'s before. In the last section we worked with the hitting time till a pattern appeared. You will recall that the pattern was one of the states of a Markov Chain that we set up.

The expected hitting time of $j$ given that the chain started at $i$ is $E(W_j \mid X_0 = i)$. This is also called the *mean first passage time* of $j$ given that the chain started in $i$. We can calculate these using first step analysis, but when $i = j$ there is a simple expression for the expectation in terms of the stationary distribution $\pi$.

### Expected Return Time ###
In the case where $i = j$, we say that $E(W_i \mid X_0 = i)$ is the expected *return time* to $i$ given that the chain started at $i$. That is because the definition of $W_i$ only involves times that are at least 1.

It turns out that there is a simple relation between $\pi(i)$ and the expected return time to $i$. We won't prove the relation but it is not hard to believe:

$$
E(W_i \mid X_0 = i) = \frac{1}{\pi(i)} , ~~~~ i \in S
$$

Since $\pi(i) > 0$ for all states $i$, the expected return time to each state is finite.

To understand what this result is saying, suppose $\pi(i) = 1/10$. Then you know that in the long run the chain spends $1/10$ of its time at $i$. Since it's at $i$ at one of every 10 steps on average, it makes sense that once the chain is at $i$ you expect it to return to $i$ in 10 steps. This is not a proof by any means, but it confirms that the result is consistent with intuition.

This result is analogous to, but not the same as, the fact that in i.i.d. Bernoulli $(p)$ trials, the expected long run proportion of successes is $p$, and the expected number of trials till the first success is $1/p$.

### Example: Ehrenfest Chain ###
Recall that in the Ehrenfest model described in an earlier chapter, there are two containers containing a total of $N$ particles. At each instant, a container is selected at random and a particle is selected at random independently of the container. Then the selected particle is placed in the selected container; if it was already in that container, it stays there.

Let $X_n$ be the number of particles in Container 1 at time $n$. Then $X_0, X_1, \ldots$ is a Markov chain with transition probabilities given by:

\begin{equation}
P(i, j) =
 \begin{cases}
      \frac{N-i}{2N} & \text{if } j = i+1 \\
      \frac{1}{2} & \text{if } j = i \\
      \frac{i}{2N} & \text{if } j = i-1 \\
      0 & \text{otherwise}
   \end{cases}
\end{equation}

We showed that the stationary distribution of this chain is binomial $(N, 1/2)$.

Now our result about expected hitting times implies that given that Containter 1 is empty, the expected amount of time till it again becomes empty is

$$
\frac{1}{\binom{N}{0}(1/2)^N} = 2^N
$$

In general for any $0 \le k \le N$, given that Container 1 has $k$ particles in it, the expected amount of time till it once again has $k$ particles is

$$
\frac{1}{\binom{N}{k}(1/2)^N} = \frac{2^N}{\binom{N}{k}}
$$

If $N$ is even, then $\binom{N}{k}$ is increasing in $k$ for $0 \le k \le N/2$ and decreasing for larger $k$, so the expected return time is the smallest for $k = N/2$. If $N$ is odd the expected return times are smallest for the two values of $k$ on either side of $N/2$.

### Expected First Passage Times ###
To get all the expected first passage times $E(W_j \mid X_0 = i)$, you can set up equations by first step analysis just as we did in the previous section.

For states $i \ne j$,

$$
E(W_j \mid X_0 = i) = 1 + \sum_{k \ne j} P(i, k)E(W_j \mid X_0 = k)
$$

As you have seen, such systems of equations can be tedious to solve. If the transition matrix is numerical, you can use the `mean_first_passage_times` method to get all of the mean first passage times at once.

Here is an example using the Ehrenfest chain with $N=6$ so that we can see the entire matrix of mean first passage times.

```python
N = 6

s = np.arange(N+1)

def trans_probs(i, j):
    if j == i:
        return 1/2
    if j == i+1:
        return (N-i)/(2*N)
    elif j == i-1:
        return i/(2*N)
    else:
        return 0

tbl = Table().states(s).transition_function(trans_probs)
ehrenfest = tbl.toMarkovChain()
```

```python
ehrenfest.mean_first_passage_times()
```

      0
      1
      2
      3
      4
      5
      6

      0
      64.0
      2.000000
      4.800000
      9.2
      17.600000
      40.400000
      166.4

      1
      126.0
      10.666667
      2.800000
      7.2
      15.600000
      38.400000
      164.4

      2
      148.8
      22.800000
      4.266667
      4.4
      12.800000
      35.600000
      161.6

      3
      157.2
      31.200000
      8.400000
      3.2
      8.400000
      31.200000
      157.2

      4
      161.6
      35.600000
      12.800000
      4.4
      4.266667
      22.800000
      148.8

      5
      164.4
      38.400000
      15.600000
      7.2
      2.800000
      10.666667
      126.0

      6
      166.4
      40.400000
      17.600000
      9.2
      4.800000
      2.000000
      64.0

The diagonal elements are the expected return times $E(W_i \mid X_0 = i)$ and are the inverses of the stationary binomial (6, 1/2) probabilities.

```python
1/(ehrenfest.steady_state().column('Probability'))
```

    array([ 64.        ,  10.66666667,   4.26666667,   3.2       ,
             4.26666667,  10.66666667,  64.        ])

The $(i, j)$th off-diagonal element is $E(W_j \mid X_0 = i)$. For example, if the chain starts at 2, you expect it to take $12.8$ steps to get to 4. The numerical value comes from solving the equations set up by first step analysis.

Notice the symmetry in the row corresponding to state 3, which is half-way between the empty and full states of Container 1. Starting at this half-way point, you expect the same amount of time (157.2 moves) for Container 1 to become empty as for it to become full.

```python

```