Cómo reducir un rango de números con un valor mínimo y máximo conocidos

230

Así que estoy tratando de descubrir cómo tomar un rango de números y escalar los valores para que se ajusten a un rango. La razón para querer hacer esto es que estoy tratando de dibujar elipses en un japa swing jpanel. Quiero que la altura y el ancho de cada elipse estén en un rango de digamos 1-30. Tengo métodos que encuentran los valores mínimo y máximo de mi conjunto de datos, pero no tendré el mínimo y el máximo hasta el tiempo de ejecución. ¿Hay una forma fácil de hacer esto?

usuario650271
fuente

Respuestas:

507

Digamos que desea escalar un rango [min,max]a [a,b]. Estás buscando una función (continua) que satisfaga

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

En su caso, asería 1 y bsería 30, pero comencemos con algo más simple e intentemos mapear [min,max]en el rango [0,1].

Poner minen una función y sacar 0 podría lograrse con

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

Entonces eso es casi lo que queremos. Pero ponerlo maxnos daría max - mincuando realmente queremos 1. Así que tendremos que escalarlo:

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

que es lo que queremos Entonces necesitamos hacer una traducción y una escala. Ahora, si en cambio queremos obtener valores arbitrarios de ay b, necesitamos algo un poco más complicado:

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

Puede verificar que poner minpor xahora da ay poner maxda b.

También puede notar que (b-a)/(max-min)es un factor de escala entre el tamaño del nuevo rango y el tamaño del rango original. Así que en realidad estamos traduciendo primero xpor -min, su reducción al factor correcta, y luego traducirlo una copia de seguridad al nuevo valor mínimo de a.

Espero que esto ayude.

irritar
fuente
Aprecio tu ayuda. Descubrí una solución que hace el trabajo de lucir estéticamente agradable. Sin embargo, aplicaré su lógica para dar un modelo más preciso. Gracias de nuevo :)
user650271
44
Solo un recordatorio: el modelo será más preciso, de lo max != mincontrario, la función resultará indeterminada :)
marcoslhc
10
¿Esto garantiza que mi variable reescalada conserva la distribución original?
Heisenberg
2
Esta es una buena implementación de una escala lineal. ¿Se puede transformar fácilmente a una escala logarighmic?
tomexx
Muy clara explicación. ¿Funciona si mines negativo y maxpositivo, o ambos tienen que ser positivos?
Andrew
48

Aquí hay algunos JavaScript para copiar y pegar fácilmente (esta es la respuesta de irritar):

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

Aplicado así, escalando el rango 10-50 a un rango entre 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

Editar:

Sé que respondí esto hace mucho tiempo, pero aquí hay una función más limpia que uso ahora:

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

Aplicado así:

[-4, 0, 5, 6, 9].scaleBetween(0, 100);

[0, 30.76923076923077, 69.23076923076923, 76.92307692307692, 100]

Charles Clayton
fuente
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"]; para este caso, según su método, los números se están volviendo demasiado pequeños. ¿Hay alguna manera, de modo que la escala debe ser (0,100) o (-100,100) y la brecha entre las salidas debe ser 0,5 (o cualquier número).
Por favor considere mi escenario para arr [] también.
1
Es un caso marginal, pero esto muere si la matriz solo contiene un valor o solo varias copias del mismo valor. Entonces [1] .scaleBetween (1, 100) y [1,1,1] .scaleBetween (1,100) llenan la salida con NaN.
Malabar Front
1
@MalabarFront, buena observación. Supongo que es indefinido si en ese caso el resultado debería ser [1, 1, 1], [100, 100, 100]o incluso [50.5, 50.5, 50.5]. Podría poner en el caso:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton
1
@CharlesClayton Fantástico, gracias. Eso funciona de maravilla!
Malabar Front
27

Por conveniencia, aquí está el algoritmo de Irritate en forma de Java. Agregue verificación de errores, manejo de excepciones y ajustes según sea necesario.

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

Ensayador:

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
Java42
fuente
21

Así es como lo entiendo:


¿Qué porcentaje se xencuentra en un rango?

Supongamos que tiene un rango de 0a 100. Dado un número arbitrario de ese rango, ¿en qué "porcentaje" de ese rango se encuentra? Esto debería ser bastante simple, 0sería 0%, 50sería 50%y 100sería 100%.

