```python
import pandas as pd
import numpy as np
import random
from random import expovariate as nexttime
import matplotlib.pyplot as plt
import sys
import cv2
import glob
sys.path.append('../../Python')
from time import time as t
import seaborn as sns
import os
from Senior_Project_Function import *
import math
import sympy
import datetime as dt
from sympy import *
```

# 1. Simple Queue Model Basics

Function: **Simplesimulate**
Description: **Simplesimulate** simulates the bike share system from a model variated from Queueing Theory model. For this function, it simulates the bike share system with 1 station only.
Input: (ArrivalRate, LeaveRate, BikesInUseInitial, BikesAtStationInitial, Capacaity, n)
Output: Time, BikesInUseList, BikesAtStationList, WaitListPick, WaitListReturn
See Senior_Project_Function.py for detailed information

1. Example Usage: Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = **Simplesimulate**(2,2,10,20,20,2000)

```python
path = 'Pictures\InfinitePatience'
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = Simplesimulate(2,2,10,20,20,2000)
```

```python
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,BikesInUseList,label="# of bikes in use")
plt.plot(Time,BikesAtStationList,label="# of bikes at station")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of Bikes',fontsize=20)
plt.title(r'$\lambda = 2, \mu = 2, T = 30, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\lambda2mu2Bikes.png')
# plt.savefig(p)
```

```python

fig=plt.figure(figsize=(15, 8))
plt.plot(Time,WaitListPick,label="# of People wait to pick")
plt.plot(Time,WaitListReturn,label="# of People wait to return")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of People',fontsize=20)
plt.title(r'$\lambda = 2, \mu = 2, T = 30, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\lambda2mu2Peoplewaiting.png')
# plt.savefig(p)
```

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = Simplesimulate(2.5,2,10,20,20,2000)
```

```python
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,BikesInUseList,label="# of bikes in use")
plt.plot(Time,BikesAtStationList,label="# of bikes at station")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of Bikes',fontsize=20)
plt.title(r'$\lambda = 2.5, \mu = 2, T = 30, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\lambda2dot5mu2Bikes.png')
# plt.savefig(p)
```

```python
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,WaitListPick,label="# of People wait to pick")
plt.plot(Time,WaitListReturn,label="# of People wait to return")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of People',fontsize=20)
plt.title(r'$\lambda = 2.5, \mu = 2, T = 30, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\lambda2dot5mu2Peoplewaiting.png')
# plt.savefig(p)
```

# 2 Birth and Death Simulation Basics

Function: **BirthDeathsimulate**
Description: **BirthDeathsimulate** simulates the bike share system from a model variated from birth and death model. For this function, it simulates the bike share system with 1 station only.
Input: (ArrivalRate, LeaveRate, BikesInUseInitial, BikesAtStationInitial, Capacaity, n)
Output: Time, BikesInUseList, BikesAtStationList, WaitListPick, WaitListReturn
See Senior_Project_Function.py for detailed information

1. Example Usage: Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = **BirthDeathsimulate**(1,1/20,10,20,20,2000)

```python
path = 'Pictures\Patience_with_Max_waiting_to_pickupsize_10'
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(1,1/20,10,20,20,2000)
```

2. Plot the list of number of bikes in use and list of number of bikes at station versus time steps in the same graph and save the picture if necessary

```python
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,BikesInUseList,label="# of bikes in use")
plt.plot(Time,BikesAtStationList,label="# of bikes at station")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of Bikes',fontsize=20)
plt.title(r'$\lambda = 1, \mu = 0.05, T = 30, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\lambda1mu0.05.png')
#plt.savefig(p)
```

3. Plot the list of number of people wait in line to pick up bikes and list of number of people wait in line to return bikes versus time steps in the same graph and save the picture if necessary

```python
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,WaitListPick,label="# of People wait to pick")
plt.plot(Time,WaitListReturn,label="# of People wait to return")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of People',fontsize=20)
plt.title(r'$\lambda = 1, \mu = 0.05, T = 30, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\Peoplewaiting.png')
#plt.savefig(p)
```

# 3 Birth and Death Simulation Stationary Distribution

1. Use **BirthDeathsimulate** to simulate the system and compare it with theoretical results

Function: **calculatex_0**
Description: Given lambda and mue values, use sympy to solve x_0.
Input: (lambdav,mue,b = 30,Cwait = 10)
Output: x_0
See Senior_Project_Function.py for detailed information

Example Usage: z = calculatex_0(1,1/35,30,10)

```python
z = calculatex_0(1,1/35,30,10)
Dict = {}
sumV,Dict = calwaittimestationary(30,1/35,10,z,1)
Dict
```

    {0: 0.150356387055402,
     1: 0.0385680185223662,
     2: 0.0449960216094272,
     3: 0.0524953585443317,
     4: 0.0612445849683870,
     5: 0.0714520157964516,
     6: 0.0833606850958601,
     7: 0.0972541326118368,
     8: 0.113463154713810,
     9: 0.132373680499445,
     10: 0.154435960582685}

Function: **time_distribution_cal**
Description: Given the state list and time list, **time_distribution_cal** function outputs the time distribution of each state
Input: (List,Time)
Output: Output
See Senior_Project_Function.py for detailed information

Example Usage: test = **time_distribution_cal**(WaitListPick,Time)
test = **normalize**(test)

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(1,1/35,10,20,20,500)
test = time_distribution_cal(WaitListPick,Time)
test = normalize(test)
# set width of bar
barWidth = 0.25

# set height of bar
bars1 = Dict.values()
bars2 = test.values()

# Set position of bar on X axis
r1 = list(Dict.keys())
r2 = [x + barWidth for x in r1]

# Make the plot
plt.bar(r1, bars1, color='Red', width=barWidth, edgecolor='white', label='Theoretical')
plt.bar(r2, bars2, color='Black', width=barWidth, edgecolor='white', label='Simulation')

# Add xticks on the middle of the group bars
plt.xlabel('Distribution of # of Waiting', fontweight='bold')
plt.ylabel('Percentage of Total', fontweight='bold')
plt.xticks([r + barWidth for r in range(len(bars1))], ['0','1', '2', '3', '4', '5','6','7','8','9','10'])

# Create legend & Show graphic
plt.legend()
p = str(path + '\Waitingt500lambda1mu1over35.png')
plt.savefig(p)
```

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(1,1/35,10,20,20,3000)
test = time_distribution_cal(WaitListPick,Time)
test = normalize(test)
# set width of bar
barWidth = 0.25

# set height of bar
bars1 = Dict.values()
bars2 = test.values()

# Set position of bar on X axis
r1 = list(Dict.keys())
r2 = [x + barWidth for x in r1]

# Make the plot
plt.bar(r1, bars1, color='Red', width=barWidth, edgecolor='white', label='Theoretical')
plt.bar(r2, bars2, color='Black', width=barWidth, edgecolor='white', label='Simulation')

# Add xticks on the middle of the group bars
plt.xlabel('Distribution of # of Waiting', fontweight='bold')
plt.ylabel('Percentage of Total', fontweight='bold')
plt.xticks([r + barWidth for r in range(len(bars1))], ['0','1', '2', '3', '4', '5','6','7','8','9','10'])

# Create legend & Show graphic
plt.legend()
p = str(path + '\Waitingt3000lambda1mu1over35.png')
# plt.savefig(p)
```

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(1,1/35,10,20,20,8000)
test = time_distribution_cal(WaitListPick,Time)
test = normalize(test)
# set width of bar
barWidth = 0.25

# set height of bar
bars1 = Dict.values()
bars2 = test.values()

# Set position of bar on X axis
r1 = list(Dict.keys())
r2 = [x + barWidth for x in r1]

# Make the plot
plt.bar(r1, bars1, color='Red', width=barWidth, edgecolor='white', label='Theoretical')
plt.bar(r2, bars2, color='Black', width=barWidth, edgecolor='white', label='Simulation')

# Add xticks on the middle of the group bars
plt.xlabel('Distribution of # of Waiting', fontweight='bold')
plt.ylabel('Percentage of Total', fontweight='bold')
plt.xticks([r + barWidth for r in range(len(bars1))], ['0','1', '2', '3', '4', '5','6','7','8','9','10'])

# Create legend & Show graphic
plt.legend()
p = str(path + '\Waitingt8000lambda1mu1over35.png')
#plt.savefig(p)
```

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(1,1/35,10,20,20,20000)
test = time_distribution_cal(WaitListPick,Time)
test = normalize(test)
# set width of bar
barWidth = 0.25

# set height of bar
bars1 = Dict.values()
bars2 = test.values()

# Set position of bar on X axis
r1 = list(Dict.keys())
r2 = [x + barWidth for x in r1]

# Make the plot
plt.bar(r1, bars1, color='Red', width=barWidth, edgecolor='white', label='Theoretical')
plt.bar(r2, bars2, color='Black', width=barWidth, edgecolor='white', label='Simulation')

# Add xticks on the middle of the group bars
plt.xlabel('Distribution of # of Waiting', fontweight='bold')
plt.ylabel('Percentage of Total', fontweight='bold')
plt.xticks([r + barWidth for r in range(len(bars1))], ['0','1', '2', '3', '4', '5','6','7','8','9','10'])

# Create legend & Show graphic
plt.legend()
p = str(path + '\Waitingt20000lambda1mu1over35.png')
#plt.savefig(p)
```

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(1,1/35,10,20,20,100000)
test = time_distribution_cal(WaitListPick,Time)
test = normalize(test)
# set width of bar
barWidth = 0.25

# set height of bar
bars1 = Dict.values()
bars2 = test.values()

# Set position of bar on X axis
r1 = list(Dict.keys())
r2 = [x + barWidth for x in r1]

# Make the plot
plt.bar(r1, bars1, color='Red', width=barWidth, edgecolor='white', label='Theoretical')
plt.bar(r2, bars2, color='Black', width=barWidth, edgecolor='white', label='Simulation')

# Add xticks on the middle of the group bars
plt.xlabel('Distribution of # of Waiting', fontweight='bold')
plt.ylabel('Percentage of Total', fontweight='bold')
plt.xticks([r + barWidth for r in range(len(bars1))], ['0','1', '2', '3', '4', '5','6','7','8','9','10'])

# Create legend & Show graphic
plt.legend()
p = str(path + '\Waitingt100000lambda1mu1over35.png')
# plt.savefig(p)
```

2. Use stationary distribution to find combination of lambda and mue that minimizes waiting time.

Function: **calculatewaittimelist**
Description: Given the lambda value, iterate through different mu values and record waiting time of each combination. (fixed lambda value)
Input: (lambdav)
Output: temp
See Senior_Project_Function.py for detailed information

Example Usage: **calculatewaittimelist**(0.5)

Find best $\mu$ values for situations with certain $\lambda$ values by hard coding.

```python
DataWait = pd.DataFrame(columns= ["mu1","mu2","mu3","mu4","WaitingTimeTotalmu1","WaitingTimeTotalmu2","WaitingTimeTotalmu3","WaitingTimeTotalmu4"])
temp1 = calculatewaittimelist(0.5)
temp2 = calculatewaittimelist(1)
temp3 = calculatewaittimelist(1.5)
temp4 = calculatewaittimelist(2)

for i in range(5,36):
    DataWait = DataWait.append({"mu1":1/i,"mu2":1/i,"mu3":1/i,"mu4":1/i,
                                "WaitingTimeTotalmu1":temp1[i - 5],
                                "WaitingTimeTotalmu2":temp2[i - 5],
                                "WaitingTimeTotalmu3":temp3[i - 5],
                                "WaitingTimeTotalmu4":temp4[i - 5]}, ignore_index=True)

fig, ax = plt.subplots()
ax.scatter(x=DataWait.iloc[:,0],y=DataWait.iloc[:,4],c='DarkBlue',label = "lambda = 0.5")
ax.scatter(x=DataWait.iloc[:,1],y=DataWait.iloc[:,5],c='Green',label = "lambda = 1")
ax.scatter(x=DataWait.iloc[:,2],y=DataWait.iloc[:,6],c='Red',label = "lambda = 1.5")
ax.scatter(x=DataWait.iloc[:,3],y=DataWait.iloc[:,7],c='Pink',label = "lambda = 2")
plt.title(r'Expected Total Waiting List with Different $ \lambda$')
plt.xlabel(r'$\mu$')
plt.ylabel('Expected Total Waiting List')
ax.legend()
path = 'Pictures'
p = str(path + "\lambda(0.5+1+1.5+2)WaitingTimeTotal.png")
plt.savefig(p)

```

#  3 Data

1. Explore data itself

```python
data = pd.read_csv("201608_trip_data.csv")
data
path = 'Pictures'
```

```python
# checking data types
data.dtypes
```

    Trip ID             int64
    Duration            int64
    Start Date         object
    Start Station      object
    Start Terminal      int64
    End Date           object
    End Station        object
    End Terminal        int64
    Bike #              int64
    Subscriber Type    object
    Zip Code           object
    dtype: object

```python
# Convert object data type to datetime data type
data["Start Date"] = pd.to_datetime(data["Start Date"])
data["End Date"] = pd.to_datetime(data["End Date"])
```

```python
data.dtypes
```

    Trip ID                     int64
    Duration                    int64
    Start Date         datetime64[ns]
    Start Station              object
    Start Terminal              int64
    End Date           datetime64[ns]
    End Station                object
    End Terminal                int64
    Bike #                      int64
    Subscriber Type            object
    Zip Code                   object
    dtype: object

```python
# Extract date part only from date
data['Start Date Only'] = data['Start Date'].dt.date
data['End Date Only'] = data['End Date'].dt.date
```

