How to scale down a range of numbers with a known min and max value
Asked Answered
S

8

271

So I am trying to figure out how to take a range of numbers and scale the values down to fit a range. The reason for wanting to do this is that I am trying to draw ellipses in a java swing jpanel. I want the height and width of each ellipse to be in a range of say 1-30. I have methods that find the minimum and maximum values from my data set, but I won't have the min and max until runtime. Is there an easy way to do this?

Saleratus answered 14/3, 2011 at 5:7 Comment(0)
L
613

Let's say you want to scale a range [min,max] to [a,b]. You're looking for a (continuous) function that satisfies

f(min) = a
f(max) = b

In your case, a would be 1 and b would be 30, but let's start with something simpler and try to map [min,max] into the range [0,1].

Putting min into a function and getting out 0 could be accomplished with

f(x) = x - min   ===>   f(min) = min - min = 0

So that's almost what we want. But putting in max would give us max - min when we actually want 1. So we'll have to scale it:

        x - min                                  max - min
f(x) = ---------   ===>   f(min) = 0;  f(max) =  --------- = 1
       max - min                                 max - min

which is what we want. So we need to do a translation and a scaling. Now if instead we want to get arbitrary values of a and b, we need something a little more complicated:

       (b-a)(x - min)
f(x) = --------------  + a
          max - min

You can verify that putting in min for x now gives a, and putting in max gives b.

You might also notice that (b-a)/(max-min) is a scaling factor between the size of the new range and the size of the original range. So really we are first translating x by -min, scaling it to the correct factor, and then translating it back up to the new minimum value of a.

Lilywhite answered 14/3, 2011 at 5:49 Comment(8)
I appreciate your help. I figured out a solution that does the job of looking aesthetically pleasing. However I will apply your logic to give a more accurate model. Thanks again :)Saleratus
Just a reminder: The model will be more accurate with max != min otherwise the function results indetermined :)Unimposing
does this ensure that my rescaled variable retains the original distribution?Pacheco
This is a nice implementation of a linear scale. Can this be easily transformed to a logarighmic scale?Freewill
Very clear explanation. Does it work if min is negative and max is positive, or does they both have to be positive?Bovill
what are the numerical overflow / underflow / round-off issues with this approach (if any). When max and min are close, it seems like there could be bad stability issues.Floccus
@Pacheco this is a linear transformation (+, -, * and /) and so the data retains its original distribution.Ruthy
@Bovill min and max can be either positive or negative. In general, in maths, we would state a condition on the variables if they needed one. If there is no condition, as in this case, we assume that min and max are any number. For linear transformations, it doesn't matter if the values are +ve or -ve (just imagine a y = mx+c curve, it looks the same whether x > 0 or x < 0).Ruthy
D
75

Here's some JavaScript for copy-paste ease (this is irritate's answer):

function scaleBetween(unscaledNum, minAllowed, maxAllowed, min, max) {
  return (maxAllowed - minAllowed) * (unscaledNum - min) / (max - min) + minAllowed;
}

Applied like so, scaling the range 10-50 to a range between 0-100.

const unscaledNums = [10, 13, 25, 28, 43, 50];

const maxRange = Math.max(...unscaledNums);
const minRange = Math.min(...unscaledNums);

for (let i = 0; i < unscaledNums.length; i++) {
  const unscaled = unscaledNums[i];
  const scaled = scaleBetween(unscaled, 0, 100, minRange, maxRange);
  console.log(scaled.toFixed(2));
}

0.00, 18.37, 48.98, 55.10, 85.71, 100.00

Edit:

I know I answered this a long time ago, but here's a cleaner function* that I use now:

Array.prototype.scaleBetween = function(scaledMin, scaledMax) {
  const max = Math.max(...this);
  const min = Math.min(...this);
  return this.map(num => (scaledMax-scaledMin)*(num-min)/(max-min)+scaledMin);
}

Applied like so:

[-4, 0, 5, 6, 9].scaleBetween(0, 100);

