Como reduzir um intervalo de números com um valor mínimo e máximo conhecido

230

Então, eu estou tentando descobrir como pegar um intervalo de números e reduzir os valores para caber em um intervalo. A razão para querer fazer isso é que estou tentando desenhar elipses em um jpanel de java swing. Quero que a altura e a largura de cada elipse estejam no intervalo de 1 a 30. Eu tenho métodos que encontram os valores mínimo e máximo do meu conjunto de dados, mas não terei o mínimo e o máximo até o tempo de execução. Existe uma maneira fácil de fazer isso?

user650271
fonte

Respostas:

507

Digamos que você queira dimensionar um intervalo [min,max]para [a,b]. Você está procurando uma função (contínua) que satisfaça

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

No seu caso, aseria 1 e b30, mas vamos começar com algo mais simples e tentar mapear [min,max]no intervalo [0,1].

Colocar minem uma função e sair 0 pode ser realizado com

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

Então isso é quase o que queremos. Mas colocar nos maxdaria max - minquando realmente queremos 1. Portanto, teremos que escalá-lo:

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

que é o que queremos. Então, precisamos fazer uma tradução e um dimensionamento. Agora, se queremos obter valores arbitrários de ae b, precisamos de algo um pouco mais complicado:

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

Você pode verificar se colocando em minpara xagora dá a, e colocando em maxb.

Você também pode perceber que (b-a)/(max-min)é um fator de escala entre o tamanho do novo intervalo e o tamanho do intervalo original. Então, realmente estamos primeira traduzir xpor -min, dimensioná-lo para o fator correto, e depois traduzi-lo backup para o novo valor mínimo de a.

Espero que isto ajude.

irritar
fonte
Eu aprecio sua ajuda. Eu descobri uma solução que faz o trabalho de parecer esteticamente agradável. No entanto, aplicarei sua lógica para fornecer um modelo mais preciso. Mais uma vez obrigado :) :) #
6562727
4
Apenas um lembrete: O modelo será mais preciso com max != minoutra forma os resultados de função indeterminada :)
marcoslhc
10
isso garante que minha variável redimensionada retenha a distribuição original?
Heisenberg
2
Esta é uma boa implementação de uma escala linear. Isso pode ser facilmente transformado em uma escala logarítmica?
tomexx
Explicação muito clara. Funciona se miné negativo e maxé positivo ou ambos têm que ser positivos?
28416 Andrew
48

Aqui está algum JavaScript para facilitar a copiar e colar (esta é a resposta irritante):

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

Aplicado dessa forma, dimensionando o intervalo de 10 a 50 para um intervalo entre 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

Editar:

Eu sei que respondi isso há muito tempo, mas aqui está uma função mais limpa que eu uso agora:

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 assim:

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

[0, 30.76923076923077, 69.23076923076923, 76.92307692307692, 100]

Charles Clayton
fonte
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"]; neste caso, pelo seu método, os números estão ficando muito pequenos. Existe alguma maneira, de modo que, a escala deva ser (0,100) ou (-100,100) e o espaço entre as saídas seja 0,5 (ou qualquer número).
Por favor, considere meu cenário para arr [] também.
1
É um pouco difícil, mas isso morre se a matriz contiver apenas um valor ou apenas várias cópias do mesmo valor. Portanto, [1] .scaleBetween (1, 100) e [1,1,1] .scaleBetween (1.100) preenchem a saída com NaN.
Front Malabar
1
@MalabarFront, boa observação. Suponho que está indefinido se, nesse caso, o resultado deve ser [1, 1, 1], [100, 100, 100]ou até mesmo [50.5, 50.5, 50.5]. Você poderia colocar no caso:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton
1
@CharlesClayton Fantastic, obrigado. Isso funciona um prazer!
Front Malabar
27

Por conveniência, aqui está o algoritmo do Irritate em um formato Java. Adicione verificação de erros, manipulação de exceções e ajuste conforme necessário.

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

Testador:

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
fonte
21

Aqui está como eu o entendo:


Qual porcentagem xestá em um intervalo

Vamos supor que você tenha um intervalo de 0até 100. Dado um número arbitrário desse intervalo, em que "porcentagem" desse intervalo ele se encontra? Isso deve ser bem simples, 0seria 0%, 50seria 50%e 100seria 100%.

