xmd79

MathStatisticsKernelDensityEstimation

Oct 8th, 2021
146
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 18.69 KB | None | 0 0
  1. // This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
  2. // © RicardoSantos
  3.  
  4. //@version=5
  5. // @description (KDE) Method for Kernel Density Estimation
  6. library(title='MathStatisticsKernelDensityEstimation')
  7.  
  8. //| reference:
  9. //| https://www.highcharts.com/blog/tutorials/data-science-and-highcharts-kernel-density-estimation/
  10. //| https://mathisonian.github.io/kde/
  11. //| https://en.wikipedia.org/wiki/Kernel_density_estimation
  12. //| https://bookdown.org/egarpor/NP-UC3M/kde-i-kde.html
  13. //| https://homepages.inf.ed.ac.uk/rbf/CVonline/LOCAL_COPIES/AV0405/MISHRA/kde.html
  14. //| https://en.wikipedia.org/wiki/Kernel_(statistics)
  15. //| https://aakinshin.net/posts/kde-discrete/
  16. //| description:
  17. //| "In statistics, kernel density estimation (KDE) is a non-parametric way to estimate the probability density function of a random variable." from wikipedia.com
  18.  
  19. // imports:
  20. import RicardoSantos/MathStatisticsKernelFunctions/1 as kernel
  21.  
  22.  
  23. // @funtion KDE method.
  24. // @param observations float array, sample data.
  25. // @param kernel string, the kernel to use, default='gaussian', options='uniform', 'triangle', 'epanechnikov', 'quartic', 'triweight', 'gaussian', 'cosine', 'logistic', 'sigmoid'.
  26. // @param bandwidth float, bandwidth to use in kernel, default=0.5, range=(0, +inf), less will smooth the data.
  27. // @param nsteps int, number of steps in range of distribution, default=20, this value is connected to how many line objects you can display per script.
  28. // @returns tuple with signature: (float array, float array)
  29. export kde (float[] observations, string kernel='gaussian', float bandwidth=0.5, int nsteps=20) => //{
  30. int _osize = array.size(observations)
  31.  
  32. switch
  33. (_osize < 1) => runtime.error('MathStatisticsKernelDensityEstimation -> kde(): "observations" has the wrong size.')
  34. (nsteps < 1) => runtime.error('MathStatisticsKernelDensityEstimation -> kde(): "nsteps" must be a positive number > 1.')
  35.  
  36. float _range = array.range(observations)
  37. float _min = array.min(observations) - _range / 2
  38. float _step = _range / nsteps
  39.  
  40. // generate the bins range of the distribution.
  41. float[] _density_range_fill = array.new_float(nsteps * 2)
  42. for _i = 0 to (nsteps * 2) - 1
  43. array.set(_density_range_fill, _i, _min + _i * _step)
  44.  
  45. float[] _x = array.new_float(0)
  46. float[] _y = array.new_float(0)
  47. for _i = 0 to array.size(_density_range_fill) - 1 by 1
  48. float _temp = 0.
  49. for _j = 0 to _osize - 1 by 1
  50. _temp += kernel.select(kernel, array.get(_density_range_fill, _i) - array.get(observations, _j), 1.0 / bandwidth)
  51. _temp
  52. array.push(_x, array.get(_density_range_fill, _i))
  53. array.push(_y, 1 / _osize * _temp)
  54. [_x, _y]
  55. //{ usage:
  56. //{ remarks:
  57. //}}}
  58.  
  59. // @function Draw a horizontal distribution at current location on chart.
  60. // @param distribution_x float array, distribution points x value.
  61. // @param distribution_y float array, distribution points y value.
  62. // @param distribution_lines line array, array to append the distribution curve lines.
  63. // @param graph_lines line array, array to append the graph lines.
  64. // @param graph_labels label array, array to append the graph labels.
  65. // @returns void, updates arrays: distribution_lines, graph_lines, graph_labels.
  66. export draw_horizontal (float[] distribution_x, float[] distribution_y, line[] distribution_lines, line[] graph_lines, label[] graph_labels) => //{
  67. int _sizex = array.size(distribution_x)
  68. int _sizey = array.size(distribution_y)
  69.  
  70. switch
  71. (_sizex < 1 or _sizex > 500) => runtime.error('MathStatisticsKernelDensityEstimation -> draw_horizontal(): "distribution_x" has the wrong size, must be within range [1, 500].')
  72. (_sizex != _sizey) => runtime.error('MathStatisticsKernelDensityEstimation -> draw_horizontal(): "distribution_x" and "distribution_y" size do not match.')
  73.  
  74. int _previousx = 0
  75. float _previousy = array.get(distribution_y, 0)
  76. if _sizex == _sizey and _sizex > 1
  77. int _nearest0_index = 0
  78. for _i = 1 to _sizex - 1 by 1
  79. _px = _i
  80. _py = array.get(distribution_y, _i)
  81. bool _xiscloserto0 = math.abs(array.get(distribution_x, _i)) < math.abs(array.get(distribution_x, _nearest0_index))
  82. if _xiscloserto0
  83. _nearest0_index := _i
  84. _nearest0_index
  85. array.push(distribution_lines, line.new(bar_index + _previousx, _previousy, bar_index + _px, _py))
  86.  
  87. _previousx := _px
  88. _previousy := _py
  89.  
  90. _previousy
  91. int _top_index = array.indexof(distribution_y, array.max(distribution_y))
  92. float _top_value = array.get(distribution_y, _top_index)
  93. float _std = array.stdev(distribution_y)
  94. array.push(graph_labels, label.new(bar_index, 0.0, str.tostring(array.get(distribution_x, 0)), style=label.style_label_right)) // range min
  95. array.push(graph_labels, label.new(bar_index + _sizex, 0.0, str.tostring(array.get(distribution_x, _sizex - 1)), style=label.style_label_left)) // range max
  96. array.push(graph_labels, label.new(bar_index + _top_index, 0.0, str.tostring(array.get(distribution_x, _top_index)), style=label.style_label_up, tooltip='Highest value')) // most frequent value
  97. array.push(graph_labels, label.new(bar_index, _std, str.format('Y std: {0, number, #.###}', _std), style=label.style_label_right, textalign=text.align_right, tooltip='Standard deviation')) // distribution std
  98. // array.push(graph_labels, label.new(bar_index, _top_value, str.format('Top probability: {0, number, #.###}\nSumY:{1, number, #.###}', nz(_top_value, 0.), array.sum(distribution_y)), style=label.style_label_right, textalign=text.align_right, tooltip=str.tostring(distribution_x) + '\n' + str.tostring(distribution_y)))
  99. array.push(graph_lines, line.new(bar_index, 0.0, bar_index+_sizex, 0.0, extend=extend.none, color=color.gray, style=line.style_dotted, width=2)) // axis y
  100. array.push(graph_lines, line.new(bar_index + _nearest0_index, 0.0 - _std, bar_index + _nearest0_index, _top_value + _std, extend=extend.none, color=color.gray, style=line.style_dotted, width=2)) // axis y
  101. array.push(graph_lines, line.new(bar_index + _top_index, 0.0, bar_index + _top_index, _top_value, extend=extend.none, color=color.blue, style=line.style_dashed, width=1)) // most frequent value
  102. array.push(graph_lines, line.new(bar_index, _std, bar_index + _sizex, _std, color=color.orange, style=line.style_dashed)) // distribution std
  103. //{ usage:
  104. // if barstate.islast
  105. // draw_horizontal(x, y)
  106. //{ remarks:
  107. //}}}
  108.  
  109.  
  110. // @function Draw a vertical distribution at current location on chart.
  111. // @param distribution_x float array, distribution points x value.
  112. // @param distribution_y float array, distribution points y value.
  113. // @param distribution_lines line array, array to append the distribution curve lines.
  114. // @param graph_lines line array, array to append the graph lines.
  115. // @param graph_labels label array, array to append the graph labels.
  116. // @returns void, updates arrays: distribution_lines, graph_lines, graph_labels.
  117. export draw_vertical (float[] distribution_x, float[] distribution_y, line[] distribution_lines, line[] graph_lines, label[] graph_labels) => //{
  118. int _sizex = array.size(distribution_x)
  119. int _sizey = array.size(distribution_y)
  120.  
  121. switch
  122. (_sizex < 1 or _sizex > 500) => runtime.error('MathStatisticsKernelDensityEstimation -> draw_vertical(): "distribution_x" has the wrong size, must be within range [1, 500].')
  123. (_sizex != _sizey) => runtime.error('MathStatisticsKernelDensityEstimation -> draw_vertical(): "distribution_x" and "distribution_y" size do not match.')
  124.  
  125. int _previousx = 0
  126. float _previousy = array.get(distribution_y, 0)
  127. if _sizex == _sizey and _sizex > 1
  128.  
  129. float _range = array.range(distribution_x)
  130. float _bottom = array.min(distribution_x)
  131. float _top = _bottom + _range
  132. float _step = _range / _sizex
  133. for _i = 1 to _sizex - 1 by 1
  134. _px = math.min(int(array.get(distribution_y, _i)*500), 500)//_i
  135. _py = _bottom +_i*_step//array.get(distribution_y, _i)
  136. array.push(distribution_lines, line.new(bar_index + _previousx, _previousy, bar_index + _px, _py))
  137.  
  138. _previousx := _px
  139. _previousy := _py
  140.  
  141. int _top_index = array.indexof(distribution_y, array.max(distribution_y))
  142. float _top_value = array.get(distribution_y, _top_index)
  143. float _std = array.stdev(distribution_y)
  144. int _std_index = int(math.min(_std * 500, 500))
  145.  
  146. float _most_dense_level = _bottom + _top_index * _step
  147. int _most_dense_index = int(_top_value*500)
  148.  
  149. array.push(graph_labels, label.new(bar_index, 0.0, str.tostring(array.get(distribution_x, 0)), style=label.style_label_up))
  150. array.push(graph_labels, label.new(bar_index, _top, str.tostring(array.get(distribution_x, _sizex - 1)), style=label.style_label_down))
  151. array.push(graph_labels, label.new(bar_index, _most_dense_level, str.tostring(array.get(distribution_x, _top_index)), style=label.style_label_right, tooltip='Highest value'))
  152. array.push(graph_labels, label.new(bar_index + _std_index, _bottom, str.format('Y std: {0, number, #.###}', _std), style=label.style_label_up, textalign=text.align_right, tooltip='Standard deviation'))
  153. // array.push(graph_labels, label.new(bar_index + _most_dense_index, _most_dense_level, str.format('\nTop probability: {0, number, #.###}\nSumY:{1, number, #.###}', nz(_top_value, 0.), array.sum(distribution_y)), style=label.style_label_left, textalign=text.align_left, tooltip=str.tostring(distribution_x) + '\n' + str.tostring(distribution_y)))
  154. array.push(graph_lines, line.new(bar_index, _bottom, bar_index, _top, extend=extend.none, color=color.gray, style=line.style_dotted, width=2)) // axis x
  155. array.push(graph_lines, line.new(bar_index, _bottom, bar_index+int(_most_dense_index*1.5), _bottom, extend=extend.none, color=color.gray, style=line.style_dotted, width=2)) // axis y
  156. array.push(graph_lines, line.new(bar_index, _most_dense_level, bar_index + _most_dense_index, _most_dense_level, extend=extend.none, color=color.blue, style=line.style_dashed, width=1)) // most frequent value
  157. array.push(graph_lines, line.new(bar_index + _std_index, _bottom, bar_index + _std_index, _top, color=color.orange, style=line.style_dashed)) // std
  158. //{ usage:
  159. //{ remarks:
  160. //}}}
  161.  
  162. // @function Style the distribution lines.
  163. // @param lines line array, distribution lines to style.
  164. // @param horizontal bool, default=true, if the display is horizontal(true) or vertical(false).
  165. // @param to_histogram bool, default=false, if graph style should be switched to histogram.
  166. // @param line_color color, default=na, if defined will change the color of the lines.
  167. // @param line_style string, defaul=na, if defined will change the line style, options=('na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both)
  168. // @param linewidth int, default=na, if defined will change the line width.
  169. // @returns void.
  170. export style_distribution (line[] lines, bool horizontal=true, bool to_histogram=false, color line_color=na, string line_style=na, int line_width=na) => //{
  171. int _size = array.size(lines)
  172.  
  173. switch
  174. (_size < 1) => runtime.error('MathStatisticsKernelDensityEstimation -> style_distribution(): "lines" has the wrong size.')
  175.  
  176. for _i = 0 to _size-1
  177. line _line = array.get(lines, _i)
  178. if to_histogram
  179. if horizontal
  180. line.set_y1(_line, 0.0)
  181. line.set_x1(_line, line.get_x2(_line)) // TODO: this is bugged at the moment
  182. else
  183. line.set_x1(_line, bar_index)
  184. line.set_y1(_line, line.get_y2(_line))
  185. if not na(line_color)
  186. line.set_color(_line, line_color)
  187. if not (line_style == '')
  188. line.set_style(_line, line_style)
  189. if not na(line_width)
  190. line.set_width(_line, line_width)
  191. //{ usage:
  192. //{ remarks:
  193. //}}}
  194.  
  195. // @function Style the graph lines and labels
  196. // @param lines line array, graph lines to style.
  197. // @param lines labels array, graph labels to style.
  198. // @param horizontal bool, default=true, if the display is horizontal(true) or vertical(false).
  199. // @param line_color color, default=na, if defined will change the color of the lines.
  200. // @param line_style string, defaul=na, if defined will change the line style, options=('na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both)
  201. // @param linewidth int, default=na, if defined will change the line width.
  202. // @returns void.
  203. export style_graph (
  204. line[] lines, label[] labels,
  205. color axis_x_color=na, string axis_x_style=na, int axis_x_width=na,
  206. color axis_y_color=na, string axis_y_style=na, int axis_y_width=na,
  207. color mostfreq_color=na, string mostfreq_style=na, int mostfreq_width=na,
  208. color std_color=na, string std_style=na, int std_width=na,
  209. color labels_color=na, color labels_text_color=na, string labels_size=na
  210. ) => //{
  211.  
  212. int _size = array.size(lines)
  213.  
  214. switch
  215. (_size < 1) => runtime.error('MathStatisticsKernelDensityEstimation -> style_distribution(): "lines" has the wrong size.')
  216.  
  217. // the lines
  218. line _axis_x = array.get(lines, 0)
  219. line _axis_y = array.get(lines, 1)
  220. line _mostfreq = array.get(lines, 2)
  221. line _stdline = array.get(lines, 3)
  222. // axis x
  223. if not na(axis_x_color)
  224. line.set_color(_axis_x, axis_x_color)
  225. if not (axis_x_style == '')
  226. line.set_style(_axis_x, axis_x_style)
  227. if not na(axis_x_width)
  228. line.set_width(_axis_x, axis_x_width)
  229. // axis y
  230. if not na(axis_y_color)
  231. line.set_color(_axis_y, axis_y_color)
  232. if not (axis_y_style == '')
  233. line.set_style(_axis_y, axis_y_style)
  234. if not na(axis_y_width)
  235. line.set_width(_axis_y, axis_y_width)
  236. // most freq
  237. if not na(mostfreq_color)
  238. line.set_color(_mostfreq, mostfreq_color)
  239. if not (mostfreq_style == '')
  240. line.set_style(_mostfreq, mostfreq_style)
  241. if not na(mostfreq_width)
  242. line.set_width(_mostfreq, mostfreq_width)
  243. // most freq
  244. if not na(std_color)
  245. line.set_color(_stdline, std_color)
  246. if not (std_style == '')
  247. line.set_style(_stdline, std_style)
  248. if not na(std_width)
  249. line.set_width(_stdline, std_width)
  250. // the labels
  251. if not na(labels_color)
  252. for _i = 0 to array.size(labels)-1
  253. label.set_color(array.get(labels, _i), labels_color)
  254. if not na(labels_text_color)
  255. for _i = 0 to array.size(labels)-1
  256. label.set_textcolor(array.get(labels, _i), labels_text_color)
  257. if not (labels_size == '')
  258. for _i = 0 to array.size(labels)-1
  259. label.set_size(array.get(labels, _i), labels_size)
  260. //{ usage:
  261. //{ remarks:
  262. //}}}
  263.  
  264.  
  265. int sample_size = input.int(100)
  266. var float[] observations = array.new_float(sample_size)
  267. if barstate.isfirst
  268. for _i = 0 to sample_size-1
  269. array.set(observations, _i, 50.0 + 100.0 * math.random())
  270.  
  271. string g_config = 'Configuration:'
  272. string i_kernel = input.string(title='Kernel:', defval='gaussian', options=['uniform', 'triangle', 'epanechnikov', 'quartic', 'triweight', 'gaussian', 'cosine', 'logistic', 'sigmoid'], tooltip='Kernel used in kde function.', group=g_config)
  273. float i_bandwidth = input.float(title='Bandwidth:', defval=0.618, tooltip='Bandwidth smoothness aplied to kernel.', group=g_config)
  274. int i_nstep = input.int(title='Nº Bins:', defval=20, tooltip='Number of Bins to split range.', group=g_config)
  275. [x, y] = kde(observations, i_kernel, i_bandwidth, i_nstep)
  276.  
  277. na_str(string value) => value == 'na' ? na : value
  278. na_col(color value) => color.t(value) > 99 ? na : value
  279. na_width(int value) => value > 199 ? na : value
  280.  
  281. bool horizontal = input.bool(true)
  282. bool histogram = input.bool(true)
  283. color line_color = na_col(input.color(defval=#ffffff00))
  284. string line_style = na_str(input.string(defval='na', options=['na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both]))
  285. int line_width = na_width(input.int(200))
  286.  
  287. // graph options:
  288. color axis_x_color = na_col(input.color(defval=#ffffff00))
  289. string axis_x_style = na_str(input.string(defval='na', options=['na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both]))
  290. int axis_x_width = na_width(input.int(200))
  291. color axis_y_color = na_col(input.color(defval=#ffffff00))
  292. string axis_y_style = na_str(input.string(defval='na', options=['na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both]))
  293. int axis_y_width = na_width(input.int(200))
  294. color mostfreq_color = na_col(input.color(defval=#ffffff00))
  295. string mostfreq_style = na_str(input.string(defval='na', options=['na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both]))
  296. int mostfreq_width = na_width(input.int(200))
  297. color std_color = na_col(input.color(defval=#ffffff00))
  298. string std_style = na_str(input.string(defval='na', options=['na', line.style_solid, line.style_dotted, line.style_dashed, line.style_arrow_right, line.style_arrow_left, line.style_arrow_both]))
  299. int std_width = na_width(input.int(200))
  300. color labels_color = na_col(input.color(defval=#ffffff00))
  301. color labels_text_color = na_col(input.color(defval=#ffffff00))
  302. string labels_size = na_str(input.string(defval='na', options=['na', size.tiny, size.small, size.normal, size.large, size.huge, size.auto]))
  303.  
  304. line[] distribution_lines = array.new_line(0)
  305. line[] graph_lines = array.new_line(0)
  306. label[] graph_labels = array.new_label(0)
  307. if barstate.islast
  308. if horizontal
  309. draw_horizontal(x, y, distribution_lines, graph_lines, graph_labels)
  310. else
  311. draw_vertical(x, y, distribution_lines, graph_lines, graph_labels)
  312.  
  313. distribution_lines := array.copy(distribution_lines)
  314. style_distribution(distribution_lines, horizontal, histogram, line_color, line_style, line_width)
  315. style_graph(graph_lines, graph_labels,
  316. axis_x_color, axis_x_style, axis_x_width,
  317. axis_y_color, axis_y_style, axis_y_width,
  318. mostfreq_color, mostfreq_style, mostfreq_width,
  319. std_color, std_style, std_width,
  320. labels_color, labels_text_color, labels_size
  321. )
  322.  
  323.  
  324.  
  325.  
  326.  
Advertisement
Add Comment
Please, Sign In to add comment