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

Posted

技术标签:

【中文标题】如何缩小具有已知最小值和最大值的数字范围【英文标题】:How to scale down a range of numbers with a known min and max value 【发布时间】:2011-07-14 18:37:58 【问题描述】:

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

【问题讨论】:

【参考方案1】:

假设您要将范围[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

您可以验证将min 放入x 现在得到a,而放入max 得到b

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

【讨论】:

提醒一下:max != min 的模型会更准确,否则函数结果不确定:) 这能确保我重新调整后的变量保留原始分布吗? 这是一个很好的线性刻度实现。这可以很容易地转换为对数尺度吗? 解释得很清楚。如果min 是负数而max 是正数,它是否有效,或者它们都必须是正数? @Andrew minmax 可以是正数或负数。一般来说,在数学中,如果变量需要条件,我们会声明它们的条件。如果没有条件,就像在这种情况下,我们假设 min 和 max 是 any 数。对于线性变换,值是 +ve 还是 -ve 无关紧要(想象一下 y = mx+c 曲线,无论 x > 0 还是 x 【参考方案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

【讨论】:

【参考方案3】:

根据 Charles Clayton 的回复,我在原始回复中加入了一些 JSDoc、ES6 调整,并结合了 cmets 的建议。

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

【讨论】:

【参考方案4】:

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

因此,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;

【讨论】:

【参考方案5】:

为方便起见,这里是 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

【讨论】:

【参考方案6】:

这里有一些用于复制粘贴的 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.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].scaleBetween(1, 100) 和 [1,1,1].scaleBetween(1,100) 都用 NaN 填充输出。 @MalabarFront,很好的观察。我想在这种情况下结果是否应该是[1, 1, 1][100, 100, 100] 甚至是[50.5, 50.5, 50.5] 是未定义的。你可以把箱子放进去:if (max-min == 0) return this.map(num =&gt; (scaledMin+scaledMax)/2); @CharlesClayton 太棒了,谢谢。这是一种享受!【参考方案7】:

我遇到了这个解决方案,但这并不真正符合我的需要。所以我在 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-version 和上面版本 (@irritate) 的返回值的差异。 比较例子1和2你的目标范围切换 功能方面的最佳答案。【参考方案8】:

我是这样理解的:


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

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

现在,如果您的范围是 20100,会怎样?我们不能应用与上述相同的逻辑(除以 100),因为:

20 / 100

没有给我们020 现在应该是0%)。这应该很容易解决,我们只需要为20 的情况设置分子0。我们可以通过减法来做到这一点:

(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

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

((20 - 10) * 0.5) + 10

更通用的公式是:

((MAX - MIN) * PERCENT) + MIN

25%[10, 20] 的原始示例是:

((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

【讨论】:

这正是我的工作方式。最棘手的部分是找出一个数字在给定范围内的比率。它应该始终在 [0, 1] 范围内,就像百分比一样,例如0.5 表示 50%。接下来,您只需扩展/拉伸并移动此数字以适合您所需的范围。

以上是关于如何缩小具有已知最小值和最大值的数字范围的主要内容,如果未能解决你的问题,请参考以下文章

PHP:将一个数字分隔为具有最小值和最大值的 10er 数组

Pyspark - 从具有最小值和最大值范围的数组中获取值

输入类型=具有最小值和最大值的文本模式数字

在 SQL (Big Query) 中生成序列/范围/数组,其中最小值和最大值取自另一个表

如何将 jQuery UI 所有选定范围(不仅是最小值和最大值)加载到数组中

用于创建具有数据框中每个日期的最小值和最大值的表的函数