Jak zmniejszyć zakres liczb o znanej wartości minimalnej i maksymalnej


230

Próbuję więc wymyślić, jak wziąć zakres liczb i przeskalować wartości w celu dopasowania zakresu. Powodem tego jest to, że próbuję narysować elipsy w panelu java swing. Chcę, aby wysokość i szerokość każdej elipsy były w zakresie, powiedzmy, 1-30. Mam metody, które znajdują wartości minimalne i maksymalne z mojego zestawu danych, ale nie będę mieć wartości minimalnej i maksymalnej do czasu uruchomienia. Czy jest na to łatwy sposób?

Odpowiedzi:


507

Powiedzmy, że chcesz skalować zakres [min,max]do [a,b]. Szukasz (ciągłej) funkcji, która spełni Twoje oczekiwania

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

W twoim przypadku abyłoby 1 i bwynosiłoby 30, ale zacznijmy od czegoś prostszego i spróbuj zmapować [min,max]do zakresu [0,1].

Włączenie minfunkcji i wyjście 0 można osiągnąć za pomocą

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

Tak więc prawie tego chcemy. Ale wstawienie maxdałoby nam, max - minkiedy naprawdę chcemy 1. Musimy to skalować:

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

czego chcemy. Musimy więc wykonać tłumaczenie i skalowanie. Teraz, jeśli zamiast tego chcemy uzyskać dowolne wartości ai b, potrzebujemy czegoś nieco bardziej skomplikowanego:

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

Możesz sprawdzić, czy wprowadzenie minna xteraz daje a, a wprowadzenie maxdaje b.

Można również zauważyć, że (b-a)/(max-min)jest to czynnik skalujący między rozmiarem nowego zakresu a rozmiarem pierwotnego zakresu. Tak naprawdę jesteśmy pierwszym tłumaczenia xprzez -minskalowanie go do odpowiedniego czynnika, a następnie tłumaczenia to kopie zapasowe do nowej wartością minimalną a.

Mam nadzieję że to pomoże.


Doceniam twoją pomoc. Wymyśliłem rozwiązanie, które wygląda estetycznie. Zastosuję jednak twoją logikę, aby podać dokładniejszy model.
Jeszcze

4
Przypominamy: model będzie dokładniejszy, w max != minprzeciwnym razie wyniki funkcji nie zostaną określone :)
marcoslhc

10
czy to zapewnia, że ​​moja zmienna zmienna zmienna zachowuje oryginalny rozkład?
Heisenberg

2
To ładna implementacja skali liniowej. Czy można to łatwo przekształcić w skalę logarytmiczną?
tomexx

Bardzo jasne wytłumaczenie. Czy to działa, jeśli minjest negatywne i maxjest pozytywne, czy oba muszą być pozytywne?
Andrew

48

Oto kilka skryptów JavaScript ułatwiających kopiowanie i wklejanie (jest to irytująca odpowiedź):

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

Zastosowano w ten sposób, skalując zakres 10-50 do zakresu między 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

Edytować:

Wiem, że odpowiedziałem na to dawno temu, ale oto czystsza funkcja, której teraz używam:

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

Zastosowano tak:

[-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”]; w tym przypadku, według twojej metody, liczby stają się zbyt małe. Czy jest jakiś sposób, aby skala wynosiła (0,100) lub (-100,100), a przerwa między wyjściami powinna wynosić 0,5 (lub dowolną liczbę).

Proszę również rozważyć mój scenariusz dotyczący arr [].

1
To trochę przypadek na krawędzi, ale to umiera, jeśli tablica zawiera tylko jedną wartość lub tylko wiele kopii tej samej wartości. Tak więc [1] .scaleBetween (1, 100) i [1,1,1] .scaleBetween (1100) oba wypełniają wyjście NaN.
Malabar Front

1
@MalabarFront, dobra obserwacja. Przypuszczam, że to nieokreślone, czy w takim przypadku wynik powinien być [1, 1, 1], [100, 100, 100]albo nawet [50.5, 50.5, 50.5]. Możesz włożyć do sprawy:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton,

1
@CharlesClayton Fantastyczne, dzięki. To działa na ucztę!
Malabar Front,

27

Dla wygody oto algorytm Irritate w formie Java. Dodaj sprawdzanie błędów, obsługę wyjątków i popraw w razie potrzeby.

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

Próbnik:

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

Oto jak to rozumiem:


Jaki procent xmieści się w zakresie

Załóżmy, że masz zakres od 0do 100. Biorąc pod uwagę dowolną liczbę z tego zakresu, w jakim „procencie” z tego zakresu się znajduje? To powinno być dość proste, 0byłoby 0%, 50byłoby 50%i 100będzie 100%.

