Skip to content

Commit 572f282

Browse files
committed
sample vs population stddev
1 parent b6ccc01 commit 572f282

File tree

2 files changed

+35
-11
lines changed

2 files changed

+35
-11
lines changed

src/transforms/aggregate.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,17 @@ var attrs = exports.attributes = {
7979
'for example a sum of dates or average of categories.',
8080
'*median* will return the average of the two central values if there is',
8181
'an even count. *mode* will return the first value to reach the maximum',
82-
'count, in case of a tie. *stddev* uses the population formula',
83-
'(denominator N, not N-1)'
82+
'count, in case of a tie.'
83+
].join(' ')
84+
},
85+
funcmode: {
86+
valType: 'enumerated',
87+
values: ['sample', 'population'],
88+
dflt: 'sample',
89+
role: 'info',
90+
description: [
91+
'*stddev* supports two formula variants: *sample* (normalize by N-1)',
92+
'and *population* (normalize by N).'
8493
].join(' ')
8594
},
8695
enabled: {
@@ -148,17 +157,24 @@ exports.supplyDefaults = function(transformIn, traceOut) {
148157

149158
var aggregationsIn = transformIn.aggregations;
150159
var aggregationsOut = transformOut.aggregations = new Array(aggregationsIn.length);
160+
var aggregationOut;
161+
162+
function coercei(attr, dflt) {
163+
return Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, attr, dflt);
164+
}
151165

152166
if(aggregationsIn) {
153167
for(i = 0; i < aggregationsIn.length; i++) {
154-
var aggregationOut = {};
155-
var target = Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, 'target');
156-
var func = Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, 'func');
157-
var enabledi = Lib.coerce(aggregationsIn[i], aggregationOut, aggAttrs, 'enabled');
168+
aggregationOut = {};
169+
var target = coercei('target');
170+
var func = coercei('func');
171+
var enabledi = coercei('enabled');
158172

159173
// add this aggregation to the output only if it's the first instance
160174
// of a valid target attribute - or an unused target attribute with "count"
161175
if(enabledi && target && (arrayAttrs[target] || (func === 'count' && arrayAttrs[target] === undefined))) {
176+
if(func === 'stddev') coercei('funcmode');
177+
162178
arrayAttrs[target] = 0;
163179
aggregationsOut[i] = aggregationOut;
164180
}
@@ -225,7 +241,7 @@ function aggregateOneArray(gd, trace, groupings, aggregation) {
225241
var targetNP = Lib.nestedProperty(trace, attr);
226242
var arrayIn = targetNP.get();
227243
var conversions = Axes.getDataConversions(gd, trace, attr, arrayIn);
228-
var func = getAggregateFunction(aggregation.func, conversions);
244+
var func = getAggregateFunction(aggregation, conversions);
229245

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

237-
function getAggregateFunction(func, conversions) {
253+
function getAggregateFunction(opts, conversions) {
254+
var func = opts.func;
238255
var d2c = conversions.d2c;
239256
var c2d = conversions.c2d;
240257

@@ -371,7 +388,11 @@ function getAggregateFunction(func, conversions) {
371388
// is a number of milliseconds, and for categories it's a number
372389
// of category differences, which is not generically meaningful but
373390
// as in other cases we don't forbid it.
374-
return Math.sqrt((total2 - (total * total / cnt)) / cnt);
391+
var norm = (opts.funcmode === 'sample') ? (cnt - 1) : cnt;
392+
// this is debatable: should a count of 1 return sample stddev of
393+
// 0 or undefined?
394+
if(!norm) return 0;
395+
return Math.sqrt((total2 - (total * total / cnt)) / norm);
375396
};
376397
}
377398
}

test/jasmine/tests/transform_aggregate_test.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ describe('aggregate', function() {
199199
y: [1, 2, 3, 4, 5],
200200
marker: {
201201
size: [1, 2, 3, 4, 5],
202-
line: {width: [1, 1, 2, 2, 1]}
202+
line: {width: [1, 1, 2, 2, 1]},
203+
color: [1, 1, 2, 2, 1]
203204
},
204205
transforms: [{
205206
type: 'aggregate',
@@ -208,7 +209,8 @@ describe('aggregate', function() {
208209
{target: 'x', func: 'mode'},
209210
{target: 'y', func: 'median'},
210211
{target: 'marker.size', func: 'rms'},
211-
{target: 'marker.line.width', func: 'stddev'}
212+
{target: 'marker.line.width', func: 'stddev', funcmode: 'population'},
213+
{target: 'marker.color', func: 'stddev'}
212214
]
213215
}]
214216
}]);
@@ -221,5 +223,6 @@ describe('aggregate', function() {
221223
expect(traceOut.y).toBeCloseToArray([3.5, 2], 5);
222224
expect(traceOut.marker.size).toBeCloseToArray([Math.sqrt(51 / 4), 2], 5);
223225
expect(traceOut.marker.line.width).toBeCloseToArray([0.5, 0], 5);
226+
expect(traceOut.marker.color).toBeCloseToArray([Math.sqrt(1 / 3), 0], 5);
224227
});
225228
});

0 commit comments

Comments
 (0)