Come ridimensionare un intervallo di numeri con un valore minimo e massimo noto


230

Quindi sto cercando di capire come prendere un intervallo di numeri e ridimensionare i valori per adattarli a un intervallo. Il motivo per voler fare questo è che sto cercando di disegnare ellissi in un jpanel swing java. Voglio che l'altezza e la larghezza di ogni ellisse siano comprese nell'intervallo 1-30. Ho metodi che trovano i valori minimo e massimo dal mio set di dati, ma non avrò il minimo e il massimo fino al runtime. C'è un modo semplice per farlo?

Risposte:


507

Diciamo che si desidera ridimensionare una serie [min,max]di [a,b]. Stai cercando una funzione (continua) che soddisfi

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

Nel tuo caso, asarebbe 1 e bsarebbe 30, ma cominciamo con qualcosa di più semplice e proviamo a mappare [min,max]nell'intervallo [0,1].

Mettere minin una funzione e uscire da 0 potrebbe essere realizzato con

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

Quindi è quasi quello che vogliamo. Ma inserirci maxci darebbe max - minquando vogliamo veramente 1. Quindi dovremo ridimensionarlo:

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

che è quello che vogliamo. Quindi dobbiamo fare una traduzione e un ridimensionamento. Ora, se invece vogliamo ottenere valori arbitrari di ae b, abbiamo bisogno di qualcosa di un po 'più complicato:

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

È possibile verificare che la messa in minper xora dà a, e mettendo in maxb.

Potresti anche notare che (b-a)/(max-min)è un fattore di ridimensionamento tra la dimensione della nuova gamma e la dimensione della gamma originale. Quindi, in realtà noi siamo prima traducendo xda -min, scalando al fattore corretta e quindi tradurlo backup al nuovo valore minimo a.

Spero che questo ti aiuti.


Apprezzo il vostro aiuto. Ho trovato una soluzione che ha il compito di apparire esteticamente piacevole. Comunque applicherò la tua logica per dare un modello più accurato. Grazie ancora :)
user650271

4
Solo un promemoria: il modello sarà più preciso, max != minaltrimenti la funzione risulta indeterminata :)
marcoslhc,

10
questo garantisce che la mia variabile riscalata mantenga la distribuzione originale?
Heisenberg,

2
Questa è una bella implementazione di una scala lineare. Questo può essere facilmente trasformato in una scala logarighmica?
tomexx,

Spiegazione molto chiara. Funziona se minè negativo ed maxè positivo o devono essere entrambi positivi?
Andrew,

48

Ecco un po 'di JavaScript per facilità di copia-incolla (questa è la risposta dell'irritato):

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

Applicato in questo modo, ridimensionando l'intervallo 10-50 su un intervallo compreso tra 0 e 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

Modificare:

So di aver risposto molto tempo fa, ma ecco una funzione più pulita che uso ora:

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

Applicato in questo modo:

