omega
omega

Reputation: 43913

How to calculate weighted center point of 4 points?

If I have 4 points

        var x1;
        var y1;
        var x2;
        var y2;
        var x3;
        var y3;
        var x4;
        var y4;

that make up a box. So

(x1,y1) is top left
(x2,y2) is top right
(x3,y3) is bottom left
(x4,y4) is bottom right

And then each point has a weight ranging from 0-522. How can I calculate a coordinate (tx,ty) that lies inside the box, where the point is closer to the the place that has the least weight (but taking all weights into account). So for example. if (x3,y3) has weight 0, and the others have weight 522, the (tx,ty) should be (x3,y3). If then (x2,y2) had weight like 400, then (tx,ty) should be move a little closer towards (x2,y2) from (x3,y3).

Does anyone know if there is a formula for this? Thanks

Upvotes: 1

Views: 3150

Answers (4)

user3297291
user3297291

Reputation: 23372

Even though this has already been answered, I feel the one, short code snippet that shows the simplicity of calculating a weighted-average is missing:

function weightedAverage(v1, w1, v2, w2) {
  if (w1 === 0) return v2;
  if (w2 === 0) return v1;
  return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}

Now, to make this specific to your problem, you have to apply this to your points via a reducer. The reducer makes it a moving average: the value it returns represents the weights of the points it merged.

// point: { x: xCoordinate, y: yCoordinate, w: weight }
function avgPoint(p1, p2) {
  return {
    x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
    x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
    w: p1.w + p2.w,
  }
}

Now, you can reduce any list of points to get an average coordinate and the weight it represents:

[ /* points */ ].reduce(avgPoint, { x: 0, y: 0, w: 0 })

I hope user naomik doesn't mind, but I used some of their test cases in this runnable example:

function weightedAverage(v1, w1, v2, w2) {
  if (w1 === 0) return v2;
  if (w2 === 0) return v1;
  return ((v1 * w1) + (v2 * w2)) / (w1 + w2);
}

function avgPoint(p1, p2) {
  return {
    x: weightedAverage(p1.x, p1.w, p2.x, p2.w),
    y: weightedAverage(p1.y, p1.w, p2.y, p2.w),
    w: p1.w + p2.w,
  }
}

function getAvgPoint(arr) {
  return arr.reduce(avgPoint, {
    x: 0,
    y: 0,
    w: 0
  });
}


const testCases = [
  { 
    data: [
      { x: 0, y: 0, w: 1 },
      { x: 0, y: 1, w: 1 },
      { x: 1, y: 1, w: 1 },
      { x: 1, y: 0, w: 1 },
    ],
    result: { x: 0.5, y: 0.5 }
  },
  
  { 
    data: [
      { x: 0, y: 0, w: 0 },
      { x: 0, y: 1, w: 0 },
      { x: 1, y: 1, w: 500 },
      { x: 1, y: 0, w: 500 },
    ],
    result: { x: 1, y: 0.5 }
  }
];

testCases.forEach(c => {
  var expected = c.result;
  var outcome = getAvgPoint(c.data);

  console.log("Expected:", expected.x, ",", expected.y);
  console.log("Returned:", outcome.x, ",", outcome.y);
  console.log("----");
});



const rndTest = (function() {
  const randomWeightedPoint = function() {
    return {
      x: Math.random() * 1000 - 500,
      y: Math.random() * 1000 - 500,
      w: Math.random() * 1000
    };
  };

  let data = []
  for (let i = 0; i < 1e6; i++)
    data[i] = randomWeightedPoint()

  return getAvgPoint(data);
}());

console.log("Expected: ~0 , ~0, 500000000")
console.log("Returned:", rndTest.x, ",", rndTest.y, ",", rndTest.w);
.as-console-wrapper {
  min-height: 100%;
}

Upvotes: 2

Mulan
Mulan

Reputation: 135397

Creating a minimum, complete, verifiable exmample

You have a little bit of a tricky problem here, but it's really quite fun. There might be better ways to solve it, but I found it most reliable to use Point and Vector data abstractions to model the problem better

I'll start with a really simple data set – the data below can be read (eg) Point D is at cartesian coordinates (1,1) with a weight of 100.

