Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions src/transforms/aggregate.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,17 @@ var attrs = exports.attributes = {
'for example a sum of dates or average of categories.',
'*median* will return the average of the two central values if there is',
'an even count. *mode* will return the first value to reach the maximum',
'count, in case of a tie. *stddev* uses the population formula',
'(denominator N, not N-1)'
'count, in case of a tie.'
].join(' ')
},
funcmode: {
valType: 'enumerated',
values: ['sample', 'population'],
dflt: 'sample',
role: 'info',
description: [
'*stddev* supports two formula variants: *sample* (normalize by N-1)',
'and *population* (normalize by N).'
].join(' ')
},
enabled: {
Expand Down Expand Up @@ -148,17 +157,24 @@ exports.supplyDefaults = function(transformIn, traceOut) {

var aggregationsIn = transformIn.aggregations;
var aggregationsOut = transformOut.aggregations = new Array(aggregationsIn.length);
var aggregationOut;

function coercei(attr, dflt) {
return Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, attr, dflt);
}

if(aggregationsIn) {
for(i = 0; i < aggregationsIn.length; i++) {
var aggregationOut = {};
var target = Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, 'target');
var func = Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, 'func');
var enabledi = Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, 'enabled');
aggregationOut = {};
var target = coercei('target');
var func = coercei('func');
var enabledi = coercei('enabled');

// add this aggregation to the output only if it's the first instance
// of a valid target attribute - or an unused target attribute with "count"
if(enabledi && target && (arrayAttrs[target] || (func === 'count' && arrayAttrs[target] === undefined))) {
if(func === 'stddev') coercei('funcmode');

arrayAttrs[target] = 0;
aggregationsOut[i] = aggregationOut;
}
Expand Down Expand Up @@ -225,7 +241,7 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
var targetNP = Lib.nestedProperty(trace, attr);
var arrayIn = targetNP.get();
var conversions = Axes.getDataConversions(gd, trace, attr, arrayIn);
var func = getAggregateFunction(aggregation.func, conversions);
var func = getAggregateFunction(aggregation, conversions);

var arrayOut = new Array(groupings.length);
for(var i = 0; i < groupings.length; i++) {
Expand All @@ -234,7 +250,8 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
targetNP.set(arrayOut);
}

function getAggregateFunction(func, conversions) {
function getAggregateFunction(opts, conversions) {
var func = opts.func;
var d2c = conversions.d2c;
var c2d = conversions.c2d;

Expand Down Expand Up @@ -371,7 +388,11 @@ function getAggregateFunction(func, conversions) {
// is a number of milliseconds, and for categories it's a number
// of category differences, which is not generically meaningful but
// as in other cases we don't forbid it.
return Math.sqrt((total2 - (total * total / cnt)) / cnt);
var norm = (opts.funcmode === 'sample') ? (cnt - 1) : cnt;
// this is debatable: should a count of 1 return sample stddev of
// 0 or undefined?
if(!norm) return 0;
return Math.sqrt((total2 - (total * total / cnt)) / norm);
Copy link
Contributor

@rreusser rreusser Aug 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After all the discussion, I felt compelled to verify the formula, though I really do trust it's correct. But anyway, I ran it against a simple online calculator and copied the function into a super quick test: https://gist.github.com/rreusser/ffe0a6cea78dd153c6eea1d54ac11ea1

It gets a hearty 👍 from me. 👏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, make that 💃

};
}
}
Expand Down
7 changes: 5 additions & 2 deletions test/jasmine/tests/transform_aggregate_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ describe('aggregate', function() {
y: [1, 2, 3, 4, 5],
marker: {
size: [1, 2, 3, 4, 5],
line: {width: [1, 1, 2, 2, 1]}
line: {width: [1, 1, 2, 2, 1]},
color: [1, 1, 2, 2, 1]
},
transforms: [{
type: 'aggregate',
Expand All @@ -208,7 +209,8 @@ describe('aggregate', function() {
{target: 'x', func: 'mode'},
{target: 'y', func: 'median'},
{target: 'marker.size', func: 'rms'},
{target: 'marker.line.width', func: 'stddev'}
{target: 'marker.line.width', func: 'stddev', funcmode: 'population'},
{target: 'marker.color', func: 'stddev'}
]
}]
}]);
Expand All @@ -221,5 +223,6 @@ describe('aggregate', function() {
expect(traceOut.y).toBeCloseToArray([3.5, 2], 5);
expect(traceOut.marker.size).toBeCloseToArray([Math.sqrt(51 / 4), 2], 5);
expect(traceOut.marker.line.width).toBeCloseToArray([0.5, 0], 5);
expect(traceOut.marker.color).toBeCloseToArray([Math.sqrt(1 / 3), 0], 5);
});
});