Cara menurunkan rentang angka dengan nilai min dan maks yang diketahui


230

Jadi saya mencoba mencari cara untuk mengambil rentang angka dan menurunkan nilainya agar sesuai dengan rentang. Alasan ingin melakukan ini adalah karena saya mencoba menggambar elips di java swing jpanel. Saya ingin tinggi dan lebar setiap elips berada dalam kisaran katakanlah 1-30. Saya memiliki metode yang menemukan nilai minimum dan maksimum dari kumpulan data saya, tetapi saya tidak akan memiliki min dan maks hingga runtime. Apakah ada cara mudah untuk melakukan ini?

Jawaban:


507

Katakanlah Anda ingin skala rentang [min,max]untuk [a,b]. Anda sedang mencari fungsi (berkelanjutan) yang memuaskan

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

Dalam kasus Anda, aakan menjadi 1 dan b30, tetapi mari kita mulai dengan sesuatu yang lebih sederhana dan mencoba memetakan [min,max]ke dalam rentang [0,1].

Dimasukkan minke dalam fungsi dan keluar 0 bisa dicapai dengan

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

Jadi itulah yang kita inginkan. Tetapi menempatkan maxakan memberi kita max - minketika kita benar-benar ingin 1. Jadi kita harus skala itu:

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

yang kami inginkan. Jadi kita perlu melakukan terjemahan dan penskalaan. Sekarang jika sebaliknya kita ingin mendapatkan nilai arbitrer dari adan b, kita perlu sesuatu yang sedikit lebih rumit:

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

Anda dapat memverifikasi bahwa menempatkan minuntuk xsekarang memberi a, dan memasukkan maxmemberi b.

Anda mungkin juga memperhatikan bahwa itu (b-a)/(max-min)adalah faktor penskalaan antara ukuran rentang baru dan ukuran rentang asli. Jadi sebenarnya kita pertama menerjemahkan xdengan -min, penskalaan ke faktor yang benar, dan kemudian menerjemahkannya kembali ke nilai minimum baru a.

Semoga ini membantu.


Saya menghargai bantuan Anda. Saya menemukan solusi yang membuat pekerjaan terlihat menyenangkan secara estetika. Namun saya akan menerapkan logika Anda untuk memberikan model yang lebih akurat. Terima kasih lagi :)
user650271

4
Sekadar pengingat: Model akan lebih akurat dengan max != minhasil fungsi yang tidak ditentukan :)
marcoslhc

10
apakah ini memastikan bahwa variabel yang saya hitung ulang mempertahankan distribusi asli?
Heisenberg

2
Ini adalah implementasi skala linear yang bagus. Bisakah ini dengan mudah ditransformasikan ke skala logarighmic?
tomexx

Penjelasannya sangat jelas. Apakah itu berfungsi jika minnegatif dan maxpositif, atau apakah keduanya harus positif?
Andrew

48

Berikut ini beberapa JavaScript untuk kemudahan salin-rekat (ini jawaban menjengkelkan):

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

Diterapkan seperti itu, skala kisaran 10-50 ke kisaran antara 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

Edit:

Saya tahu saya sudah menjawab ini sejak lama, tapi inilah fungsi pembersih yang saya gunakan sekarang:

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

Diterapkan seperti:

[-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"]; untuk kasus ini, dengan metode Anda, jumlahnya menjadi terlalu kecil. Apakah ada cara, sehingga skala harus (0,100) atau (-100,100) dan kesenjangan antara output harus 0,5 (atau angka apa pun).

Silakan pertimbangkan skenario saya untuk arr [] juga.

1
Ini sedikit kasus tepi, tetapi ini mati jika array hanya berisi satu nilai atau hanya beberapa salinan dengan nilai yang sama. Jadi [1] .scaleBetween (1, 100) dan [1,1,1] .scaleBetween (1.100) keduanya mengisi output dengan NaN.
Malabar Front

1
@MalabarFront, pengamatan yang baik. Saya kira itu tidak terdefinisi apakah dalam kasus itu hasilnya harus [1, 1, 1], [100, 100, 100]atau bahkan [50.5, 50.5, 50.5]. Anda dapat memasukkannya ke dalam case:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton

1
@CharlesClayton Fantastis, terima kasih. Itu berhasil!
Malabar Front

27

Untuk kenyamanan, berikut adalah algoritma Irritate dalam bentuk Java. Tambahkan pengecekan kesalahan, penanganan pengecualian dan penyesuaian yang diperlukan.

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

Penguji:

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

Begini cara saya memahaminya:


Persentase apa yang xada dalam kisaran

Anggaplah Anda memiliki rentang dari 0hingga 100. Dengan nomor acak dari kisaran itu, "persen" berapa dari kisaran itu? Ini harus cukup sederhana, 0akan 0%, 50akan 50%dan 100akan terjadi 100%.

Sekarang, bagaimana jika jangkauan Anda adalah 20untuk 100? Kami tidak dapat menerapkan logika yang sama seperti di atas (bagi 100) karena:

20 / 100

tidak memberi kita 0( 20seharusnya 0%sekarang). Ini harus mudah diperbaiki, kita hanya perlu membuat pembilang 0untuk kasus 20. Kita bisa melakukannya dengan mengurangi:

(20 - 20) / 100

Namun, ini tidak berfungsi 100lagi karena:

(100 - 20) / 100

tidak memberi kita 100%. Sekali lagi, kita dapat memperbaikinya dengan mengurangi dari penyebut juga:

(100 - 20) / (100 - 20)

Persamaan yang lebih digeneralisasi untuk mengetahui% xterletak pada rentang adalah:

(x - MIN) / (MAX - MIN)

Rentang skala ke rentang lain

Sekarang kita tahu berapa persen angka yang berada dalam suatu rentang, kita dapat menerapkannya untuk memetakan angka ke rentang lain. Mari kita lihat sebuah contoh.

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

Jika kami memiliki nomor dalam rentang lama, berapakah angka tersebut dalam rentang baru? Katakanlah angkanya 400. Pertama, cari tahu berapa persen 400dalam kisaran lama. Kita dapat menerapkan persamaan kita di atas.

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

Jadi, 400terletak pada 25%kisaran lama. Kami hanya perlu mencari tahu nomor berapa 25%dari kisaran baru. Pikirkan tentang apa 50%dari [0, 20]yang. Apakah itu 10benar? Bagaimana Anda sampai pada jawaban itu? Baiklah, kita bisa melakukan:

20 * 0.5 = 10

Tapi, bagaimana dengan dari [10, 20]? Kita perlu mengubah segalanya 10sekarang. misalnya:

((20 - 10) * 0.5) + 10

formula yang lebih umum adalah:

((MAX - MIN) * PERCENT) + MIN

Untuk contoh asli apa 25%dari [10, 20]adalah:

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

Jadi, 400dalam kisaran [200, 1000]akan memetakan ke 12.5dalam kisaran[10, 20]


TLDR

Untuk memetakan xdari rentang lama ke rentang baru:

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

1
Begitulah cara saya mengatasinya. Bagian tersulit adalah untuk mengetahui rasio di mana angka terletak pada kisaran yang diberikan. Itu harus selalu dalam kisaran [0, 1] seperti persentase, misalnya 0,5 untuk 50%. Selanjutnya Anda hanya perlu memperluas / meregangkan dan menggeser nomor ini agar sesuai dengan rentang yang Anda butuhkan.
SMUsamaShah

Terima kasih telah menjelaskan langkah-langkahnya dengan cara yang sangat sederhana - copypasta atas jawaban bekerja, tetapi mengetahui langkah-langkahnya bagus.
RozzA

11

Saya menemukan solusi ini tetapi ini tidak benar-benar sesuai dengan kebutuhan saya. Jadi saya menggali sedikit dalam kode sumber d3. Saya pribadi akan merekomendasikan untuk melakukannya seperti halnya d3.scale.

Jadi di sini Anda mengukur domain ke kisaran. Keuntungannya adalah Anda dapat membalikkan tanda ke rentang target Anda. Ini berguna karena sumbu y pada layar komputer naik ke atas sehingga nilai-nilai besar memiliki y kecil.

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

Dan di sini adalah tes di mana Anda dapat melihat apa yang saya maksud

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

"Keuntungannya adalah kamu bisa membalikkan tanda ke kisaran targetmu." Saya tidak mengerti ini. Bisakah Anda jelaskan? Saya tidak dapat menemukan perbedaan nilai yang dikembalikan dari versi d3 Anda dan versi dari atas (@irritate).
nimo23

Bandingkan contoh 1 dan 2 rentang target Anda diaktifkan
KIC

2

Saya telah mengambil jawaban Irritate dan mengembalikannya untuk meminimalkan langkah-langkah komputasi untuk perhitungan selanjutnya dengan memasukkannya ke dalam konstanta yang paling sedikit. Motivasinya adalah untuk memungkinkan scaler dilatih pada satu set data, dan kemudian dijalankan pada data baru (untuk algo ML). Efeknya, ini sangat mirip dengan MinMaxScaler preprocessing SciKit untuk Python dalam penggunaan.

Jadi, x' = (b-a)(x-min)/(max-min) + a(di mana b! = A) menjadi x' = x(b-a)/(max-min) + min(-b+a)/(max-min) + ayang dapat direduksi menjadi dua konstanta dalam bentuk x' = x*Part1 + Part2.

Berikut ini adalah implementasi C # dengan dua konstruktor: satu untuk melatih, dan satu untuk memuat ulang instance yang terlatih (misalnya, untuk mendukung kegigihan).

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

Berdasarkan tanggapan Charles Clayton, saya memasukkan beberapa JSDoc, tweak ES6, dan memasukkan saran dari komentar dalam tanggapan asli.

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.