|
|
| B(0,1) #10        D(1,1) #100
|                
| 
|         ? solve weighted average
|
| 
| A(0,0) #20        C(1,0) #40
+----------------------------------

Here's how we'll do it

  1. find the unweighted midpoint, m
  2. convert each Point to a Vector of Vector(degrees, magnitude) using m as the origin
  3. add all the Vectors together, vectorSum
  4. divide vectorSum's magnitude by the total magnitude
  5. convert the vector to a point, p
  6. offset p by unweighted midpoint m

Possible JavaScript implementation

I'll go thru the pieces one at a time then there will be a complete runnable example at the bottom.

The Math.atan2, Math.cos, and Math.sin functions we'll be using return answers in radians. That's kind of a bother, so there's a couple helpers in place to work in degrees.

// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))

Now we'll need a way to represent our Point and Point-related functions

// Point
const Point = (x,y) => ({
  x,
  y,
  add: ({x: x2, y: y2}) =>
    Point(x + x2, y + y2),
  sub: ({x: x2, y: y2}) =>
    Point(x - x2, y - y2),
  bind: f =>
    f(x,y),
  inspect: () =>
    `Point(${x}, ${y})`
})

Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))

And of course the same goes for Vector – strangely enough adding Vectors together is actually easier when you convert them back to their x and y cartesian coordinates. other than that, this code is pretty straightforward

// Vector
const Vector = (a,m) => ({
  a,
  m,
  scale: x =>
    Vector(a, m*x),
  add: v =>
    Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
  inspect: () =>
    `Vector(${a}, ${m})`
})

Vector.zero = Vector(0,0)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))

Lastly we'll need to represent our data above in JavaScript and create a function which calculates the weighted point. With Point and Vector by our side, this will be a piece of cake

// data
const data = [
  [Point(0,0), 20],
  [Point(0,1), 10],
  [Point(1,1), 100],
  [Point(1,0), 40],
]

// calc weighted point
const calcWeightedMidpoint = points => {
  let midpoint = calcMidpoint(points)
  let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
  let vectorSum = points.reduce((acc, [point, weight]) =>
    acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
  return Point.fromVector(vectorSum).add(midpoint)
}

console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)

Runnable script

// math
const pythag = (a,b) => Math.sqrt(a * a + b * b)
const rad2deg = rad => rad * 180 / Math.PI
const deg2rad = deg => deg * Math.PI / 180
const atan2 = (y,x) => rad2deg(Math.atan2(y,x))
const cos = x => Math.cos(deg2rad(x))
const sin = x => Math.sin(deg2rad(x))

// Point
const Point = (x,y) => ({
  x,
  y,
  add: ({x: x2, y: y2}) =>
    Point(x + x2, y + y2),
  sub: ({x: x2, y: y2}) =>
    Point(x - x2, y - y2),
  bind: f =>
    f(x,y),
  inspect: () =>
    `Point(${x}, ${y})`
})

Point.origin = Point(0,0)
Point.fromVector = ({a,m}) => Point(m * cos(a), m * sin(a))

// Vector
const Vector = (a,m) => ({
  a,
  m,
  scale: x =>
    Vector(a, m*x),
  add: v =>
    Vector.fromPoint(Point.fromVector(Vector(a,m)).add(Point.fromVector(v))),
  inspect: () =>
    `Vector(${a}, ${m})`
})

Vector.zero = Vector(0,0)
Vector.unitFromPoint = ({x,y}) => Vector(atan2(y,x), 1)
Vector.fromPoint = ({x,y}) => Vector(atan2(y,x), pythag(x,y))


// data
const data = [
  [Point(0,0), 20],
  [Point(0,1), 10],
  [Point(1,1), 100],
  [Point(1,0), 40],
]

// calc unweighted midpoint
const calcMidpoint = points => {
  let count = points.length;
  let midpoint = points.reduce((acc, [point, _]) => acc.add(point), Point.origin)
  return midpoint.bind((x,y) => Point(x/count, y/count))
}

// calc weighted point
const calcWeightedMidpoint = points => {
  let midpoint = calcMidpoint(points)
  let totalWeight = points.reduce((acc, [_, weight]) => acc + weight, 0)
  let vectorSum = points.reduce((acc, [point, weight]) =>
    acc.add(Vector.fromPoint(point.sub(midpoint)).scale(weight/totalWeight)), Vector.zero)
  return Point.fromVector(vectorSum).add(midpoint)
}

