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


Así que estoy tratando de averiguar cómo tomar un rango de números y escalar los valores hacia abajo para ajustarse a un rango. La razón para querer hacer esto es que estoy tratando de dibujar elipses en un java swing jpanel. Quiero que la altura y el ancho de cada elipse estén en un rango de 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 manera fácil de hacer esto?

Author: irritate, 2011-03-14

6 answers

Digamos que desea escalar un rango [min,max] a [a,b]. Usted está buscando una función (continua) que satisface

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

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

Poner min en una función y salir 0 se podría lograr con

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

Así que eso es casi lo que queremos. Pero poner max nos daría max - min cuando realmente queremos 1. Así que tendremos para escalarlo:

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

Que es lo Que queremos. Así que tenemos que hacer una traducción y una escala. Ahora bien, si en cambio queremos obtener valores arbitrarios de a y b, necesitamos algo un poco más complicado:

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

Puedes verificar que poner min para x ahora da a, y poner max da 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 realmente somos los primeros traduciendo x por -min, escalándolo al factor correcto, y luego traduciéndolo nuevamente al nuevo valor mínimo de a.

Espero que esto ayude.

 432
Author: irritate,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-03-14 05:49:25

Aquí hay algo de JavaScript para la facilidad de copiar y pegar (esta es la respuesta de irritate):

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]

 33
Author: Charles Clayton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-29 12:05:12

Por conveniencia, aquí está el algoritmo de Irritate en forma Java. Agregue comprobació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;
    }
}

Probador:

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
 25
Author: Java42,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-06 21:54:37

Me encontré con esta solución, pero esto no se ajusta realmente a mi necesidad. Así que cavé un poco en el código fuente d3. Yo personalmente recomendaría hacerlo como d3.la escala lo hace.

Así que aquí escala el dominio al rango. La ventaja es que puede voltear los signos 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 pequeña 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));
    }
}

Y aquí está la prueba donde se puede ver lo que quiero decir

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);
    }
}
 9
Author: KIC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-29 11:42:56

Así es como lo entiendo:{[49]]}


Qué porcentaje x se encuentra en un rango

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

Ahora, ¿y si tu rango fuera 20 a 100? No podemos aplicar la misma lógica que la anterior (dividir por 100) porque:

20 / 100

No nos da 0 (20 debe ser 0% ahora). Esto debería ser fácil de arreglar, solo necesitamos hacer el numerador 0 para el caso de 20. Podemos hacer eso restando:

(20 - 20) / 100

Sin embargo, esto ya no funciona para 100 porque:

(100 - 20) / 100

No nos da 100%. Una vez más, podemos arreglar esto restando del denominador también:

(100 - 20) / (100 - 20)

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

(x - MIN) / (MAX - MIN)

Rango de escala a otro rango

Ahora que sabemos qué porcentaje se encuentra un número en un rango, podemos aplicarlo para mapear 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 antiguo, ¿cuál sería el número en el nuevo rango? Digamos que el número es 400. Primero, averigüe qué porcentaje 400 está dentro del rango antiguo. Podemos aplicar nuestra ecuación anterior.

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

Por lo tanto, 400 se encuentra en 25% de la antigua gama. Solo tenemos que averiguar qué número es 25% del nuevo rango. Piensa en lo que 50% de [0, 20] es. Sería 10 correcto? ¿Cómo llegaste a esa respuesta? Bueno, podemos hacer:

20 * 0.5 = 10

Pero, ¿qué hay de [10, 20]? Tenemos que cambiar todo por 10 ahora. eg:

((20 - 10) * 0.5) + 10

Una fórmula más generalizada sería:{[49]]}

((MAX - MIN) * PERCENT) + MIN

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

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

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


TLDR

Para mapear x del rango antiguo al nuevo rango:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
 6
Author: Vic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-29 16:02:02

He tomado la respuesta de Irritate y la refactoricé para minimizar los pasos computacionales para los cálculos posteriores factorizándola en la menor cantidad de constantes. La motivación es permitir que un escalador sea entrenado en un conjunto de datos, y luego se ejecute en nuevos datos (para un ML algo). En efecto, es muy parecido al MinMaxScaler de preprocesamiento de SciKit para Python en uso.

Así, x' = (b-a)(x-min)/(max-min) + a (donde b!= a) se convierte en x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + a que se puede reducir a dos constantes en la forma x' = x*Part1 + Part2.

Aquí hay una C# implementación 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;
}
 1
Author: Kevin Fichter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-08 18:40:30