Как уменьшить диапазон чисел с известным минимальным и максимальным значением

Итак, я пытаюсь понять, как взять диапазон чисел и масштабировать значения, чтобы они соответствовали диапазону. Причина, по которой я хочу это сделать, заключается в том, что я пытаюсь рисовать эллипсы в java-панели Swing. Я хочу, чтобы высота и ширина каждого эллипса находились в диапазоне, скажем, 1-30. У меня есть методы, которые находят минимальные и максимальные значения из моего набора данных, но у меня не будет минимальных и максимальных значений до времени выполнения. Есть простой способ сделать это?


person user650271    schedule 14.03.2011    source источник


Ответы (8)


Допустим, вы хотите масштабировать диапазон от [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

Так что это почти то, что мы хотим. Но добавление max даст нам max - min, когда мы действительно хотим 1. Так что нам придется масштабировать его:

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

чего мы и хотим. Итак, нам нужно сделать перевод и масштабирование. Теперь, если вместо этого мы хотим получить произвольные значения a и b, нам нужно что-то посложнее:

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

Вы можете проверить, что ввод min для x теперь дает a, а ввод max дает b.

Вы также можете заметить, что (b-a)/(max-min) - это коэффициент масштабирования между размером нового диапазона и размером исходного диапазона. Итак, на самом деле мы сначала переводим x на -min, масштабируем его до правильного коэффициента, а затем переводим обратно до нового минимального значения a.

Надеюсь это поможет.

person irritate    schedule 14.03.2011
comment
Я ценю вашу помощь. Я придумал решение, которое выглядит эстетично. Однако я применю вашу логику, чтобы дать более точную модель. Спасибо еще раз :) - person user650271; 16.03.2011
comment
Напоминаем: модель будет более точной с max != min, иначе результат будет неопределенным :) - person marcoslhc; 13.06.2013
comment
гарантирует ли это, что моя измененная переменная сохранит исходное распределение? - person Heisenberg; 09.09.2013
comment
Это хорошая реализация линейной шкалы. Можно ли это легко преобразовать в логаригмический масштаб? - person tomexx; 22.09.2015
comment
Очень четкое объяснение. Работает ли это, если min отрицательно, а max положительно, или они оба должны быть положительными? - person Andrew; 29.07.2016
comment
каковы проблемы числового переполнения / потери значимости / округления при таком подходе (если таковые имеются). Когда max и min близки, кажется, что могут быть проблемы со стабильностью. - person ely; 15.07.2019
comment
@Heisenberg - это линейное преобразование (+, -, * и /), поэтому данные сохраняют свое исходное распределение. - person Adam Murphy; 30.03.2021
comment
@Andrew min и max могут быть как положительными, так и отрицательными. В общем, в математике мы бы сформулировали условие для переменных, если бы они в нем нуждались. Если нет условия, как в этом случае, мы предполагаем, что min и max являются любыми числами. Для линейных преобразований не имеет значения, являются ли значения + ve или -ve (просто представьте кривую y = mx + c, она выглядит одинаково независимо от того, x ›0 или x‹ 0). - person Adam Murphy; 30.03.2021

Вот немного 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]

person Charles Clayton    schedule 28.07.2015
comment
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 (или любое число). - person ; 25.07.2017
comment
Пожалуйста, рассмотрите также мой сценарий для arr []. - person ; 25.07.2017
comment
Это немного крайний случай, но он умирает, если массив содержит только одно значение или только несколько копий одного и того же значения. Итак, [1] .scaleBetween (1, 100) и [1,1,1] .scaleBetween (1,100) оба заполняют вывод значением NaN. - person Malabar Front; 08.02.2018
comment
@MalabarFront, хорошее наблюдение. Я полагаю, не определено, должен ли результат быть [1, 1, 1], [100, 100, 100] или даже [50.5, 50.5, 50.5]. Вы можете поместить в футляр: if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2); - person Charles Clayton; 09.02.2018
comment
@CharlesClayton Фантастика, спасибо. Это работает! - person Malabar Front; 09.02.2018
comment
Math.max.apply(Math, unscaledNums); в es6 можно заменить на Math.max(...unscaledNums); - person Sergio Ivanuzzo; 15.03.2019
comment
Старые функции отлично подходят для масштабирования одного числа из одного диапазона в другой. Новая функция, я не могу этого сделать - person RozzA; 26.08.2019

Для удобства здесь приведен алгоритм 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
person Java42    schedule 18.04.2014

Вот как я это понимаю:


Какой процент x лежит в диапазоне

Предположим, у вас есть диапазон от 0 до 100. Учитывая произвольное число из этого диапазона, в каком «проценте» из этого диапазона оно находится? Это должно быть довольно просто, 0 будет 0%, 50 будет 50% и 100 будет 100%.

А что, если бы ваш диапазон был от 20 до 100? Мы не можем применить ту же логику, что и выше (разделить на 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% в новом диапазоне. Подумайте, что такое 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]


TL; DR

Чтобы отобразить x из старого диапазона в новый диапазон:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
person Vic    schedule 29.11.2017
comment
Именно так я это решил. Самая сложная часть - это выяснить, в каком соотношении находится число в заданном диапазоне. Он всегда должен быть в пределах [0, 1], как и процент, например 0,5 - это 50%. Затем вам нужно только расширить / растянуть и сдвинуть это число, чтобы оно соответствовало вашему требуемому диапазону. - person SMUsamaShah; 06.10.2018

Я наткнулся на это решение, но оно мне не подходит. Поэтому я немного покопался в исходном коде 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);
    }
}
person KIC    schedule 10.03.2015
comment
Преимущество состоит в том, что вы можете переворачивать знаки в соответствии с целевым диапазоном. Я этого не понимаю. Вы можете объяснить? Я не могу найти разницу в возвращаемых значениях вашей d3-версии и версии сверху (@irritate). - person nimo23; 18.11.2017
comment
Сравните пример 1 и 2, ваш целевой диапазон переключен. - person KIC; 06.12.2017
comment
Лучший ответ с точки зрения функциональности. - person jfunk; 25.09.2020

Я взял ответ Irritate и отредактировал его, чтобы минимизировать вычислительные шаги для последующих вычислений, разложив его на наименьшее количество констант. Мотивация состоит в том, чтобы позволить скейлеру обучаться на одном наборе данных, а затем запускаться на новых данных (для алгоритма машинного обучения). По сути, это очень похоже на предварительную обработку MinMaxScaler для Python в SciKit.

Таким образом, 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;
}
person Kevin Fichter    schedule 08.04.2018

Основываясь на ответе Чарльза Клейтона, я включил некоторые настройки 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; }

person Mr. Polywhirl    schedule 03.03.2020

Иногда я нахожу вариант этого полезным.

  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

person tirithen    schedule 13.09.2020