From cc030719ef3243c6cd6b8f21e6f64aba3d3e7364 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 28 Aug 2018 14:06:21 -0700 Subject: [PATCH 1/3] extract TTestTable into another file. --- .../assets/src/visualizations/TTestTable.jsx | 238 +++++++++++++ .../src/visualizations/paired_ttest.css | 10 +- .../src/visualizations/paired_ttest.jsx | 325 ++++-------------- 3 files changed, 308 insertions(+), 265 deletions(-) create mode 100644 superset/assets/src/visualizations/TTestTable.jsx diff --git a/superset/assets/src/visualizations/TTestTable.jsx b/superset/assets/src/visualizations/TTestTable.jsx new file mode 100644 index 0000000000000..9f553e3fa0758 --- /dev/null +++ b/superset/assets/src/visualizations/TTestTable.jsx @@ -0,0 +1,238 @@ +import dist from 'distributions'; +import React from 'react'; +import { Table, Tr, Td, Thead, Th } from 'reactable'; +import PropTypes from 'prop-types'; + +import './paired_ttest.css'; + +const propTypes = { + metric: PropTypes.string.isRequired, + groups: PropTypes.array.isRequired, + data: PropTypes.array.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) => ( + {group} + )); + const numGroups = groups.length; + // Columns for p-value, lift-value, and significance (true/false) + columns.push(p-value); + columns.push(Lift %); + columns.push(Significant); + const rows = data.map((entry, i) => { + const values = groups.map((group, j) => ( // group names + + )); + values.push( + , + ); + values.push( + , + ); + values.push( + , + ); + return ( + + {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 ( +
+

{metric}

+ + + {columns} + + {rows} +
+
+ ); + } +} + +TTestTable.propTypes = propTypes; +TTestTable.defaultProps = defaultProps; + +export default TTestTable; diff --git a/superset/assets/src/visualizations/paired_ttest.css b/superset/assets/src/visualizations/paired_ttest.css index 0a2c1b8d28f28..95068ca3011ac 100644 --- a/superset/assets/src/visualizations/paired_ttest.css +++ b/superset/assets/src/visualizations/paired_ttest.css @@ -1,5 +1,5 @@ .paired_ttest .scrollbar-container { - overflow: scroll; + overflow: auto; } .paired-ttest-table .scrollbar-content { @@ -8,6 +8,10 @@ margin-bottom: 0; } +.paired-ttest-table table { + margin-bottom: 0; +} + .paired-ttest-table h1 { margin-left: 5px; } @@ -61,7 +65,3 @@ position: absolute; right: 10px; } - -.paired-ttest-table table { - margin-bottom: 0; -} diff --git a/superset/assets/src/visualizations/paired_ttest.jsx b/superset/assets/src/visualizations/paired_ttest.jsx index e715f02358615..02d52294826a1 100644 --- a/superset/assets/src/visualizations/paired_ttest.jsx +++ b/superset/assets/src/visualizations/paired_ttest.jsx @@ -1,277 +1,82 @@ -import d3 from 'd3'; -import dist from 'distributions'; -import React from 'react'; -import { Table, Tr, Td, Thead, Th } from 'reactable'; -import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import TTestTable from './TTestTable'; -import './paired_ttest.css'; - -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 (isNaN(liftVal) || !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 (isNaN(pVal) || !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; - } - } +const propTypes = { + className: PropTypes.string, + metrics: PropTypes.arrayOf(PropTypes.string).isRequired, + groups: PropTypes.array.isRequired, + data: PropTypes.object.isRequired, + alpha: PropTypes.number, + liftValPrec: PropTypes.number, + pValPrec: PropTypes.number, +}; - 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 }); - } +const defaultProps = { + className: '', + alpha: 0.05, + liftValPrec: 4, + pValPrec: 6, +}; +class PairedTTest extends React.PureComponent { 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) => ( - {group} - )); - const numGroups = groups.length; - // Columns for p-value, lift-value, and significance (true/false) - columns.push(p-value); - columns.push(Lift %); - columns.push(Significant); - const rows = data.map((entry, i) => { - const values = groups.map((group, j) => ( // group names - - )); - values.push( - , - ); - values.push( - , - ); - values.push( - , - ); - return ( - - {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 - }, - }, - ]); + const { + className, + metrics, + groups, + data, + alpha, + pValPrec, + liftValPrec, + } = this.props; return ( -
-

{metric}

