既知の最小値と最大値で数値の範囲を縮小する方法


230

だから私は数値の範囲を取り、範囲に合うように値をスケールダウンする方法を理解しようとしています。これを実行する理由は、Javaスイングjpanelで楕円を描画しようとしているためです。各楕円の高さと幅を1〜30の範囲にしたい。データセットから最小値と最大値を見つけるメソッドがありますが、実行時まで最小値と最大値はありません。これを行う簡単な方法はありますか?

回答:


507

範囲[min,max]をにスケーリングするとします[a,b]。あなたは(連続的な)関数を探しています

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

あなたの場合、a1とb30になりますが、もっと簡単なものから始め[min,max]て、範囲にマッピングしてみましょう[0,1]

置くmin関数に0を抜け出すすることで達成することができます

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

これが、私たちが望んでいることです。しかし、実際に1が必要な場合は、配置maxすることで得られmax - minます。そのため、スケーリングする必要があります。

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

これが私たちの望みです。したがって、変換とスケーリングを行う必要があります。ここで、代わりにaandの任意の値を取得したい場合はb、もう少し複雑なものが必要です。

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

あなたには入れていることを確認することができるminためにはx、今与えa、そして中に入れてmaxいますb

また、それ(b-a)/(max-min)が新しい範囲のサイズと元の範囲のサイズとの間のスケーリング係数であることにお気づきかもしれません。したがって、実際にはまずで変換x-min、それを正しい係数にスケーリングしてから、を新しい最小値に変換して戻しますa

お役に立てれば。


私はあなたの助けに感謝します。見た目が美しく見えるソリューションを見つけました。ただし、より正確なモデルを提供するために、ロジックを適用します。ありがとうございました:)
user650271

4
念のため:モデルはより正確になりmax != minます。それ以外の場合、関数の結果は不定になります:)
marcoslhc

10
これにより、再スケーリングされた変数が元の分布を維持することが保証されますか?
ハイゼンベルク

2
これは線形スケールの素晴らしい実装です。これは簡単に対数スケールに変換できますか?
tomexx

非常に明確な説明。min負でmax正の場合に機能しますか、それとも両方とも正でなければなりませんか?
Andrew

48

コピーと貼り付けを簡単にするJavaScriptを以下に示します(これはイライラの答えです):

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

このように適用すると、10〜50の範囲が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

編集:

私はずっと前にこれに答えたことを知っていますが、ここで私が現在使用しているよりクリーンな関数があります:

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

そのように適用されます:

[-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"]; この場合、あなたの方法では、数値が小さすぎます。スケールは(0,100)または(-100,100)であり、出力間のギャップは0.5(または任意の数)である必要があります。

私のarr []のシナリオも検討してください。

1
これは端的なケースですが、配列に含まれる値が1つだけの場合、または同じ値の複数のコピーのみが含まれている場合は、これが発生します。したがって、[1] .scaleBetween(1、100)と[1,1,1] .scaleBetween(1,100)はどちらも出力をNaNで満たします。
フロント

1
@MalabarFront、良い観察。私は、その場合には、結果がどうあるべきかどうかは未定義だと仮定し[1, 1, 1][100, 100, 100]でも、または[50.5, 50.5, 50.5]。あなたはケースに入れることができます:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
チャールズ・クレイトン

1
@CharlesClayton素晴らしい、ありがとう。それはごちそうです!
マラバルフロント

27

便宜上、ここにJava形式のIrritateのアルゴリズムがあります。エラーチェック、例外処理を追加し、必要に応じて調整します。

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

テスター:

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

ここに私がそれを理解する方法があります:


何パーセントがx範囲内にあるか

から0までの範囲があるとします100。その範囲からの任意の数が与えられた場合、それはその範囲のどの「パーセント」にありますか?これは、非常にシンプルであるべき0だろう0%50だろう50%100なります100%

さて、あなたの範囲であった場合どのよう20100?上記と同じロジック(100で除算)を適用することはできません。

20 / 100

私たちに与えることはありません0(今ある20はずです0%)。これは簡単に修正でき、0の場合の分子を作成するだけです20。これを減算することにより、次のことができます。

(20 - 20) / 100

ただし、次の100理由により、これは機能しません。

(100 - 20) / 100

与えない100%。この場合も、分母から差し引くことでこれを修正できます。

(100 - 20) / (100 - 20)

xが範囲内にあるものを見つけるためのより一般化された方程式は次のとおりです。

(x - MIN) / (MAX - MIN)

範囲を別の範囲にスケーリング

数値が範囲内に何パーセントあるかがわかったので、それを適用して数値を別の範囲にマッピングできます。例を見てみましょう。

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

古い範囲の数値がある場合、その数値は新しい範囲に何があるでしょうか?数がであるとしましょう400。最初に、何パーセント400が古い範囲内にあるかを調べます。上記の方程式を適用できます。

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

だから、400中にある25%古い範囲の。25%新しい範囲の数を把握する必要があるだけです。何50%[0, 20]あるかを考えてください。それは10正しいでしょうか?どのようにしてその答えに到達しましたか?まあ、私たちはただ行うことができます:

20 * 0.5 = 10

しかし、どう[10, 20]ですか?今までにすべてをシフトする必要があり10ます。例えば:

((20 - 10) * 0.5) + 10

より一般化された式は次のとおりです。

((MAX - MIN) * PERCENT) + MIN

何の元の例に25%[10, 20]です:

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

したがって、400範囲内は範囲内[200, 1000]にマッピングさ12.5れます[10, 20]


TLDR

x古い範囲から新しい範囲にマッピングするには:

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

1
それがまさに私がそれを解決した方法です。最もトリッキーな部分は、数値が特定の範囲にある比率を見つけることです。パーセンテージと同じように、常に[0、1]の範囲内である必要があります。たとえば、0.5は50%です。次に、必要な範囲に収まるようにこの数値を拡張/ストレッチしてシフトするだけです。
SMUsamaShah 2018年

手順を非常に単純な方法で説明していただきありがとうございます-上記のcopypastaは機能しますが、手順を知っていることは非常にすばらしいことです。
RozzA

11

私はこの解決策に出くわしましたが、これは本当に私のニーズに合いません。そこで、d3のソースコードを少し掘り下げました。個人的には、d3.scaleのように実行することをお勧めします。

したがって、ここでドメインを範囲にスケーリングします。利点は、標識を目標範囲に反転できることです。コンピューター画面のy軸が上から下に移動するので、大きな値の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));
    }
}

そして、これはあなたが私の意味を見ることができるテストです

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

「利点は標識を目標範囲に反転できることです。」わかりません。説明できますか?d3-バージョンと上記のバージョン(@irritate)からの戻り値の違いが見つかりません。
nimo23

例1と2を比較して、ターゲット範囲を切り替えます
KIC

2

Irritateの答えを取り入れてリファクタリングし、定数を最小にすることで後続の計算の計算ステップを最小限に抑えました。その動機は、スケーラーを1つのデータセットでトレーニングし、新しいデータで実行できるようにすることです(MLアルゴの場合)。実際には、SciKitのPython用の前処理MinMaxScalerの使用方法とよく似ています。

したがって、x' = (b-a)(x-min)/(max-min) + a(ここでb!= a)x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + aは、次の形式で2つの定数に減らすことができますx' = x*Part1 + Part2

これは2つのコンストラクターを持つC#実装です。1つはトレーニング用で、もう1つはトレーニング済みインスタンスを再ロードします(たとえば、永続性をサポートするため)。

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

Charles Claytonの回答に基づいて、JSDoc、ES6の微調整をいくつか加え、コメントからの提案を元の回答に組み込みました。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.