```python
data
```

      Trip ID
      Duration
      Start Date
      Start Station
      Start Terminal
      End Date
      End Station
      End Terminal
      Bike #
      Subscriber Type
      Zip Code
      Start Date Only
      End Date Only

      0
      913465
      746
      2015-09-01 00:10:00
      San Francisco Caltrain 2 (330 Townsend)
      69
      2015-09-01 00:23:00
      San Francisco City Hall
      58
      238
      Subscriber
      94107
      2015-09-01
      2015-09-01

      1
      913466
      969
      2015-09-01 00:15:00
      Clay at Battery
      41
      2015-09-01 00:31:00
      Washington at Kearny
      46
      16
      Subscriber
      94133
      2015-09-01
      2015-09-01

      2
      913467
      233
      2015-09-01 00:15:00
      Davis at Jackson
      42
      2015-09-01 00:19:00
      Commercial at Montgomery
      45
      534
      Subscriber
      94111
      2015-09-01
      2015-09-01

      3
      913468
      213
      2015-09-01 01:29:00
      Clay at Battery
      41
      2015-09-01 01:32:00
      Steuart at Market
      74
      312
      Subscriber
      94107
      2015-09-01
      2015-09-01

      4
      913469
      574
      2015-09-01 01:33:00
      Steuart at Market
      74
      2015-09-01 01:42:00
      San Francisco Caltrain 2 (330 Townsend)
      69
      279
      Subscriber
      94107
      2015-09-01
      2015-09-01

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      313684
      1338404
      511
      2016-08-31 23:05:00
      Davis at Jackson
      42
      2016-08-31 23:14:00
      Post at Kearny
      47
      370
      Subscriber
      94109
      2016-08-31
      2016-08-31

      313685
      1338405
      701
      2016-08-31 23:06:00
      San Francisco Caltrain (Townsend at 4th)
      70
      2016-08-31 23:17:00
      Spear at Folsom
      49
      175
      Subscriber
      94133
      2016-08-31
      2016-08-31

      313686
      1338406
      457
      2016-08-31 23:06:00
      Davis at Jackson
      42
      2016-08-31 23:13:00
      Spear at Folsom
      49
      504
      Subscriber
      94111
      2016-08-31
      2016-08-31

      313687
      1338407
      260
      2016-08-31 23:09:00
      Embarcadero at Bryant
      54
      2016-08-31 23:14:00
      Steuart at Market
      74
      226
      Subscriber
      94114
      2016-08-31
      2016-08-31

      313688
      1338408
      378
      2016-08-31 23:32:00
      Washington at Kearny
      46
      2016-08-31 23:38:00
      Embarcadero at Sansome
      60
      667
      Subscriber
      94111
      2016-08-31
      2016-08-31

313689 rows × 13 columns

2. Explore the trip duration distribution of the whole system, converting unit from seconds to minutes

```python
test =  data[data["Duration"] <= 4000]
test["Duration"] = test["Duration"]/60
test["Duration"].plot.hist(bins=1000, alpha=0.5)
plt.xlabel("Durantion (minutes)")
path = 'Pictures'
p = str(path + "/DurationDistribution.png")
# plt.savefig(p)
```

3. Explore arrival rate and trip duration distribution of certain station

```python
# Checking how many stations are in the system
test['Start Station'].value_counts()
```

    San Francisco Caltrain (Townsend at 4th)    23465
    San Francisco Caltrain 2 (330 Townsend)     22286
    Harry Bridges Plaza (Ferry Building)        15469
    2nd at Townsend                             13967
    Steuart at Market                           13482
                                                ...
    Sequoia Hospital                               13
    Mezes Park                                      7
    Redwood City Medical Center                     5
    Franklin at Maple                               1
    5th St at E. San Salvador St                    1
    Name: Start Station, Length: 80, dtype: int64

```python
# Extract information on one station
teststation1arrival = test[test["Start Station"] == "San Francisco Caltrain (Townsend at 4th)"]
teststation1return = test[test["End Station"] == "San Francisco Caltrain (Townsend at 4th)"]
```

```python
# Checking if the distributions are similar
teststation1arrival["Duration"].plot.hist(bins=1000, alpha=0.5,label = "Arrival",density = True)
teststation1return["Duration"].plot.hist(bins=1000, alpha=0.5,label = "Return",density = True)
plt.xlabel("Durantion (minutes)")
plt.legend(loc='upper right')
p = str(path + '\DataDistributionCheck.png')
# plt.savefig(p)
```

```python
# Extract bike trips that are related to one station
teststation1 = test[(test["Start Station"] == "San Francisco Caltrain (Townsend at 4th)") | (test["End Station"] == "San Francisco Caltrain (Townsend at 4th)")]
```

```python
teststation1
```

      Trip ID
      Duration
      Start Date
      Start Station
      Start Terminal
      End Date
      End Station
      End Terminal
      Bike #
      Subscriber Type
      Zip Code
      Start Date Only
      End Date Only

      11
      913476
      12.950000
      2015-09-01 05:02:00
      Davis at Jackson
      42
      2015-09-01 05:15:00
      San Francisco Caltrain (Townsend at 4th)
      70
      86
      Subscriber
      94111
      2015-09-01
      2015-09-01

      14
      913479
      7.416667
      2015-09-01 05:43:00
      Civic Center BART (7th at Market)
      72
      2015-09-01 05:50:00
      San Francisco Caltrain (Townsend at 4th)
      70
      315
      Subscriber
      94103
      2015-09-01
      2015-09-01

      16
      913481
      7.650000
      2015-09-01 06:01:00
      San Francisco Caltrain (Townsend at 4th)
      70
      2015-09-01 06:09:00
      2nd at Folsom
      62
      579
      Subscriber
      94002
      2015-09-01
      2015-09-01

      31
      913496
      14.516667
      2015-09-01 06:36:00
      San Francisco Caltrain (Townsend at 4th)
      70
      2015-09-01 06:50:00
      Market at 10th
      67
      531
      Subscriber
      94306
      2015-09-01
      2015-09-01

      33
      913500
      12.250000
      2015-09-01 06:44:00
      Broadway St at Battery St
      82
      2015-09-01 06:57:00
      San Francisco Caltrain (Townsend at 4th)
      70
      282
      Subscriber
      94133
      2015-09-01
      2015-09-01

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      313645
      1338350
      9.950000
      2016-08-31 20:17:00
      Steuart at Market
      74
      2016-08-31 20:27:00
      San Francisco Caltrain (Townsend at 4th)
      70
      377
      Subscriber
      94158
      2016-08-31
      2016-08-31

      313672
      1338392
      10.616667
      2016-08-31 22:05:00
      San Francisco Caltrain (Townsend at 4th)
      70
      2016-08-31 22:16:00
      South Van Ness at Market
      66
      524
      Subscriber
      94117
      2016-08-31
      2016-08-31

      313674
      1338394
      5.750000
      2016-08-31 22:10:00
      San Francisco Caltrain (Townsend at 4th)
      70
      2016-08-31 22:16:00
      2nd at Townsend
      61
      415
      Subscriber
      94107
      2016-08-31
      2016-08-31

      313677
      1338397
      16.300000
      2016-08-31 22:28:00
      Harry Bridges Plaza (Ferry Building)
      50
      2016-08-31 22:45:00
      San Francisco Caltrain (Townsend at 4th)
      70
      630
      Subscriber
      94158
      2016-08-31
      2016-08-31

      313685
      1338405
      11.683333
      2016-08-31 23:06:00
      San Francisco Caltrain (Townsend at 4th)
      70
      2016-08-31 23:17:00
      Spear at Folsom
      49
      175
      Subscriber
      94133
      2016-08-31
      2016-08-31

52039 rows × 13 columns

```python
# Trip Duration
teststation1["Duration"].mean()
```

    11.082856127135376

```python
# return rate
1/11.082856127135376
```

    0.09022944884681756

```python
# Arrival Rate
teststation1["Trip ID"].count()/(teststation1["Start Date Only"].nunique()*8*60)
```

    0.29702625570776253

```python
teststation1["Bike #"].value_counts()
```

    586    242
    497    202
    134    194
    484    192
    356    188
          ...
    57       2
    27       2
    84       1
    82       1
    657      1
    Name: Bike #, Length: 444, dtype: int64

```python
Time,BikesInUseList,BikesAtStationList,WaitListPick,WaitListReturn = BirthDeathsimulate(0.29702625570776253,1/11.082856127135376,424,20,20,2000)
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,BikesInUseList,label="# of bikes in use")
plt.plot(Time,BikesAtStationList,label="# of bikes at station")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of Bikes',fontsize=20)
plt.title(r'$\lambda = 0.3, \mu = 0.09, T = 444, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\Data.png')
plt.savefig(p)
```

```python
fig=plt.figure(figsize=(15, 8))
plt.plot(Time,WaitListPick,label="# of People wait to pick")
plt.plot(Time,WaitListReturn,label="# of People wait to return")
plt.legend(loc="upper left",fontsize = 11)
plt.xlabel('Time step',fontsize = 20)
plt.ylabel('Number of People',fontsize=20)
plt.title(r'$\lambda = 0.3, \mu = 0.09, T = 444, c = 10$, 2000 steps',fontsize = 20)
p = str(path + '\DataPeoplewaiting.png')
plt.savefig(p)
```

```python

```

```python

```


# 線形代数

## 行列

### 一次変換
$2\times{2}$ 行列は座標平面上の点から座標平面上の点への変換と見ることが出来る

実際、$2\times{2}$ 行列 $\boldsymbol{A}$ が与えられたときに $xy$ 平面上の点の位置ベクトルを $x$ とすると、

$$
\boldsymbol{y} = \boldsymbol{A}\boldsymbol{x}
$$

とすることで、$x$ が $y$ に写される写像と考えることができる

このような写像を **一次変換** と呼ぶ

ここで $\boldsymbol{A} = \begin{pmatrix}3&1\\2&2\end{pmatrix}$ とすると、$x$ が平面全体を動くときに $\boldsymbol{y} = \boldsymbol{A}\boldsymbol{x}$ がどこを動くか考えてみる

行列式を計算してみると

$$
\det{\boldsymbol{A}} = 3\times{2}-1\times{2} = 4 \neq 0
$$

より、$\boldsymbol{A}$ は正則であることが分かる

このとき、

$$
\boldsymbol{x} = \boldsymbol{A}^T\boldsymbol{y}
$$

のため、任意の $y$ に対して、その $y$ に写されるような点 $x$ は $\boldsymbol{x} = \boldsymbol{A}^T\boldsymbol{y}$ で求められることが分かる

つまり、平面上のすべての点について、$\boldsymbol{A}$ によってそこに写されるような点が存在するということになるため、$x$ が平面上全てを動くと、その変換 $\boldsymbol{A}\boldsymbol{x}$ も平面上全てを動くことになる

### 固有値
- **固有値**:
    - 線型変換によって写されたあるベクトルが、写される前のベクトルのスカラー倍になっているとき、そのスカラー量のこと
    - ゼロベクトルでないベクトル $\boldsymbol{v}$ とスカラー $\lambda$ が次の式を満たすときの $\boldsymbol{v}$ を $\boldsymbol{A}$ の **固有ベクトル**、$\lambda$ を **固有値** と呼ぶ
        - $\boldsymbol{A}\boldsymbol{v} = \lambda\boldsymbol{v}$

ここで、2次正方行列の具体例として、以下の行列を考える

$$
\boldsymbol{A} = \begin{pmatrix}1&4\\1&1\end{pmatrix}
$$

この行列の固有値を求める

$$
\boldsymbol{A}\boldsymbol{v} = \lambda\boldsymbol{v} = \lambda\boldsymbol{I}\boldsymbol{v}
$$

より、以下の式が成り立つ

$$
(\boldsymbol{A} - \lambda\boldsymbol{I})\boldsymbol{v} = 0
$$

もし $(\boldsymbol{A} - \lambda\boldsymbol{I})$ が正則行列だった場合は、両辺から $(\boldsymbol{A} - \lambda\boldsymbol{I})^T$ を掛けて $\boldsymbol{v}=0$ となってしまうため、$\boldsymbol{v}\neq\boldsymbol{0}$ となる $\boldsymbol{v}$ が存在するためには $(\boldsymbol{A} - \lambda\boldsymbol{I})$ が非正則である必要がある

この条件は、行列式が 0 であれば成り立つため、

$$
\begin{align}
\det(\boldsymbol{A} - \lambda\boldsymbol{I}) &= det\begin{pmatrix}1-\lambda & 4 \\ 1 & 1-\lambda\end{pmatrix} \\
&= (1-\lambda)(1-\lambda)-4\times{1} = 0
\end{align}
$$

という2次方程式になる

この方程式を解くと、

$$
固有値 \lambda = -1, 3
$$

となる

次に固有ベクトルを求める

それぞれの固有値に対して対応する固有ベクトルがあるため、一つずつ求めていく

まず $\lambda=-1$ の場合は、以下のようになる

$$
\begin{pmatrix}2&4\\1&2\end{pmatrix}\boldsymbol{v} = 0
$$

これは例えば $\boldsymbol{v}=\begin{pmatrix}2\\-1\end{pmatrix}$ のときに成り立つ

ここで「例えば」と言ったのは、固有ベクトルに 0 以外の定数を掛けても固有ベクトルになるためである（実際、両辺の $\boldsymbol{v}$ を $k\boldsymbol{v}$（$k\neq{0}$）で置き換えても上式は成り立つ）

このように固有ベクトルは 0 以外のスカラー値を掛けても固有ベクトルの条件を満たすため、1つのベクトルを代表として考えることが多い

同様の計算から、$\lambda=3$ の場合は $\boldsymbol{v}=\begin{pmatrix}2\\1\end{pmatrix}$ のときなどに成り立つ

以上より、固有値と固有ベクトルの組み合わせは以下の2つとなる