[-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 questo caso, con il tuo metodo, i numeri stanno diventando troppo piccoli. Esiste un modo, quindi che la scala dovrebbe essere (0,100) o (-100,100) e lo spazio tra le uscite dovrebbe essere 0,5 (o qualsiasi numero).

Si prega di considerare anche il mio scenario per arr [].

1
È un po 'un caso limite, ma questo muore se l'array contiene solo un valore o solo più copie dello stesso valore. Quindi [1] .scaleB Between (1, 100) e [1,1,1] .scaleB Between (1.100) riempiono entrambi l'output con NaN.
Malabar Front,

1
@MalabarFront, buona osservazione. Suppongo che è undefined se in questo caso il risultato dovrebbe essere [1, 1, 1], [100, 100, 100]o addirittura [50.5, 50.5, 50.5]. Potresti inserire il caso:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton,

1
@CharlesClayton Fantastico, grazie. Funziona a meraviglia!
Malabar Front,

27

Per comodità, ecco l'algoritmo di Irritate in forma Java. Aggiungi controllo errori, gestione delle eccezioni e modifica se necessario.

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

Tester:

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

Ecco come lo capisco:


Quale percentuale si xtrova in un intervallo

Supponiamo che tu abbia un intervallo da 0a 100. Dato un numero arbitrario di quell'intervallo, in quale "percentuale" di quell'intervallo si trova? Questo dovrebbe essere piuttosto semplice, 0sarebbe 0%, 50sarebbe 50%e 100sarebbe 100%.

Ora, che cosa se l'intervallo è stato 20per 100? Non possiamo applicare la stessa logica di cui sopra (dividere per 100) perché:

20 / 100

non ci dà 0( 20dovrebbe essere 0%ora). Questo dovrebbe essere semplice da risolvere, dobbiamo solo creare il numeratore 0per il caso di 20. Possiamo farlo sottraendo:

(20 - 20) / 100

Tuttavia, questo non funziona 100più perché:

(100 - 20) / 100

non ci dà 100%. Ancora una volta, possiamo risolvere questo problema sottraendo anche dal denominatore:

(100 - 20) / (100 - 20)

Un'equazione più generalizzata per scoprire quale% si xtrova in un intervallo sarebbe:

(x - MIN) / (MAX - MIN)

Scala l'intervallo su un altro intervallo

Ora che sappiamo in quale percentuale un numero si trova in un intervallo, possiamo applicarlo per mappare il numero su un altro intervallo. Facciamo un esempio.

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

Se abbiamo un numero nel vecchio intervallo, quale sarebbe il numero nel nuovo intervallo? Diciamo che il numero è 400. In primo luogo, capire quale percentuale 400è all'interno del vecchio intervallo. Possiamo applicare la nostra equazione sopra.

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

Quindi, 400risiede nella 25%vecchia gamma. Dobbiamo solo capire quale sia il numero 25%della nuova gamma. Pensa a ciò che 50%di [0, 20]è. Sarebbe 10giusto? Come sei arrivato a quella risposta? Bene, possiamo semplicemente fare:

20 * 0.5 = 10

Ma che dire di [10, 20]? Dobbiamo spostare tutto 10ormai. per esempio:

((20 - 10) * 0.5) + 10

una formula più generalizzata sarebbe:

((MAX - MIN) * PERCENT) + MIN

Per l'esempio originale di ciò che 25%di [10, 20]è:

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

Quindi, 400nell'intervallo [200, 1000]sarebbe mappato 12.5nell'intervallo[10, 20]


TLDR

Per mappare xdal vecchio intervallo al nuovo intervallo:

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

1
È esattamente così che l'ho capito. La parte più difficile è scoprire il rapporto in cui un numero si trova in un determinato intervallo. Dovrebbe sempre rientrare nell'intervallo [0, 1] proprio come percentuale, ad esempio 0,5 è per il 50%. Successivamente devi solo espandere / allungare e spostare questo numero per adattarlo all'intervallo richiesto.
SMUsamaShah,

Grazie per aver spiegato i passaggi in un modo molto semplice: copypasta sopra le risposte funziona ma conoscere i passaggi è semplicemente fantastico.
RozzA

11

Mi sono imbattuto in questa soluzione ma questo non si adatta perfettamente alle mie esigenze. Quindi ho scavato un po 'nel codice sorgente d3. Personalmente consiglierei di farlo come fa d3.scale.

Quindi qui ridimensionate il dominio nell'intervallo. Il vantaggio è che puoi capovolgere i segni sul tuo raggio d'azione. Ciò è utile poiché l'asse y sullo schermo di un computer va verso il basso, quindi i valori più grandi hanno una piccola 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));
    }
}

Ed ecco il test in cui puoi vedere cosa intendo

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

"Il vantaggio è che puoi capovolgere i segni sul tuo raggio d'azione." Non lo capisco. Puoi spiegare? Non riesco a trovare la differenza tra i valori restituiti dalla tua versione d3 e la versione dall'alto (@irritate).
nimo23,

Confronta gli esempi 1 e 2 con il tuo intervallo target commutato
KIC

2

Ho preso la risposta di Irritate e la ho riformattata in modo da ridurre al minimo i passaggi computazionali per i calcoli successivi fattorizzandola nelle poche costanti. La motivazione è quella di consentire a uno scaler di essere addestrato su un set di dati e quindi di essere eseguito su nuovi dati (per un algo ML). In effetti, è molto simile al MinMaxScaler preelaborato di SciKit per Python in uso.

Pertanto, x' = (b-a)(x-min)/(max-min) + a(dove b! = A) diventa x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + ache può essere ridotto a due costanti nella forma x' = x*Part1 + Part2.

Ecco un'implementazione in C # con due costruttori: uno per l'addestramento e uno per ricaricare un'istanza addestrata (ad esempio, per supportare la persistenza).

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

Sulla base della risposta di Charles Clayton, ho incluso alcune modifiche JSDoc, ES6 e ho incorporato suggerimenti dai commenti nella risposta originale.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.