如何按比例缩小具有已知最小值和最大值的数字


230

因此,我试图弄清楚如何采用一定范围的数字并将值缩小以适应范围。想要这样做的原因是我试图在java swing jpanel中绘制椭圆。我希望每个椭圆的高度和宽度在1-30的范围内。我有一些方法可以从我的数据集中找到最小值和最大值,但是直到运行时我都没有最小值和最大值。是否有捷径可寻?

Answers:


507

假设您要将范围缩放[min,max][a,b]。您正在寻找一个令人满意的(连续)功能

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

在您的情况下,a将为1,b将为30,但让我们从更简单的内容开始,尝试映射[min,max]到range [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

这就是我们想要的。因此,我们需要进行平移和缩放。现在,如果相反,我们想要获取aand的任意值b,我们需要更复杂的东西:

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

您可以验证投入minx现在给a,并把在maxb

您可能还会注意到,这(b-a)/(max-min)是新范围的大小和原始范围的大小之间的比例因子。因此,其实我们是第一平移x通过-min,它扩展到了正确的因素,然后翻译它备份到的新的最小值a

希望这可以帮助。


我感谢您的帮助。我想出了一种解决方案,可以使外观看起来令人满意。但是,我将运用您的逻辑给出更准确的模型。再次感谢:)
user650271 2011年

4
提醒一下:该模型将更加准确,max != min否则函数结果不确定:)
marcoslhc 2013年

10
这样是否可以确保我重新缩放后的变量保留原始分布?
海森堡2013年

2
这是线性刻度的不错的实现。是否可以轻松将其转换为对数刻度?
tomexx

非常清楚的解释。如果min为负且max为正,是否奏效?还是都必须为正?
安德鲁

48

这是一些简化复制粘贴的JavaScript(这是激怒的答案):

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.92307692307692,100]


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”]];对于这种情况,根据您的方法,数字变得太小了。有什么办法可以使比例尺为(0,100)或(-100,100),输出之间的间隙应为0.5(或任何数字)。

请也考虑我的arr []方案。

1
有点极端的情况,但是如果数组仅包含一个值或相同值的多个副本,则此方法无效。因此[1] .scaleBetween(1,100)和[1,1,1] .scaleBetween(1,100)都用NaN填充输出。
马拉巴尔前线

1
@MalabarFront,观察得很好。我想这是不确定是否在这种情况下,结果应该是[1, 1, 1][100, 100, 100]或甚[50.5, 50.5, 50.5]。您可以输入以下内容:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
查尔斯·克莱顿

1
@CharlesClayton太好了,谢谢。那就请客!
马拉巴尔前线

27

为方便起见,以下是Java形式的Irritate算法。添加错误检查,异常处理并根据需要进行调整。

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

21

这是我的理解方式:


x范围内有多少百分比

假设您的范围是从0100。给定该范围内的任意数字,它位于该范围内的“百分比”是多少?这应该是非常简单的,0将会是0%50将会是50%100将会是100%

现在,如果您20要到达该100怎么办?我们不能采用与上述相同的逻辑(除以100),因为:

20 / 100

没有给我们020应该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%新范围内的数字即可。想想什么50%[0, 20]是。是这样10吗 您是如何得出这个答案的?好吧,我们可以做:

20 * 0.5 = 10

但是,那又如何[10, 20]呢?我们现在需要转移一切10。例如:

((20 - 10) * 0.5) + 10

一个更通用的公式是:

((MAX - MIN) * PERCENT) + MIN

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

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

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


TLDR

要将x旧范围映射到新范围:

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

1
那正是我的解决方法。最棘手的部分是找出数字在给定范围内的比率。就像百分比一样,它应始终在[0,1]范围内,例如,对于50%,应为0.5。接下来,您只需要扩展/拉伸和移动此数字即可适合您所需的范围。
SMUsamaShah

感谢您以非常简单的方式解释这些步骤-答案上方的copypasta可以正常工作,但是知道这些步骤非常有用。
RozzA

11

我遇到了这个解决方案,但这并不完全符合我的需求。所以我在d3源代码中做了一些挖掘。我个人建议这样做,就像d3.scale一样。

因此,在此处将域缩放到范围。优点是您可以将标志翻转到目标范围。这很有用,因为计算机屏幕上的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);
    }
}

“优点是您可以将标志翻转到目标范围。” 我不明白这一点。你可以解释吗?我找不到您的d3版本返回的值与上面的版本(@irritate)的区别。
nimo23

比较实施例1和2的目标范围切换
KIC

2

我采用了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;
}

2

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.