So verkleinern Sie einen Zahlenbereich mit einem bekannten Min- und Max-Wert


230

Ich versuche also herauszufinden, wie man einen Zahlenbereich nimmt und die Werte verkleinert, um sie an einen Bereich anzupassen. Der Grund dafür ist, dass ich versuche, Ellipsen in einem Java Swing Jpanel zu zeichnen. Ich möchte, dass die Höhe und Breite jeder Ellipse in einem Bereich von beispielsweise 1 bis 30 liegt. Ich habe Methoden, die die Minimal- und Maximalwerte aus meinem Datensatz ermitteln, aber ich habe die Min- und Maximalwerte erst zur Laufzeit. Gibt es eine einfache Möglichkeit, dies zu tun?

Antworten:


507

Angenommen, Sie möchten einen Bereich [min,max]auf skalieren [a,b]. Sie suchen eine (kontinuierliche) Funktion, die erfüllt

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

In Ihrem Fall awäre 1 und b30, aber beginnen wir mit etwas Einfacherem und versuchen, [min,max]den Bereich abzubilden [0,1].

Das Setzen mineiner Funktion und das Herausholen von 0 könnte mit erreicht werden

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

Das ist fast das, was wir wollen. Aber das Einfügen maxwürde uns geben, max - minwann wir tatsächlich wollen 1. Also müssen wir es skalieren:

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

Welches ist, was wir wollen. Wir müssen also eine Übersetzung und eine Skalierung durchführen. Wenn wir stattdessen willkürliche Werte von aund erhalten wollen b, brauchen wir etwas Komplizierteres:

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

Sie können sicherstellen , dass bei der Umsetzung minfür xjetzt gibt a, und setzen in maxgibt b.

Möglicherweise stellen Sie auch fest, dass dies (b-a)/(max-min)ein Skalierungsfaktor zwischen der Größe des neuen Bereichs und der Größe des ursprünglichen Bereichs ist. Wir übersetzen also wirklich zuerst, xindem wir -mines auf den richtigen Faktor skalieren und dann wieder auf den neuen Mindestwert von übersetzen a.

Hoffe das hilft.


Ich schätze Ihre Hilfe. Ich habe eine Lösung gefunden, die ästhetisch ansprechend aussieht. Ich werde jedoch Ihre Logik anwenden, um ein genaueres Modell zu erhalten.
Nochmals vielen

4
Nur zur Erinnerung: Das Modell wird genauer sein, max != minansonsten sind die Funktionsergebnisse unbestimmt :)
marcoslhc

10
Stellt dies sicher, dass meine neu skalierte Variable die ursprüngliche Verteilung beibehält?
Heisenberg

2
Dies ist eine schöne Implementierung einer linearen Skala. Kann dies leicht in eine logarighmische Skala umgewandelt werden?
Toméxx

Sehr klare Erklärung. Funktioniert es, wenn mines negativ und maxpositiv ist, oder müssen beide positiv sein?
Andrew

48

Hier ist etwas JavaScript zum einfachen Kopieren und Einfügen (dies ist die Antwort von irritate):

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

Wird so angewendet und skaliert den Bereich 10-50 auf einen Bereich zwischen 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

Bearbeiten:

Ich weiß, dass ich das vor langer Zeit beantwortet habe, aber hier ist eine sauberere Funktion, die ich jetzt benutze:

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

So angewendet:

