Index: tracing/tracing/value/numeric.html |
diff --git a/tracing/tracing/value/numeric.html b/tracing/tracing/value/numeric.html |
index f8fc53f890a6ecd424619850dbcc716a1a47cc0a..2d11c620596b4e249ff2a3145cc89316ca91774c 100644 |
--- a/tracing/tracing/value/numeric.html |
+++ b/tracing/tracing/value/numeric.html |
@@ -18,7 +18,17 @@ found in the LICENSE file. |
tr.exportTo('tr.v', function() { |
var Range = tr.b.Range; |
- var MAX_SOURCE_INFOS = 16; |
+ var MAX_DIAGNOSTICS = 16; |
+ |
+ // p-values less than this indicate statistical significance. |
+ var DEFAULT_ALPHA = 0.05; |
+ |
+ /** @enum */ |
+ var Significance = { |
+ DONT_CARE: -1, |
+ INSIGNIFICANT: 0, |
+ SIGNIFICANT: 1 |
+ }; |
function NumericBase(unit) { |
if (!(unit instanceof tr.v.Unit)) |
@@ -28,6 +38,32 @@ tr.exportTo('tr.v', function() { |
} |
NumericBase.prototype = { |
+ merge: function(other) { |
+ if (this.unit !== other.unit) |
+ throw new Error('Merging Numerics with different units'); |
+ |
+ // Two Numerics that were built using the same NumericBuilder |
+ // can be merged using addNumeric(). |
+ if (this instanceof Numeric && other instanceof Numeric && |
+ this.canAddNumeric(other)) { |
+ var result = this.clone(); |
+ result.addNumeric(other.clone()); |
+ return result; |
+ } |
+ |
+ // Either a Scalar and a Numeric, or two Scalars... |
+ // or two Numerics that were not built using the same NumericBuilder, |
+ // should be built from their raw samples. |
+ var samples = []; |
+ this.sampleValuesInto(samples); |
+ other.sampleValuesInto(samples); |
+ return Numeric.buildFromSamples(this.unit, samples); |
+ }, |
+ |
+ sampleValuesInto: function(samples) { |
+ throw new Error('Not implemented'); |
+ }, |
+ |
asDict: function() { |
var d = { |
unit: this.unit.asJSON() |
@@ -52,7 +88,7 @@ tr.exportTo('tr.v', function() { |
this.parentNumeric = parentNumeric; |
this.range = opt_range || (new tr.b.Range()); |
this.count = 0; |
- this.sourceInfos = []; |
+ this.diagnostics = []; |
} |
NumericBin.fromDict = function(parentNumeric, d) { |
@@ -60,22 +96,29 @@ tr.exportTo('tr.v', function() { |
n.range.min = d.min; |
n.range.max = d.max; |
n.count = d.count; |
- n.sourceInfos = d.sourceInfos; |
+ if (d.diagnostics) |
+ n.diagnostics = d.diagnostics.map(dd => tr.v.d.Diagnostic.fromDict(dd)); |
return n; |
}; |
NumericBin.prototype = { |
- add: function(value, sourceInfo) { |
+ /** |
+ * @param {*} value |
+ * @param {!tr.v.d.Diagnostic=} opt_diagnostic |
+ */ |
+ add: function(value, opt_diagnostic) { |
this.count += 1; |
- tr.b.Statistics.uniformlySampleStream(this.sourceInfos, this.count, |
- sourceInfo, MAX_SOURCE_INFOS); |
+ if (opt_diagnostic) { |
+ tr.b.Statistics.uniformlySampleStream( |
+ this.diagnostics, this.count, opt_diagnostic, MAX_DIAGNOSTICS); |
+ } |
}, |
addBin: function(other) { |
if (!this.range.equals(other.range)) |
throw new Error('Merging incompatible Numeric bins.'); |
- tr.b.Statistics.mergeSampledStreams(this.sourceInfos, this.count, |
- other.sourceInfos, other.count, MAX_SOURCE_INFOS); |
+ tr.b.Statistics.mergeSampledStreams(this.diagnostics, this.count, |
+ other.diagnostics, other.count, MAX_DIAGNOSTICS); |
this.count += other.count; |
}, |
@@ -84,7 +127,7 @@ tr.exportTo('tr.v', function() { |
min: this.range.min, |
max: this.range.max, |
count: this.count, |
- sourceInfos: this.sourceInfos.slice(0) |
+ diagnostics: this.diagnostics.map(d => d.asDict()) |
}; |
}, |
@@ -99,7 +142,7 @@ tr.exportTo('tr.v', function() { |
this.range = range; |
this.numNans = 0; |
- this.nanSourceInfos = []; |
+ this.nanDiagnostics = []; |
this.running = new tr.b.RunningStatistics(); |
this.maxCount_ = 0; |
@@ -118,6 +161,9 @@ tr.exportTo('tr.v', function() { |
this.maxCount_ = bin.count; |
}, this); |
+ this.sampleValues_ = []; |
+ this.maxNumSampleValues = this.allBins.length * 10; |
+ |
this.summaryOptions = this.defaultSummaryOptions(); |
} |
@@ -138,10 +184,49 @@ tr.exportTo('tr.v', function() { |
if (d.summaryOptions) |
n.customizeSummaryOptions(d.summaryOptions); |
n.numNans = d.numNans; |
- n.nanSourceInfos = d.nanSourceInfos; |
+ if (d.nanDiagnostics) { |
+ n.nanDiagnostics = d.nanDiagnostics.map( |
+ dd => tr.v.d.Diagnostic.fromDict(dd)); |
+ } |
+ n.maxNumSampleValues = d.maxNumSampleValues; |
+ n.sampleValues_ = d.sampleValues; |
return n; |
}; |
+ /** |
+ * @param {!tr.v.Unit} unit |
+ * @param {!Array.<number>} samples |
+ * @return {!Numeric} |
+ */ |
+ Numeric.buildFromSamples = function(unit, samples) { |
+ var range = new tr.b.Range(); |
+ // Prevent non-numeric samples from introducing NaNs into the range. |
+ for (var sample of samples) |
+ if (!isNaN(Math.max(sample))) |
+ range.addValue(sample); |
+ |
+ // NumericBuilder.addLinearBins() requires this. |
+ if (range.isEmpty) |
+ range.addValue(1); |
+ if (range.min === range.max) |
+ range.addValue(range.min - 1); |
+ |
+ // This optimizes the resolution when samples are uniformly distributed |
+ // (which is almost never the case). |
+ var numBins = Math.ceil(Math.sqrt(samples.length)); |
+ var builder = new NumericBuilder(unit, range.min); |
+ builder.addLinearBins(range.max, numBins); |
+ |
+ var result = builder.build(); |
+ result.maxNumSampleValues = 1000; |
+ |
+ // TODO(eakuefner): Propagate diagnostics? |
+ for (var sample of samples) |
+ result.add(sample); |
+ |
+ return result; |
+ }; |
+ |
Numeric.prototype = { |
__proto__: NumericBase.prototype, |
@@ -163,6 +248,35 @@ tr.exportTo('tr.v', function() { |
return this.maxCount_; |
}, |
+ /** |
+ * Requires that units agree. |
+ * Returns DONT_CARE if that is the units' improvementDirection. |
+ * Returns SIGNIFICANT if the Mann-Whitney U test returns a |
+ * p-value less than opt_alpha or DEFAULT_ALPHA. Returns INSIGNIFICANT if |
+ * the p-value is greater than alpha. |
+ * |
+ * @param {!tr.v.Numeric} other |
+ * @param {number=} opt_alpha |
+ * @return {!tr.v.Significance} |
+ */ |
+ getDifferenceSignificance: function(other, opt_alpha) { |
+ if (this.unit !== other.unit) |
+ throw new Error('Cannot compare Numerics with different units'); |
+ |
+ if (this.unit.improvementDirection === |
+ tr.v.ImprovementDirection.DONT_CARE) { |
+ return tr.v.Significance.DONT_CARE; |
+ } |
+ |
+ if (!(other instanceof Numeric)) |
+ throw new Error('Unable to compute a p-value'); |
+ |
+ var mwu = tr.b.Statistics.mwu.test(this.sampleValues, other.sampleValues); |
+ if (mwu.p < (opt_alpha || DEFAULT_ALPHA)) |
+ return tr.v.Significance.SIGNIFICANT; |
+ return tr.v.Significance.INSIGNIFICANT; |
+ }, |
+ |
/* |
* Compute an approximation of percentile based on the counts in the bins. |
* If the real percentile lies within |this.range| then the result of |
@@ -255,29 +369,70 @@ tr.exportTo('tr.v', function() { |
return this.allBins[binIndex] || this.overflowBin; |
}, |
- add: function(value, sourceInfo) { |
+ /** |
+ * @param {*} value |
+ * @param {!tr.v.d.Diagnostic=} opt_diagnostic |
+ */ |
+ add: function(value, opt_diagnostic) { |
if (typeof(value) !== 'number' || isNaN(value)) { |
this.numNans++; |
- tr.b.Statistics.uniformlySampleStream(this.nanSourceInfos, this.numNans, |
- sourceInfo, MAX_SOURCE_INFOS); |
- return; |
+ if (opt_diagnostic) { |
+ tr.b.Statistics.uniformlySampleStream(this.nanDiagnostics, |
+ this.numNans, opt_diagnostic, MAX_DIAGNOSTICS); |
+ } |
+ } else { |
+ var bin = this.getBinForValue(value); |
+ bin.add(value, opt_diagnostic); |
+ this.running.add(value); |
+ if (bin.count > this.maxCount_) |
+ this.maxCount_ = bin.count; |
} |
- var bin = this.getBinForValue(value); |
- bin.add(value, sourceInfo); |
- this.running.add(value); |
- if (bin.count > this.maxCount_) |
- this.maxCount_ = bin.count; |
+ tr.b.Statistics.uniformlySampleStream(this.sampleValues_, |
+ this.numValues + this.numNans, value, this.maxNumSampleValues); |
+ }, |
+ |
+ sampleValuesInto: function(samples) { |
+ for (var sampleValue of this.sampleValues) |
+ samples.push(sampleValue); |
+ }, |
+ |
+ /** |
+ * Return true if this Numeric can be added to |other|. |
+ * |
+ * @param {!tr.v.Numeric} other |
+ * @return {boolean} |
+ */ |
+ canAddNumeric: function(other) { |
+ if (!this.range.equals(other.range)) |
+ return false; |
+ if (this.unit !== other.unit) |
+ return false; |
+ if (this.allBins.length !== other.allBins.length) |
+ return false; |
+ |
+ for (var i = 0; i < this.allBins.length; ++i) |
+ if (!this.allBins[i].range.equals(other.allBins[i].range)) |
+ return false; |
+ |
+ return true; |
}, |
+ /** |
+ * Add |other| to this Numeric in-place if they can be added. |
+ * |
+ * @param {!tr.v.Numeric} other |
+ */ |
addNumeric: function(other) { |
- if (!this.range.equals(other.range) || |
- !this.unit === other.unit || |
- this.allBins.length !== other.allBins.length) { |
+ if (!this.canAddNumeric(other)) |
throw new Error('Merging incompatible Numerics.'); |
- } |
- tr.b.Statistics.mergeSampledStreams(this.nanSourceInfos, this.numNans, |
- other.nanSourceInfos, other.numNans, MAX_SOURCE_INFOS); |
+ |
+ tr.b.Statistics.mergeSampledStreams(this.nanDiagnostics, this.numNans, |
+ other.nanDiagnostics, other.numNans, MAX_DIAGNOSTICS); |
+ tr.b.Statistics.mergeSampledStreams( |
+ this.sampleValues, this.numValues, |
+ other.sampleValues, other.numValues, tr.b.Statistics.mean( |
+ [this.maxNumSampleValues, other.maxNumSampleValues])); |
this.numNans += other.numNans; |
this.running = this.running.merge(other.running); |
for (var i = 0; i < this.allBins.length; ++i) { |
@@ -307,6 +462,7 @@ tr.exportTo('tr.v', function() { |
std: true, |
min: true, |
max: true, |
+ nans: false, |
percentile: [] |
}; |
}, |
@@ -365,9 +521,15 @@ tr.exportTo('tr.v', function() { |
scalar: new tr.v.ScalarNumeric(this.unit, percentile) |
}); |
}, this); |
+ } else if (stat === 'nans') { |
+ results.push({ |
+ name: 'nans', |
+ scalar: new tr.v.ScalarNumeric( |
+ tr.v.Unit.byName.count_smallerIsBetter, this.numNans) |
+ }); |
} else { |
var statUnit = stat === 'count' ? |
- tr.v.Unit.byName.unitlessNumber_smallerIsBetter : this.unit; |
+ tr.v.Unit.byName.count_smallerIsBetter : this.unit; |
var key = statNameToKey(stat); |
var statValue = this.running[key]; |
if (typeof(statValue) === 'number') { |
@@ -381,6 +543,10 @@ tr.exportTo('tr.v', function() { |
return results; |
}, |
+ get sampleValues() { |
+ return this.sampleValues_; |
+ }, |
+ |
clone: function() { |
return Numeric.fromDict(this.asDict()); |
}, |
@@ -394,11 +560,13 @@ tr.exportTo('tr.v', function() { |
max: this.range.max, |
numNans: this.numNans, |
- nanSourceInfos: this.nanSourceInfos, |
+ nanDiagnostics: this.nanDiagnostics.map(d => d.asDict()), |
running: this.running.asDict(), |
summaryOptions: this.summaryOptions, |
+ sampleValues: this.sampleValues, |
+ maxNumSampleValues: this.maxNumSampleValues, |
underflowBin: this.underflowBin.asDict(), |
centralBins: this.centralBins.map(function(bin) { |
return bin.asDict(); |
@@ -508,8 +676,8 @@ tr.exportTo('tr.v', function() { |
var curMaxBinBoundary = this.maxBinBoundary; |
if (curMaxBinBoundary >= nextMaxBinBoundary) { |
- throw new Error('The last added max boundary must be greater than ' + |
- 'the current max boundary boundary'); |
+ throw new Error('The new max bin boundary must be greater than ' + |
+ 'the previous max bin boundary'); |
} |
var binWidth = (nextMaxBinBoundary - curMaxBinBoundary) / binCount; |
@@ -601,6 +769,11 @@ tr.exportTo('tr.v', function() { |
/** |
* Create a linearly scaled tr.v.NumericBuilder with |numBins| bins ranging |
* from |range.min| to |range.max|. |
+ * |
+ * @param {!tr.v.Unit} unit |
+ * @param {!tr.b.Range} range |
+ * @param {number} numBins |
+ * @return {tr.v.NumericBuilder} |
*/ |
NumericBuilder.createLinear = function(unit, range, numBins) { |
if (range.isEmpty) |
@@ -609,7 +782,26 @@ tr.exportTo('tr.v', function() { |
range.max, numBins); |
}; |
+ /** |
+ * Create an exponentially scaled tr.v.NumericBuilder with |numBins| bins |
+ * ranging from |range.min| to |range.max|. |
+ * |
+ * @param {!tr.v.Unit} unit |
+ * @param {!tr.b.Range} range |
+ * @param {number} numBins |
+ * @return {tr.v.NumericBuilder} |
+ */ |
+ NumericBuilder.createExponential = function(unit, range, numBins) { |
+ if (range.isEmpty) |
+ throw new Error('Range must be non-empty'); |
+ return new NumericBuilder(unit, range.min).addExponentialBins( |
+ range.max, numBins); |
+ }; |
+ |
function ScalarNumeric(unit, value) { |
+ if (!(unit instanceof tr.v.Unit)) |
+ throw new Error('Expected Unit'); |
+ |
if (!(typeof(value) == 'number')) |
throw new Error('Expected value to be number'); |
@@ -635,6 +827,10 @@ tr.exportTo('tr.v', function() { |
d.value = this.value; |
}, |
+ sampleValuesInto: function(samples) { |
+ samples.push(this.value); |
+ }, |
+ |
toString: function() { |
return this.unit.format(this.value); |
} |
@@ -657,6 +853,7 @@ tr.exportTo('tr.v', function() { |
}; |
return { |
+ Significance: Significance, |
NumericBase: NumericBase, |
NumericBin: NumericBin, |
Numeric: Numeric, |