Ahora, lo que si su rango fue 20de 100? No podemos aplicar la misma lógica que la anterior (dividir por 100) porque:

20 / 100

no nos da 0( 20debería ser 0%ahora). Esto debería ser simple de solucionar, solo necesitamos hacer el numerador 0para el caso de 20. Podemos hacer eso restando:

(20 - 20) / 100

Sin embargo, esto ya no funciona 100porque:

(100 - 20) / 100

no nos da 100%. Nuevamente, podemos solucionar esto restando también del denominador:

(100 - 20) / (100 - 20)

Una ecuación más generalizada para averiguar qué% se xencuentra en un rango sería:

(x - MIN) / (MAX - MIN)

Rango de escala a otro rango

Ahora que sabemos qué porcentaje de un número se encuentra en un rango, podemos aplicarlo para asignar el número a otro rango. Veamos un ejemplo.

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

Si tenemos un número en el rango anterior, ¿cuál sería el número en el nuevo rango? Digamos que el número es 400. Primero, calcule qué porcentaje 400está dentro del rango anterior. Podemos aplicar nuestra ecuación anterior.

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

Por lo tanto, se 400encuentra en 25%el rango anterior. Solo tenemos que averiguar qué número es 25%del nuevo rango. Pensar en lo que 50%de [0, 20]es. Estaría 10bien? ¿Cómo llegaste a esa respuesta? Bueno, solo podemos hacer:

20 * 0.5 = 10

Pero, ¿qué pasa con [10, 20]? Necesitamos cambiar todo por 10ahora. p.ej:

((20 - 10) * 0.5) + 10

una fórmula más generalizada sería:

((MAX - MIN) * PERCENT) + MIN

Para el ejemplo original de lo que 25%de [10, 20]es:

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

Entonces, 400en el rango [200, 1000]se mapearía 12.5en el rango[10, 20]


TLDR

Para asignar xdel rango antiguo al nuevo rango:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
Vic
fuente
1
Así es exactamente como lo resolví. La parte más complicada es encontrar la relación donde un número se encuentra en un rango dado. Siempre debe estar dentro del rango [0, 1] al igual que el porcentaje, por ejemplo, 0.5 es para 50%. A continuación, solo tiene que expandir / estirar y cambiar este número para que se ajuste a su rango requerido.
SMUsamaShah
Gracias por explicar los pasos de una manera muy simple: copypasta arriba de la / s respuesta / s funciona pero conocer los pasos es genial.
RozzA
11

Encontré esta solución, pero esto realmente no se ajusta a mi necesidad. Así que busqué un poco en el código fuente d3. Yo personalmente recomendaría hacerlo como lo hace d3.scale.

Entonces aquí escalas el dominio al rango. La ventaja es que puede voltear las señales a su rango objetivo. Esto es útil ya que el eje y en la pantalla de una computadora va de arriba hacia abajo, por lo que los valores grandes tienen una y pequeña.

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

Y aquí está la prueba donde puedes ver a qué me refiero

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);
    }
}
KIC
fuente
"La ventaja es que puedes voltear señales a tu rango objetivo". No entiendo esto ¿Puedes explicar? No puedo encontrar la diferencia de los valores devueltos de su versión d3 y la versión anterior (@irritate).
nimo23
Compare los ejemplos 1 y 2 con el rango objetivo cambiado
KIC
2

Tomé la respuesta de Irritate y la refactoré para minimizar los pasos computacionales para cálculos posteriores factorizándola en la menor cantidad de constantes. La motivación es permitir que un escalador se entrene en un conjunto de datos y luego se ejecute en nuevos datos (para un algoritmo ML). En efecto, es muy similar al preprocesamiento de MinMaxScaler para Python en uso.

Por lo tanto, x' = (b-a)(x-min)/(max-min) + a(donde b! = A) se convierte en lo x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + aque se puede reducir a dos constantes en la forma x' = x*Part1 + Part2.

Aquí hay una implementación de C # con dos constructores: uno para entrenar y otro para recargar una instancia entrenada (por ejemplo, para soportar la persistencia).

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;
}
Kevin Fichter
fuente
2

Basado en la respuesta de Charles Clayton, incluí algunos ajustes de JSDoc, ES6 e incorporé sugerencias de los comentarios en la respuesta original.

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

Mr. Polywhirl
fuente