[0, 30.76923076923077, 69.23076923076923, 76.92307692307692, 100]

*It's strongly advised not to modify the native Array prototype, so use this one with caution.

Darb answered 28/7, 2015 at 21:7 Comment(7)
var arr = ["-40000.00","2","3.000","4.5825","0.00008","1000000000.00008","0.02008","100","-5000","-82.0000048","0.02","0.005","-3.0008","5","8","600","-1000","-5000"]; for this case, by your method ,numbers are getting too small . Is there any way, so that, scale should be(0,100) or (-100,100) and gap between outputs should be 0.5 (or any number).Atkinson
Please consider my scenario for arr[] too.Atkinson
It's a bit of an edge case, but this dies if the array only contains one value or only multiple copies of the same value. So [1].scaleBetween(1, 100) and [1,1,1].scaleBetween(1,100) both fill the output with NaN.Dotdotage
@MalabarFront, good observation. I suppose it's undefined whether in that case the result should be [1, 1, 1], [100, 100, 100], or even [50.5, 50.5, 50.5]. You could put in the case: if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);Darb
@CharlesClayton Fantastic, thanks. That works a treat!Dotdotage
Math.max.apply(Math, unscaledNums); in es6 can be replaced with Math.max(...unscaledNums);Plotter
The old functions works a treat for scaling a single number from one range to another. The new function, I cannot get to do thatContrapuntal
P
31

For convenience, here is Irritate's algorithm in a Java form. Add error checking, exception handling and tweak as necessary.

public class Algorithms { 
    public static double scale(final double valueIn, final double baseMin, final double baseMax, final double limitMin, final double limitMax) {
        return ((limitMax - limitMin) * (valueIn - baseMin) / (baseMax - baseMin)) + limitMin;
    }
}

Tester:

final double baseMin = 0.0;
final double baseMax = 360.0;
final double limitMin = 90.0;
final double limitMax = 270.0;
double valueIn = 0;
System.out.println(Algorithms.scale(valueIn, baseMin, baseMax, limitMin, limitMax));
valueIn = 360;
System.out.println(Algorithms.scale(valueIn, baseMin, baseMax, limitMin, limitMax));
valueIn = 180;
System.out.println(Algorithms.scale(valueIn, baseMin, baseMax, limitMin, limitMax));

90.0
270.0
180.0
Probity answered 18/4, 2014 at 15:58 Comment(0)
P
29

Here's how I understand it:


What percent does x lie in a range

Let's assume you have a range from 0 to 100. Given an arbitrary number from that range, what "percent" from that range does it lie in? This should be pretty simple, 0 would be 0%, 50 would be 50% and 100 would be 100%.

Now, what if your range was 20 to 100? We cannot apply the same logic as above (divide by 100) because:

20 / 100

doesn't give us 0 (20 should be 0% now). This should be simple to fix, we just need to make the numerator 0 for the case of 20. We can do that by subtracting:

(20 - 20) / 100

However, this doesn't work for 100 anymore because:

(100 - 20) / 100

doesn't give us 100%. Again, we can fix this by subtracting from the denominator as well:

(100 - 20) / (100 - 20)

A more generalized equation for finding out what % x lies in a range would be:

(x - MIN) / (MAX - MIN)

Scale range to another range

Now that we know what percent a number lies in a range, we can apply it to map the number to another range. Let's go through an example.

old range = [200, 1000]
new range = [10, 20]

If we have a number in the old range, what would the number be in the new range? Let's say the number is 400. First, figure out what percent 400 is within the old range. We can apply our equation above.

(400 - 200) / (1000 - 200) = 0.25

So, 400 lies in 25% of the old range. We just need to figure out what number is 25% of the new range. Think about what 50% of [0, 20] is. It would be 10 right? How did you arrive at that answer? Well, we can just do:

20 * 0.5 = 10

But, what about from [10, 20]? We need to shift everything by 10 now. eg:

((20 - 10) * 0.5) + 10

a more generalized formula would be:

((MAX - MIN) * PERCENT) + MIN