Agora, o que se o intervalo foi 20de 100? Não podemos aplicar a mesma lógica acima (dividir por 100) porque:

20 / 100

não nos dá 0( 20deveria ser 0%agora). Isso deve ser simples de corrigir, basta criar o numerador 0para o caso de 20. Podemos fazer isso subtraindo:

(20 - 20) / 100

No entanto, isso não funciona 100mais porque:

(100 - 20) / 100

não nos dá 100%. Novamente, podemos corrigir isso subtraindo também o denominador:

(100 - 20) / (100 - 20)

Uma equação mais generalizada para descobrir qual% xestá em um intervalo seria:

(x - MIN) / (MAX - MIN)

Escala de escala para outra escala

Agora que sabemos qual a porcentagem de um número em um intervalo, podemos aplicá-lo para mapear o número para outro intervalo. Vamos passar por um exemplo.

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

Se tivermos um número no intervalo antigo, qual seria o número no novo intervalo? Digamos que o número seja 400. Primeiro, descubra qual porcentagem 400está dentro do intervalo antigo. Podemos aplicar nossa equação acima.

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

Então, 400está na 25%faixa antiga. Só precisamos descobrir qual é o número 25%da nova faixa. Pense sobre o que 50%de [0, 20]é. Seria 10certo? Como você chegou a essa resposta? Bem, podemos apenas fazer:

20 * 0.5 = 10

Mas e daí [10, 20]? Precisamos mudar tudo 10agora. por exemplo:

((20 - 10) * 0.5) + 10

uma fórmula mais generalizada seria:

((MAX - MIN) * PERCENT) + MIN

Para o exemplo original do que 25%de [10, 20]é:

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

Então, 400no intervalo [200, 1000]mapearia 12.5no intervalo[10, 20]


TLDR

Para mapear xdo intervalo antigo para o novo intervalo:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
Vic
fonte
1
Foi exatamente assim que resolvi. A parte mais complicada é descobrir a proporção em que um número se encontra em um determinado intervalo. Sempre deve estar dentro da faixa de [0, 1] exatamente como a porcentagem, por exemplo, 0,5 corresponde a 50%. Em seguida, você só precisa expandir / esticar e mudar esse número para caber no intervalo necessário.
SMUsamaShah
Obrigado por explicar as etapas de uma maneira muito simples - o copypasta acima da resposta / s funciona, mas saber as etapas é ótimo.
RozzA 25/08/19
11

Me deparei com esta solução, mas isso realmente não se encaixa na minha necessidade. Então eu cavei um pouco no código-fonte d3. Pessoalmente, eu recomendaria fazê-lo como o d3.scale faz.

Então, aqui você dimensiona o domínio para o intervalo. A vantagem é que você pode virar os sinais para o seu alcance-alvo. Isso é útil, uma vez que o eixo y na tela do computador desce de forma que valores grandes têm um y pequeno.

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

E aqui está o teste onde você pode ver o que quero dizer

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
fonte
"A vantagem é que você pode mudar os sinais para o seu alcance-alvo". Eu não entendo isso. Você pode explicar? Não consigo encontrar a diferença dos valores retornados da versão d3 e da versão acima (@irritate).
N1723
Compare os exemplos 1 e 2 com o intervalo alvo alterado
KIC
2

Peguei a resposta de Irritate e a refatorei para minimizar as etapas computacionais para cálculos subsequentes, fatorando-a no menor número de constantes. A motivação é permitir que um escalador seja treinado em um conjunto de dados e, em seguida, executado em novos dados (para um algo de ML). Na verdade, é muito parecido com o pré-processamento do MiniMaxScaler para Python do SciKit em uso.

Assim, x' = (b-a)(x-min)/(max-min) + a(onde b! = A) se torna o x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + aque pode ser reduzido a duas constantes no formulário x' = x*Part1 + Part2.

Aqui está uma implementação de C # com dois construtores: um para treinar e outro para recarregar uma instância treinada (por exemplo, para dar suporte à persistência).

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
fonte
2

Com base na resposta de Charles Clayton, incluí alguns ajustes no JSDoc, ES6 e incorporei sugestões dos comentários na resposta 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
fonte