- - - {columns} - - {rows} -
+
+
+ {metrics.map((metric, i) => ( + + ))} +
); } } -TTestTable.propTypes = { - metric: PropTypes.string.isRequired, - groups: PropTypes.array.isRequired, - data: PropTypes.array.isRequired, - alpha: PropTypes.number.isRequired, - liftValPrec: PropTypes.number.isRequired, - pValPrec: PropTypes.number.isRequired, -}; -TTestTable.defaultProps = { - metric: '', - groups: [], - data: [], - alpha: 0.05, - liftValPrec: 4, - pValPrec: 6, -}; +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; -function pairedTTestVis(slice, payload) { - const div = d3.select(slice.selector); - const container = slice.container; - const height = slice.container.height(); - const fd = slice.formData; - const data = payload.data; - const alpha = fd.significance_level; - const pValPrec = fd.pvalue_precision; - const liftValPrec = fd.liftvalue_precision; - const tables = fd.metrics.map((metric, i) => ( // create a table for each metric - 32 ? 32 : pValPrec} - liftValPrec={liftValPrec > 32 ? 32 : liftValPrec} - /> - )); - div.html(''); ReactDOM.render( -
-
-
-
- {tables} -
-
-
-
, - div.node(), + , + element, ); - container.find('.scrollbar-container').css('max-height', height); } -module.exports = pairedTTestVis; +export default adaptor; From 0b66d36b7ce4c1836efb1a010969c5f4703c5efa Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Tue, 28 Aug 2018 14:11:15 -0700 Subject: [PATCH 2/3] Move into directory. --- .../{paired_ttest.css => PairedTTest/PairedTTest.css} | 0 .../{paired_ttest.jsx => PairedTTest/PairedTTest.jsx} | 1 + .../assets/src/visualizations/{ => PairedTTest}/TTestTable.jsx | 2 -- superset/assets/src/visualizations/index.js | 2 +- 4 files changed, 2 insertions(+), 3 deletions(-) rename superset/assets/src/visualizations/{paired_ttest.css => PairedTTest/PairedTTest.css} (100%) rename superset/assets/src/visualizations/{paired_ttest.jsx => PairedTTest/PairedTTest.jsx} (98%) rename superset/assets/src/visualizations/{ => PairedTTest}/TTestTable.jsx (99%) diff --git a/superset/assets/src/visualizations/paired_ttest.css b/superset/assets/src/visualizations/PairedTTest/PairedTTest.css similarity index 100% rename from superset/assets/src/visualizations/paired_ttest.css rename to superset/assets/src/visualizations/PairedTTest/PairedTTest.css diff --git a/superset/assets/src/visualizations/paired_ttest.jsx b/superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx similarity index 98% rename from superset/assets/src/visualizations/paired_ttest.jsx rename to superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx index 02d52294826a1..de923c4bf8e70 100644 --- a/superset/assets/src/visualizations/paired_ttest.jsx +++ b/superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import React from 'react'; import TTestTable from './TTestTable'; +import './PairedTTest.css'; const propTypes = { className: PropTypes.string, diff --git a/superset/assets/src/visualizations/TTestTable.jsx b/superset/assets/src/visualizations/PairedTTest/TTestTable.jsx similarity index 99% rename from superset/assets/src/visualizations/TTestTable.jsx rename to superset/assets/src/visualizations/PairedTTest/TTestTable.jsx index 9f553e3fa0758..0fd1fa3e4f318 100644 --- a/superset/assets/src/visualizations/TTestTable.jsx +++ b/superset/assets/src/visualizations/PairedTTest/TTestTable.jsx @@ -3,8 +3,6 @@ import React from 'react'; import { Table, Tr, Td, Thead, Th } from 'reactable'; import PropTypes from 'prop-types'; -import './paired_ttest.css'; - const propTypes = { metric: PropTypes.string.isRequired, groups: PropTypes.array.isRequired, diff --git a/superset/assets/src/visualizations/index.js b/superset/assets/src/visualizations/index.js index 66cff81859497..93e680915cb18 100644 --- a/superset/assets/src/visualizations/index.js +++ b/superset/assets/src/visualizations/index.js @@ -113,7 +113,7 @@ const vizMap = { [VIZ_TYPES.event_flow]: () => loadVis(import(/* webpackChunkName: "EventFlow" */ './EventFlow.jsx')), [VIZ_TYPES.paired_ttest]: () => - loadVis(import(/* webpackChunkName: "paired_ttest" */ './paired_ttest.jsx')), + loadVis(import(/* webpackChunkName: "paired_ttest" */ './PairedTTest/PairedTTest.jsx')), [VIZ_TYPES.partition]: () => loadVis(import(/* webpackChunkName: "partition" */ './partition.js')), [VIZ_TYPES.deck_scatter]: () => From 751eef588620bff8c58692dee2c3f14ec56aea12 Mon Sep 17 00:00:00 2001 From: Krist Wongsuphasawat Date: Wed, 5 Sep 2018 14:53:08 -0700 Subject: [PATCH 3/3] update proptypes --- .../src/visualizations/PairedTTest/PairedTTest.jsx | 8 +++++--- .../src/visualizations/PairedTTest/TTestTable.jsx | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx b/superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx index de923c4bf8e70..3a26e9d399467 100644 --- a/superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx +++ b/superset/assets/src/visualizations/PairedTTest/PairedTTest.jsx @@ -2,14 +2,14 @@ import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import React from 'react'; -import TTestTable from './TTestTable'; +import TTestTable, { dataPropType } from './TTestTable'; import './PairedTTest.css'; const propTypes = { className: PropTypes.string, metrics: PropTypes.arrayOf(PropTypes.string).isRequired, - groups: PropTypes.array.isRequired, - data: PropTypes.object.isRequired, + groups: PropTypes.arrayOf(PropTypes.string).isRequired, + data: PropTypes.objectOf(dataPropType).isRequired, alpha: PropTypes.number, liftValPrec: PropTypes.number, pValPrec: PropTypes.number, @@ -67,6 +67,8 @@ function adaptor(slice, payload) { significance_level: alpha, } = formData; + console.log('groups', groups, payload.data); + ReactDOM.render(