$$
\left(-1, \begin{pmatrix}2\\-1\end{pmatrix}\right),\ \left(3,\ \begin{pmatrix}2\\1\end{pmatrix}\right)
$$

ここで $\boldsymbol{v_1}=\begin{pmatrix}2\\-1\end{pmatrix},\ \boldsymbol{v_2}=\begin{pmatrix}2\\1\end{pmatrix}$ と置くと、行列 $\boldsymbol{A}$ による一次変換は、$v_1$ 方向に -1 倍、$v_2$ 方向に 3 倍拡大したものと考えることが出来る（下図参照）

固有ベクトルの応用例として、べき乗の計算がある

上記の固有ベクトル $\boldsymbol{v_1}$, $\boldsymbol{v_2}$ に対する固有値をそれぞれ $\lambda_1$, $\lambda_2$ とすると、

$$
\boldsymbol{A}\begin{pmatrix}\boldsymbol{v_1}&\boldsymbol{v_2}\end{pmatrix} = \begin{pmatrix}\lambda_1\boldsymbol{v_1}&\lambda_2\boldsymbol{v_2}\end{pmatrix} = \begin{pmatrix}\boldsymbol{v_1}&\boldsymbol{v_2}\end{pmatrix}\begin{pmatrix}\lambda_1&\\&\lambda_2\end{pmatrix}
$$

となる

ここで、

$$
\begin{pmatrix}\boldsymbol{v_1}&\boldsymbol{v_2}\end{pmatrix}=\boldsymbol{V},\ \begin{pmatrix}\lambda_1&\\&\lambda_2\end{pmatrix}=\boldsymbol{\Lambda}
$$

とおくと、以下のように変形できる

$$
\boldsymbol{A}\begin{pmatrix}\boldsymbol{v_1}&\boldsymbol{v_2}\end{pmatrix} = \begin{pmatrix}\boldsymbol{v_1}&\boldsymbol{v_2}\end{pmatrix}\begin{pmatrix}\lambda_1&\\&\lambda_2\end{pmatrix} \\
\downarrow \\
\boldsymbol{A}\boldsymbol{V} = \boldsymbol{V}\boldsymbol{\Lambda}
$$

両辺に右から $\boldsymbol{V}^{-1}$ を掛けて、

$$
\boldsymbol{A} = \boldsymbol{V}\boldsymbol{\Lambda}\boldsymbol{V}^{-1}
$$

より、以下の計算式が成り立つ

$$
\begin{align}
\boldsymbol{A}^n &= (\boldsymbol{V}\boldsymbol{\Lambda}\boldsymbol{V}^{-1})^n \\
&= \underbrace{(\boldsymbol{V}\boldsymbol{\Lambda}\boldsymbol{V}^{-1}) \cdot (\boldsymbol{V}\boldsymbol{\Lambda}\boldsymbol{V}^{-1}) \cdots (\boldsymbol{V}\boldsymbol{\Lambda}\boldsymbol{V}^{-1})}_{n個} \\
&= \boldsymbol{V}\boldsymbol{\Lambda}(\boldsymbol{V}^{-1}\boldsymbol{V})\boldsymbol{\Lambda}(\boldsymbol{V}^{-1}\boldsymbol{V})\boldsymbol{\Lambda} \cdots \boldsymbol{\Lambda}\boldsymbol{V}^{-1} \\
&= \boldsymbol{V}\boldsymbol{\Lambda}^n\boldsymbol{V}^{-1}
\end{align}
$$

ここで $\boldsymbol{\Lambda}$ は対角行列であるため、

$$
\boldsymbol{\Lambda}^n = \begin{pmatrix}\lambda_1^n & \\ & \lambda_2^n\end{pmatrix}
$$

となる

具体的な数字を入れて計算してみると、以下のようにべき乗計算を行うことができる

$$
\boldsymbol{V}=\begin{pmatrix}2&2\\-1&1\end{pmatrix},\ \boldsymbol{V}^{-1}=\frac{1}{4}\begin{pmatrix}1&-2\\1&2\end{pmatrix},\ \boldsymbol{\Lambda}=\begin{pmatrix}-1&\\&3\end{pmatrix} \\
\begin{align}
\\
\boldsymbol{A}^n &= \begin{pmatrix}2&2\\-1&1\end{pmatrix}\begin{pmatrix}(-1)^n&\\&3^n\end{pmatrix} \cdot \frac{1}{4}\begin{pmatrix}1&-2\\1&2\end{pmatrix} \\
&= \frac{1}{4}\begin{pmatrix}2\times(-1)^n+2\times{3}^n & -4\times(-1)^n+4\times{3}^n \\ -(-1)^n+3^n & 2\times(-1)^n+2\times{3}^n\end{pmatrix}
\end{align}
$$

与えられた正方行列 $\boldsymbol{A}$ に対して行列 $\boldsymbol{V}$ と対角行列 $\boldsymbol{\Lambda}$ を用いて $\boldsymbol{A} = \boldsymbol{V}\boldsymbol{\Lambda}\boldsymbol{V}^{-1}$ を表すことを **対角化** と呼ぶ

すべての正方行列が対角化可能なわけではないため、注意が必要である

#### 演習
次の行列の固有値と固有ベクトルを求めよ

$$
\boldsymbol{A} = \begin{pmatrix}5&-2\\9&-6\end{pmatrix}
$$

#### 解
$\det(\boldsymbol{A} - \lambda\boldsymbol{I}) = 0$ より、

$$
\begin{align}
(5-\lambda)(-6-\lambda) - (-2)\times{9} &= 0 \\
\lambda^2 + \lambda - 12 &= 0 \\
(\lambda+4)(\lambda-3) &= 0 \\
\therefore \lambda &= -4, 3
\end{align}
$$

- $\lambda=-4$ の場合:
    - $\boldsymbol{A}-(-4)\boldsymbol{I}=\begin{pmatrix}9&-2\\9&-2\end{pmatrix}$ より、固有ベクトルは $\begin{pmatrix}2\\9\end{pmatrix}$
- $\lambda=3$ の場合:
    - $\boldsymbol{A}-3\boldsymbol{I}=\begin{pmatrix}2&-2\\9&-9\end{pmatrix}$ より、固有ベクトルは $\begin{pmatrix}1\\1\end{pmatrix}$

以上より、固有値と固有ベクトルの組み合わせは以下の通り

$$
\left(-4, \begin{pmatrix}2\\9\end{pmatrix}\right),\ \left(3, \begin{pmatrix}1\\1\end{pmatrix}\right)
$$

```julia

```


# Hydrogen storage tank

A hydrogen-powered vehicle is storing fuel using three onboard cylindrical tanks, each with height 65 cm and diameter 28 cm. To refuel, the tanks are connected to a reservoir of hydrogen, supplied at 300 bar and ambient temperature; a fill valve connects the reservoir to the tanks. Initially, the storage tanks have hydrogen at 60 bar and ambient temperature. The ambient temperature is 25°C.

The mass flow rate through the valve is given by
\begin{equation}
\dot{m} = C_{\text{valve}} \sqrt{ P_{\text{supply}} - P } \;,
\end{equation}
where $C_{\text{valve}} = 2.68 \cdot 10^{-6} \, \frac{\text{kg}}{\text{s Pa}^{0.5}}$ is the valve coefficient.
The heat transfer rate from the tank walls to the hydrogen is given by
\begin{equation}
\dot{Q} = h_{\text{conv}} A_s \left( T_{\text{wall}} - T \right) \;,
\end{equation}
where $h_{\text{conv}} = 40 \frac{\text{W}}{\text{K m}^2}$ is the convection heat transfer coefficient.

The tanks are connected for filling for three minutes.

**Problem:** Determine the pressure and temperature of the hydrogen in the storage tanks as a function of time. Determine the mass of fuel added as a function of time.

```python
import numpy as np
import cantera as ct
from scipy.integrate import solve_ivp

from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity

import matplotlib.pyplot as plt
%matplotlib inline

# these are mostly for making the saved figures nicer
import matplotlib_inline.backend_inline
matplotlib_inline.backend_inline.set_matplotlib_formats('pdf', 'png')
plt.rcParams['figure.dpi']= 150
plt.rcParams['savefig.dpi'] = 150
```

First, let's specify the thermodynamic states of the supply and hydrogen in the tank (initially):

```python
temp_ambient = Q_(25, 'degC')

# supply of hydrogen has constant conditions
supply = ct.Hydrogen()
supply.TP = (
    temp_ambient.to('K').magnitude,
    Q_(300, 'bar').to('Pa').magnitude
    )

# tank properties
tank = {'number': 3,
        'height': Q_(65, 'cm'),
        'diameter': Q_(28, 'cm')
        }

volume_tank = np.pi * tank['height'] * tank['diameter']**2 / 4.0

# initial state in the tanks, fixed by temperature and pressure
temp_initial = temp_ambient
pres_initial = Q_(60, 'bar')
hydrogen = ct.Hydrogen()
hydrogen.TP = temp_initial.to('K').magnitude, pres_initial.to('Pa').magnitude
```

To figure out how the system changes with time, we can do a mass balance and energy balance for the control volume. The mass balance gives
\begin{equation}
\frac{dm}{dt} = \dot{m} = C_{\text{valve}} \sqrt{ P_{\text{supply}} - P(t) }
\end{equation}
and the energy balance gives
\begin{equation}
\dot{m} h_{\text{supply}} + \dot{Q} = \frac{dU}{dt} \;,
\end{equation}
which provide the governing equations for how this system evolves.

We can use mass ($m$) and total internal energy ($U$) as our state variables, and construct an ODE system for these variables. We need to be able to convert between internal system properties and these:

$$
m = \frac{ N_{\text{tank}} V_{\text{tank}} }{v} \\
U = u \, m \;.
$$

We need to define a function that evaluates the time derivative system:

```python
def tank_rates(t, y, supply, tank, temp_ambient):
    '''Evaluates time derivatives for mass and internal energy (dm/dt and dU/dt)
    '''
    mass = Q_(y[0], 'kg')
    internal_energy = Q_(y[1], 'J')

    surface_area_tank = tank['number'] * (
        2 * np.pi*tank['diameter']**2 / 4.0 +
        np.pi*tank['diameter']*tank['height']
        )
    volume_tank = np.pi * tank['height'] * tank['diameter']**2 / 4.0

    # specify state based on mass and internal_energy
    specific_volume = (tank['number'] * volume_tank) / mass
    specific_internal_energy = internal_energy / mass
    f = ct.Hydrogen()
    f.UV = (
        specific_internal_energy.to('J/kg').magnitude,
        specific_volume.to('m^3/kg').magnitude
        )

    # evaluate dm/dt
    C_valve = Q_(2.68e-6, 'kg/(s*Pa**0.5)')
    mdot = C_valve * np.sqrt(Q_(supply.P, 'Pa') - Q_(f.P, 'Pa'))
    dmdt = mdot

    # evaluate dU/dt
    h_conv = Q_(40, 'W/(m**2 K)')
    Qdot = h_conv * surface_area_tank * (temp_ambient - Q_(f.T, 'K'))
    dUdt = (mdot * Q_(supply.h, 'J/kg')) + Qdot

    return [dmdt.to('kg/s').magnitude, dUdt.to('J/s').magnitude]
```

```{margin} Choice of method
Here we use the `'BDF'` for the `method` argument, which tells the `solve_ivp()` function to use an implicit integration method based on backward differentiation formula. Other methods [are available](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html) and may be more efficient for some problems, but this is fairly accurate and can handle stiffness in the ODE system.
```

Finally, we can calcluate the initial mass and internal energy, then integrate over the filling time:

```python
# initial system mass and (total) internal energy
mass_initial = (tank['number'] * volume_tank) / Q_(hydrogen.v, 'm^3/kg')
int_energy_initial = mass_initial * Q_(hydrogen.u, 'J/kg')

# Integrate over 3 minutes,
# using backward differentiation formula (BDF) method
sol = solve_ivp(
    tank_rates, [0, 60*3],
    [mass_initial.to('kg').magnitude, int_energy_initial.to('J').magnitude],
    args=(supply, tank, temp_ambient,),
    method='BDF'
    )
```

The integration was successful (it did not report an error), so now plot temperature and pressure of the tank as a function of time.

We have the system mass and internal energy as a function of time, and we'll need to use those to specify the system at each state to obtain its properties as a function of time.

```python
pressures = np.zeros(len(sol.y[0]))
temperatures = np.zeros(len(sol.y[0]))

for idx in range(len(sol.y[0])):
    mass = Q_(sol.y[0][idx], 'kg')
    internal_energy = Q_(sol.y[1][idx], 'J')

    volume_tank = np.pi * tank['height'] * tank['diameter']**2 / 4.0

    # calculate specific volume and specific internal energy
    # based on mass and (total) internal energys
    specific_volume = (tank['number'] * volume_tank) / mass
    specific_internal_energy = internal_energy / mass
    f = ct.Hydrogen()
    f.UV = (
        specific_internal_energy.to('J/kg').magnitude,
        specific_volume.to('m^3/kg').magnitude
        )

    pressures[idx] = f.P
    temperatures[idx] = f.T

pressures *= ureg.Pa
temperatures *= ureg.K

fig, ax1 = plt.subplots(figsize=(5, 3))

color = 'red'
ax1.set_xlabel('Time (s)')
ax1.set_ylabel('Pressure (bar)', color=color)
ax1.plot(sol.t, pressures.to('bar').magnitude, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax1.axis([0, 180, 50, 300])
ax1.grid(True)

ax2 = ax1.twinx()  # instantiate a second axes that shares the same x-axis

color = 'blue'
# we already handled the x-label with ax1
ax2.set_ylabel('Temperature (°C)', color=color)
ax2.plot(sol.t, temperatures.to('degC').magnitude, color=color)
ax2.tick_params(axis='y', labelcolor=color)
ax2.axis([0, 180, 20, 120])
ax2.grid(True)

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()
```