[-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"]; In diesem Fall werden die Zahlen nach Ihrer Methode zu klein. Gibt es eine Möglichkeit, dass die Skalierung (0,100) oder (-100,100) und der Abstand zwischen den Ausgängen 0,5 (oder eine beliebige Zahl) betragen sollte?

Bitte beachten Sie auch mein Szenario für arr [].

1
Es ist ein bisschen wie ein Randfall, aber dies stirbt, wenn das Array nur einen Wert oder nur mehrere Kopien desselben Werts enthält. Also füllen [1] .scaleBetween (1, 100) und [1,1,1] .scaleBetween (1.100) beide die Ausgabe mit NaN.
Malabar Front

1
@ MalabarFront, gute Beobachtung. Ich nehme an, es ist nicht definiert , ob in diesem Fall sollte das Ergebnis sein [1, 1, 1], [100, 100, 100]oder sogar [50.5, 50.5, 50.5]. Sie könnten in den Fall setzen:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton

1
@ Charles Clayton Fantastisch, danke. Das ist ein Vergnügen!
Malabar Front

27

Der Einfachheit halber ist hier der Irritate-Algorithmus in einer Java-Form. Fügen Sie nach Bedarf Fehlerprüfung, Ausnahmebehandlung und Optimierung hinzu.

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üfer:

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

So verstehe ich es:


Wie viel Prozent xliegen in einem Bereich ?

Nehmen wir an, Sie haben einen Bereich von 0bis 100. In welchem ​​"Prozent" aus diesem Bereich liegt eine beliebige Zahl aus diesem Bereich? Dies sollte ziemlich einfach sein, 0wäre 0%, 50wäre 50%und 100wäre 100%.

Nun, was ist, wenn Ihr Angebot war 20zu 100? Wir können nicht dieselbe Logik wie oben anwenden (durch 100 teilen), weil:

20 / 100

gibt uns nicht 0( 20sollte 0%jetzt sein). Dies sollte einfach zu beheben sein, wir müssen nur den Zähler 0für den Fall von machen 20. Wir können das tun, indem wir subtrahieren:

(20 - 20) / 100

Dies funktioniert jedoch nicht 100mehr, weil:

(100 - 20) / 100

gibt uns nicht 100%. Auch dies können wir beheben, indem wir vom Nenner abziehen:

(100 - 20) / (100 - 20)

Eine allgemeinere Gleichung, um herauszufinden, wie viel Prozent xin einem Bereich liegen, wäre:

(x - MIN) / (MAX - MIN)

Skalieren Sie den Bereich auf einen anderen Bereich

Nachdem wir nun wissen, wie viel Prozent einer Zahl in einem Bereich liegen, können wir sie anwenden, um die Zahl einem anderen Bereich zuzuordnen. Lassen Sie uns ein Beispiel durchgehen.

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

Wenn wir eine Nummer im alten Bereich haben, welche wäre die Nummer im neuen Bereich? Nehmen wir an, die Nummer ist 400. Stellen Sie zunächst fest, wie viel Prozent 400im alten Bereich liegen. Wir können unsere obige Gleichung anwenden.

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

Liegt also 400im 25%alten Bereich. Wir müssen nur herausfinden, welche Nummer 25%aus dem neuen Bereich stammt. Überlegen Sie, was 50%von [0, 20]ist. Es wäre 10richtig? Wie sind Sie zu dieser Antwort gekommen? Nun, wir können einfach tun:

20 * 0.5 = 10

Aber was ist mit von [10, 20]? Wir müssen 10jetzt alles verschieben . z.B:

((20 - 10) * 0.5) + 10

Eine allgemeinere Formel wäre:

((MAX - MIN) * PERCENT) + MIN

Zum Original Beispiel dafür , was 25%der [10, 20]ist:

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

Also, 400in der Reichweite [200, 1000]würde 12.5in der Reichweite abgebildet[10, 20]


TLDR

So ordnen Sie den xalten Bereich dem neuen Bereich zu:

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

1
Genau so habe ich es herausgefunden. Am schwierigsten ist es, das Verhältnis herauszufinden, in dem eine Zahl in einem bestimmten Bereich liegt. Es sollte immer im Bereich [0, 1] liegen, genau wie der Prozentsatz, z. B. 0,5 für 50%. Als nächstes müssen Sie diese Zahl nur erweitern / strecken und verschieben, um sie in den gewünschten Bereich zu bringen.
SMUsamaShah

Vielen Dank, dass Sie die Schritte auf sehr einfache Weise erklärt haben - Copypasta über Antwort / en funktioniert, aber die Schritte zu kennen ist einfach großartig.
RozzA

11

Ich bin auf diese Lösung gestoßen, aber das passt nicht wirklich zu meinen Bedürfnissen. Also habe ich ein bisschen im d3-Quellcode gegraben. Ich persönlich würde empfehlen, es so zu machen, wie es d3.scale tut.

Hier skalieren Sie die Domain auf den Bereich. Der Vorteil ist, dass Sie Zeichen in Ihren Zielbereich drehen können. Dies ist nützlich, da die y-Achse auf einem Computerbildschirm von oben nach unten zeigt, sodass große Werte ein kleines y haben.

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

Und hier ist der Test, bei dem Sie sehen können, was ich meine

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

"Der Vorteil ist, dass Sie Zeichen in Ihren Zielbereich drehen können." Ich verstehe das nicht. Können Sie erklären? Ich kann den Unterschied zwischen den zurückgegebenen Werten Ihrer d3-Version und der Version von oben (@irritate) nicht finden.
Nimo23

Vergleichen Sie Beispiel 1 und 2, in denen Ihr Zielbereich umgeschaltet wurde
KIC

2

Ich habe die Antwort von Irritate genommen und überarbeitet, um die Rechenschritte für nachfolgende Berechnungen zu minimieren, indem ich sie in die wenigsten Konstanten zerlege. Die Motivation besteht darin, einem Scaler zu ermöglichen, an einem Datensatz zu trainieren und dann mit neuen Daten (für ein ML-Algo) ausgeführt zu werden. Tatsächlich ähnelt es SciKits Vorverarbeitung von MinMaxScaler für Python in der Verwendung.

Somit wird x' = (b-a)(x-min)/(max-min) + a(wobei b! = A), x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + awas in der Form auf zwei Konstanten reduziert werden kann x' = x*Part1 + Part2.

Hier ist eine C # -Implementierung mit zwei Konstruktoren: einer zum Trainieren und einer zum erneuten Laden einer trainierten Instanz (z. B. zur Unterstützung der Persistenz).

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

Basierend auf der Antwort von Charles Clayton habe ich einige JSDoc- und ES6-Optimierungen aufgenommen und Vorschläge aus den Kommentaren in die ursprüngliche Antwort aufgenommen.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.