Comment réduire une plage de nombres avec une valeur min et max connue

230

J'essaie donc de comprendre comment prendre une plage de nombres et réduire les valeurs pour qu'elles correspondent à une plage. La raison de vouloir le faire est que j'essaie de dessiner des ellipses dans un panneau java swing. Je veux que la hauteur et la largeur de chaque ellipse soient comprises entre 1 et 30. J'ai des méthodes qui trouvent les valeurs minimales et maximales dans mon ensemble de données, mais je n'aurai pas les valeurs min et max avant l'exécution. Y a-t-il un moyen facile de faire ceci?

user650271
la source

Réponses:

507

Supposons que vous souhaitiez mettre une plage [min,max]à l' échelle [a,b]. Vous recherchez une fonction (continue) qui satisfait

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

Dans votre cas, aserait 1 et bserait 30, mais commençons par quelque chose de plus simple et essayons de mapper [min,max]dans la plage [0,1].

Mettre mindans une fonction et sortir 0 pourrait être accompli avec

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

C'est donc presque ce que nous voulons. Mais la mise en place maxnous donnerait max - minquand nous en aurions réellement besoin 1. Il nous faudra donc la mettre à l'échelle:

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

c'est ce que nous voulons. Nous devons donc faire une traduction et une mise à l'échelle. Maintenant, si au contraire nous voulons obtenir des valeurs arbitraires de aet b, nous avons besoin de quelque chose d'un peu plus compliqué:

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

Vous pouvez vérifier que la mise minpour l' xinstant donne aet la mise maxdonne b.

Vous pouvez également remarquer qu'il (b-a)/(max-min)s'agit d'un facteur d'échelle entre la taille de la nouvelle plage et la taille de la plage d'origine. Donc, vraiment, nous traduisons d'abord xpar -min, le mettons à l'échelle au facteur correct, puis le traduisons à la nouvelle valeur minimale de a.

J'espère que cela t'aides.

irriter
la source
J'apprécie ton aide. J'ai trouvé une solution qui rend le travail esthétique. Cependant j'appliquerai votre logique pour donner un modèle plus précis. Merci encore :)
user650271
4
Juste un rappel: le modèle sera plus précis avec max != minsinon les résultats de la fonction
seront
10
cela garantit-il que ma variable redimensionnée conserve la distribution d'origine?
Heisenberg
2
Il s'agit d'une belle mise en œuvre d'une échelle linéaire. Peut-il être facilement transformé à une échelle logarighmique?
tomexx
Explication très claire. Cela fonctionne-t-il s'il minest négatif et maxpositif, ou les deux doivent-ils être positifs?
Andrew
48