To the original example of what 25% of [10, 20] is:

((20 - 10) * 0.25) + 10 = 12.5

So, 400 in the range [200, 1000] would map to 12.5 in the range [10, 20]


TLDR

To map x from old range to new range:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
Phoneme answered 29/11, 2017 at 14:51 Comment(1)
That's exactly how I worked it out. Trickiest part is to find out the ratio where a number lies in a given range. It should always be within [0, 1] range just like percentage, e.g. 0.5 is for 50%. Next you only have to expand/stretch and shift this number to fit in your required range.Theiss
S
12

I came across this solution but this does not really fit my need. So I digged a bit in the d3 source code. I personally would recommend to do it like d3.scale does.

So here you scale the domain to the range. The advantage is that you can flip signs to your target range. This is useful since the y axis on a computer screen goes top down so large values have a small y.

public class Rescale {
    private final double range0,range1,domain0,domain1;

    public Rescale(double domain0, double domain1, double range0, double range1) {
        this.range0 = range0;
        this.range1 = range1;
        this.domain0 = domain0;
        this.domain1 = domain1;
    }

    private double interpolate(double x) {
        return range0 * (1 - x) + range1 * x;
    }

    private double uninterpolate(double x) {
        double b = (domain1 - domain0) != 0 ? domain1 - domain0 : 1 / domain1;
        return (x - domain0) / b;
    }

    public double rescale(double x) {
        return interpolate(uninterpolate(x));
    }
}

And here is the test where you can see what I mean

public class RescaleTest {

    @Test
    public void testRescale() {
        Rescale r;
        r = new Rescale(5,7,0,1);
        Assert.assertTrue(r.rescale(5) == 0);
        Assert.assertTrue(r.rescale(6) == 0.5);
        Assert.assertTrue(r.rescale(7) == 1);

        r = new Rescale(5,7,1,0);
        Assert.assertTrue(r.rescale(5) == 1);
        Assert.assertTrue(r.rescale(6) == 0.5);
        Assert.assertTrue(r.rescale(7) == 0);

        r = new Rescale(-3,3,0,1);
        Assert.assertTrue(r.rescale(-3) == 0);
        Assert.assertTrue(r.rescale(0) == 0.5);
        Assert.assertTrue(r.rescale(3) == 1);

        r = new Rescale(-3,3,-1,1);
        Assert.assertTrue(r.rescale(-3) == -1);
        Assert.assertTrue(r.rescale(0) == 0);
        Assert.assertTrue(r.rescale(3) == 1);
    }
}
Slop answered 10/3, 2015 at 7:8 Comment(3)
"The advantage is that you can flip signs to your target range." I dont understand this. Can you explain? I cannot find the difference of the returned values from your d3-version and the version from above (@irritate).Phiona
Compare example 1 and 2 your target range switchedSlop
Best answer in terms of functionality.Accad
T
3

I've taken Irritate's answer and refactored it so as to minimize the computational steps for subsequent computations by factoring it into the fewest constants. The motivation is to allow a scaler to be trained on one set of data, and then be run on new data (for an ML algo). In effect, it's much like SciKit's preprocessing MinMaxScaler for Python in usage.

Thus, x' = (b-a)(x-min)/(max-min) + a (where b!=a) becomes x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + a which can be reduced to two constants in the form x' = x*Part1 + Part2.

Here's a C# implementation with two constructors: one to train, and one to reload a trained instance (e.g., to support persistence).

public class MinMaxColumnSpec
{
    /// <summary>
    /// To reduce repetitive computations, the min-max formula has been refactored so that the portions that remain constant are just computed once.
    /// This transforms the forumula from
    /// x' = (b-a)(x-min)/(max-min) + a
    /// into x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + a
    /// which can be further factored into
    /// x' = x*Part1 + Part2
    /// </summary>
    public readonly double Part1, Part2;

    /// <summary>
    /// Use this ctor to train a new scaler.
    /// </summary>
    public MinMaxColumnSpec(double[] columnValues, int newMin = 0, int newMax = 1)
    {
        if (newMax <= newMin)
            throw new ArgumentOutOfRangeException("newMax", "newMax must be greater than newMin");

        var oldMax = columnValues.Max();
        var oldMin = columnValues.Min();

        Part1 = (newMax - newMin) / (oldMax - oldMin);
        Part2 = newMin + (oldMin * (newMin - newMax) / (oldMax - oldMin));
    }

    /// <summary>
    /// Use this ctor for previously-trained scalers with known constants.
    /// </summary>
    public MinMaxColumnSpec(double part1, double part2)
    {
        Part1 = part1;
        Part2 = part2;
    }

    public double Scale(double x) => (x * Part1) + Part2;
}
Trishtrisha answered 8/4, 2018 at 17:54 Comment(0)
H
3

I sometimes find a variation of this useful.

  1. Wrapping the scale function in a class so that I do not need to pass around the min/max values if scaling the same ranges in several places
  2. Adding two small checks that ensures that the result value stays within the expected range.

Example in JavaScript:

class Scaler {
  constructor(inMin, inMax, outMin, outMax) {
    this.inMin = inMin;
    this.inMax = inMax;
    this.outMin = outMin;
    this.outMax = outMax;
  }

  scale(value) {
    const result = (value - this.inMin) * (this.outMax - this.outMin) / (this.inMax - this.inMin) + this.outMin;

    if (result < this.outMin) {
      return this.outMin;
    } else if (result > this.outMax) {
      return this.outMax;
    }

    return result;
  }
}

This example along with a function based version comes from the page https://writingjavascript.com/scaling-values-between-two-ranges

Hatcher answered 13/9, 2020 at 23:27 Comment(0)
T
2

Based on Charles Clayton's response, I included some JSDoc, ES6 tweaks, and incorporated suggestions from the comments in the original response.

/**
 * Returns a scaled number within its source bounds to the desired target bounds.
 * @param {number} n - Unscaled number
 * @param {number} tMin - Minimum (target) bound to scale to
 * @param {number} tMax - Maximum (target) bound to scale to
 * @param {number} sMin - Minimum (source) bound to scale from
 * @param {number} sMax - Maximum (source) bound to scale from
 * @returns {number} The scaled number within the target bounds.
 */
const scaleBetween = (n, tMin, tMax, sMin, sMax) => {
  return (tMax - tMin) * (n - sMin) / (sMax - sMin) + tMin;
}

if (Array.prototype.scaleBetween === undefined) {
  /**
   * Returns a scaled array of numbers fit to the desired target bounds.
   * @param {number} tMin - Minimum (target) bound to scale to
   * @param {number} tMax - Maximum (target) bound to scale to
   * @returns {number} The scaled array.
   */
  Array.prototype.scaleBetween = function(tMin, tMax) {
    if (arguments.length === 1 || tMax === undefined) {
      tMax = tMin; tMin = 0;
    }
    let sMax = Math.max(...this), sMin = Math.min(...this);
    if (sMax - sMin == 0) return this.map(num => (tMin + tMax) / 2);
    return this.map(num => (tMax - tMin) * (num - sMin) / (sMax - sMin) + tMin);
  }
}

// ================================================================
// Usage
// ================================================================

let nums = [10, 13, 25, 28, 43, 50], tMin = 0, tMax = 100,
    sMin = Math.min(...nums), sMax = Math.max(...nums);

// Result: [ 0.0, 7.50, 37.50, 45.00, 82.50, 100.00 ]
console.log(nums.map(n => scaleBetween(n, tMin, tMax, sMin, sMax).toFixed(2)).join(', '));

// Result: [ 0, 30.769, 69.231, 76.923, 100 ]
console.log([-4, 0, 5, 6, 9].scaleBetween(0, 100).join(', '));

// Result: [ 50, 50, 50 ]
console.log([1, 1, 1].scaleBetween(0, 100).join(', '));
.as-console-wrapper { top: 0; max-height: 100% !important; }
Tanika answered 3/3, 2020 at 19:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.