Now plot the mass of fuel added as a function of time:

```python
mass_added = sol.y[0] - mass_initial.to('kg').magnitude
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(sol.t, mass_added)
plt.xlabel('Time (s)')
plt.axis([0, 180, 0, 1.6])
plt.ylabel('Fuel added (kg)')
plt.grid(True)
fig.tight_layout()
plt.show()
```


# Explore the bias of NPI on $s$

In this notebook, we use a simple model and synthetic data to explore the effect of NPI on the esitmate of $s$ in the population genetics model.

Let's first import some modules.

```python
import cmdstanpy
import numpy as np
import scipy.stats as sts
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.special import expit
from matplotlib.gridspec import GridSpec
```

```python
#cmdstanpy.install_cmdstan()
```

## ODE model

Define the SEIR model with two variants (wild-type and mutant) as a system of ODEs:

\begin{equation}
\begin{split}
\frac{dS}{dt} &= -\beta S (I_w + (1+s) I_m) \\
\frac{dE_w}{dt} &= \beta S I_w - \alpha E_w \\
\frac{dE_m}{dt} &= \beta (1+s) S I_m - \alpha E_m \\
\frac{dI_w}{dt} &= \alpha E_w - \gamma I_w \\
\frac{dI_m}{dt} &= \alpha E_m - \gamma I_m \\
\end{split}
\end{equation}

The parameter $\beta$ is a function of time, given by

\begin{equation}
\beta(t) = (1-H_t) \beta_0 + H_t \beta_1
\end{equation}
where $H_t = (1 + e^{t-t_1})^{-1}$ is a smoothed step function

```python
def betat(t, beta0, beta1, t1):
    Ht = expit(t - t1)
    return beta0 * (1 - Ht) + beta1 * Ht

def ode_seir(t, y, par):
    ## unpack parameters and state
    beta0, beta1, t1, alpha, gamma, s, zeta, p0 = par
    S, Ew, Em, Iw, Im, DeltaEIw, DeltaEIm = y
    ## auxiliary values
    beta = betat(t, beta0, beta1, t1)
    FOIw = beta * Iw
    FOIm = beta * Im * (1+s)
    ## define derivative
    dSdt = -S*(FOIw + FOIm)
    dEwdt = S*FOIw - alpha * Ew
    dEmdt = S*FOIm - alpha * Em
    dIwdt = alpha * Ew - gamma * Iw
    dImdt = alpha * Em - gamma * Im
    DeltaEIw = alpha * Ew
    DeltaEIm = alpha * Em
    return np.array([dSdt, dEwdt, dEmdt, dIwdt, dImdt, DeltaEIw, DeltaEIm])

def get_init(par):
    beta0, beta1, t1, alpha, gamma, s, zeta, p0 = par
    xw = 0.5*(-(alpha + gamma) + np.sqrt((alpha+gamma)**2 + 4*alpha*(beta0 - gamma)))
    xm = 0.5*(-(alpha + gamma) + np.sqrt((alpha+gamma)**2 + 4*alpha*(beta0*(1+s) - gamma)))
    yw = alpha / (xw + alpha + gamma)
    ym = alpha / (xm + alpha + gamma)
    y0 = np.array([1-zeta, (1-yw)*zeta*(1-p0), (1-ym)*zeta*p0, yw*zeta*(1-p0), ym*zeta*p0, 0, 0])
    return y0

def gen_data(N, M, par, t_span):
    y0 = get_init(par)
    sol = solve_ivp(lambda t, y: ode_seir(t,y,par), t_span, y0, dense_output=True)
    ts = np.linspace(*t_span, N)
    ys = sol.sol(ts)
    fm = ys[2,:] / (ys[1,:] + ys[2,:])
    Ms = np.array([sts.poisson.rvs(M) for _ in range(N)])
    Fm = sts.binom.rvs(Ms, fm)
    return ts, Fm, Ms

```

```python
beta0 = 0.8
beta1 = 0.1
t1 = 25
zeta = 5e-6
alpha = 1/3
gamma = 1/4
R0 = beta0/gamma
s = 0.3
p0 = 0.25

kwarg_ivp = {
    "dense_output" : True,
    "rtol" : 1e-8,
    "atol" : 1e-8
}

print("R0 =", R0)

par = (beta0, beta1, t1, alpha, gamma, s, zeta, p0)

y0 = get_init(par)

print(y0)

t_span = (0, 50)
ts = np.linspace(*t_span, 1000)

sol = solve_ivp(lambda t, y: ode_seir(t,y,par), t_span, y0, **kwarg_ivp)

tobs, numvar, numtotal = gen_data(50, 100, par, t_span)
```

```python
ys = sol.sol(ts)
fs = ys[2,:] / (ys[1,:] + ys[2,:])

fig, axs = plt.subplots(2,1, figsize=(7,7))

axs[0].plot(ts, ys[3], label = '$I_w$')
axs[0].plot(ts, ys[4], label = '$I_m$')

#axs[0].plot(ts, ys[1], label = '$E_w$')
#axs[0].plot(ts, ys[2], label = '$E_m$')

axs[0].legend()

#axs[0].set_yscale('log')

axs[1].plot(ts, fs, color='k')

axs[1].scatter(tobs, numvar / numtotal, s=10)

for t, k, n in zip(tobs, numvar, numtotal):
    CI = sts.beta.interval(0.95, k+0.5, n-k+0.5)
    axs[1].plot([t, t], CI, color='k', alpha=0.5)
```

```python
sm = cmdstanpy.CmdStanModel(stan_file="../popgen_simple.stan")
```

```python
N = 50
M = 100
t_span = (0,50)
tobs, numvar, numtotal = gen_data(N, M, par, t_span)

data_dict = {
    "N" : N,
    "NumSam" : numtotal,
    "NumVar" : numvar,
    "T" : tobs,
    "T_G" : 1/alpha + 1/gamma
}

sam = sm.sample(data=data_dict, output_dir="../stan-cache/")
```

```python
chain = sam.stan_variables()

fig, ax = plt.subplots(1, 1)

ax.hist(chain["s"], 50)

print(np.mean(chain["s"]))
```

```python
fig, ax = plt.subplots(1, 1)

## plot data
ax.scatter(tobs, numvar / numtotal, s=10, color='k')

for t, k, n in zip(tobs, numvar, numtotal):
    CI = sts.beta.interval(0.95, k+0.5, n-k+0.5)
    ax.plot([t, t], CI, color='k', alpha=0.5)

## plot fit
phat = chain["phat"]
pl, pu = np.percentile(phat, axis=0, q=[2.5, 97.5])
pm = np.mean(phat, axis=0)

ax.plot(tobs, pm, color='tab:blue')
ax.fill_between(tobs, pl, pu, color='tab:blue', alpha=0.3)
```

## Estimate s for several values of $\beta_1/\beta_0$

```python
beta0 = 0.6
t1 = 30
zeta = 5e-6
alpha = 1/3
gamma = 1/4
R0 = beta0/gamma
s = 0.35
p0 = 0.05
N = 50
M = 100
t_span = (0,60)

ratios = np.linspace(0, 1, 11)
s_samples = []
phats = []
datas = []
sols = []

for i, r in enumerate(ratios):
    beta1 = beta0 * r
    par = (beta0, beta1, t1, alpha, gamma, s, zeta, p0)
    y0 = get_init(par)
    sol = solve_ivp(lambda t, y: ode_seir(t,y,par), t_span, y0, **kwarg_ivp)
    sols.append(sol)
    tobs, numvar, numtotal = gen_data(N, M, par, t_span)
    datas.append((tobs, numvar, numtotal))

    data_dict = {
        "N" : N,
        "NumSam" : numtotal,
        "NumVar" : numvar,
        "T" : tobs,
        "T_G" : 1/alpha + 1/gamma
    }

    sam = sm.sample(data=data_dict, output_dir="../stan-cache/")

    s_samples.append(sam.stan_variable("s"))
    phats.append(sam.stan_variable("phat"))
```

```python
def diff_r_pade(x):
    D = (gamma-alpha)**2 + 4*alpha*beta0
    return alpha*beta0*np.sqrt(D) * x / (D + alpha*beta0*x)

def get_r(x):
    D = (gamma-alpha)**2 + 4*alpha*beta0*(1+x)
    return 0.5*(-(gamma+alpha)**2 + np.sqrt(D))

def diff_r(x):
    return get_r(x) - get_r(0)

def diff_r_lin(x):
    R0 = beta0/gamma
    T_G = 1/alpha + 1/gamma
    return x*R0/T_G / np.sqrt(1 + 4*(R0-1)/(T_G*(alpha+gamma)))
```

### make figure for supplement

```python
fig = plt.figure(figsize=(10,8))

gs = GridSpec(2,2)
ax = fig.add_subplot(gs[1,1])

c='tab:blue'
w = 0.05
ax.violinplot(s_samples, positions=ratios, showextrema=False, widths=w)
ax.boxplot(s_samples, positions=ratios, widths=w,
           showfliers=False, whis=(2.5, 97.5),
           boxprops=dict(color=c), capprops=dict(color=c),
           whiskerprops=dict(color=c), medianprops=dict(color=c))

ax.set_xlim(-0.1,1.1)
ax.set_xticks(ratios)
ax.set_xticklabels([f'{x:0.1f}' for x in ratios])
ax.set_xlabel("reduction in transmission rate ($\\rho$)")
ax.set_ylabel("estimate of $s$")

T_G = 1/alpha + 1/gamma
s_adj = diff_r(s) * T_G

ax.axhline(y=s_adj, color='k')

## plot trajectories

bx = fig.add_subplot(gs[0,0])

for i in range(len(ratios)):
    sol = sols[i]
    ts = np.linspace(0, t_span[1], 1000)
    ys = sol.sol(ts)
    bx.plot(ts, ys[3] + ys[4], color='k', alpha=0.5)

ymax = 0.002
bx.set_ylim(0-ymax/50, ymax + ymax/50)
bx.set_yticks(np.linspace(0, ymax, 5))
bx.set_ylabel('fraction infected ($I$)')

## plot mutant fraction

cx = fig.add_subplot(gs[0,1])

for i in range(len(ratios)):
    sol = sols[i]
    ts = np.linspace(0, t_span[1], 1000)
    ys = sol.sol(ts)
    cx.plot(ts, ys[2] / (ys[1] + ys[2]), color='k', alpha=0.5)

## plot example fit

dx = fig.add_subplot(gs[1,0])

idx = 3

tobs, numvar, numtotal = datas[idx]

## plot data
dx.scatter(tobs, numvar / numtotal, s=10, color='k')

for t, k, n in zip(tobs, numvar, numtotal):
    CI = sts.beta.interval(0.95, k+0.5, n-k+0.5)
    dx.plot([t, t], CI, color='k', alpha=0.5)

## plot fit
phat = phats[idx]
pl, pu = np.percentile(phat, axis=0, q=[2.5, 97.5])
pm = np.mean(phat, axis=0)

dx.plot(tobs, pm, color='tab:blue')
dx.fill_between(tobs, pl, pu, color='tab:blue', alpha=0.3)

dx.text(0.1, 0.9, f"$\\rho = {ratios[idx]:0.1f}$",
        ha='left', va='top', transform=dx.transAxes)

for xx in (cx, dx):
    xx.set_ylabel("mutant frequency ($f_{mt}$)")

xmax = t_span[1]+2
xmin = -2

for xx in (bx, cx, dx):
    xx.axvspan(t1, xmax, color='r', alpha=0.2, linewidth=0)
    xx.set_xlim(xmin, xmax)
    xx.set_xlabel('time ($t$)')

## indicate panel C in panel D
ax.axvspan(0.25, 0.35, color='k', alpha=0.2, linewidth=0)

## indicate value of beta_1 / beta_2 in trajectories

kwargs = {
    "arrowprops" : {"arrowstyle": "->"},
    "ha" : "right",
    "va" : "center"
}

bx.annotate("$\\rho = 0$", xytext=(33, 0.0001), xy=(39, 0.0001), **kwargs)
bx.annotate("$\\rho = 1$", xytext=(32, 0.0017), xy=(38, 0.0017), **kwargs)

kwargs = {
    "arrowprops" : {"arrowstyle": "->"},
    "ha" : "center",
}

cx.annotate("$\\rho = 0$", xytext=(40, 0.2), xy=(40, 0.33), **kwargs)
cx.annotate("$\\rho = 1$", xytext=(40, 0.7), xy=(40, 0.55), **kwargs)

for xx, X in zip([bx, cx, dx, ax], 'ABCD'):
    xx.text(-0.18, 1.04, X, fontsize='xx-large', transform=xx.transAxes)

fig.align_ylabels()

fig.savefig("../effect-NPI-on-s.pdf", bbox_inches='tight')
```

### How accurate is the Pade approximation?

```python
xs = np.linspace(-1, 2.0, 100)

fig, ax = plt.subplots(1, 1)

ys = [diff_r_pade(x) for x in xs]
ax.plot(xs, ys, label="Pade")
ys = [diff_r(x) for x in xs]
ax.plot(xs, ys, label="exact")
ys = [diff_r_lin(x) for x in xs]
ax.plot(xs, ys, label="linear")

ax.legend()
```

```python

```

```python

```

```python

```


# Hindmarsh-Roseモデル

