[SIP-5] Refactor Paired t-test (#5762)

* extract TTestTable into another file.

* Move into directory.

* update proptypes
This commit is contained in:
Krist Wongsuphasawat
2018-09-06 16:14:27 -07:00
committed by Chris Williams
parent b284788fd8
commit 68e7794daf
4 changed files with 118 additions and 66 deletions

View File

@@ -0,0 +1,67 @@
.paired_ttest .scrollbar-container {
overflow: auto;
}
.paired-ttest-table .scrollbar-content {
padding-left: 5px;
padding-right: 5px;
margin-bottom: 0;
}
.paired-ttest-table table {
margin-bottom: 0;
}
.paired-ttest-table h1 {
margin-left: 5px;
}
.reactable-data tr,
.reactable-header-sortable {
-webkit-transition: ease-in-out 0.1s;
transition: ease-in-out 0.1s;
}
.reactable-data tr:hover {
background-color: #e0e0e0;
}
.reactable-data tr .false {
color: #f44336;
}
.reactable-data tr .true {
color: #4caf50;
}
.reactable-data tr .control {
color: #2196f3;
}
.reactable-data tr .invalid {
color: #ff9800;
}
.reactable-data .control td {
background-color: #eeeeee;
}
.reactable-header-sortable:hover,
.reactable-header-sortable:focus,
.reactable-header-sort-asc,
.reactable-header-sort-desc {
background-color: #e0e0e0;
position: relative;
}
.reactable-header-sort-asc:after {
content: '\25bc';
position: absolute;
right: 10px;
}
.reactable-header-sort-desc:after {
content: '\25b2';
position: absolute;
right: 10px;
}

View File

@@ -0,0 +1,85 @@
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import React from 'react';
import TTestTable, { dataPropType } from './TTestTable';
import './PairedTTest.css';
const propTypes = {
className: PropTypes.string,
metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
groups: PropTypes.arrayOf(PropTypes.string).isRequired,
data: PropTypes.objectOf(dataPropType).isRequired,
alpha: PropTypes.number,
liftValPrec: PropTypes.number,
pValPrec: PropTypes.number,
};
const defaultProps = {
className: '',
alpha: 0.05,
liftValPrec: 4,
pValPrec: 6,
};
class PairedTTest extends React.PureComponent {
render() {
const {
className,
metrics,
groups,
data,
alpha,
pValPrec,
liftValPrec,
} = this.props;
return (
<div className={`paired-ttest-table scrollbar-container ${className}`}>
<div className="scrollbar-content">
{metrics.map((metric, i) => (
<TTestTable
key={i}
metric={metric}
groups={groups}
data={data[metric]}
alpha={alpha}
pValPrec={Math.min(pValPrec, 32)}
liftValPrec={Math.min(liftValPrec, 32)}
/>
))}
</div>
</div>
);
}
}
PairedTTest.propTypes = propTypes;
PairedTTest.defaultProps = defaultProps;
function adaptor(slice, payload) {
const { formData, selector } = slice;
const element = document.querySelector(selector);
const {
groupby: groups,
metrics,
liftvalue_precision: liftValPrec,
pvalue_precision: pValPrec,
significance_level: alpha,
} = formData;
console.log('groups', groups, payload.data);
ReactDOM.render(
<PairedTTest
metrics={metrics}
groups={groups}
data={payload.data}
alpha={alpha}
pValPrec={parseInt(pValPrec, 10)}
liftValPrec={parseInt(liftValPrec, 10)}
/>,
element,
);
}
export default adaptor;

View File

@@ -0,0 +1,244 @@
import dist from 'distributions';
import React from 'react';
import { Table, Tr, Td, Thead, Th } from 'reactable';
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 = {
metric: PropTypes.string.isRequired,
groups: PropTypes.arrayOf(PropTypes.string).isRequired,
data: dataPropType.isRequired,
alpha: PropTypes.number,
liftValPrec: PropTypes.number,
pValPrec: PropTypes.number,
};
const defaultProps = {
alpha: 0.05,
liftValPrec: 4,
pValPrec: 6,
};
class TTestTable extends React.Component {
constructor(props) {
super(props);
this.state = {
pValues: [],
liftValues: [],
control: 0,
};
}
componentWillMount() {
this.computeTTest(this.state.control); // initially populate table
}
getLiftStatus(row) {
// Get a css class name for coloring
if (row === this.state.control) {
return 'control';
}
const liftVal = this.state.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) {
if (row === this.state.control) {
return 'control';
}
const pVal = this.state.pValues[row];
if (Number.isNaN(pVal) || !Number.isFinite(pVal)) {
return 'invalid';
}
return ''; // p-values won't normally be colored
}
getSignificance(row) {
// Color significant as green, else red
if (row === this.state.control) {
return 'control';
}
// p-values significant below set threshold
return this.state.pValues[row] <= this.props.alpha;
}
computeLift(values, control) {
// Compute the lift value between two time series
let sumValues = 0;
let sumControl = 0;
for (let i = 0; i < values.length; i++) {
sumValues += values[i].y;
sumControl += control[i].y;
}
return (((sumValues - sumControl) / sumControl) * 100)
.toFixed(this.props.liftValPrec);
}
computePValue(values, control) {
// Compute the p-value from Student's t-test
// between two time series
let diffSum = 0;
let diffSqSum = 0;
let finiteCount = 0;
for (let i = 0; i < values.length; i++) {
const diff = control[i].y - values[i].y;
if (global.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(this.props.pValPrec); // two-sided test
} catch (err) {
return NaN;
}
}
computeTTest(control) {
// Compute lift and p-values for each row
// against the selected control
const data = this.props.data;
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({ pValues, liftValues, control });
}
render() {
const data = this.props.data;
const metric = this.props.metric;
const groups = this.props.groups;
// 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={this.state.pValues[i]}
/>,
);
values.push(
<Td
key={numGroups + 2}
className={this.getLiftStatus(i)}
column="liftValue"
data={this.state.liftValues[i]}
/>,
);
values.push(
<Td
key={numGroups + 3}
className={this.getSignificance(i)}
column="significant"
data={this.getSignificance(i)}
/>,
);
return (
<Tr
key={i}
onClick={this.computeTTest.bind(this, i)}
className={i === this.state.control ? 'control' : ''}
>
{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;