console.log(calcWeightedMidpoint(data))
// Point(0.9575396819442366, 0.7079725827019256)

Going back to our original visualization, everything looks right!

|
|
| B(0,1) #10        D(1,1) #100
|
|
|                 * <-- about right here
|
| 
| 
| A(0,0) #20        C(1,0) #40
+----------------------------------

Checking our work

Using a set of points with equal weighting, we know what the weighted midpoint should be. Let's verify that our two primary functions calcMidpoint and calcWeightedMidpoint are working correctly

const data = [
  [Point(0,0), 5],
  [Point(0,1), 5],
  [Point(1,1), 5],
  [Point(1,0), 5],
]

calcMidpoint(data)
// => Point(0.5, 0.5)

calcWeightedMidpoint(data)
// => Point(0.5, 0.5)

Great! Now we'll test to see how some other weights work too. First let's just try all the points but one with a zero weight

const data = [
  [Point(0,0), 0],
  [Point(0,1), 0],
  [Point(1,1), 0],
  [Point(1,0), 1],
]

calcWeightedMidpoint(data)
// => Point(1, 0)

Notice if we change that weight to some ridiculous number, it won't matter. Scaling of the vector is based on the point's percentage of weight. If it gets 100% of the weight, it (the point) will not pull the weighted midpoint past (the point) itself

const data = [
  [Point(0,0), 0],
  [Point(0,1), 0],
  [Point(1,1), 0],
  [Point(1,0), 1000],
]

calcWeightedMidpoint(data)
// => Point(1, 0)

Lastly, we'll verify one more set to ensure weighting is working correctly – this time we'll have two pairs of points that are equally weighted. The output is exactly what we're expecting

const data = [
  [Point(0,0), 0],
  [Point(0,1), 0],
  [Point(1,1), 500],
  [Point(1,0), 500],
]

calcWeightedMidpoint(data)
// => Point(1, 0.5)

Millions of points

Here we will create a huge point cloud of random coordinates with random weights. If points are random and things are working correctly with our function, the answer should be pretty close to Point(0,0)

const RandomWeightedPoint = () => [
  Point(Math.random() * 1000 - 500, Math.random() * 1000 - 500),
  Math.random() * 1000
]

let data = []
for (let i = 0; i < 1e6; i++)
  data[i] = RandomWeightedPoint()

calcWeightedMidpoint(data)
// => Point(0.008690554978970092, -0.08307212085822799)

A++

Upvotes: 2

Rob Wilkins
Rob Wilkins

Reputation: 1650

A very simple approach is this:

  1. Convert each point's weight to 522 minus the actual weight.
  2. Multiply each x/y co-ordinate by its adjusted weight.
  3. Sum all multiplied x/y co-ordinates together, and --
  4. Divide by the total adjusted weight of all points to get your adjusted average position.

That should produce a point with a position that is biased proportionally towards the "lightest" points, as described. Assuming that weights are prefixed w, a quick snippet (followed by JSFiddle example) is:

var tx = ((522-w1)*x1 + (522-w2)*x2 + (522-w3)*x3 + (522-w4)*x4) / (2088-(w1+w2+w3+w4));
var ty = ((522-w1)*y1 + (522-w2)*y2 + (522-w3)*y3 + (522-w4)*y4) / (2088-(w1+w2+w3+w4));

JSFiddle example of this

Upvotes: 1

Luka
Luka

Reputation: 3089

Assume w1, w2, w3, w4 are the weights. You can start with this (pseudocode):

M = 522
a = 1
b = 1 / ( (1 - w1/M)^a + (1 - w2/M)^a + (1 - w3/M)^a + (1 - w4/M)^a )

tx = b * (x1*(1-w1/M)^a + x2*(1-w2/M)^a + x3*(1-w3/M)^a + x4*(1-w4/M)^a)
ty = b * (y1*(1-w1/M)^a + y2*(1-w2/M)^a + y3*(1-w3/M)^a + y4*(1-w4/M)^a)

This should approximate the behavior you want to accomplish. For the simplest case set a=1 and your formula will be simpler. You can adjust behavior by changing a.

Make sure you use Math.pow instead of ^ if you use Javascript.

Upvotes: 1

Related Questions