├── .hgignore ├── DecayFunctions.py ├── GenuineMultivariateHawkesProcess.py ├── GenuineMultivariateHawkesProcessFitter.py ├── GenuineMultivariateHawkesProcessTest.py ├── ImmigrationDescendantParameters.py ├── LinigerThesis.pdf ├── MarkDistributions.py ├── README.md └── __init__.py /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | -------------------------------------------------------------------------------- /DecayFunctions.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tjohnson' 2 | import math 3 | 4 | class ExponentialDecayFunction: 5 | """ 6 | Exponential decay function from Liniger thesis p. 32 7 | """ 8 | 9 | def __init__(self,params): 10 | self.setParams(params) 11 | self.epsilon=1e-5 #For Q function 12 | 13 | @staticmethod 14 | def getNumParameters(): 15 | return 1 16 | 17 | @staticmethod 18 | def getParameterBounds(): 19 | return [[1e-5,None]] 20 | 21 | def setParams(self,params): 22 | """ 23 | Set parameters with an iterable to support log-likelihood calculation 24 | This will set alpha=params[0] 25 | """ 26 | self.alpha=params[0] 27 | 28 | def getW(self,t): 29 | """ 30 | Get value of decay function 31 | 32 | :param componentIdx: Index of multivariate process component 33 | :param t: Time 34 | :return: Value of decay function w 35 | """ 36 | return self.alpha*math.exp(-self.alpha*t) 37 | 38 | def getWBar(self,t): 39 | """Get value of cumulative decay function""" 40 | 41 | return 1.0-math.exp(-self.alpha*t) 42 | 43 | def getQ(self): 44 | """Get value of quantile function""" 45 | return -math.log(self.epsilon)/self.alpha 46 | -------------------------------------------------------------------------------- /GenuineMultivariateHawkesProcess.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | __author__ = 'tjohnson' 4 | import random 5 | 6 | class GenuineMultivariateHawkesProcess: 7 | def __init__(self,immigrationDescendantParameters,decayFunctions,markDistributions): 8 | self.numComponents=immigrationDescendantParameters.numComponents 9 | self.immigrationDescendantParameters=immigrationDescendantParameters 10 | self.decayFunctions=decayFunctions #w_j(t) in Liniger thesis 11 | self.markDistributions=markDistributions 12 | 13 | def getLambda(self,componentIdx,timeComponentMarkTriplets,t,includeT=False): 14 | """Returns lambda hat as defined in algorithm 1.28 at bottom of Liniger p. 41""" 15 | lambdaHat=self.immigrationDescendantParameters.nu[componentIdx] 16 | 17 | j=componentIdx 18 | decayFunction=self.decayFunctions[j] 19 | quantile=decayFunction.getQ() 20 | 21 | for s,k,x in timeComponentMarkTriplets: 22 | timeSinceEvent=t-s 23 | if(timeSinceEvent>quantile or timeSinceEvent<0): 24 | continue 25 | if not includeT and timeSinceEvent==0: 26 | continue 27 | 28 | branchingFactor=self.immigrationDescendantParameters.q[j,k] 29 | decayFunctionValue=decayFunction.getW(t-s) 30 | impactFunctionValue=self.markDistributions[k].getImpactFunction(x) 31 | 32 | lambdaHat+=branchingFactor*decayFunctionValue*impactFunctionValue 33 | 34 | return lambdaHat 35 | 36 | def __simulationInnerLoop(self,componentIdx,previousTime,timeComponentMarkTriplets): 37 | """Liniger thesis bottom p. 30""" 38 | 39 | tau=previousTime 40 | lambd=self.getLambda(componentIdx,timeComponentMarkTriplets,previousTime,includeT=True) 41 | while True: 42 | 43 | E=random.expovariate(1.0) 44 | tau=tau+E/lambd 45 | lambdaNew=self.getLambda(componentIdx,timeComponentMarkTriplets,tau) 46 | 47 | U=random.random() 48 | u=U*lambd 49 | if u<=lambdaNew: 50 | return tau 51 | 52 | def getLogLikelihood(self,timeComponentMarkTriplets): 53 | """ 54 | Liniger thesis, Algorithm 1.27, p. 41 55 | """ 56 | 57 | lambdaTermSum=0 58 | markDensityTermSum=0 59 | 60 | for t,d,x in timeComponentMarkTriplets: 61 | lambdaTermSum+=math.log(self.getLambda(d,timeComponentMarkTriplets,t,includeT=False)) 62 | markDensityTermSum+=math.log(self.markDistributions[d].getDensityFunction(x)) 63 | 64 | compensatorTermSum=0 65 | for j in range(0,self.numComponents): 66 | compensatorTermSum+=self.getCompensator(j,timeComponentMarkTriplets) 67 | 68 | return lambdaTermSum+markDensityTermSum-compensatorTermSum 69 | 70 | def getCompensator(self,componentIdx,timeComponentMarkTriplets): 71 | """ 72 | Big Lambda from Liniger Thesis 73 | Algorithm from Liniger thesis p. 44 74 | """ 75 | #TODO: This can be made a little faster using the approximation at the bottom of Liniger p. 44. 76 | #TODO: Just don't calculate wBar if lastTime-s > q_j 77 | firstTime=timeComponentMarkTriplets[0][0] 78 | lastTime=timeComponentMarkTriplets[-1][0] 79 | firstTerm=self.immigrationDescendantParameters.nu[componentIdx]*(lastTime-firstTime) 80 | 81 | j=componentIdx 82 | secondTerm=0 83 | for s,k,x in timeComponentMarkTriplets: 84 | branchingFactor=self.immigrationDescendantParameters.q[j,k] 85 | wBarValue=self.decayFunctions[j].getWBar(lastTime-s) 86 | gValue=self.markDistributions[k].getImpactFunction(x) 87 | secondTerm+=branchingFactor*wBarValue*gValue 88 | 89 | retval=firstTerm+secondTerm 90 | return retval 91 | 92 | def simulate(self,numTimesteps): 93 | timeComponentMarkTriplets=[] 94 | 95 | currentTime=0 96 | for timestep in range(0,numTimesteps): 97 | newTime=float('inf') 98 | newComponent=0 99 | 100 | for j in range(0,self.numComponents): 101 | tau_n_j=self.__simulationInnerLoop(j,currentTime,timeComponentMarkTriplets) 102 | if tau_n_j= 0 20 | numParameters=ImmigrationDescendantParameters.getNumParameters(numComponents) 21 | bounds=[[1e-5,None]]*numParameters 22 | return bounds 23 | 24 | def setParameters(self,params): 25 | for nuIdx,paramVal in zip(range(0,self.numComponents),params[0:self.numComponents]): 26 | self.nu[nuIdx]=paramVal 27 | 28 | for qIdx,paramVal in zip(range(0,self.numComponents*self.numComponents),params[self.numComponents:]): 29 | self.q.put(qIdx,paramVal) 30 | 31 | def getSpectralRadius(self): 32 | eigenVals,eigenVects=np.linalg.eig(self.q) 33 | maxVal=0 34 | for eigenVal in eigenVals: 35 | maxVal=max(maxVal,abs(eigenVal)) 36 | 37 | return maxVal -------------------------------------------------------------------------------- /LinigerThesis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasj02/PyHawkes/a475cb1028f7fdc65a1fc312d642de4a3e4babb8/LinigerThesis.pdf -------------------------------------------------------------------------------- /MarkDistributions.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tjohnson' 2 | import random 3 | import math 4 | 5 | class ParetoMarkDistribution: 6 | def __init__(self,params): 7 | """ 8 | Pareto-distributed impact function 9 | Must have rho>2 10 | From Liniger thesis p. 37 11 | Seems to be different than standard pareto distribution?? 12 | """ 13 | self.setParams(params) 14 | 15 | @staticmethod 16 | def getNumParameters(): 17 | return 5 18 | 19 | @staticmethod 20 | def getParameterBounds(): 21 | #This specifies that alpha, is positive, but strictly speaking only one of alpha, beta, gamma have to be greater than zero. 22 | #See Liniger thesis p. 34 23 | return [[1e-5,None],[2.0+1e-5,None],[1e-5,None],[0,None],[0,None]] 24 | 25 | def setParams(self, params): 26 | """ 27 | Set parameters with an iterable to support log-likelihood calculation 28 | This will set: 29 | mu=params[0] 30 | rho=params[1] 31 | alpha=params[2] 32 | beta=params[3] 33 | gamma=params[4] 34 | """ 35 | 36 | self.mu = params[0] 37 | self.rho = params[1] 38 | self.alpha=params[2] 39 | self.beta=params[3] 40 | self.gamma=params[4] 41 | 42 | def getRandomValue(self): 43 | randomVal=random.random() 44 | return self.__inverseCumulativeDistribution(randomVal) 45 | 46 | def __inverseCumulativeDistribution(self,x): 47 | """From wolfram alpha""" 48 | firstMultiplier=(1.0-x)**(1.0/self.rho)-1.0 49 | secondMultiplier=(1.0-x)**(-1.0/self.rho) 50 | retval=-self.mu*firstMultiplier*secondMultiplier 51 | return retval 52 | 53 | def getDensityFunction(self,x): 54 | """Liniger f_u,p(x) (p. 21)""" 55 | numerator=self.rho*self.mu**self.rho 56 | denominator=(x+self.mu)**(self.rho+1) 57 | return numerator/denominator 58 | 59 | def getCumulativeDistributionFunction(self,x): 60 | """Liniger F_u,p(x)""" 61 | return 1.0-(self.mu/(x+self.mu))**self.rho 62 | 63 | def getImpactFunction(self,x): 64 | """Impact function for pareto distribution. Liniger g_k(x)""" 65 | term1Numerator=(self.rho-1.0)*(self.rho-2.0) 66 | term1Denominator=self.alpha*(self.rho-1.0)*(self.rho-2.0)+self.beta*self.mu*(self.rho-2.0)+2.0*self.gamma*self.mu*self.mu 67 | term2=self.alpha+self.beta*x+self.gamma*x*x 68 | 69 | return term1Numerator/term1Denominator*term2 70 | 71 | 72 | class VoidMarkDistribution: 73 | def __init__(self,params): 74 | """ 75 | Void impact function 76 | From Liniger thesis p. 21 77 | """ 78 | self.setParams(params) 79 | 80 | @staticmethod 81 | def getNumParameters(): 82 | return 0 83 | 84 | @staticmethod 85 | def getParameterBounds(): 86 | return [] 87 | 88 | def setParams(self, params): 89 | pass 90 | 91 | def getRandomValue(self): 92 | return 1.0 93 | 94 | def getDensityFunction(self,x): 95 | return 1.0 96 | 97 | def getCumulativeDistributionFunction(self,x): 98 | return 1.0 99 | 100 | def getImpactFunction(self,x): 101 | return 1.0 102 | 103 | 104 | class _NonNormalizedExponentialImpactFunction3Params: 105 | def __init__(self,params): 106 | """ 107 | Impact function for exponential distribution 108 | Liniger p.35 109 | """ 110 | self.setParams(params) 111 | 112 | @staticmethod 113 | def getNumParameters(): 114 | return 3 115 | 116 | @staticmethod 117 | def getParameterBounds(): 118 | return [[1e-5,None]]*3 119 | 120 | def setParams(self,params): 121 | self.alpha=params[0] 122 | self.beta=params[1] 123 | self.gamma=params[2] 124 | 125 | def getImpactFunction(self,lambd,x): 126 | term1Numerator=lambd*lambd 127 | term1Denominator=self.alpha*lambd*lambd+self.beta*lambd+2.0*self.gamma 128 | term2=self.alpha+self.beta*x+self.gamma*x*x 129 | 130 | return (term1Numerator/term1Denominator)*term2 131 | 132 | 133 | class _NonNormalizedExponentialImpactFunction1Params: 134 | def __init__(self, params): 135 | """ 136 | Impact function for exponential distribution 137 | Liniger p.35 138 | """ 139 | self.setParams(params) 140 | 141 | @staticmethod 142 | def getNumParameters(): 143 | return 1 144 | 145 | @staticmethod 146 | def getParameterBounds(): 147 | return [[1e-5, None]] 148 | 149 | def setParams(self, params): 150 | self.alpha = params[0] 151 | 152 | def getImpactFunction(self, lambd, x): 153 | term1Numerator=lambd**self.alpha 154 | term1Denominator=math.gamma(self.alpha+1.0) 155 | term2=x**self.alpha 156 | return (term1Numerator/term1Denominator)*term2 157 | 158 | 159 | 160 | class _GenericExponentialMarkDistribution: 161 | def __init__(self,params,impactFunctionClass): 162 | """ 163 | Exponential-distributed impact function 164 | Must have lambda>0 165 | Pass either _NonNormalizedExponentialImpactFunction3Params or _NonNormalizedExponentialImpactFunction1Param as impactFunctionClass 166 | See Liniger thesis p.35 167 | """ 168 | self.impactFunctionClass=impactFunctionClass 169 | self.setParams(params) 170 | 171 | 172 | def getNumParameters(self): 173 | return 1+self.impactFunctionClass.getNumParameters() 174 | 175 | def getParameterBounds(self): 176 | parameterBounds=[[1e-5,None]]+self.impactFunctionClass.getParameterBounds() 177 | return parameterBounds 178 | 179 | def setParams(self,params): 180 | self.lambd=params[0] 181 | self.impactFunctionClass.setParams(params[1:]) 182 | 183 | def getRandomValue(self): 184 | return random.expovariate(self.lambd) 185 | 186 | def getDensityFunction(self,x): 187 | return self.lambd*math.exp(-self.lambd*x) 188 | 189 | def getCumulativeDistributionFunction(self,x): 190 | return 1.0-math.exp(-self.lambd*x) 191 | 192 | def getImpactFunction(self,x): 193 | return self.impactFunctionClass.getImpactFunction(self.lambd,x) 194 | 195 | class ExponentialMarkDistribution1Param: 196 | def __init__(self,params): 197 | self.impactFunction=_NonNormalizedExponentialImpactFunction1Params(params[1:]) 198 | self.delegateMarkDistribution=_GenericExponentialMarkDistribution(params,self.impactFunction) 199 | 200 | self.setParams(params) 201 | 202 | @staticmethod 203 | def getNumParameters(): 204 | return _NonNormalizedExponentialImpactFunction1Params.getNumParameters()+1 205 | 206 | @staticmethod 207 | def getParameterBounds(): 208 | return [[1e-5,None]]+_NonNormalizedExponentialImpactFunction1Params.getParameterBounds() 209 | 210 | def setParams(self,params): 211 | self.delegateMarkDistribution.setParams(params) 212 | 213 | def getRandomValue(self): 214 | return self.delegateMarkDistribution.getRandomValue() 215 | 216 | def getDensityFunction(self,x): 217 | return self.delegateMarkDistribution.getDensityFunction(x) 218 | 219 | def getCumulativeDistributionFunction(self,x): 220 | return self.delegateMarkDistribution.getCumulativeDistributionFunction(x) 221 | 222 | def getImpactFunction(self,x): 223 | return self.delegateMarkDistribution.getImpactFunction(x) 224 | 225 | 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyHawkes 2 | ======== 3 | 4 | Hawkes Point Processes in Python 5 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tjohnson' 2 | --------------------------------------------------------------------------------