Як зменшити масштаб діапазону чисел із відомим значенням min та max


230

Тому я намагаюся розібратися, як взяти діапазон чисел і масштабувати значення вниз, щоб відповідати діапазону. Причиною, що хочу зробити це, є те, що я намагаюся намалювати еліпси в jvanel swing java. Я хочу, щоб висота і ширина кожного еліпса знаходилися в діапазоні, наприклад, 1-30. У мене є методи, які знаходять мінімальні та максимальні значення з мого набору даних, але я не матиму мінімум та максимум до часу виконання. Чи є простий спосіб це зробити?

Відповіді:


507

Припустимо, ви хочете плавно змінювати масштаб діапазон [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.

Сподіваюсь, це допомагає.


Я вдячний за твою допомогу. Я придумав рішення, яке виконує завдання виглядати естетично. Однак я застосую вашу логіку, щоб дати більш точну модель. Ще раз дякую :)
user650271

4
Лише нагадування: Модель буде більш точною, max != minінакше результати функції визначені :)
marcoslhc

10
чи гарантує це, що моя змінена масштаб змінна зберегла початковий розподіл?
Гейзенберг

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]. Масштаб між (1, 100) і [1,1,1]. Масштаб між (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

Для зручності ось алгоритм 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

21

Ось як я це розумію:


Який відсоток 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]


TLDR

Щоб зіставити xзі старого діапазону до нового діапазону:

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

1
Саме так я і відпрацював. Найскладніша частина - це з’ясувати співвідношення, де число лежить у заданому діапазоні. Він завжди повинен бути в межах [0, 1] приблизно так само, як відсоток, наприклад, 0,5 - для 50%. Далі вам потрібно лише розгорнути / розтягнути та змістити цей номер, щоб він відповідав необхідному діапазону.
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 і відремонтував її так, щоб мінімізувати обчислювальні кроки для наступних обчислень, розподіливши її на найменші постійні. Мотивація полягає в тому, щоб дозволити тренувальнику пройти навчання на одному наборі даних, а потім працювати над новими даними (для альго-мелодії) Насправді це дуже схоже на попередню обробку SciKit MinMaxScaler для 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;
}

2

На основі відповіді Чарльза Клейтона я включив декілька налаштувань 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; }

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.