TTestTable.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /* eslint-disable no-plusplus, react/no-array-index-key, react/jsx-no-bind */
  20. import dist from 'distributions';
  21. import React from 'react';
  22. import { Table, Tr, Td, Thead, Th } from 'reactable-arc';
  23. import PropTypes from 'prop-types';
  24. export const dataPropType = PropTypes.arrayOf(PropTypes.shape({
  25. group: PropTypes.arrayOf(PropTypes.string),
  26. values: PropTypes.arrayOf(PropTypes.shape({
  27. x: PropTypes.number,
  28. y: PropTypes.number
  29. }))
  30. }));
  31. const propTypes = {
  32. alpha: PropTypes.number,
  33. data: dataPropType.isRequired,
  34. groups: PropTypes.arrayOf(PropTypes.string).isRequired,
  35. liftValPrec: PropTypes.number,
  36. metric: PropTypes.string.isRequired,
  37. pValPrec: PropTypes.number
  38. };
  39. const defaultProps = {
  40. alpha: 0.05,
  41. liftValPrec: 4,
  42. pValPrec: 6
  43. };
  44. class TTestTable extends React.Component {
  45. constructor(props) {
  46. super(props);
  47. this.state = {
  48. control: 0,
  49. liftValues: [],
  50. pValues: []
  51. };
  52. }
  53. componentDidMount() {
  54. const {
  55. control
  56. } = this.state;
  57. this.computeTTest(control); // initially populate table
  58. }
  59. getLiftStatus(row) {
  60. const {
  61. control,
  62. liftValues
  63. } = this.state; // Get a css class name for coloring
  64. if (row === control) {
  65. return 'control';
  66. }
  67. const liftVal = liftValues[row];
  68. if (Number.isNaN(liftVal) || !Number.isFinite(liftVal)) {
  69. return 'invalid'; // infinite or NaN values
  70. }
  71. return liftVal >= 0 ? 'true' : 'false'; // green on true, red on false
  72. }
  73. getPValueStatus(row) {
  74. const {
  75. control,
  76. pValues
  77. } = this.state;
  78. if (row === control) {
  79. return 'control';
  80. }
  81. const pVal = pValues[row];
  82. if (Number.isNaN(pVal) || !Number.isFinite(pVal)) {
  83. return 'invalid';
  84. }
  85. return ''; // p-values won't normally be colored
  86. }
  87. getSignificance(row) {
  88. const {
  89. control,
  90. pValues
  91. } = this.state;
  92. const {
  93. alpha
  94. } = this.props; // Color significant as green, else red
  95. if (row === control) {
  96. return 'control';
  97. } // p-values significant below set threshold
  98. return pValues[row] <= alpha;
  99. }
  100. computeLift(values, control) {
  101. const {
  102. liftValPrec
  103. } = this.props; // Compute the lift value between two time series
  104. let sumValues = 0;
  105. let sumControl = 0;
  106. values.forEach((value, i) => {
  107. sumValues += value.y;
  108. sumControl += control[i].y;
  109. });
  110. return ((sumValues - sumControl) / sumControl * 100).toFixed(liftValPrec);
  111. }
  112. computePValue(values, control) {
  113. const {
  114. pValPrec
  115. } = this.props; // Compute the p-value from Student's t-test
  116. // between two time series
  117. let diffSum = 0;
  118. let diffSqSum = 0;
  119. let finiteCount = 0;
  120. values.forEach((value, i) => {
  121. const diff = control[i].y - value.y;
  122. /* eslint-disable-next-line */
  123. if (isFinite(diff)) {
  124. finiteCount++;
  125. diffSum += diff;
  126. diffSqSum += diff * diff;
  127. }
  128. });
  129. const tvalue = -Math.abs(diffSum * Math.sqrt((finiteCount - 1) / (finiteCount * diffSqSum - diffSum * diffSum)));
  130. try {
  131. return (2 * new dist.Studentt(finiteCount - 1).cdf(tvalue)).toFixed(pValPrec); // two-sided test
  132. } catch (error) {
  133. return NaN;
  134. }
  135. }
  136. computeTTest(control) {
  137. // Compute lift and p-values for each row
  138. // against the selected control
  139. const {
  140. data
  141. } = this.props;
  142. const pValues = [];
  143. const liftValues = [];
  144. if (!data) {
  145. return;
  146. }
  147. for (let i = 0; i < data.length; i++) {
  148. if (i === control) {
  149. pValues.push('control');
  150. liftValues.push('control');
  151. } else {
  152. pValues.push(this.computePValue(data[i].values, data[control].values));
  153. liftValues.push(this.computeLift(data[i].values, data[control].values));
  154. }
  155. }
  156. this.setState({
  157. control,
  158. liftValues,
  159. pValues
  160. });
  161. }
  162. render() {
  163. const {
  164. data,
  165. metric,
  166. groups
  167. } = this.props;
  168. const {
  169. control,
  170. liftValues,
  171. pValues
  172. } = this.state; // Render column header for each group
  173. const columns = groups.map((group, i) => React.createElement(Th, {
  174. key: i,
  175. column: group
  176. }, group));
  177. const numGroups = groups.length; // Columns for p-value, lift-value, and significance (true/false)
  178. columns.push(React.createElement(Th, {
  179. key: numGroups + 1,
  180. column: "pValue"
  181. }, "p-value"));
  182. columns.push(React.createElement(Th, {
  183. key: numGroups + 2,
  184. column: "liftValue"
  185. }, "Lift %"));
  186. columns.push(React.createElement(Th, {
  187. key: numGroups + 3,
  188. column: "significant"
  189. }, "Significant"));
  190. const rows = data.map((entry, i) => {
  191. const values = groups.map((group, j) => // group names
  192. React.createElement(Td, {
  193. key: j,
  194. column: group,
  195. data: entry.group[j]
  196. }));
  197. values.push(React.createElement(Td, {
  198. key: numGroups + 1,
  199. className: this.getPValueStatus(i),
  200. column: "pValue",
  201. data: pValues[i]
  202. }));
  203. values.push(React.createElement(Td, {
  204. key: numGroups + 2,
  205. className: this.getLiftStatus(i),
  206. column: "liftValue",
  207. data: liftValues[i]
  208. }));
  209. values.push(React.createElement(Td, {
  210. key: numGroups + 3,
  211. className: this.getSignificance(i).toString(),
  212. column: "significant",
  213. data: this.getSignificance(i)
  214. }));
  215. return React.createElement(Tr, {
  216. key: i,
  217. className: i === control ? 'control' : '',
  218. onClick: this.computeTTest.bind(this, i)
  219. }, values);
  220. }); // When sorted ascending, 'control' will always be at top
  221. const sortConfig = groups.concat([{
  222. column: 'pValue',
  223. sortFunction: (a, b) => {
  224. if (a === 'control') {
  225. return -1;
  226. }
  227. if (b === 'control') {
  228. return 1;
  229. }
  230. return a > b ? 1 : -1; // p-values ascending
  231. }
  232. }, {
  233. column: 'liftValue',
  234. sortFunction: (a, b) => {
  235. if (a === 'control') {
  236. return -1;
  237. }
  238. if (b === 'control') {
  239. return 1;
  240. }
  241. return parseFloat(a) > parseFloat(b) ? -1 : 1; // lift values descending
  242. }
  243. }, {
  244. column: 'significant',
  245. sortFunction: (a, b) => {
  246. if (a === 'control') {
  247. return -1;
  248. }
  249. if (b === 'control') {
  250. return 1;
  251. }
  252. return a > b ? -1 : 1; // significant values first
  253. }
  254. }]);
  255. return React.createElement("div", null, React.createElement("h3", null, metric), React.createElement(Table, {
  256. className: "table",
  257. id: "table_" + metric,
  258. sortable: sortConfig
  259. }, React.createElement(Thead, null, columns), rows));
  260. }
  261. }
  262. TTestTable.propTypes = propTypes;
  263. TTestTable.defaultProps = defaultProps;
  264. export default TTestTable;