123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- /**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
- /* eslint-disable no-plusplus, react/no-array-index-key, react/jsx-no-bind */
- import dist from 'distributions';
- import React from 'react';
- import { Table, Tr, Td, Thead, Th } from 'reactable-arc';
- import PropTypes from 'prop-types';
- export const dataPropType = PropTypes.arrayOf(PropTypes.shape({
- group: PropTypes.arrayOf(PropTypes.string),
- values: PropTypes.arrayOf(PropTypes.shape({
- x: PropTypes.number,
- y: PropTypes.number
- }))
- }));
- const propTypes = {
- alpha: PropTypes.number,
- data: dataPropType.isRequired,
- groups: PropTypes.arrayOf(PropTypes.string).isRequired,
- liftValPrec: PropTypes.number,
- metric: PropTypes.string.isRequired,
- pValPrec: PropTypes.number
- };
- const defaultProps = {
- alpha: 0.05,
- liftValPrec: 4,
- pValPrec: 6
- };
- class TTestTable extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- control: 0,
- liftValues: [],
- pValues: []
- };
- }
- componentDidMount() {
- const {
- control
- } = this.state;
- this.computeTTest(control); // initially populate table
- }
- getLiftStatus(row) {
- const {
- control,
- liftValues
- } = this.state; // Get a css class name for coloring
- if (row === control) {
- return 'control';
- }
- const liftVal = liftValues[row];
- if (Number.isNaN(liftVal) || !Number.isFinite(liftVal)) {
- return 'invalid'; // infinite or NaN values
- }
- return liftVal >= 0 ? 'true' : 'false'; // green on true, red on false
- }
- getPValueStatus(row) {
- const {
- control,
- pValues
- } = this.state;
- if (row === control) {
- return 'control';
- }
- const pVal = pValues[row];
- if (Number.isNaN(pVal) || !Number.isFinite(pVal)) {
- return 'invalid';
- }
- return ''; // p-values won't normally be colored
- }
- getSignificance(row) {
- const {
- control,
- pValues
- } = this.state;
- const {
- alpha
- } = this.props; // Color significant as green, else red
- if (row === control) {
- return 'control';
- } // p-values significant below set threshold
- return pValues[row] <= alpha;
- }
- computeLift(values, control) {
- const {
- liftValPrec
- } = this.props; // Compute the lift value between two time series
- let sumValues = 0;
- let sumControl = 0;
- values.forEach((value, i) => {
- sumValues += value.y;
- sumControl += control[i].y;
- });
- return ((sumValues - sumControl) / sumControl * 100).toFixed(liftValPrec);
- }
- computePValue(values, control) {
- const {
- pValPrec
- } = this.props; // Compute the p-value from Student's t-test
- // between two time series
- let diffSum = 0;
- let diffSqSum = 0;
- let finiteCount = 0;
- values.forEach((value, i) => {
- const diff = control[i].y - value.y;
- /* eslint-disable-next-line */
- if (isFinite(diff)) {
- finiteCount++;
- diffSum += diff;
- diffSqSum += diff * diff;
- }
- });
- const tvalue = -Math.abs(diffSum * Math.sqrt((finiteCount - 1) / (finiteCount * diffSqSum - diffSum * diffSum)));
- try {
- return (2 * new dist.Studentt(finiteCount - 1).cdf(tvalue)).toFixed(pValPrec); // two-sided test
- } catch (error) {
- return NaN;
- }
- }
- computeTTest(control) {
- // Compute lift and p-values for each row
- // against the selected control
- const {
- data
- } = this.props;
- const pValues = [];
- const liftValues = [];
- if (!data) {
- return;
- }
- for (let i = 0; i < data.length; i++) {
- if (i === control) {
- pValues.push('control');
- liftValues.push('control');
- } else {
- pValues.push(this.computePValue(data[i].values, data[control].values));
- liftValues.push(this.computeLift(data[i].values, data[control].values));
- }
- }
- this.setState({
- control,
- liftValues,
- pValues
- });
- }
- render() {
- const {
- data,
- metric,
- groups
- } = this.props;
- const {
- control,
- liftValues,
- pValues
- } = this.state; // Render column header for each group
- const columns = groups.map((group, i) => React.createElement(Th, {
- key: i,
- column: group
- }, group));
- const numGroups = groups.length; // Columns for p-value, lift-value, and significance (true/false)
- columns.push(React.createElement(Th, {
- key: numGroups + 1,
- column: "pValue"
- }, "p-value"));
- columns.push(React.createElement(Th, {
- key: numGroups + 2,
- column: "liftValue"
- }, "Lift %"));
- columns.push(React.createElement(Th, {
- key: numGroups + 3,
- column: "significant"
- }, "Significant"));
- const rows = data.map((entry, i) => {
- const values = groups.map((group, j) => // group names
- React.createElement(Td, {
- key: j,
- column: group,
- data: entry.group[j]
- }));
- values.push(React.createElement(Td, {
- key: numGroups + 1,
- className: this.getPValueStatus(i),
- column: "pValue",
- data: pValues[i]
- }));
- values.push(React.createElement(Td, {
- key: numGroups + 2,
- className: this.getLiftStatus(i),
- column: "liftValue",
- data: liftValues[i]
- }));
- values.push(React.createElement(Td, {
- key: numGroups + 3,
- className: this.getSignificance(i).toString(),
- column: "significant",
- data: this.getSignificance(i)
- }));
- return React.createElement(Tr, {
- key: i,
- className: i === control ? 'control' : '',
- onClick: this.computeTTest.bind(this, i)
- }, values);
- }); // When sorted ascending, 'control' will always be at top
- const sortConfig = groups.concat([{
- column: 'pValue',
- sortFunction: (a, b) => {
- if (a === 'control') {
- return -1;
- }
- if (b === 'control') {
- return 1;
- }
- return a > b ? 1 : -1; // p-values ascending
- }
- }, {
- column: 'liftValue',
- sortFunction: (a, b) => {
- if (a === 'control') {
- return -1;
- }
- if (b === 'control') {
- return 1;
- }
- return parseFloat(a) > parseFloat(b) ? -1 : 1; // lift values descending
- }
- }, {
- column: 'significant',
- sortFunction: (a, b) => {
- if (a === 'control') {
- return -1;
- }
- if (b === 'control') {
- return 1;
- }
- return a > b ? -1 : 1; // significant values first
- }
- }]);
- return React.createElement("div", null, React.createElement("h3", null, metric), React.createElement(Table, {
- className: "table",
- id: "table_" + metric,
- sortable: sortConfig
- }, React.createElement(Thead, null, columns), rows));
- }
- }
- TTestTable.propTypes = propTypes;
- TTestTable.defaultProps = defaultProps;
- export default TTestTable;
|