fit_curves.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. # !/usr/bin/env python3.7
  2. # -*- coding: utf-8 -*-
  3. """
  4. Tool lib for curve fitting and modeling.
  5. Created on 02.09.20
  6. @author yannansu
  7. """
  8. from __future__ import absolute_import, division, print_function
  9. from builtins import object
  10. import numpy as np
  11. from scipy import optimize
  12. import lmfit
  13. import matplotlib.pyplot as plt
  14. from scipy.stats import vonmises
  15. import scipy.stats as st
  16. from scipy.special import i0
  17. class _baseFunctionFit(object):
  18. """
  19. =================================================================================
  20. Fit PF to cumulative Gaussian and include all parameter fitting (lapse, chance).
  21. Adapted from Psychopy.data.FitCumNormal.
  22. =================================================================================
  23. Not needed by most users except as a superclass for developing
  24. your own functions
  25. Derived classes must have _eval and _inverse methods with @staticmethods
  26. """
  27. def __init__(self, xx, yy, sems=1.0, guess=None, display=1,
  28. expectedMin=0.5, lapse=0.05, optimize_kws=None):
  29. super(_baseFunctionFit, self).__init__()
  30. self.xx = np.array(xx)
  31. self.yy = np.array(yy)
  32. self.sems = np.array(sems)
  33. if not hasattr(sems, "__len__"):
  34. # annoyingly in numpy 1.13 len(numpy.array(1)) gives an error
  35. self.sems.shape = (1,) # otherwise we can't get len (in numpy 1.13)
  36. self.expectedMin = expectedMin
  37. self.lapse = lapse
  38. self.guess = guess
  39. self.optimize_kws = {}
  40. if optimize_kws is not None:
  41. self.optimize_kws = optimize_kws
  42. # for holding error calculations:
  43. self.ssq = 0
  44. self.rms = 0
  45. self.chi = 0
  46. # do the calculations:
  47. self._doFit()
  48. def _doFit(self):
  49. """The Fit class that derives this needs to specify its _evalFunction
  50. """
  51. # get some useful variables to help choose starting fit vals
  52. # self.params = optimize.fmin_powell(self._getErr, self.params,
  53. # (self.xx,self.yy,self.sems),disp=self.display)
  54. # self.params = optimize.fmin_bfgs(self._getErr, self.params, None,
  55. # (self.xx,self.yy,self.sems),disp=self.display)
  56. from scipy import optimize
  57. # don't import optimize at top of script. Slow and not always present!
  58. global _chance
  59. global _lapse
  60. _chance = self.expectedMin
  61. _lapse = self.lapse
  62. if len(self.sems) == 1:
  63. sems = None
  64. else:
  65. sems = self.sems
  66. self.params, self.covar = optimize.curve_fit(
  67. self._eval, self.xx, self.yy, p0=self.guess, sigma=sems,
  68. **self.optimize_kws)
  69. self.ssq = self._getErr(self.params, self.xx, self.yy, 1.0)
  70. self.chi = self._getErr(self.params, self.xx, self.yy, self.sems)
  71. self.rms = self.ssq / len(self.xx)
  72. def _getErr(self, params, xx, yy, sems):
  73. mod = self.eval(xx, params)
  74. err = sum((yy - mod) ** 2 / sems)
  75. return err
  76. def eval(self, xx, params=None):
  77. """Evaluate xx for the current parameters of the model, or for
  78. arbitrary params if these are given.
  79. """
  80. if params is None:
  81. params = self.params
  82. global _chance
  83. global _lapse
  84. _chance = self.expectedMin
  85. _lapse = self.lapse
  86. # _eval is a static method - must be done this way because the
  87. # curve_fit function doesn't want to have any `self` object as
  88. # first arg
  89. yy = self._eval(xx, *params)
  90. return yy
  91. def inverse(self, yy, params=None):
  92. """Evaluate yy for the current parameters of the model,
  93. or for arbitrary params if these are given.
  94. """
  95. if params is None:
  96. # so the user can set params for this particular inv
  97. params = self.params
  98. xx = self._inverse(yy, *params)
  99. return xx
  100. class FitCumNormal(_baseFunctionFit):
  101. """
  102. =================================================================================
  103. Fit PF to cumulative Gaussian and include all parameter fitting (lapse, chance).
  104. Adapted from Psychopy.data.FitCumNormal.
  105. =================================================================================
  106. Fit a Cumulative Normal function (aka error function or erf)
  107. of the form::
  108. y = chance + (1-chance-lapse)*((special.erf((xx-xShift)/(sqrt(2)*sd))+1)*0.5)
  109. and with inverse::
  110. x = xShift+sqrt(2)*sd*(erfinv(((yy-chance)/(1-chance-lapse)-.5)*2))
  111. After fitting the function you can evaluate an array of x-values
  112. with fit.eval(x), retrieve the inverse of the function with
  113. fit.inverse(y) or retrieve the parameters from fit.params (a list
  114. with [centre, sd] for the Gaussian distribution forming the cumulative)
  115. NB: Prior to version 1.74 the parameters had different meaning, relating
  116. to xShift and slope of the function (similar to 1/sd). Although that is
  117. more in with the parameters for the Weibull fit, for instance, it is less
  118. in keeping with standard expectations of normal (Gaussian distributions)
  119. so in version 1.74.00 the parameters became the [centre,sd] of the normal
  120. distribution.
  121. """
  122. # static methods have no `self` and this is important for
  123. # optimise.curve_fit
  124. @staticmethod
  125. def _eval(xx, xShift, sd):
  126. from scipy import special
  127. global _chance
  128. global _lpase
  129. xx = np.asarray(xx)
  130. # NB np.special.erf() goes from -1:1
  131. yy = (_chance + (1 - _chance - _lapse) *
  132. ((special.erf((xx - xShift) / (np.sqrt(2) * sd)) + 1) * 0.5))
  133. return yy
  134. @staticmethod
  135. def _inverse(yy, xShift, sd):
  136. from scipy import special
  137. global _chance
  138. global _lpase
  139. yy = np.asarray(yy)
  140. # xx = (special.erfinv((yy-chance)/(1-chance)*2.0-1)+xShift)/xScale
  141. # NB: np.special.erfinv() goes from -1:1
  142. xx = (xShift + np.sqrt(2) * sd *
  143. special.erfinv(((yy - _chance) / (1 - _chance - _lapse) - 0.5) * 2))
  144. return xx