Will we explore basic symbolic computing inside Python. In symbolic computing, the software is able to compute \(2x = x+x\) or \(\int_0^x x^2 dx = \tfrac{x^3}{3}\) without having a numerical value for \(x\). Popular software includes, Maple, Mathematica, and Sage; Python’s sympy is a basic in-house substitute, which we will explore.
import sympy as sym
x = sym.pi # the digit pi
print(x)
## pi
print("#############")
## #############
print(x.evalf())
## 3.14159265358979
print("#############")
## #############
print(x.evalf(100))
## 3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117068
print("#############")
## #############
x = sym.Symbol('x') # you got to tell Python that x is symbol
print(sym.simplify((x + x)))
## 2*x
y = sym.Symbol('y')
y=sym.integrate(3 * x ** 2, x) # integrate 3x^2
print(y)
## x**3
print("#############")
## #############
We can also do linear algebra
a = sym.Symbol('a')
b = sym.Symbol('b')
c = sym.Symbol('c')
d = sym.Symbol('d')
M = sym.Symbol('M')
M=sym.Matrix([[a, b], [c, d]])
print(M)
## Matrix([[a, b], [c, d]])
print("#############")
## #############
print(M.det())
## a*d - b*c
print("#############")
## #############
print(M.T)
## Matrix([[a, c], [b, d]])
print("#############")
## #############
print(M.eigenvects()) # eigenvalue, multiplicity, eigenvector
## [(a/2 + d/2 - sqrt(a**2 - 2*a*d + 4*b*c + d**2)/2, 1, [Matrix([
## [-d/c + (a/2 + d/2 - sqrt(a**2 - 2*a*d + 4*b*c + d**2)/2)/c],
## [ 1]])]), (a/2 + d/2 + sqrt(a**2 - 2*a*d + 4*b*c + d**2)/2, 1, [Matrix([
## [-d/c + (a/2 + d/2 + sqrt(a**2 - 2*a*d + 4*b*c + d**2)/2)/c],
## [ 1]])])]
print("#############")
## #############
Msub = M.subs(a,1).subs(b,1).subs(c,1).subs(d,0)
values = Msub.eigenvects()
print("#############")
## #############
print(values)
## [(1/2 - sqrt(5)/2, 1, [Matrix([
## [1/2 - sqrt(5)/2],
## [ 1]])]), (1/2 + sqrt(5)/2, 1, [Matrix([
## [1/2 + sqrt(5)/2],
## [ 1]])])]
print("#############")
## #############
Consider the two state Markov chain on \(\{1,2\}\) where \[p_{11} = a = 1-p_{12}\] and
\[p_{21} = b=1- p_{22}\]
We will consider the case where \(a,b \in (0,1)\).
Write down the transition matrix.
Find the stationary distribution of the transition matrix.
Simulate the chain, (starting at \(1\)) and leave \(a\) and \(b\) as variables that you can choose.
Choose \(a=0.3\) and \(b=0.2\).
At \(t=100\) of your simulation, for this snapshot, record the value of the Markov chain; repeat this for \(N=300\) times, and find the proportion times that the chain is at \(1\) and \(2\).
For a given simulation, of length \(t=1000\), also record the total number of times the Markov chain takes the value \(1\); take this number and divide it by \(1000\).
What did you observe?
import numpy as np
a = sym.Symbol('a')
b = sym.Symbol('b')
P = sym.Symbol('P')
P=sym.Matrix([[a, 1-a], [b,1-b]])
Pt = sym.Symbol('Pt')
Pt= P.T
print("#############")
## #############
print(Pt.eigenvects())
## [(1, 1, [Matrix([
## [-b/(a - 1)],
## [ 1]])]), (a - b, 1, [Matrix([
## [-1],
## [ 1]])])]
print("#############")
## #############
Q=P.subs(a,0.1).subs(b,0.4) ## testing code for subs (will not be using these values)
print(Q)
## Matrix([[0.100000000000000, 0.900000000000000], [0.400000000000000, 0.600000000000000]])
print(Q[0])
## 0.100000000000000
print(Q[1])
## 0.900000000000000
print(Q[2])
## 0.400000000000000
print(Q[3])
## 0.600000000000000
v1= np.array( [Q[0],Q[1]] )
v2= np.array( [Q[2],Q[3]] )
Q = np.array([v1,v2])
print(Q)
## [[0.100000000000000 0.900000000000000]
## [0.400000000000000 0.600000000000000]]
So we read off that the eigenvector we are after is \([-b/(a-1), 1]\), and after normalization, we have \[\frac{1}{1-a+b} *[b, 1-a].\]
Note, we would get the nicer looker expression, if we had chosen: \(a \to 1-a\) at the outset.
def step(i,A,B): # advancing the MC by one step, when you are at state i, with values for a and b
Q = P.subs(a,A).subs(b,B)
v1= np.array([Q[0],Q[1]])
v2=np.array([Q[2],Q[3]])
Q = np.array([v1,v2])
q = Q[i-1] # this the transition vector for state i
x=-1
u = np.random.uniform() # imagine the interval [0,1] split up into smaller intervals with probabilities q_j summing to 1
j=0
cumq = np.cumsum(q)
while(x==-1):
j = j+1
if (u <= cumq[j-1]): # if u lands in the interval of length q_j, then we jump to state j
x = j
return x
def steps(i,n,A,B): # advances the MC by n steps, starting at state i, (with values A and B) keeping a complete history
x = np.array([i])
for j in range(n):
x = np.append(x, step(x[j],A,B) )
return x
print(steps(1,10, 0.3,0.2))
## [1 2 2 2 2 2 2 2 2 1 2]
x= [steps(1,100,0.3,0.2)[100] for _ in range(300)]
freq = np.unique(x,return_counts = True)
print(freq)
## (array([1, 2]), array([ 68, 232], dtype=int64))
stat = (1/300)* np.array( [freq[1][0], freq[1][1] ])
thstat = (1/(1-0.3+0.2))*np.array([0.2, 1-0.3])
print("#############")
## #############
print(stat)
## [0.22666667 0.77333333]
print(thstat)
## [0.22222222 0.77777778]
print("#############")
## #############
In the sample path average, we have
path = steps(1, 1000, 0.3,0.2)
freq = np.unique(path,return_counts = True)
print(freq)
## (array([1, 2]), array([185, 816], dtype=int64))
stat = (1/1001)* np.array( [freq[1][0], freq[1][1] ])
print(stat)
## [0.18481518 0.81518482]
In both cases, we recover the stationary distribution.
And in a final example, we count the occurrences of \((1,1,2)\) occurs
path = steps(1, 1000, 0.3,0.2)
oneonetwo = 0
for i in range(998):
if (path[i]==1 and path[i+1]==1 and path[i+2]==2):
oneonetwo = oneonetwo +1
print(oneonetwo/1000)
## 0.045
tans= (thstat[0])*(0.3) * (0.7)
print(tans)
## 0.04666666666666666