วิธีลดช่วงของตัวเลขด้วยค่า min และ max ที่รู้จัก


230

ดังนั้นฉันจึงพยายามหาวิธีหาช่วงของตัวเลขและลดค่าลงเพื่อให้พอดีกับช่วง เหตุผลที่ต้องการทำเช่นนี้คือฉันพยายามวาดจุดไข่ปลาใน java swing jpanel ฉันต้องการความสูงและความกว้างของวงรีแต่ละวงให้อยู่ในช่วงที่บอกว่า 1-30 ฉันมีวิธีที่ค้นหาค่าต่ำสุดและค่าสูงสุดจากชุดข้อมูลของฉัน แต่ฉันจะไม่มีค่าต่ำสุดและสูงสุดจนกระทั่งถึงรันไทม์ มีวิธีง่าย ๆ ในการทำเช่นนี้?

คำตอบ:


507

สมมติว่าคุณต้องการขนาดช่วงไป[min,max] [a,b]คุณกำลังมองหาฟังก์ชั่น (ต่อเนื่อง) ที่ตอบสนอง

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

ในกรณีของคุณaจะเป็นที่ 1 และbจะเป็นวันที่ 30 แต่ขอเริ่มต้นด้วยสิ่งที่ง่ายและพยายามที่จะ map ลงในช่วง[min,max][0,1]

การใส่minฟังก์ชั่นและออกจาก 0 สามารถทำได้ด้วย

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

นั่นคือสิ่งที่เราต้องการ แต่การใส่เข้าไปmaxจะทำให้เราmax - minเมื่อเราต้องการ 1. ดังนั้นเราจะต้องขยายมัน:

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

ซึ่งเป็นสิ่งที่เราต้องการ ดังนั้นเราต้องทำการแปลและปรับขนาด ตอนนี้หากเราต้องการได้รับคุณค่าโดยพลการaและbเราต้องการบางสิ่งที่ซับซ้อนกว่านี้เล็กน้อย:

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

คุณสามารถตรวจสอบว่าการวางในminสำหรับxตอนนี้จะช่วยให้aและวางในให้maxb

คุณอาจสังเกตเห็นว่า(b-a)/(max-min)เป็นปัจจัยที่กำหนดอัตราส่วนระหว่างขนาดของช่วงใหม่และขนาดของช่วงเดิม ดังนั้นจริงๆเรามีการแปลครั้งแรกxโดยปรับให้มันเป็นปัจจัยที่ถูกต้องแล้วแปลว่ามันกลับไปที่ค่าต่ำสุดใหม่-mina

หวังว่านี่จะช่วยได้


ฉันขอขอบคุณสำหรับความช่วยเหลือของคุณ ฉันคิดหาวิธีแก้ปัญหาที่ทำให้งานดูน่าพอใจ อย่างไรก็ตามฉันจะใช้ตรรกะของคุณเพื่อให้แบบจำลองที่แม่นยำยิ่งขึ้น ขอขอบคุณอีกครั้ง :)
user650271

4
เพียงแค่เตือน: รูปแบบจะมีความแม่นยำมากขึ้นด้วยmax != minมิฉะนั้นผลการทำงาน indetermined :)
marcoslhc

10
สิ่งนี้ทำให้มั่นใจได้ว่าตัวแปรที่ได้รับการช่วยเหลือของฉันยังคงมีการแจกจ่ายดั้งเดิม
ไฮเซนเบิร์ก

2
นี่เป็นการใช้งานที่ดีของสเกลเชิงเส้น สิ่งนี้สามารถเปลี่ยนเป็นมาตราส่วนลอการิทึมได้ง่ายหรือไม่
tomexx

คำอธิบายที่ชัดเจนมาก มันจะทำงานถ้าminเป็นลบและmaxเป็นบวกหรือว่าพวกเขาทั้งสองจะต้องเป็นบวก?
แอนดรูว์

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", "- 5,000", "- 82.0000048", "0.02" "0.005", "- 3.0008", "5", "8", "600", "- 1000" "- 5000"]; สำหรับกรณีนี้วิธีการของคุณตัวเลขมีขนาดเล็กเกินไป มีวิธีใดดังนั้นขนาดควรเป็น (0,100) หรือ (-100,100) และช่องว่างระหว่างเอาต์พุตควรเป็น 0.5 (หรือตัวเลขใด ๆ )

โปรดพิจารณาสถานการณ์ของฉันสำหรับ arr [] ด้วย

1
มันเป็นตัวพิมพ์เล็กนิดหน่อย แต่มันจะตายถ้าอาเรย์นั้นมีค่าเดียวหรือมีหลายสำเนาที่มีค่าเท่ากัน ดังนั้น [1] .scaleBetween (1, 100) และ [1,1,1] .scaleBetween (1,100) ทั้งคู่เติมเอาต์พุตด้วย NaN
ด้านหน้าของ Malabar

1
@ Malabar ด้านหน้าสังเกตดี ฉันคิดว่ามันไม่ได้กำหนดว่าในกรณีที่ว่าผลที่ควรจะเป็น[1, 1, 1], หรือแม้กระทั่ง[100, 100, 100] [50.5, 50.5, 50.5]คุณสามารถใส่ในกรณี:if (max-min == 0) return this.map(num => (scaledMin+scaledMax)/2);
Charles Clayton

1
@CharlesClayton ยอดเยี่ยมขอบคุณ นั่นทำงานได้ดี!
ด้านหน้าของ Malabar

27

เพื่อความสะดวกนี่คืออัลกอริทึมของระคายเคืองในรูปแบบ Java เพิ่มการตรวจสอบข้อผิดพลาดการจัดการข้อยกเว้นและปรับแต่งตามความจำเป็น

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

Tester:

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%และจะเป็น100100%

ตอนนี้สิ่งที่ถ้าช่วงของคุณเป็น20ไป100? เราไม่สามารถใช้ตรรกะเดียวกันกับด้านบน (หารด้วย 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

ขอบคุณสำหรับการอธิบายขั้นตอนในลักษณะที่เรียบง่ายมาก - 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-version ของคุณและ version จากด้านบน (@irritate)
nimo23

เปรียบเทียบตัวอย่างที่ 1 และ 2 ช่วงเป้าหมายของคุณเปลี่ยนไป
KIC

2

ฉันได้รับคำตอบที่ทำให้ระคายเคืองและปรับโครงสร้างใหม่เพื่อลดขั้นตอนการคำนวณสำหรับการคำนวณที่ตามมาให้น้อยที่สุดโดยการคำนวณให้เป็นค่าคงที่ที่น้อยที่สุด แรงจูงใจคือการอนุญาตให้ scaler ได้รับการฝึกฝนในชุดข้อมูลหนึ่งชุดจากนั้นเรียกใช้ข้อมูลใหม่ (สำหรับ ML algo) ผลก็เหมือน MinMaxScaler ของ SciKit สำหรับการใช้งาน Python

ดังนั้นx' = (b-a)(x-min)/(max-min) + a(ที่ข! = a) กลายเป็นที่สามารถลดลงได้คงที่สองในรูปแบบx' = x(b-a)/(max-min) + min(-b+a)/(max-min) + ax' = x*Part1 + Part2

ต่อไปนี้เป็นการนำ C # มาใช้กับคอนสตรัคเตอร์สองตัว: หนึ่งอันสำหรับฝึกอบรมและอีกอันหนึ่งเพื่อโหลดอินสแตนซ์ที่ผ่านการฝึกอบรม (เช่นเพื่อสนับสนุนการคงอยู่)

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.