Voici un peu de JavaScript pour faciliter le copier-coller (c'est la réponse de irritate):

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

Appliqué comme tel, la mise à l'échelle de la plage 10-50 à une plage 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

Éditer:

Je sais que j'ai répondu à cela il y a longtemps, mais voici une fonction plus propre que j'utilise maintenant:

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

Appliqué comme ça:

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

[0, 30,76923076923077, 69,23076923076923, 76,92307692307692, 100]

Charles Clayton
la source
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"]; dans ce cas, selon votre méthode, les nombres deviennent trop petits. Existe-t-il un moyen, de sorte que l'échelle doit être (0,100) ou (-100,100) et l'écart entre les sorties doit être de 0,5 (ou n'importe quel nombre).
Veuillez également considérer mon scénario pour arr [].
1
C'est un peu un cas de bord, mais cela meurt si le tableau ne contient qu'une seule valeur ou seulement plusieurs copies de la même valeur. Ainsi, [1] .scaleBetween (1, 100) et [1,1,1] .scaleBetween (1,100) remplissent tous les deux la sortie avec NaN.
Malabar Front
1
@MalabarFront, bonne observation. Je suppose qu'il est précisé si dans ce cas , le résultat devrait être [1, 1, 1], [100, 100, 100]ou même [50.5, 50.5, 50.5]. Vous pourriez mettre dans le cas:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton
1
@CharlesClayton Fantastique, merci. Ça marche un régal!
Malabar Front
27

Pour plus de commodité, voici l'algorithme d'Irritate sous une forme Java. Ajoutez la vérification des erreurs, la gestion des exceptions et ajustez si nécessaire.

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

Testeur:

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
la source
21

Voici comment je le comprends:


Quel pourcentage xse situe dans une fourchette

Supposons que vous ayez une plage de 0à 100. Étant donné un nombre arbitraire de cette plage, dans quel "pourcentage" de cette plage se situe-t-il? Cela devrait être assez simple, 0serait 0%, 50serait 50%et 100serait 100%.

Maintenant, si votre gamme était 20à 100? Nous ne pouvons pas appliquer la même logique que ci-dessus (diviser par 100) car:

20 / 100

ne nous donne pas 0( 20devrait être 0%maintenant). Cela devrait être simple à corriger, il suffit de faire le numérateur 0pour le cas de 20. Nous pouvons le faire en soustrayant:

(20 - 20) / 100

Cependant, cela ne fonctionne 100plus car:

(100 - 20) / 100

ne nous donne pas 100%. Encore une fois, nous pouvons résoudre ce problème en soustrayant également le dénominateur:

(100 - 20) / (100 - 20)

Une équation plus généralisée pour découvrir ce que% xse situe dans une fourchette serait:

(x - MIN) / (MAX - MIN)

Ajuster la plage à une autre plage

Maintenant que nous savons quel pourcentage se trouve un nombre dans une plage, nous pouvons l'appliquer pour mapper le nombre à une autre plage. Voyons un exemple.

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

Si nous avons un nombre dans l'ancienne plage, quel serait le nombre dans la nouvelle plage? Disons que le nombre est 400. Tout d'abord, déterminez quel pourcentage se 400situe dans l'ancienne fourchette. Nous pouvons appliquer notre équation ci-dessus.

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

Donc, 400se situe dans 25%l'ancienne gamme. Nous avons juste besoin de déterminer le numéro 25%de la nouvelle gamme. Pensez à ce que 50%de [0, 20]est. Ce serait 10bien? Comment êtes-vous arrivé à cette réponse? Eh bien, nous pouvons simplement faire:

20 * 0.5 = 10

Mais qu'en est-il de [10, 20]? Nous devons tout changer 10maintenant. par exemple:

((20 - 10) * 0.5) + 10

une formule plus généralisée serait:

((MAX - MIN) * PERCENT) + MIN

Pour l'exemple original de ce 25%de [10, 20]est:

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

Ainsi, 400dans la plage [200, 1000]correspondrait à 12.5la plage[10, 20]


TLDR

Pour mapper xde l'ancienne plage vers la nouvelle plage:

OLD PERCENT = (x - OLD MIN) / (OLD MAX - OLD MIN)
NEW X = ((NEW MAX - NEW MIN) * OLD PERCENT) + NEW MIN
Vic
la source
1
C'est exactement comme ça que j'ai travaillé. La partie la plus délicate consiste à trouver le rapport où un nombre se situe dans une plage donnée. Il doit toujours être dans la plage [0, 1] tout comme le pourcentage, par exemple 0,5 correspond à 50%. Ensuite, il vous suffit de développer / étirer et de déplacer ce nombre pour l'adapter à votre plage requise.
SMUsamaShah
Merci d'avoir expliqué les étapes d'une manière très simple - copypasta ci-dessus fonctionne, mais connaître les étapes est tout simplement génial.
RozzA
11

Je suis tombé sur cette solution mais cela ne correspond pas vraiment à mon besoin. J'ai donc creusé un peu dans le code source d3. Personnellement, je recommanderais de le faire comme le fait d3.scale.

Alors ici, vous mettez le domaine à l'échelle. L'avantage est que vous pouvez retourner les signes dans votre plage cible. Cela est utile car l'axe y sur un écran d'ordinateur descend de façon à ce que les grandes valeurs aient un petit 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));
    }
}

Et voici le test où vous pouvez voir ce que je veux dire

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
la source
"L'avantage est que vous pouvez retourner les signes à votre cible." Je ne comprends pas ça. Peux-tu expliquer? Je ne trouve pas la différence des valeurs renvoyées par votre version d3 et la version ci-dessus (@irritate).
nimo23
Comparez les exemples 1 et 2 de votre plage cible commutée
KIC
2

J'ai pris la réponse d'Irritate et l'ai refactorisée afin de minimiser les étapes de calcul pour les calculs ultérieurs en la factorisant dans le moins de constantes. La motivation est de permettre à un mesureur d'être formé sur un ensemble de données, puis d'être exécuté sur de nouvelles données (pour un algo ML). En effet, il ressemble beaucoup au prétraitement MinMaxScaler de Python pour SciKit.

Ainsi, x' = (b-a)(x-min)/(max-min) + a(où b! = A) devient x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + ace qui peut être réduit à deux constantes dans le formulaire x' = x*Part1 + Part2.

Voici une implémentation C # avec deux constructeurs: un pour former et un pour recharger une instance formée (par exemple, pour prendre en charge la persistance).

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
la source
2

Sur la base de la réponse de Charles Clayton, j'ai inclus quelques ajustements JSDoc, ES6 et incorporé des suggestions des commentaires dans la réponse 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; }

M. Polywhirl
la source