blob: 086b285f876dc838578ac79484627fc2e02b1f2a [file] [log] [blame]
/**
* 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 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 += 1;
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 += 1) {
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) => (
<Th key={i} column={group}>
{group}
</Th>
));
const numGroups = groups.length;
// Columns for p-value, lift-value, and significance (true/false)
columns.push(
<Th key={numGroups + 1} column="pValue">
p-value
</Th>,
);
columns.push(
<Th key={numGroups + 2} column="liftValue">
Lift %
</Th>,
);
columns.push(
<Th key={numGroups + 3} column="significant">
Significant
</Th>,
);
const rows = data.map((entry, i) => {
const values = groups.map((
group,
j, // group names
) => <Td key={j} column={group} data={entry.group[j]} />);
values.push(
<Td
key={numGroups + 1}
className={this.getPValueStatus(i)}
column="pValue"
data={pValues[i]}
/>,
);
values.push(
<Td
key={numGroups + 2}
className={this.getLiftStatus(i)}
column="liftValue"
data={liftValues[i]}
/>,
);
values.push(
<Td
key={numGroups + 3}
className={this.getSignificance(i).toString()}
column="significant"
data={this.getSignificance(i)}
/>,
);
return (
<Tr
key={i}
className={i === control ? 'control' : ''}
onClick={this.computeTTest.bind(this, i)}
>
{values}
</Tr>
);
});
// 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 (
<div>
<h3>{metric}</h3>
<Table className="table" id={`table_${metric}`} sortable={sortConfig}>
<Thead>{columns}</Thead>
{rows}
</Table>
</div>
);
}
}
TTestTable.propTypes = propTypes;
TTestTable.defaultProps = defaultProps;
export default TTestTable;