\begin{equation}
\left\{
    \begin{aligned}
        \dot{V} &= n - a V^3 + b V^2 - h + I,\\
        \dot{n} &= c - d V^2 -n, \\
        \dot{h} &= r(\sigma (V-V_0)-h).
    \end{aligned}
\label{eqn:HR}
\right.
\end{equation}

$a = 1, b = 3, c = 1, d = 5, I = 2, r=0.001, \sigma =4, V_0=-1.6$とする

```python
import numpy as np
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set('poster', 'whitegrid', 'dark', rc={"lines.linewidth": 2, 'grid.linestyle': '--'})
```

```python
def HR(t, x, a,b,c,d,I,r,s,V0):
    V,n,h = x[0],x[1],x[2]
    return [n - a * V**3 + b * V**2 - h + I, c - d * V**2 - n, r*(s*(V-V0)-h)]
```

```python
t0 = 0.0
t1 = 2000.0
```

```python
x0 = [-1.0, 2.0, 0.0]
s0 = solve_ivp(HR, [t0, t1], x0, args=([1.0, 3.0, 1.0, 5.0, 2.0, 0.001, 4.0, -1.6]), method='DOP853', dense_output=True, rtol=1e-10, atol=1e-12)
```

```python
T = np.linspace(500, 2000, 10000)
sol = s0.sol(T)
fig = plt.figure(figsize=(9,6))
ax = fig.add_subplot(111)
ax.set_xlabel("$t$")
ax.set_xlim(500,2000)
ax.set_ylim(-2,2.5)
ax.plot(T, sol.T[:,0], '-k', linewidth=2, label="$V(t)$")
ax.plot(T, sol.T[:,2], '-', color='gray', linewidth=2, label="$h(t)$")
ax.legend(loc='upper center', bbox_to_anchor=(.5, -.15), ncol=2)
# plt.savefig("burst_vh.pdf", bbox_inches='tight')
```

```python
T = np.linspace(500, 2000, 40000)
sol = s0.sol(T)
fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(111)
ax.set_xlabel("$V$")
ax.set_ylabel("$n$")
ax.plot(sol.T[:,0],sol.T[:,1], '-k', linewidth=2)
# plt.savefig("burst_vn.pdf", bbox_inches='tight')
```

```python
T = np.linspace(500, 2000, 40000)
sol = s0.sol(T)
fig = plt.figure(figsize=(8,5))
ax = fig.add_subplot(111)
ax.set_xlabel("$n$")
ax.set_ylabel("$h$")
ax.plot(sol.T[:,1],sol.T[:,2], '-k', linewidth=2)
# plt.savefig("burst_vh.pdf", bbox_inches='tight')
```


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

```python
lam = sp.Symbol('\lambda')
phi_R = sp.Symbol('{{\phi_R}}')
phi_X  = sp.Symbol('{{\phi_X}}')
phi_O = sp.Symbol('{{\phi_O}}')
phi_P = sp.Symbol('{{\phi_P}}')
gamma = sp.Symbol('{{\gamma^*}}')
nu = sp.Symbol(r'\nu')
beta = sp.Symbol(r'\beta')
Kd = sp.Symbol('K_D')
_phi_P = 1 - phi_X - phi_O - phi_R
eq = phi_R * gamma * (1 + Kd / ((nu * _phi_P / lam) - 1))**-1
# a = Kd - 1
# b = nu * _phi_P + gamma * phi_R
# c = nu * _phi_P * gamma * phi_R
# soln = (-b + sp.sqrt(b**2 - 4 * a * c))/(2 * a)
_lam = sp.solve(lam - eq, lam)
_lam[0]
```

$\displaystyle \frac{\nu {{\phi_O}} + \nu {{\phi_R}} + \nu {{\phi_X}} - \nu - {{\gamma^*}} {{\phi_R}} - \sqrt{- 4 K_{D} \nu {{\gamma^*}} {{\phi_O}} {{\phi_R}} - 4 K_{D} \nu {{\gamma^*}} {{\phi_R}}^{2} - 4 K_{D} \nu {{\gamma^*}} {{\phi_R}} {{\phi_X}} + 4 K_{D} \nu {{\gamma^*}} {{\phi_R}} + \nu^{2} {{\phi_O}}^{2} + 2 \nu^{2} {{\phi_O}} {{\phi_R}} + 2 \nu^{2} {{\phi_O}} {{\phi_X}} - 2 \nu^{2} {{\phi_O}} + \nu^{2} {{\phi_R}}^{2} + 2 \nu^{2} {{\phi_R}} {{\phi_X}} - 2 \nu^{2} {{\phi_R}} + \nu^{2} {{\phi_X}}^{2} - 2 \nu^{2} {{\phi_X}} + \nu^{2} + 2 \nu {{\gamma^*}} {{\phi_O}} {{\phi_R}} + 2 \nu {{\gamma^*}} {{\phi_R}}^{2} + 2 \nu {{\gamma^*}} {{\phi_R}} {{\phi_X}} - 2 \nu {{\gamma^*}} {{\phi_R}} + \left({{\gamma^*}}\right)^{2} {{\phi_R}}^{2}}}{2 \left(K_{D} - 1\right)}$

```python
deriv = sp.diff(_lam[1], phi_R)
# optimal 1 is real
optimal = sp.solve(deriv, phi_R)
```

```python
deriv
```

$\displaystyle \frac{\nu - {{\gamma^*}} + \frac{- 2 K_{D} \nu {{\gamma^*}} {{\phi_O}} - 4 K_{D} \nu {{\gamma^*}} {{\phi_R}} - 2 K_{D} \nu {{\gamma^*}} {{\phi_X}} + 2 K_{D} \nu {{\gamma^*}} + \nu^{2} {{\phi_O}} + \nu^{2} {{\phi_R}} + \nu^{2} {{\phi_X}} - \nu^{2} + \nu {{\gamma^*}} {{\phi_O}} + 2 \nu {{\gamma^*}} {{\phi_R}} + \nu {{\gamma^*}} {{\phi_X}} - \nu {{\gamma^*}} + \left({{\gamma^*}}\right)^{2} {{\phi_R}}}{\sqrt{- 4 K_{D} \nu {{\gamma^*}} {{\phi_O}} {{\phi_R}} - 4 K_{D} \nu {{\gamma^*}} {{\phi_R}}^{2} - 4 K_{D} \nu {{\gamma^*}} {{\phi_R}} {{\phi_X}} + 4 K_{D} \nu {{\gamma^*}} {{\phi_R}} + \nu^{2} {{\phi_O}}^{2} + 2 \nu^{2} {{\phi_O}} {{\phi_R}} + 2 \nu^{2} {{\phi_O}} {{\phi_X}} - 2 \nu^{2} {{\phi_O}} + \nu^{2} {{\phi_R}}^{2} + 2 \nu^{2} {{\phi_R}} {{\phi_X}} - 2 \nu^{2} {{\phi_R}} + \nu^{2} {{\phi_X}}^{2} - 2 \nu^{2} {{\phi_X}} + \nu^{2} + 2 \nu {{\gamma^*}} {{\phi_O}} {{\phi_R}} + 2 \nu {{\gamma^*}} {{\phi_R}}^{2} + 2 \nu {{\gamma^*}} {{\phi_R}} {{\phi_X}} - 2 \nu {{\gamma^*}} {{\phi_R}} + \left({{\gamma^*}}\right)^{2} {{\phi_R}}^{2}}}}{2 \left(K_{D} - 1\right)}$

```python
optimal[1].factor()
```

$\displaystyle - \frac{\left({{\phi_O}} + {{\phi_X}} - 1\right) \left(2 K_{D} \nu {{\gamma^*}} - \nu^{2} - \nu {{\gamma^*}} + \nu \sqrt{K_{D} \nu {{\gamma^*}}} - {{\gamma^*}} \sqrt{K_{D} \nu {{\gamma^*}}}\right)}{4 K_{D} \nu {{\gamma^*}} - \nu^{2} - 2 \nu {{\gamma^*}} - \left({{\gamma^*}}\right)^{2}}$

```python
def growth(phi_R, phi_X, gamma, nu, Kd, phi_O = 0.5):
    phi_P =  1 - phi_X - phi_O - phi_R
    a = (Kd - 1)
    b = (nu * phi_P + phi_R * gamma)
    c = -nu * gamma * phi_R * phi_P
    return (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a)
```

```python
def optimal_allocation(phi_X, nu, gamma, Kd, phi_O=0.5):
    numer = (phi_O + phi_X - 1) * (2 * Kd * nu * gamma - nu**2  -nu * gamma + np.sqrt(Kd * nu * gamma) * (nu - gamma))
    denom = 4 * Kd * nu * gamma - nu**2 - 2 * nu * gamma - gamma**2
    return -numer / denom
```

```python
phi_O = 0.5
phi_X_range = [0.1, 0.2, 0.3, 0.4]
phi_R_range = np.linspace(0, 0.5, 200)
gamma = 17.1 * 3600 / 7459
nu = 2.5
Kd = 0.02
color = ['red', 'green', 'blue', 'purple', 'black']
fig, ax = plt.subplots(1,1)
for i, x in enumerate(phi_X_range):
    find_opt = optimal_allocation(x, nu, gamma, Kd, phi_O=phi_O)
    growth_range = growth(phi_R_range , x, gamma, nu, Kd, phi_O=phi_O)
    ax.plot(phi_R_range , growth_range, '-', color=color[i])
    ax.vlines(find_opt, 0, 3, linestyle='--', color=color[i])
# ax.set_xlim([0, 0.2])
ax.set_ylim([0, 1.5])

```

```python

```

```python

```


## Random walks and random stumbles

Random walks are a gold mine for a wide variety of stochastic theory and practice. They are easy to explain, easy to code, and easy to misunderstand. In this section, we start out with the simplest imaginable random walk and then show how things can go wrong.

Let's examine the problem of the one-dimensional random walk. Consider a particle at the origin ($x=0$), with $p$ probability of moving to the right and $q=1-p$ probability of moving to the left. When the particle reaches $x=1$, the experiment terminates. On average, how many steps are required for this to terminate if $p=1/2$? This seems like a reasonable question, doesn't it?

The first thing we have to do is establish the conditions under which termination occurs. Thus, we need the probability that the particle ultimately reaches $x=1$. We'll call this probability $P$. On average, how many steps does it make to terminate?

This experiment is easy enough to code as shown below:

```
def walk():
     'starting at x=0, step left/right with probability 1/2 until x=1'
     x=0
     while x!=1:
         x+=random.choice([-1,1]) # equi-probable left-right move
         yield x
```

Some classical deft reasoning leads to:

$$ P = p + q P^2 $$

where $p$ is the probability it moves to $x=1$ from $x=0$ on the first try, and $q$ is the probability it does not, but then ultimately makes it back to $x=0$ (with probability $P$) and then again makes it from there to $x=1$, again with probability $P$ (thus, $P^2$).

There are two solutions to $P$, $P=1$ and $P=p/(1-p)$. The crossover point is $p=1/2$. This is drawn below.

```
from __future__ import  division # want floating point division

fig,ax=subplots()
p = linspace(0,0.5,20)
ax.plot(p,p/(1-p),label=r'$p<1/2$',lw=3.)
ax.plot([0.5,1],[1,1],label=r'$p > 1/2$',lw=3.)
ax.axis(ymax=1.1)
ax.legend(loc=0)
ax.set_xlabel('$p$',fontsize=16)
ax.set_ylabel('$P$',fontsize=16)
ax.set_title(r'Probability of reaching $x=1$ from $x=0$',fontsize=16)
ax.grid()
```

## Average number of steps to termination

A straight-forward question to ask is what is the average number of steps it takes to ultimately terminate this random walk? The quick analysis above says that the particle will *ultimately* terminate, but, on average, how many steps are required for this to happen?

Unfortunately, the analysis above is not very helpful here because the statement is about the probability of ultimate termination, not the probability of termination *given* a particular point somewhere on the left of the origin.

Fortunately, we have all the Python-based tools to experimentally get at this. The following generator describes the $p=1/2$ random walker.

```
def walk():
     'starting at x=0, step left/right with probability 1/2 until x=1'
     x=0
     while x!=1:
         x+=random.choice([-1,1]) # equi-probable left-right move
         yield x
```

Let's try generating a realization of this walk.

```
random.seed(123) #  set seed for reproducibility

s = list(walk()) # generate the random steps

fig,ax=subplots()
ax.plot(s)
ax.set_ylabel("particle's x-position",fontsize=16)
ax.set_xlabel('step index k',fontsize=16)
ax.set_title('Example of random walk',fontsize=16)
```

Now, that we're set up, we can generate a whole list of these walks and then average their lengths to estimate the mean of these walks.

```
s = [list(walk()) for i in range(50)] # generate 50 random walks
len_walk=map(len,s) # lengths of each walk
```

The following plots out a few of these random walks so we can get a feel of what's going on with the average length.

```
fig,ax=subplots()
for i in s:
    ax.plot(i)
ax.set_ylabel("particle's x-position",fontsize=16)
ax.set_xlabel('step index k',fontsize=16)
ax.set_title('average length=%3.2f'%(mean(len_walk)))
```

Now, here's where things get interesting! Fifty samples of the random walk is really not that many, so we'd like to hopefully get a better average by generating a lot more. If you try to generate, say, 1000 realizations, you'd be in for a long wait! This is because some of the random walks are really, really long! Furthermore, this is a **persistent** phenomenon; it's not just a bad draw from the random deck. Even if there is only one really long walk, it seriously distorts the average. It's tempting to conclude that this is just some outlier and get on with it, but **not** doing so will lead us to a very powerful theorem in stochastic processes.

To get a better picture of what's going on here, let's re-define our random walker function so we can limit how far it can go and thereby how long we'd have to wait.

```
def walk(limit=50):
     'limited version of random walker'
     x=0
     while x!=1 and abs(x)<limit:
         x+=random.choice([-1,1])
         yield x
```

Because we really just want to count the walks, we can save memory by not collecting the individual steps and just reporting the length of the walk.

```
def nwalk(limit=500):
     'limited version of random walker. Only returns length of path, not path itself'
     n=x=0
     while x!=1 and n<limit:
         x+=random.choice([-1,1])
         n+=1
     return n # return length of walk
```

```
len_walk = [nwalk() for i in range(500)] # generate 500 limited random walks
```

The usual practice would be to draw a histogram (i.e. an approximation of the probability *density*) here, but sometimes the automatic binning makes things hard to see. Instead, a *cumulative* distribution plot is helpful here. The excellent `pandas` module and some very useful data structures for this kind of work.

```
import pandas as pd
from collections import Counter, OrderedDict

lw = pd.Series(Counter(len_walk))/len(len_walk)
fig,ax=subplots()

ax.plot(lw.index,lw.cumsum(),'-o',alpha=0.3)
ax.axis(xmin=-10,ymin=0)
ax.set_title('Estimated Cumulative Distribution',fontsize=16)
ax.grid()
```

What's interesting about the above plot is how steep the slope is. For a path length of one (terminating at the first step), we already have 50% of the probability accounted for. By a path-length of 100, we already have about 90% of the probability. The problem is squeezing out the remaining probability mass means computing random walks longer than 500 (our arbitrary limit). We can do all the above steps for higher limits far above 500, but this observation still holds. Thus, the problem with averaging this is that getting more probability further out competes with the lengths of those further paths. For the average to converge, we want to asymptotically get more improbable paths relatively faster than those paths grow!

Let's examine the standard deviation of our averages.

```
def estimate_std(limit=10,ncount=50):
    'quick estimate of the standard deviation of the averages'
    ws= array([[ nwalk(limit) for i in range(ncount)] for k in range(ncount)])
    return (limit,ws.mean(), ws.mean(1).std())

for limit in [10,20,50,100,200,300,500,1000]:
    print 'limit=%d,\t average = %3.2f,\t std=%3.2f'% estimate_std(limit)
```

    limit=10,	 average = 4.15,	 std=0.55
    limit=20,	 average = 6.25,	 std=1.08
    limit=50,	 average = 10.00,	 std=2.36
    limit=100,	 average = 15.55,	 std=3.52
    limit=200,	 average = 21.59,	 std=7.20
    limit=300,	 average = 25.60,	 std=8.70
    limit=500,	 average = 32.20,	 std=12.71
    limit=1000,	 average = 47.38,	 std=21.40

If this were converging, then the standard deviation of the mean (and the mean itself) should start converging as the walk limit increased. This is obviously not happening here.

## Graph-based combinatorial approach

So far, we have been looking at this using samples and started suspecting that there is something going on with the convergence of the average. However, this is not uncovering what's going on under the sheets with the convergence of the average. Yes, we've tinkered with varying the sample size, but it could still be the case that there is some much larger sample size out there that would cure all the problems we have so far experienced.

The next code-block assembles some drawing utilities on the excellent `networkx` package so we can analyze this problem using graph combinatoric algorithms.

```
import networkx as nx
import itertools as it

class Graph(nx.DiGraph):
   '''
   operations assuming `pos` attribute in nodes to support drawing and
   manipulating path lattice.
   '''
   def draw(self, ax=None,**kwds):
      '''
      Draw based on `pos` attribute and pass kwds to nx.draw
      :param: axes(optional, default is None)
      '''
      pos = self.get_pos()
      node_size=kwds.pop('node_size',200)
      alpha=kwds.pop('alpha',0.3)
      if ax is None: nx.draw(self,pos=pos,
                             node_size=node_size,
                             alpha=alpha,
                             **kwds)
      else:          nx.draw(self,pos=pos,
                             node_size=node_size,
                             alpha=alpha,
                             ax=ax,**kwds)

   def get_pos(self,n=None):
      '''
      n := str name of node
      get positions as returned dictionary
      '''
      pos=dict([( i,j['pos'] ) for i,j in self.nodes(data=True)])
      if n is None:
         return pos
      else:
         return pos[n]

   def getx(self,x):
      return sorted([(i[0],i[1]['val'] ) for i in self.nodes(True) if i[0][0]==x])

   def gety(self,y):
      return sorted([(i[0],i[1]['val'] ) for i in self.nodes(True) if i[0][1]==y])

# functions to allow diagonal lattice walking
def diagwalk(level,n):
   x = level
   y = -level
   while y<=1 and x<n+1:
      yield (x,y)
      x+=1
      y+=1

def diagwalker(n):
   'daisy-chains the individual diagonal walkers'
   assert n%2 # odd only
   return it.chain(*(diagwalk(i,n) for i in range(n+1)))

```

This next code-block constructs the lattice graph that we'll explain below.

```
def construct_graph(level=5):
    g=Graph()
    g.level = level
    g.add_nodes_from([(i,dict(pos=i)) for i in diagwalker(g.level)])

    for x,y in g.nodes():
       if y!=1 and x< g.level:
          g.add_edge((x,y), (x+1,y-1) )
          g.add_edge((x,y), (x+1,y+1) )

    g.node[(0,0)]['val']=1L # long int
    for j in diagwalker(g.level):
        if j==(0,0): continue
        x,y=j
        g.node[j]['val']=sum(g.node[k]['val'] for k in g.predecessors(j))
    return g
g=construct_graph()
```

The figuree below shows a directed graph with the individual pathways the particle can take from $(0,0)$ to a particular end. The notation $(k,j)$ means that at iteration $k$, the particle is at $x=-j$ on the line. For example,  all paths start at $(0,0)$ and there is only one path from $(0,0)$ to $(1,1)$. Likewise, the next terminus is at $(3,1)$, which is the pathway corresponding to reaching $x=1$ in three steps. There is only one path on the digraph that leads here. However, in this case, there are many pathways that are three-steps long, but that do not reach the terminus. For example, $(3,-1)$ is a path that leads to $x=-1$ in three steps.

Let's use some of the powerful algorithms in `networkx` to pursue these ideas.

```
fig,ax=subplots()
fig.set_size_inches((6,6))
ax.set_aspect(1)
ax.set_title('Directed Path Lattice',fontsize=18)
g.draw(ax=ax)
```

The lattice diagram above shows the potential paths to termination for the random walk. For example, the all paths that lead to the node labeled (5,1) are those paths that take exactly five steps to terminate. Note that this is a directed graph so the graph can only be tranversed in the direction of the arrows (arrowheads denoted by thick ends as shown). Fortunately, `networkx` has powerful tools for computing these paths as shown in the following.

```
l5=list(nx.all_simple_paths(g,(0,0),(5,1)))
for i in l5:
    print i

fig,ax=subplots()
fig.set_size_inches((6,6))
g.draw(ax,with_labels=0)
g.subgraph(l5[0]).draw(ax=ax,with_labels=1,node_color='b',node_size=700)
g.subgraph(l5[1]).draw(ax=ax,with_labels=1,node_color='b',node_size=800)
ax.set_title('Pathways that terminate in five steps',fontsize=16)
```

The figure shows the two pathways that terminate in five steps. The other fact we need is how many pathways do *not* terminate in five steps, then we'll be on our way to computing a probability. Fortunately, we already have this built into our initial graph construction.

```
for i in g.getx(5):
    print 'node (%d,%d)\t'%(i[0]),
    print 'number of paths = %d'%(i[1])
```

    node (5,-5)	number of paths = 1
    node (5,-3)	number of paths = 4
    node (5,-1)	number of paths = 5
    node (5,1)	number of paths = 2

Now, we can compute the conditional probability of terminating in five steps as simply:

$$ \mathbb{P}(5|\hat{3},\hat{1})  = \frac{\text{no. paths to (5,1)}}{\text{total no. of paths of length 5}}$$

This is a conditional probability because the only way to terminate in five steps is to not have terminated at step 1 or 3, which is emphasized by the hat on top of those digits in the above equation. To compute the unconditional probability, we need to account for

$$ \mathbb{P}(\hat{3},\hat{1}) =  \mathbb{P}(\hat{3}|\hat{1}) \mathbb{P}(\hat{1})$$

by computing this recursively,

$$ \mathbb{P}(\hat{3}|\hat{1}) = 1- \mathbb{P}({3}|\hat{1}) $$

where $\mathbb{P}(\hat{1})=1/2$. Plugging and chugging with the above result gives

$$ \mathbb{P}(5) = \frac{1}{16} $$

Because this is tedious to do by hand, we can automate this entire process as shown below.

```
import operator as op

def get_cond_prob(g):
   'compute conditional probability for later'
   cp = OrderedDict()
   for i,j in g.gety(1):
      if (i[0],-i[0] ) not in g: continue
      cp[i[0]] = g.node[i]['val']/sum(map(op.itemgetter(1),g.getx(i[0])))
   return cp

def get_prob(g,cp=None):
   'get unconditional probability of termination '
   prob = OrderedDict()
   cq = OrderedDict()
   if cp is None: cp = get_cond_prob(g)
   for i,j in cp.iteritems(): cq[i]=1-j
   prob[1]=  0.5  # initial condition
   for i in cp.keys():
      prob[i]= cp[i]*prod(cq.values()[:(i-1)//2][::-1])
   return prob

```

The following quick check matches the result we computed earlier for termination in exactly five steps.

```
g=construct_graph(15)
p=get_prob(g)
print 'prob of termination in 5 steps = ',p[5]
```

    prob of termination in 5 steps =  0.0625

The following plot shows the probability of termination for each number of steps.

```
fig,ax=subplots()
ax.axis(ymax=1)
ax.plot(p.keys(),p.values(),'-o')
ax.set_xlabel('exact no. of steps to terminate',fontsize=18)
ax.set_ylabel('probability',fontsize=18)
ax.axis(ymax=.6)
```

Thus, we have a computational method for computing the probability of terminating in a given number of steps. What we need now is a separate analytical result that confirms our work so far. This is where the book by Feller comes in (Feller, William. *An Introduction to Probability Theory and Its Applications: Volume One*. John Wiley & Sons, 1950.).

## Using Generating Functions (Feller, Vol I, p. 271)

In random walk terminology, the probability that first visit to $x=1$ takes place at the nth step is denoted as $\phi_n$. Furthermore, the generating function is defined as:

$$ \Phi(s) = \sum_{n=0}^\infty \phi_n s^n$$

we will need two other random variables. $N$ is the first time that the particle reaches $x=1$. This is a random variable with probability $\phi_n$. Likewise, $N_1$ is the number of trials required to move the particle from anywhere to the left of $x=-1$ to $x=0$. Also, $N_2$ is the number of trials required to move  the particle from $x=0$ to $x=1$. Now, here comes the key step: these three random variables are independent with the same probability distribution. With these definitions, we have

$$ N = 1 + N_1 + N_2$$

In other words, you need $N_1$ trials to make it from wherever the particle was on the left of the origin back to the origin; and then you need $N_2$ trials to make it from there to $x=1$. The first $1$ accounts for the first step to the left from the origin. Thus we have,

$$ \mathbb{E}(s^N|X_1=-1) = \mathbb{E}(s^{1 + N_1 + N_2}|X_1=-1)  = s\Phi(s)^2$$

where we can multiply through because of the mutual independence of the random variables. Additionally,

$$ \mathbb{E}(s^N|X_1=1) = s $$

because $N_1 = N_2 = 0$ in this case. Unwinding the conditional expectation yields,

$$\mathbb{E}(s^N) =  \mathbb{E}(s^N|X_1=1) p +  \mathbb{E}(s^N|X_1=-1) q $$

Writing this out yields,

$$ \Phi(s) = p s + s \Phi(s)^2 (1-p)  $$

This is a quadratic equation in $\Phi(s)$, so we have two solutions:

$$ \Phi_1(s) = \frac{-1-\sqrt{1+4 (p-1) p s^2}}{2 s(p -1 )} $$

and

$$ \Phi_2(s) = \frac{-1+\sqrt{1+4 (p-1) p s^2}}{2 s(p -1 )} $$

How do we pick between them? The limit of $ \Phi_2(s) $ is unbounded as $s \rightarrow 0$. Thus, $\Phi(s=1) = \Phi_1(s=1)$.

One of the properties of the $\Phi$ function is that $\Phi(s=1)$ should sum to one. In this case, we have

$$ \Phi(s=1) = \Phi_1(s=1)= \frac{1-|p-q|}{2 q}$$

Thus, if $p < q$, we have

$$ \sum \phi_n =  p/q$$

which doesn't equal one. The problem here is that the definition of the random variable $N$ only considered the conclusion that the particle would ultimately reach $x=1$. The other possibility, which accounts for the missing probability here, is that the particle *never* terminates. Similarly, when $ p \ge q $, we have

$$ \sum \phi_n =  1$$

This should be no surprise because we saw this exact same result when we first started investigating this! Namely, $p \ge q \implies p \ge 1/2$, which is what we concluded from our first plot. This is the situation where the particle *always* terminates.

The first derivative of $\Phi(s)$ function evaluated at $s=1$ is the mean. Thus,

$$\bar{N} = \mathbb{E}(N) = \Phi'(s=1)= -\left( \frac{-1 + 4\,p\,q + {\sqrt{1 - 4\,p\,q}}} {-2\,q + 8\,p\,q^2} \right) $$

Note that this is unbounded when $p=1/2$ which is exactly what we have been observing experimentally.

To get at the individual probabilities for $p=1/2$, we can use a power series expansion on

$$ \Phi(s)\bigg|_{p=1/2}=-\left( \frac{-1 +       {\sqrt{1 - s^2}}}{s}    \right)$$

Using the binomial expansion theorem, this gives

$$\phi_{2k-1} = (-1)^{k-1} \binom{1/2}{k} $$

Now, we can try this formula out against our graph construction and see if it matches. The downside is that `scipy.misc.comb` cannot handle fractional terms, so we need to use `sympy` instead, as shown below.

```
import sympy

# analytical result from Feller
pfeller = dict([(2*k-1,sympy.binomial(1/2,k)*(-1)**(k-1)) for k in range(1,10)])

for k,v in p.iteritems():
    assert v==pfeller[k] # matches every item!
```

Now that we know exactly why the mean does not converge for $p=1/2$, let's check the calculation for when we know it does converge, say, for $p=2/3$. In this case the average number of steps to terminate is

$$ \mathbb{E}(N)\bigg|_{p=2/3} = 3 $$

The code below redefines our earlier code for this case.

```
def nwalk(limit=500):
     'p=2/3 version of random walker. Only returns length of path'
     n=x=0
     while x!=1 and n<limit:
         x+=random.choice([-1,1,1]) # twice as many 1's as before
         n+=1
     return n # return length of walk
```

```
mean([nwalk() for i in range(100)])
```

    3.2999999999999998

```
for limit in [10,20,50,100,200,300,500,1000]:
    print 'limit=%d,\t average = %3.2f,\t std=%3.2f'% estimate_std(limit,100)
```

    limit=10,	 average = 2.50,	 std=0.26
    limit=20,	 average = 2.83,	 std=0.39
    limit=50,	 average = 2.95,	 std=0.52
    limit=100,	 average = 2.96,	 std=0.43
    limit=200,	 average = 3.02,	 std=0.55
    limit=300,	 average = 2.96,	 std=0.48
    limit=500,	 average = 3.03,	 std=0.49
    limit=1000,	 average = 3.02,	 std=0.52

Notice that the standard deviation of the average did not change much as the limit increased. This did not happen earlier with $p=1/2$. Now we know why.

## Summary

In this long post, we thoroughly investigated the random walk and the lack of convergence in the average we noted when equiprobable steps are used. We then pursued this using both computational graph methods as well as analytical results. It's important to reflect on what would have happened if we had not noticed the strange convergence of the equiprobable case. Most likely, we would have just ignored it as some kind of sampling problem that is cured asymptotically. However, that did not happen here, and this kind of thing is easy to miss in real problems that have not been so heavily studied as the random walk. Thus, the moral of the story is that it pays to have a wide variety of analytical and computational tools (e.g. from the scientific Python stack) available, and to use both of them in tandem to chase down strange results, however mildly unexpected. Furthermore, concepts that sit at the core of more elaborate methods should be understood, or at least characterized as carefully as possible, because once these bleed into complicated meta-models, it may be impossible to track the resulting errors down to the source.

As usual, the source notebook for this post is available [here](https://github.com/unpingco/Python-for-Signal-Processing)


# Modelo del rendimiento de una cuenta de ahorro

> **¿Tiene el dinero el mismo valor a lo largo del tiempo?** La respuesta es *no*. Todos lo hemos vivido.

> Dos situaciones básicas:
1. Inflación: ¿Cuánto dinero necesitabas para comprar unas papas y un refresco hace 10 años? ¿Cuánto necesitas hoy?
2. Interés: no es lo mismo tener $\$10000$ MXN disponibles hoy a recibir $\$10000$ MXN en un año, pues los primeros pueden ser invertidos en un negocio o una cuenta bancaria para generar *interés*. Por lo tanto los $\$10000$ MXN disponibles hoy valen más que los $\$10000$ MXN que se recibirán en un año.

Referencia:
- Vidaurri Aguirre, Héctor Manuel. *Ingeniería económica básica*, ISBN: 978-607-519-017-4. (Disponible en biblioteca)

Referencias:
- http://www.sympy.org
- http://matplotlib.org
- http://www.numpy.org
- http://ipywidgets.readthedocs.io/en/latest/index.html

___
## Interés
Nos centraremos en como cambia el valor del dinero en el tiempo debido al **interés**. Existen dos tipos:

### Capitalización por interés simple
Este tipo de interés se calcula única y exclusivamente sobre la cantidad original que se invirtió. Como consecuencia, el interés generado no forma parte del dinero que se invierte, es decir, los intereses no ganan intereses.

Suponga que se tiene un capital inicial $C_0$ y se invierte a un plazo de $k$ periodos (pueden ser meses, trimestres, semestres, años...) a una tasa de **interés simple** por periodo $i$. Al final del primer periodo, el capital $C_1$ que se obtiene es:

$$C_1=C_0+iC_0=C_0(1+i).$$

De la misma manera, como el interés solo se calcula sobre el capital inicial, al final del segundo periodo, el capital $C_2$ que se obtiene es:

$$C_2=C_1+iC_0=C_0+iC_0+iC_0=C_0(1+2i).$$

Así, al final del $k-$ésimo periodo, el capital $C_k$ que se obtiene es:

$$C_k=C_{k-1}+iC_0=C_0+kiC_0=C_0(1+ki).$$

> **Ejemplo.** Suponga que se tiene un capital de \$10000 MXN, el cual se pone en un fondo de inversión que paga una tasa de interés simple del 0.8% mensual.

> Si se tiene una meta de ahorro de \$11000 MXN sin inversiones adicionales, ¿cuántos meses se debería dejar invertido el dinero?

```python
# Librería para cálculo numérico
import numpy as np
```

```python
# Valores dados en el enunciado
C0 = 10000
i = 0.8 / 100
Meta = 11000
```

```python
# Despejamos k tal que C_k=meta
k = np.ceil((Meta / C0 - 1) / i).astype(int)
```

```python
k
```

    13

```python
(1 + k * i) * C0
```

    11040.000000000002

```python
# Imprimimos respuesta en pantalla
print(f"Para llegar a nuestra meta de $11000, necesitamos {k} meses, con lo que obtenemos"
      f" al final de este periodo ${round((1 + k * i) * C0)}")
```

    Para llegar a nuestra meta de $11000, necesitamos 13 meses, con lo que obtenemos al final de este periodo $11040.0

> **Actividad.**
> - ¿Qué pasa si el interés no es del 0.8% mensual sino del 1% mensual?
> - ¿Qué pasa si la meta no son $\$11000$ MXN si no $\$12000$ MXN?

```python
# Solución

```

> Una gráfica que nos permite ilustrar la situación anterior se puede realizar de la siguiente manera.

```python
# Librerías para gráficos
from matplotlib import pyplot as plt
# Para que se muestren las gráficas en la misma ventana
%matplotlib inline
# Librería para widgets de python interactivo
from ipywidgets import interact, interact_manual, fixed, widgets
```

Explicar la siguiente función

```python
# Función de gráficos de interés simple
def interes_simple(C_0, meta, i):
    # Despejamos k
    k = np.ceil((meta / C_0 - 1) / i).astype(int)  # Notar el uso de la función ceil
    C_k = C_0 * (1 + k * i)  # Cálculo del capital al final del periodo k
    C_k = round(C_k, 2)  # Redondeo con dos cifras decimales
    # Vector de periodos
    kk = np.linspace(0, k, k + 1)
    # Vector de capitales por periodo
    CC = C_0 * (1 + kk * i)
    # Gráfico
    plt.figure(num=1)  # Figura 1
    plt.clf()  # Borrar lo que contenga la figura
    plt.plot(kk, CC,'*',linewidth=3)  # Se grafica la evolución de los capitales
    plt.plot(kk, meta * np.ones(k + 1), '--k')  # Se grafica la meta
    plt.xlabel('k')  # Etiqueta eje x
    plt.ylabel('C_k')  # Etiqueta eje y
    plt.grid()  # Malla en la gráfica
    plt.show()  # Mostrar la figura
    print("El número de periodos que se debe dejar invertido el dinero para llegar a la meta de ", meta," es ", k,
          ". Al final del periodo ", k,", el capital es ", C_k, ".", sep="")
```

```python
interact_manual(interes_simple,
                C_0=fixed(10000),
                meta=(10000, 20000, 100),
                i=widgets.FloatSlider(value=0.008,min=0.005,max=0.015,step=0.001,readout_format='.3f'))
```

    interactive(children=(IntSlider(value=15000, description='meta', max=20000, min=10000, step=100), FloatSlider(…

    <function __main__.interes_simple(C_0, meta, i)>

Como se esperaba, el capital en el $k-$ésimo periodo $C_k=C_0(1+ki)$ crece linealmente con $k$.

### Capitalización por interés compuesto
El capital que genera el interés simple permanece constante todo el tiempo de duración de la inversión. En cambio, el que produce el interés compuesto en un periodo se convierte en capital en el siguiente periodo. Esto es, el interés generado al final de un periodo se reinvierte para el siguiente periodo para también producir interés.

Suponga que se tiene un capital inicial $C_0$, y se va a ceder el uso de este capital por un periodo de tiempo determinado a una tasa de interés $i$. El capital que se obtiene al final del primer periodo $C_1$ se puede calcular por

$$C_1=C_0(1+i).$$

Si la anterior suma se vuelve a ceder a la misma tasa de interés, al final del periodo dos el capital $C_2$ es

$$C_2=C_1(1+i)=C_0(1+i)^2.$$

Si se repite el anterior proceso $k$ veces, el capital al final del $k-$ésimo periodo $C_k$ es

$$C_k=C_{k-1}(1+i)=C_0(1+i)^k.$$

**Referencia**:
- https://es.wikipedia.org/wiki/Inter%C3%A9s_compuesto.

> **Ejemplo.** Suponga que se tiene un capital de \$10000 MXN, el cual se pone en un fondo de inversión que paga una tasa de interés del 0.8% mensual.

> Si se tiene una meta de ahorro de \$11000 MXN sin inversiones adicionales, ¿cuántos meses se debería dejar invertido el dinero?

> Muestre una gráfica que ilustre la situación.

```python
def interes_compuesto(C_0, meta, i):
    # Despejamos k
    k = np.ceil(np.log(meta / C_0) / np.log(1 + i)).astype(int)
    C_k = C_0 * (1 + i)**k  # Cálculo del capital al final del periodo k
    C_k = round(C_k, 2)  # Redondeo con dos cifras decimales
    # Vector de periodos
    kk = np.linspace(0, k, k + 1)
    # Vector de capitales por periodo
    CC = C_0 * (1 + i)**kk
    # Gráfico
    plt.figure(num=1)  # Figura 1
    plt.clf()  # Borrar lo que contenga la figura
    plt.plot(kk, CC, '*', linewidth=3)  # Se grafica la evolución de los capitales
    plt.plot(kk, meta * np.ones(k + 1), '--k')  # Se grafica la meta
    plt.xlabel('k')  # Etiqueta eje x
    plt.ylabel('C_k')  # Etiqueta eje y
    plt.grid()  # Malla en la gráfica
    plt.show()  # Mostrar la figura
    print("El número de periodos que se debe dejar invertido el dinero para llegar a la meta de ", meta," es ", k,
          ". Al final del periodo ", k,", el capital es ", C_k, ".", sep="")
```

```python
interact_manual(interes_compuesto,
                C_0=fixed(10000),
                meta=(10000, 100000, 100),
                i=fixed(0.008))
```

    interactive(children=(IntSlider(value=55000, description='meta', max=100000, min=10000, step=100), Button(desc…

    <function __main__.interes_compuesto(C_0, meta, i)>

El capital en el $k-$ésimo periodo $C_k=C_0(1+i)^k$ crece de manera exponencial con $k$.

> **Actividad.**
> - Modificar el código anterior para dejar fija la meta de ahorro y variar la tasa de interés compuesta.

```python

```

### Capitalización continua de intereses
La capitalización continua se considera un tipo de capitalización compuesta, en la que a cada instante de tiempo $t$ se se capitalizan los intereses. Es decir, la frecuencia de capitalización es infinita (o, equivalentemente, el periodo de capitalización tiende a cero).

Suponga que se tiene un capital inicial $C_0$, y que el capital acumulado en el tiempo $t$ es $C(t)$. Queremos saber cuanto será el capital pasado un periodo de tiempo $\Delta t$, dado que la tasa de interés efectiva para este periodo de tiempo es $i$. De acuerdo a lo anterior tenemos

$$C(t+\Delta t)=C(t)(1+i)=C(t)(1+r\Delta t),$$

donde $r=\frac{i}{\Delta t}$ es la tasa de interés instantánea. Manipulando la anterior expresión, obtenemos

$$\frac{C(t+\Delta t)-C(t)}{\Delta t}=r\; C(t).$$

Haciendo $\Delta t\to 0$, obtenemos la siguiente ecuación diferencial

$$\frac{d C(t)}{dt}=r\; C(t),$$

sujeta a la condición inicial (monto o capital inicial) $C(0)=C_0$.

La anterior, es una ecuación diferencial lineal de primer orden, para la cual se puede calcular la *solución analítica*.

```python
# Librería de cálculo simbólico
import sympy as sym

# Para imprimir en formato TeX
sym.init_printing(use_latex="mathjax")
```

```python
# Símbolos t(para el tiempo) y r(para el interés instantáneo)
sym.var("t r")

# Función de capital
C = sym.Function("C")
```

```python
# Ecuación diferencial
ode = sym.Eq(sym.Derivative(C(t), t) - r * C(t), 0)
# Mostrar ecuación
display(ode)
```

$\displaystyle - r C{\left(t \right)} + \frac{d}{d t} C{\left(t \right)} = 0$

```python
# Resolver
sym.dsolve(ode, C(t))
```

$\displaystyle C{\left(t \right)} = C_{1} e^{r t}$

con $C_1=C_0$.

La equivalencia entre la tasa de interés compuesta $i$ y la tasa de interés instantánea $r$ viene dada por

$$e^r=1+i.$$

___
¿Cómo podemos calcular la *solución numérica*?

> **Ejemplo.** Suponga que se tiene un capital de \$10000 MXN, el cual se pone en un fondo de inversión que paga una tasa de interés del 0.8% mensual.

> Si se tiene una meta de ahorro de \$11000 MXN sin inversiones adicionales, ¿cuánto tiempo se debe dejar invertido el dinero?

> Muestre una gráfica que ilustre la situación.

```python
# Librerías para integración numérica
from scipy.integrate import odeint
```

```python
help(odeint)
```

    Help on function odeint in module scipy.integrate.odepack:

    odeint(func, y0, t, args=(), Dfun=None, col_deriv=0, full_output=0, ml=None, mu=None, rtol=None, atol=None, tcrit=None, h0=0.0, hmax=0.0, hmin=0.0, ixpr=0, mxstep=0, mxhnil=0, mxordn=12, mxords=5, printmessg=0, tfirst=False)
        Integrate a system of ordinary differential equations.

        .. note:: For new code, use `scipy.integrate.solve_ivp` to solve a
                  differential equation.

        Solve a system of ordinary differential equations using lsoda from the
        FORTRAN library odepack.

        Solves the initial value problem for stiff or non-stiff systems
        of first order ode-s::

            dy/dt = func(y, t, ...)  [or func(t, y, ...)]

        where y can be a vector.

        .. note:: By default, the required order of the first two arguments of
                  `func` are in the opposite order of the arguments in the system
                  definition function used by the `scipy.integrate.ode` class and
                  the function `scipy.integrate.solve_ivp`.  To use a function with
                  the signature ``func(t, y, ...)``, the argument `tfirst` must be
                  set to ``True``.

        Parameters
        ----------
        func : callable(y, t, ...) or callable(t, y, ...)
            Computes the derivative of y at t.
            If the signature is ``callable(t, y, ...)``, then the argument
            `tfirst` must be set ``True``.
        y0 : array
            Initial condition on y (can be a vector).
        t : array
            A sequence of time points for which to solve for y.  The initial
            value point should be the first element of this sequence.
            This sequence must be monotonically increasing or monotonically
            decreasing; repeated values are allowed.
        args : tuple, optional
            Extra arguments to pass to function.
        Dfun : callable(y, t, ...) or callable(t, y, ...)
            Gradient (Jacobian) of `func`.
            If the signature is ``callable(t, y, ...)``, then the argument
            `tfirst` must be set ``True``.
        col_deriv : bool, optional
            True if `Dfun` defines derivatives down columns (faster),
            otherwise `Dfun` should define derivatives across rows.
        full_output : bool, optional
            True if to return a dictionary of optional outputs as the second output
        printmessg : bool, optional
            Whether to print the convergence message
        tfirst: bool, optional
            If True, the first two arguments of `func` (and `Dfun`, if given)
            must ``t, y`` instead of the default ``y, t``.

            .. versionadded:: 1.1.0

        Returns
        -------
        y : array, shape (len(t), len(y0))
            Array containing the value of y for each desired time in t,
            with the initial value `y0` in the first row.
        infodict : dict, only returned if full_output == True
            Dictionary containing additional output information

            =======  ============================================================
            key      meaning
            =======  ============================================================
            'hu'     vector of step sizes successfully used for each time step.
            'tcur'   vector with the value of t reached for each time step.
                     (will always be at least as large as the input times).
            'tolsf'  vector of tolerance scale factors, greater than 1.0,
                     computed when a request for too much accuracy was detected.
            'tsw'    value of t at the time of the last method switch
                     (given for each time step)
            'nst'    cumulative number of time steps
            'nfe'    cumulative number of function evaluations for each time step
            'nje'    cumulative number of jacobian evaluations for each time step
            'nqu'    a vector of method orders for each successful step.
            'imxer'  index of the component of largest magnitude in the
                     weighted local error vector (e / ewt) on an error return, -1
                     otherwise.
            'lenrw'  the length of the double work array required.
            'leniw'  the length of integer work array required.
            'mused'  a vector of method indicators for each successful time step:
                     1: adams (nonstiff), 2: bdf (stiff)
            =======  ============================================================

        Other Parameters
        ----------------
        ml, mu : int, optional
            If either of these are not None or non-negative, then the
            Jacobian is assumed to be banded.  These give the number of
            lower and upper non-zero diagonals in this banded matrix.
            For the banded case, `Dfun` should return a matrix whose
            rows contain the non-zero bands (starting with the lowest diagonal).
            Thus, the return matrix `jac` from `Dfun` should have shape
            ``(ml + mu + 1, len(y0))`` when ``ml >=0`` or ``mu >=0``.
            The data in `jac` must be stored such that ``jac[i - j + mu, j]``
            holds the derivative of the `i`th equation with respect to the `j`th
            state variable.  If `col_deriv` is True, the transpose of this
            `jac` must be returned.
        rtol, atol : float, optional
            The input parameters `rtol` and `atol` determine the error
            control performed by the solver.  The solver will control the
            vector, e, of estimated local errors in y, according to an
            inequality of the form ``max-norm of (e / ewt) <= 1``,
            where ewt is a vector of positive error weights computed as
            ``ewt = rtol * abs(y) + atol``.
            rtol and atol can be either vectors the same length as y or scalars.
            Defaults to 1.49012e-8.
        tcrit : ndarray, optional
            Vector of critical points (e.g. singularities) where integration
            care should be taken.
        h0 : float, (0: solver-determined), optional
            The step size to be attempted on the first step.
        hmax : float, (0: solver-determined), optional
            The maximum absolute step size allowed.
        hmin : float, (0: solver-determined), optional
            The minimum absolute step size allowed.
        ixpr : bool, optional
            Whether to generate extra printing at method switches.
        mxstep : int, (0: solver-determined), optional
            Maximum number of (internally defined) steps allowed for each
            integration point in t.
        mxhnil : int, (0: solver-determined), optional
            Maximum number of messages printed.
        mxordn : int, (0: solver-determined), optional
            Maximum order to be allowed for the non-stiff (Adams) method.
        mxords : int, (0: solver-determined), optional
            Maximum order to be allowed for the stiff (BDF) method.

        See Also
        --------
        solve_ivp : Solve an initial value problem for a system of ODEs.
        ode : a more object-oriented integrator based on VODE.
        quad : for finding the area under a curve.

        Examples
        --------
        The second order differential equation for the angle `theta` of a
        pendulum acted on by gravity with friction can be written::

            theta''(t) + b*theta'(t) + c*sin(theta(t)) = 0

        where `b` and `c` are positive constants, and a prime (') denotes a
        derivative.  To solve this equation with `odeint`, we must first convert
        it to a system of first order equations.  By defining the angular
        velocity ``omega(t) = theta'(t)``, we obtain the system::

            theta'(t) = omega(t)
            omega'(t) = -b*omega(t) - c*sin(theta(t))

        Let `y` be the vector [`theta`, `omega`].  We implement this system
        in python as:

        >>> def pend(y, t, b, c):
        ...     theta, omega = y
        ...     dydt = [omega, -b*omega - c*np.sin(theta)]
        ...     return dydt
        ...

        We assume the constants are `b` = 0.25 and `c` = 5.0:

        >>> b = 0.25
        >>> c = 5.0

        For initial conditions, we assume the pendulum is nearly vertical
        with `theta(0)` = `pi` - 0.1, and is initially at rest, so
        `omega(0)` = 0.  Then the vector of initial conditions is

        >>> y0 = [np.pi - 0.1, 0.0]

        We will generate a solution at 101 evenly spaced samples in the interval
        0 <= `t` <= 10.  So our array of times is:

        >>> t = np.linspace(0, 10, 101)

        Call `odeint` to generate the solution.  To pass the parameters
        `b` and `c` to `pend`, we give them to `odeint` using the `args`
        argument.

        >>> from scipy.integrate import odeint
        >>> sol = odeint(pend, y0, t, args=(b, c))

        The solution is an array with shape (101, 2).  The first column
        is `theta(t)`, and the second is `omega(t)`.  The following code
        plots both components.

        >>> import matplotlib.pyplot as plt
        >>> plt.plot(t, sol[:, 0], 'b', label='theta(t)')
        >>> plt.plot(t, sol[:, 1], 'g', label='omega(t)')
        >>> plt.legend(loc='best')
        >>> plt.xlabel('t')
        >>> plt.grid()
        >>> plt.show()

```python
# Modelo de capitalización continua
def cap_continua(C, t, r):
    return r * C
```

```python
def interes_continuo(C_0, meta, r):
    # Despejamos t
    t = np.log(meta / C_0) / r
    # Vector de periodos
    tt = np.linspace(0, t, 100)
    # Vector de capitales por periodo
    CC = odeint(cap_continua, C_0, tt, args = (r,))
    # Gráfico
    plt.figure(num=1)  # Figura 1
    plt.clf()  # Borrar lo que contenga
    plt.plot(tt, CC, '-', linewidth=3)  # Se grafica la evolución de los capitales
    plt.plot(tt,meta*np.ones(len(tt)),'--k')  # Se grafica la meta
    plt.xlabel('t')  # Etiqueta eje x
    plt.ylabel('C(t)')  # Etiqueta eje y
    plt.grid()  # Malla en la gráfica
    plt.show()  # Mostrar la figura
    print("El tiempo que se debe dejar invertido el dinero para llegar a la meta de ", meta," es ", t, " meses.", sep="")
```

```python
interact_manual(interes_continuo,
                C_0=fixed(10000),
                meta=(10000,100000,100),
                r=fixed(np.log(1+0.008)));
```

    interactive(children=(IntSlider(value=55000, description='meta', max=100000, min=10000, step=100), Button(desc…

### Actividad.
1. Resolver simbólicamente y numéricamente la siguiente ecuación diferencial:
$$\frac{dx}{dt}=\frac{x}{t+1}; \quad x(0) = 1$$

```python
# Símbolos t(para el tiempo) y r(para el interés instantáneo)

# Función de capital

# Ecuacion diferencial

```

```python
# Resolver

```

Despejamos la constante usando la condición inicial:

$$
1=x(0)=C_1(0 + 1)=C_1
$$

Finalmente, la solución es:

$$
x(t) = t + 1
$$

2. Obtener la solución numérica.

```python
# Funcion a integrar

```

```python

```

```python
# Condicion inicial

# Vector de tiempo

```

```python

```

```python
# Solucion numerica

```

```python

```

```python

```

```python
# Grafica x vs.t

```

3. Comparar.

```python
# Grafica x vs.t

```

___
## Tabla de abonos
Como aplicación importante del concepto de interés compuesto se encuentra la creación de un modelo de cuenta de ahorro.

Referencia:
- Vidaurri Aguirre, Héctor Manuel. *Ingeniería económica básica*, ISBN: 978-607-519-017-4. (Disponible en biblioteca)
- http://pbpython.com/amortization-model.html

```python
# Librería de análisis de datos
import pandas as pd
# Librería para manipulación de fechas
from datetime import date
```

```python
# Datos para la cuenta de ahorro
Tasa_interes = 0.08  # 8% anual
Anos = 30
Abonos_ano = 12
Inicial = 50000
Meta = 100000
fecha_inicio = date(2021, 7, 1)

# Tasa interés por periodo
i = Tasa_interes / Abonos_ano  # Tasa de interés mensual
# Total de periodos
T = Anos * Abonos_ano  #

# Cálculo de abonos mensuales iguales para llegar a la meta de ahorro en el tiempo deseado
Abono = 1000# i * (Meta - Inicial * (1 + i)**T) / ((1 + i)**T - 1)

# Rango de fechas en la tabla
rng = pd.date_range(fecha_inicio, periods=T, freq='MS')
rng.name = "Fecha del abono"

# Columnas de abonos y extracto de cuenta en las fechas respectivas
df = pd.DataFrame(index=rng, columns=['Abono', 'Balance'], dtype='float')

# Para poner el indice con los periodos y no con las fechas
df.reset_index(inplace=True)
df.index += 1
df.index.name = "Periodo"  # Columna correspondiente al periodo

# Los abonos son iguales
df["Abono"] = Abono

# Vector indizador de los periodos de los abonos
index_vector = np.arange(1,len(rng)+1)
# Extracto de la cuenta mes a mes
df["Balance"] = Inicial * (1 + i)**index_vector + Abono * (((1 + i)**index_vector - 1) / i)

# Redondear con dos cifras decimales
df = df.round(2)
df

# La siguiente solución es también válida. Sin embargo, evitar el uso de 'for'

#df.loc[1, "Balance"] = Inicial*(1+Tasa_interes/Abonos_ano)**1 + df.loc[1, "Abono"]

#for i in range(2, len(df)+1):
#    # Get the previous balance as well as current payments
#    prev_balance = df.loc[i-1, "Balance"]

#    df.loc[i, "Balance"] = prev_balance*(1+Tasa_interes/Abonos_ano)**1 + df.loc[i, "Abono"]
```

      Fecha del abono
      Abono
      Balance

      Periodo

      1
      2021-07-01
      1000
      51333.33

      2
      2021-08-01
      1000
      52675.56

      3
      2021-09-01
      1000
      54026.73

      4
      2021-10-01
      1000
      55386.90

      5
      2021-11-01
      1000
      56756.15

      ...
      ...
      ...
      ...

      356
      2051-02-01
      1000
      1979781.29

      357
      2051-03-01
      1000
      1993979.84

      358
      2051-04-01
      1000
      2008273.03

      359
      2051-05-01
      1000
      2022661.52

      360
      2051-06-01
      1000
      2037145.93

360 rows × 3 columns

```python
df['Abono'].sum() + Inicial
```

$\displaystyle 70829.0$

```python
df['Balance'].plot();
```

> **Tarea.**
> - Averiguar tasas de interés reales en algún banco y proyectar un ahorro mensual para que al terminar su carrera tengan $50000 MXN en su cuenta.
> - Hacer esto en un nuevo archivo, llamarlo Tarea6_ApellidoNombre.ipynb y subirlo a moodle.
> - Plazo: Lunes 5 de Julio.

Created with Jupyter by Esteban Jiménez Rodríguez.