A co jeśli twój zasięg miałby to 20zrobić 100? Nie możemy zastosować tej samej logiki jak powyżej (podzielić przez 100), ponieważ:

20 / 100

nie daje nam 0( 20powinno być 0%teraz). Powinno to być łatwe do naprawienia, wystarczy zrobić licznik 0dla przypadku 20. Możemy to zrobić, odejmując:

(20 - 20) / 100

Jednak to już nie działa, 100ponieważ:

(100 - 20) / 100

nie daje nam 100%. Ponownie możemy to naprawić, odejmując również od mianownika:

(100 - 20) / (100 - 20)

Bardziej uogólnionym równaniem służącym do ustalenia, jaki% xmieści się w zakresie, byłoby:

(x - MIN) / (MAX - MIN)

Skaluj zakres do innego zakresu

Teraz, gdy wiemy, jaki procent liczby mieści się w zakresie, możemy zastosować go, aby odwzorować liczbę na inny zakres. Przejdźmy przez przykład.

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

Gdybyśmy mieli liczbę ze starego zakresu, jaka byłaby liczba z nowego zakresu? Powiedzmy, że liczba jest 400. Najpierw dowiedz się, jaki procent 400mieści się w starym zakresie. Możemy zastosować powyższe równanie.

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

Tak więc 400leży w 25%starym zakresie. Musimy tylko dowiedzieć się, jaka jest liczba 25%z nowego zakresu. Pomyśl o tym, co 50%o [0, 20]to. Byłoby 10dobrze? Jak doszedłeś do tej odpowiedzi? Cóż, możemy po prostu zrobić:

20 * 0.5 = 10

Ale co z [10, 20]? Musimy już wszystko zmienić 10. na przykład:

((20 - 10) * 0.5) + 10

bardziej ogólną formułą byłoby:

((MAX - MIN) * PERCENT) + MIN

Do pierwotnego przykład tego, co 25%z [10, 20]to:

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

Tak więc 400w zakresie [200, 1000]zostanie zmapowane do 12.5zakresu[10, 20]


TLDR

Aby zmapować xze starego zakresu na nowy zakres:

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

1
Właśnie tak to wypracowałem. Najtrudniejszą częścią jest ustalenie stosunku, w którym liczba leży w danym zakresie. Powinien zawsze znajdować się w przedziale [0, 1] podobnie jak procent, np. 0,5 oznacza 50%. Następnie musisz tylko rozwinąć / rozciągnąć i przesunąć tę liczbę, aby zmieściła się w wymaganym zakresie.
SMUsamaShah,

Dziękujemy za wyjaśnienie kroków w bardzo prosty sposób - copypasta powyżej odpowiedzi / odpowiedzi działa, ale znajomość kroków jest po prostu świetna.
RozzA

11

Natknąłem się na to rozwiązanie, ale tak naprawdę nie spełnia moich potrzeb. Więc trochę zagłębiłem się w kodzie źródłowym d3. Osobiście poleciłbym to zrobić tak, jak robi to d3.scale.

Tutaj skalujesz domenę do zakresu. Zaletą jest to, że możesz przerzucić znaki do swojego zakresu docelowego. Jest to przydatne, ponieważ oś y na ekranie komputera przesuwa się z góry na dół, więc duże wartości mają małe 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));
    }
}

A oto test, w którym możesz zobaczyć, co mam na myśli

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

„Zaletą jest to, że możesz przerzucić znaki do docelowego zakresu”. Nie rozumiem tego. Możesz wytłumaczyć? Nie mogę znaleźć różnicy zwracanych wartości z wersji d3 i wersji z góry (@irritate).
nimo23,

Porównaj przykłady 1 i 2, zmieniając zakres docelowy
KIC

2

Wziąłem odpowiedź Irritate i ponownie ją przeredagowałem, aby zminimalizować kroki obliczeniowe dla kolejnych obliczeń poprzez faktorowanie jej na najmniejszą stałą. Motywacją jest umożliwienie trenowania skalera na jednym zestawie danych, a następnie uruchomienie nowych danych (dla algo ML). W efekcie jest to bardzo podobne do użycia MinMaxScaler SciKit dla Pythona w użyciu.

Tak więc x' = (b-a)(x-min)/(max-min) + a(gdzie b! = A) staje się, x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + aco można zredukować do postaci dwóch stałych x' = x*Part1 + Part2.

Oto implementacja C # z dwoma konstruktorami: jednym do trenowania, a drugim do przeładowywania przeszkolonej instancji (np. W celu wspierania trwałości).

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

Na podstawie odpowiedzi Charlesa Claytona dołączyłem niektóre poprawki JSDoc, ES6 i włączyłem sugestie z komentarzy w pierwotnej odpowiedzi.

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.