Skip to content

Instantly share code, notes, and snippets.

@nkullman
Last active April 15, 2021 10:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nkullman/f65d5619843dc22e061d957249121408 to your computer and use it in GitHub Desktop.
Save nkullman/f65d5619843dc22e061d957249121408 to your computer and use it in GitHub Desktop.
Correlation and conflict sandbox
See how 2D correlation and conflict measures change as you add/delete data.

Correlation and conflict sandbox

Click the graph area to add data points; click points again to remove. As you modify the data, various measures of correlation and conflict are updated.

Conflict coefficients shown here:

  • "Our 1" uses Spearman's rho and is weighted by average distance to the ideal solution

Correlation coefficients shown here:

/**
* Test metric. Computes array of average tradeoff angles
* for points defined by arrX and arrY and returns the
* average.
*/
var computeTEST = function (arrX, arrY) {
var result = [];
var n = arrX.length;
for (var i=0; i<n; i++){
var avgForThisPt = computeAvgTOAngle(arrX,arrY,i);
result.push(avgForThisPt);
}
return computeAvgOfArrElements(result);
}
/**
* Computes the average of elements in an array.
* @param {*} arr array over which to take the average
*/
var computeAvgOfArrElements = function(arr) {
if (arr.length) {
sum = arr.reduce(function(a, b) { return a + b; });
return sum / arr.length;
}
return null;
}
/**
* Computes the average angle of the tradeoff vector
* between point (arrX[idx],arrY[idx]) and all other
* points:
* (arrX[i],arrY[i]) for all i!=idx in arrX.length
*
* Tradeoff vector is {arrX[i]-arrX[idx], arrY[i]-arrY[idx]}
*
*/
var computeAvgTOAngle = function (arrX, arrY, idx) {
// simple error handling for input arrays of nonequal lengths
if (arrX.length !== arrY.length) { return null; }
var n = arrX.length;
var x = arrX[idx];
var y = arrY[idx];
var result = 0;
var ptX,ptY,val;
for (var i=0; i<n; i++){
if (i===idx) continue;
ptX = arrX[i];
ptY = arrY[i];
if (y===ptY) { // if same y-coordinate
if (x<ptX){
val=0;
} else if (x>ptX){
val=-Math.PI;
} else {
continue;
}
} else if (x===ptX) { // if same x-coordinate
if (y<ptY){
val=Math.PI/2;
} else if (y>ptY){
val=-Math.PI/2;
} else {
continue;
}
} else {
val=Math.atan((ptY-y)/(ptX-x));
}
result += val;
}
return result/=n;
}
/** Compute customized conflict metric.
* Metric is a combination of Spearman's rho and
* average distance to the ideal solution */
var computeOur1 = function (arrX, arrY) {
var rho = computeSpearmans(arrX, arrY);
var d = computeDistanceRatio(arrX, arrY);
return d * (1.0 - rho) / 2.0;
}
/** Computes the distance ratio:
*
* (avg dist from solutions to ideal)
* ----------------------------------
* (distance from nadir to ideal)
*
*
* Both arrays must be for maximized objectives
* */
var computeDistanceRatio = function (arrX, arrY) {
// simple error handling for input arrays of nonequal lengths
if (arrX.length !== arrY.length) { return null; }
var n = arrX.length;
// get nadir and ideal solutions (assumes objs are to be maxed)
var nadirSolution = [Math.min.apply(null, arrX), Math.min.apply(null, arrY)];
var idealSolution = [Math.max.apply(null, arrX), Math.max.apply(null, arrY)];
// max distance
var dmax = dist(nadirSolution, idealSolution);
// average distance from points to the ideal solution
var dbar = 0;
for (var i = 0; i < n; i++) {
dbar += dist([arrX[i], arrY[i]], idealSolution);
}
dbar /= n;
return dbar / dmax;
}
/** Computes the distance between 2-d points pt1 and pt2 */
var dist = function (pt1, pt2) {
return Math.sqrt(Math.pow((pt1[0] - pt2[0]), 2) + Math.pow((pt1[1] - pt2[1]), 2));
}
/** Computes Spearman's rho, adjusted for ties */
var computeSpearmans = function (arrX, arrY) {
// simple error handling for input arrays of nonequal lengths
if (arrX.length !== arrY.length) { return null; }
// number of observations
var n = arrX.length;
// rank datasets
var xRanked = rankArray(arrX),
yRanked = rankArray(arrY);
// sum of distances between ranks
var dsq = 0;
for (var i = 0; i < n; i++) {
dsq += Math.pow(xRanked[i] - yRanked[i], 2);
}
// compute correction for ties
var xTies = countTies(arrX),
yTies = countTies(arrY);
var xCorrection = 0,
yCorrection = 0;
for (var tieLength in xTies) {
xCorrection += xTies[tieLength] * tieLength * (Math.pow(tieLength, 2) - 1)
}
xCorrection /= 12.0;
for (var tieLength in yTies) {
yCorrection += yTies[tieLength] * tieLength * (Math.pow(tieLength, 2) - 1)
}
yCorrection /= 12.0;
// denominator
var denominator = n * (Math.pow(n, 2) - 1) / 6.0;
// compute rho
var rho = denominator - dsq - xCorrection - yCorrection;
rho /= Math.sqrt((denominator - 2 * xCorrection) * (denominator - 2 * yCorrection))
return rho;
}
/** Computes the rank array for arr, where each entry in arr is
* assigned a value 1 thru n, where n is arr.length.
*
* Tied entries in arr are each given the average rank of the ties.
* Lower ranks are not increased
*/
var rankArray = function (arr) {
// ranking without averaging
var sorted = arr.slice().sort(function (a, b) { return b - a });
var ranks = arr.slice().map(function (v) { return sorted.indexOf(v) + 1 });
// counts of each rank
var counts = {};
ranks.forEach(function (x) { counts[x] = (counts[x] || 0) + 1; });
// average duplicates
ranks = ranks.map(function (x) { return x + 0.5 * ((counts[x] || 0) - 1); });
return ranks;
}
/** Counts the number of ties in arr, and returns
* an object with
* a key for each tie length (an entry n for each n-way tie) and
* a value corresponding to the number of key-way (n-way) ties
*/
var countTies = function (arr) {
var ties = {},
arrSorted = arr.slice().sort(),
currValue = arrSorted[0],
tieLength = 1;
for (var i = 1; i < arrSorted.length; i++) {
if (arrSorted[i] === currValue) {
tieLength++;
} else {
if (tieLength > 1) {
if (ties[tieLength] === undefined) ties[tieLength] = 0;
ties[tieLength]++;
}
currValue = arrSorted[i];
tieLength = 1;
}
}
if (tieLength > 1) {
if (ties[tieLength] === undefined) ties[tieLength] = 0;
ties[tieLength]++;
}
return ties;
}
/** Compute Pearson's correlation coefficient */
var computePearsons = function (arrX, arrY) {
var num = covar(arrX, arrY);
var denom = d3.deviation(arrX) * d3.deviation(arrY);
return num / denom;
}
/** Kendall's tau-a (does not handle tie breaking) */
var computeKendalls = function (arrX, arrY) {
var n = arrX.length;
return con_dis_diff(arrX,arrY)/(n*(n-1)/2);
}
/** Computes the covariance between random variable observations
* arrX and arrY
*/
var covar = function (arrX, arrY) {
var u = d3.mean(arrX);
var v = d3.mean(arrY);
var arrXLen = arrX.length;
var sq_dev = new Array(arrXLen);
var i;
for (i = 0; i < arrXLen; i++)
sq_dev[i] = (arrX[i] - u) * (arrY[i] - v);
return d3.sum(sq_dev) / (arrXLen - 1);
}
/** Computes Schott's spacing metric:
* the standard deviation of minimal spacing between pairs (i,j)
* of observations of X and Y
*/
var computeSchottsSpacing = function (arrX, arrY) {
// minimum distances from each solution to another
var dists = getMinSolutionToSolutionDistances(arrX,arrY);
// average dist between solutions:
var dbar = d3.mean(dists);
// computing Schott's spacing metric:
// std deviation of dist between pts on frontier
for (var i = 0; i < dists.length; i++) dists[i] = Math.pow(dists[i] - dbar, 2);
var spacing = Math.sqrt(d3.sum(dists) / (dists.length - 1));
return spacing;
}
/** Computes the ratio of the average minimal spacing
* between observations and the max spacing between observations
*/
var computeSpacingRatio = function(arrX,arrY) {
var dists = getMinSolutionToSolutionDistances(arrX,arrY);
// get nadir and ideal solutions (assumes objs are to be maxed)
var nadirSolution = [Math.min.apply(null, arrX), Math.min.apply(null, arrY)];
var idealSolution = [Math.max.apply(null, arrX), Math.max.apply(null, arrY)];
var dist_max = dist(idealSolution,nadirSolution);
return d3.mean(dists)/dist_max;
}
/** Returns an array of the minimum distance from each observation (xi,yi)
* to another observation (xj,yj) (j != i)
*/
var getMinSolutionToSolutionDistances = function(arrX,arrY) {
var dists = [],
n = arrX.length;
for (var i = 0; i < n; i++) {
// get min dist from each solution to another solution
var minDist = Infinity;
for (var j = 0; j < n; j++) {
if (i === j) continue;
dist_ij = dist([arrX[i],arrY[i]],[arrX[j],arrY[j]]);
if (dist_ij < minDist) minDist = dist_ij;
}
dists.push(minDist);
}
return dists;
}
/** Computes the difference between concordant and discordant observation pairs in X and Y
* Does not elegantly handle ties
*/
var con_dis_diff = function (arrX,arrY) {
var n = arrX.length,
nc = 0,
nd = 0;
for (var i=0;i<n;i++){
for (var j=i+1;j<n;j++){
if (i === j) continue;
(arrX[i]-arrX[j]>0) === (arrY[i]-arrY[j]>0) ? nc++ : nd++;
}
}
return nc-nd;
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
circle {
fill: steelblue;
opacity: 0.4;
}
circle:hover {
opacity: 1;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.hidden {
display: none;
}
.actionableText:hover {
cursor: pointer;
text-decoration: underline;
}
</style>
<body>
<!-- D3 -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- MathJax -->
<script type="text/x-mathjax-config">MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}});</script>
<script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_CHTML"></script>
<!-- Stats computations -->
<script type="text/javascript" src="computeStats.js"></script>
<div id="scatterplotDiv"></div>
<div id="buttonsDiv">
<button id="clearDataButton" class="actionableText">Clear all</button> |
<button id="downloadDataButton" class="actionableText">Download data</button> |
<button>
<label for="file-upload" class="custom-file-upload">Browse</label>
<input id="file-upload" type="file" style="display:none;"/>
</button>
</div>
<script>
var obs = [], // main data object
datCounter = 0; // for giving points unique IDs
// statistics reported
var stats = [{
name: "Avg Tradeoff Angle",
type: "conflict",
value: null,
update: function() {
this.value = computeTEST(obs.map(function(e) {
return e.x;
}), obs.map(function(e) {
return e.y;
}))
}
}, {
name: "Our 1",
type: "conflict",
value: null,
update: function() {
this.value = computeOur1(obs.map(function(e) {
return e.x;
}), obs.map(function(e) {
return e.y;
}))
}
}, {
name: "Spearman's",
type: "correlation",
value: null,
update: function() {
this.value = computeSpearmans(obs.map(function(e) {
return e.x;
}), obs.map(function(e) {
return e.y;
}))
}
}, {
name: "Pearson's",
type: "correlation",
value: null,
update: function() {
this.value = computePearsons(obs.map(function(e) {
return e.x;
}), obs.map(function(e) {
return e.y;
}))
}
}, {
name: "Kendall's",
type: "conflict",
value: null,
update: function() {
this.value = computeKendalls(obs.map(function(e) {
return e.x;
}), obs.map(function(e) {
return e.y;
}))
}
}];
// setting dimensions and margins
var margin = {
top: 20,
right: 200,
bottom: 70,
left: 70
},
width = 660 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// defining scales
var x = d3.scaleLinear().domain([0, 1]).range([0, width]).nice();
var y = d3.scaleLinear().domain([0, 1]).range([height, 0]).nice();
// append svg object
var svg = d3.select("#scatterplotDiv").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// define and add droplines
var droplineX = svg.append("line")
.attr("class","hidden")
.attr("x1",0)
.attr("x2",width)
.style("stroke","#7e7e7e");
var droplineY = svg.append("line")
.attr("class","hidden")
.attr("y1",0)
.attr("y2",height)
.style("stroke","#7e7e7e");
// add rectangle overlay to catch pointer events
svg.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', 'none')
.style('pointer-events', 'all')
.style('cursor','crosshair')
.on("mouseover",function(){ // show the droplines
droplineX.classed("hidden",false);
droplineY.classed("hidden",false);
})
.on("mousemove",function(){ // update droplines
var mx = d3.mouse(this)[0],
my = d3.mouse(this)[1];
droplineX
.attr("y1",my)
.attr("y2",my);
droplineY
.attr("x1",mx)
.attr("x2",mx);
})
.on("mouseout",function(){ // hide droplines
droplineX.classed("hidden",true);
droplineY.classed("hidden",true);
})
.on('click', function() { // add circle on click
var locx = d3.mouse(this)[0],
locy = d3.mouse(this)[1];
var datx = x.invert(d3.mouse(this)[0]),
daty = y.invert(d3.mouse(this)[1]);
addNewDatum(datCounter, [datx, daty]);
drawCircle(datCounter, [locx, locy]);
datCounter++;
});
// Add x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// text label for x axis
svg.append("text")
.attr("transform",
"translate(" + (width) + " ," +
(height + margin.top + 20) + ")")
.style("text-anchor", "end")
.text("Objective 1 (max)");
// Add y Axis
svg.append("g")
.call(d3.axisLeft(y));
// text label for y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 )
.attr("dy", "1em")
.style("text-anchor", "end")
.text("Objective 2 (max)");
// add area for statistics
var statsG = d3.select('svg').append("g")
.attr("transform", "translate(" + (margin.left + width) + "," + (margin.top + 0.4 * height) + ")");
statsG.selectAll(".statsField")
.data(stats)
.enter().append("text")
.attr("y", function(d, i) {
return (i * 1.5) + "em";
})
.attr("x", 5)
.attr("class","statsField")
.style("text-anchor", "left")
.text(function(d) {
return d.name + ": " + d.value;
});
// function for clear all button
d3.select("#clearDataButton")
.on("click",function(){
clearAllSolutions();
});
// function for download data button
d3.select("#downloadDataButton")
.on("click",function(){
downloadData();
});
// function for upload data button
// uploaded data replaces existing data
d3.select("#file-upload").on("change", function(){
if (window.File && window.FileReader && window.FileList && window.Blob) {
var filereader = new window.FileReader();
filereader.onload = function(){
var newObs = prep(filereader.result);
if (newObs[0]) {
clearAllSolutions();
obs = newObs[1];
for (var i=0;i<obs.length;i++){
drawCircle(obs[i].id,[x(obs[i].x),y(obs[i].y)]);
}
recalculateStatistics();
refreshStatsDisplay();
datCounter = d3.max(obs,function(e){return e.id;})+1; // so next ID is unique
};
}
filereader.readAsText(this.files[0]);
} else { console.log("Error with file upload. Please try again."); }
});
/** Given a CSV as text, returns an array whose
* 0th component is true for parsing success, false for failure
* 1st component is an obs-like data object of points
*/
var prep = function (csvAsText) {
var arr2d = csvAsText.split("\n").map(function(str){return str.split(",",2).map(Number);});
// if last row empty, remove it.
// all others assumed to be well formatted
if (arr2d[arr2d.length-1].length <2) arr2d.pop();
var result = arr2d.map(function(d,i){
return {id:i,x:d[0],y:d[1]};
})
// in case error-handling added later...
if (true) return [true,result]
else return [false,null];
}
/** Removes all current solutions from the viz */
var clearAllSolutions = function() {
var currData = obs.map(function(e){return e.id;});
d3.selectAll("circle.datapoint").remove();
for (var i=0;i<currData.length;i++){
removeDatum(currData[i]);
}
}
/** Adds a new point to the obs array and updates the corr/conflict statistics */
var addNewDatum = function(id, datapoint) {
obs.push({
id: id,
x: datapoint[0],
y: datapoint[1]
});
recalculateStatistics();
refreshStatsDisplay();
}
/** Removes a data point from obs and updates corr/conflict statistics */
var removeDatum = function(id) {
for (var i = 0; i < obs.length; i++) {
if (obs[i].id === id) {
obs.splice(i, 1);
break;
}
}
recalculateStatistics();
refreshStatsDisplay();
}
/** Adds a circle to the svg at the location given by coords */
var drawCircle = function(id, coords) {
svg.append("circle")
.attr('class', 'datapoint')
.attr('id', 'datum-' + id)
.attr("cx", coords[0])
.attr("cy", coords[1])
.attr("r", 5)
.on("click", function() {
removeDatum(id);
d3.select(this).remove();
});
}
/** Recalculates the values for each of the corr/conflict statistics */
var recalculateStatistics = function() {
for (var stat = 0; stat < stats.length; stat++) {
stats[stat].update();
}
}
/** Updates the values displayed for the statistics */
var refreshStatsDisplay = function() {
statsG.selectAll(".statsField")
.data(stats)
.text(function(d) {
return d.name + ": " + d.value;
});
}
/** Opens a file dialog to let user save the current data in CSV format */
var downloadData = function() {
var csvContent = "data:text/csv;charset=utf-8,";
obs.forEach(function(dat, index){
dataString = dat.x+","+dat.y;
csvContent += index < obs.length ? dataString+ "\n" : dataString;
});
var encodedUri = encodeURI(csvContent);
window.open(encodedUri);
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment