如何缩小与已知的最小值和最大值的数字范围

所以我试着弄清楚如何取一组数字,并将值缩小到适合这个范围。这样做的原因是我试图在java swing jpanel中绘制椭圆。我希望每个椭圆的高度和宽度都在1-30的范围内。我有一些方法可以从我的数据集中找到最小值和最大值,但直到运行时才会有最小值和最大值。有什么简单的方法吗?

300980 次浏览

假设你想将范围[min,max]扩展到[a,b]。你在寻找一个(连续的)函数满足

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

在你的例子中,a将是1,而b将是30,但让我们从更简单的东西开始,并尝试将[min,max]映射到[0,1]范围。

min放入函数并得到0可以通过

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

这就是我们想要的。但是,当我们实际需要1时,输入max会得到max - min。所以我们需要缩放它:

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

这就是我们想要的。所以我们需要平移和缩放。现在,如果我们想要获得ab的任意值,我们需要一些更复杂的东西:

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

你可以验证为x输入min会得到a,而为max会得到b

你可能还注意到(b-a)/(max-min)是新范围大小和原始范围大小之间的缩放因子。因此,实际上我们首先通过-min转换x,将其缩放为正确的因子,然后将其转换回新的最小值a

为了方便起见,下面是Irritate的Java格式算法。添加错误检查、异常处理和必要的调整。

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;
}
}

测试人员:

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

我偶然发现了这个解决方案,但它并不真正符合我的需要。所以我在d3源代码中挖掘了一些。我个人建议像d3一样。规模。

这里定义域缩放到值域。这样做的好处是可以将符号转换到目标范围内。这很有用,因为计算机屏幕上的y轴是自上而下的,所以大的值有一个小的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));
}
}

下面这个测试你可以明白我的意思

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);
}
}

下面是一些简单的复制粘贴JavaScript(这是irritate的答案):


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

像这样应用,将范围从10-50扩展到0-100。

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

var maxRange = Math.max.apply(Math, unscaledNums);
var minRange = Math.min.apply(Math, unscaledNums);
    

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

0.00, 18.37, 48.98, 55.10, 85.71, 100.00

编辑:

我知道我很久以前就回答过这个问题,但这里有一个我现在使用的更简洁的函数:

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

像这样应用:

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

[0, 30.76923076923077, 69.23076923076923, 76.923076923076992, 100]

以下是我的理解:


x在范围内的百分比是多少

让我们假设你有一个从0100的范围。给定这个范围内的任意一个数字,它在这个范围内的“百分比”是多少?这应该很简单,0将是0%50将是50%,而100将是100%

现在,如果你的范围是20100呢?我们不能应用与上面相同的逻辑(除以100),因为:

20 / 100

没有给我们0 (20现在应该是0%)。这应该很容易修复,我们只需要将分子0设置为20的情况。我们可以通过减去:

(20 - 20) / 100

然而,这不再适用于100,因为:

(100 - 20) / 100

没有给我们100%。同样,我们也可以通过减去分母来解决这个问题:

(100 - 20) / (100 - 20)

求出% x在范围中的位置的一个更一般化的方程是:

(x - MIN) / (MAX - MIN)

将范围缩放到另一个范围

现在我们知道了一个数字在一个范围内所占的百分比,我们可以应用它来将这个数字映射到另一个范围。让我们看一个例子。

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

如果我们有一个在旧范围内的数,在新范围内的数是多少?假设这个数字是400。首先,计算出400在旧范围内的百分比。我们可以应用上面的方程。

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

因此,400位于旧范围的25%中。我们只需要弄清楚新范围的25%是什么数字。想想[0, 20]50%是什么。它应该是10,对吧?你是怎么得出那个答案的?我们可以这样做:

20 * 0.5 = 10

但是,from [10, 20]呢?现在我们需要通过10来移位所有内容。例如:

((20 - 10) * 0.5) + 10

更一般化的公式是:

((MAX - MIN) * PERCENT) + MIN

对于[10, 20]25%是什么的原始示例:

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

因此,范围[200, 1000]中的400将映射到范围[10, 20]中的12.5


TLDR

x从旧范围映射到新范围:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN

我将Irritate的答案进行重构,通过将其分解成最少的常数来最小化后续计算的计算步骤。其动机是允许在一组数据上训练一个缩放器,然后在新的数据上运行(对于ML算法)。实际上,它很像SciKit对Python的预处理MinMaxScaler。

因此,x' = (b-a)(x-min)/(max-min) + a(其中b!=a)变成了x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + a,它可以简化为x' = x*Part1 + Part2形式的两个常量。

下面是一个带有两个构造函数的c#实现:一个用于训练,另一个用于重新加载训练过的实例(例如,支持持久性)。

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;
}

基于Charles Clayton的回复,我加入了一些JSDoc、ES6的调整,并在原始回复中加入了评论中的建议。

/**
* 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; }

我有时发现这种方法的变体很有用。

  1. 将缩放函数包装在一个类中,这样如果在几个地方缩放相同的范围,就不需要传递最小/最大值
  2. 添加两个小检查,确保结果值保持在预期范围内。

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;
}
}

这个例子以及一个基于函数的版本来自页面https://writingjavascript.com/scaling-values-between-two-ranges