Cara membuat persentase membulat menambahkan hingga 100%


193

Pertimbangkan empat persentase di bawah ini, direpresentasikan sebagai floatangka:

    13.626332%
    47.989636%
     9.596008%
    28.788024%
   -----------
   100.000000%

Saya perlu mewakili persentase ini sebagai bilangan bulat. Jika saya hanya menggunakan Math.round(), saya berakhir dengan total 101%.

14 + 48 + 10 + 29 = 101

Jika saya gunakan parseInt(), saya berakhir dengan total 97%.

13 + 47 + 9 + 28 = 97

Apa algoritma yang baik untuk mewakili sejumlah persentase sebagai bilangan bulat dengan tetap mempertahankan total 100%?


Sunting : Setelah membaca beberapa komentar dan jawaban, jelas ada banyak cara untuk menyelesaikannya.

Dalam pikiran saya, untuk tetap setia pada angka-angka, hasil "benar" adalah hasil yang meminimalkan kesalahan keseluruhan, yang ditentukan oleh seberapa banyak pembulatan kesalahan akan diperkenalkan relatif terhadap nilai aktual:

        value  rounded     error               decision
   ----------------------------------------------------
    13.626332       14      2.7%          round up (14)
    47.989636       48      0.0%          round up (48)
     9.596008       10      4.0%    don't round up  (9)
    28.788024       29      2.7%          round up (29)

Dalam kasus seri (3.33, 3.33, 3.33) keputusan sewenang-wenang dapat dibuat (mis. 3, 4, 3).


21
Misalkan Anda memiliki 3,33, 3.33 dan 3.33. Yang mana yang akan Anda buat 4?
RobG

3
Persis. Pertanyaannya mengandung kontradiksi dalam istilah.
Marquis of Lorne

4
Ini adalah skenario yang sangat umum dalam pelaporan - cara menampilkan "total" nilai desimal yang tidak selalu cocok dengan jumlah nilai yang ditampilkan.
D Stanley

1
Apa yang "benar" hasil dalam kasus contoh Anda? Itu bisa menyelesaikan ketidaksepakatan tentang apa solusi "terbaik" itu.
D Stanley

Jawaban:


35

Karena tidak ada jawaban di sini yang tampaknya menyelesaikannya dengan benar, inilah versi semi-membingungkan saya menggunakan underscorejs :

function foo(l, target) {
    var off = target - _.reduce(l, function(acc, x) { return acc + Math.round(x) }, 0);
    return _.chain(l).
            sortBy(function(x) { return Math.round(x) - x }).
            map(function(x, i) { return Math.round(x) + (off > i) - (i >= (l.length + off)) }).
            value();
}

foo([13.626332, 47.989636, 9.596008, 28.788024], 100) // => [48, 29, 14, 9]
foo([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100) // => [17, 17, 17, 17, 16, 16]
foo([33.333, 33.333, 33.333], 100) // => [34, 33, 33]
foo([33.3, 33.3, 33.3, 0.1], 100) // => [34, 33, 33, 0]

6
Koreksi saya jika saya salah, tetapi bukankah ini implementasi dari Algoritma yang diusulkan oleh jawaban saya? (Tidak menghapus pada underscorejs)
vvohra87

@VarunVohra maaf saya tidak melihat ini sampai sekarang, ya sepertinya algoritma Anda sama :) tidak yakin mengapa posting saya adalah jawaban yang diterima, kode yang dikaburkan hanya untuk lolz ...
yonilevy

@yonilevy Menghapus komentar saya; Saya hanya tidak menyadari itu seharusnya mengembalikan daftar yang diurutkan. Saya minta maaf!
Zack Burt

2
Ada masalah dengan fungsi ini ketika elemen terakhir adalah 0 dan yang sebelumnya menambah 100. Misalnya [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. Yang terakhir secara logis mengembalikan -1. Saya memikirkan solusi berikut ini dengan sangat cepat tetapi mungkin ada sesuatu yang lebih baik: jsfiddle.net/0o75bw43/1
Cruclax

1
@ Cruclax itu menunjukkan semua 1 ketika semua entri nol dalam array input
tony.0919

159

Ada banyak cara untuk melakukan ini, asalkan Anda tidak khawatir tentang ketergantungan pada data desimal asli.

Metode pertama dan mungkin paling populer adalah Metode Sisa Terbesar

Yang pada dasarnya adalah:

  1. Membulatkan semuanya
  2. Mendapatkan perbedaan dalam jumlah dan 100
  3. Mendistribusikan selisih dengan menambahkan 1 pada item dalam urutan menurun bagian desimal mereka

Dalam kasus Anda, akan seperti ini:

13.626332%
47.989636%
 9.596008%
28.788024%

Jika Anda mengambil bagian integer, Anda dapatkan

13
47
 9
28

yang menambahkan hingga 97, dan Anda ingin menambahkan tiga lagi. Sekarang, Anda melihat bagian desimal, yaitu

.626332%
.989636%
.596008%
.788024%

dan ambil yang terbesar sampai total mencapai 100. Jadi Anda akan mendapatkan:

14
48
 9
29

Atau, Anda bisa memilih untuk menunjukkan satu tempat desimal daripada nilai integer. Jadi jumlahnya akan menjadi 48,3 dan 23,9 dll. Ini akan menjatuhkan varians dari 100 oleh banyak.


5
"Kolom Fitur" ini di situs web American Mathematical Society - Bagian II: Sistem Bagian - menjelaskan beberapa metode 'pembagian' yang serupa.
Kenny Evitt

1
Ini hampir terlihat seperti salin dan tempel jawaban saya di sini stackoverflow.com/questions/5227215/… .
sawa

Perhatikan bahwa, berbeda dengan komentar Anda pada jawaban @Dstanley, dalam jawaban Anda 9,596008% dibulatkan menjadi 9% yang merupakan perbedaan lebih dari 0,5%. Namun, masih merupakan jawaban yang bagus.
Rolazaro Azeveires

33

Mungkin "terbaik" cara untuk melakukan hal ini (dikutip sejak "terbaik" adalah istilah subjektif) adalah untuk menjaga berjalan (non-integral) penghitungan dari mana Anda berada, dan bulat yang nilai.

Kemudian gunakan itu bersama dengan sejarah untuk mencari tahu nilai apa yang harus digunakan. Misalnya, menggunakan nilai yang Anda berikan:

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
13.626332   13.626332            14             0    14 ( 14 -  0)
47.989636   61.615968            62            14    48 ( 62 - 14)
 9.596008   71.211976            71            62     9 ( 71 - 62)
28.788024  100.000000           100            71    29 (100 - 71)
                                                    ---
                                                    100

Pada setiap tahap, Anda tidak membulatkan angka itu sendiri. Alih-alih, Anda membulatkan nilai akumulasi dan mencari bilangan bulat terbaik yang mencapai nilai itu dari baseline sebelumnya - bahwa baseline adalah nilai kumulatif (bulat) dari baris sebelumnya.

Ini berfungsi karena Anda tidak kehilangan informasi di setiap tahap, tetapi menggunakan informasi itu dengan lebih cerdas. Nilai bulat 'benar' ada di kolom terakhir dan Anda dapat melihat bahwa jumlahnya berjumlah 100.

Anda dapat melihat perbedaan antara ini dan membulatkan setiap nilai secara buta, pada nilai ketiga di atas. Sementara 9.596008biasanya akan dibulatkan ke atas 10, akumulasi yang 71.211976benar dibulatkan ke 71- ini berarti bahwa hanya 9diperlukan untuk menambah baseline sebelumnya 62.


Ini juga berfungsi untuk urutan "bermasalah" seperti tiga nilai kasar , di mana salah satunya harus dibulatkan:1/3

Value      CumulValue  CumulRounded  PrevBaseline  Need
---------  ----------  ------------  ------------  ----
                                  0
33.333333   33.333333            33             0    33 ( 33 -  0)
33.333333   66.666666            67            33    34 ( 67 - 33)
33.333333   99.999999           100            67    33 (100 - 67)
                                                    ---
                                                    100

1
Pendekatan kedua memperbaiki kedua masalah tersebut. Yang pertama memberi 26, 25, 26, 23, yang kedua 1, 0, 1, 0, 1, 0, ....
paxdiablo

Pendekatan ini juga bekerja dengan baik untuk membulatkan angka kecil karena mencegah angka negatif dari hasilnya
Jonty5817

19

Tujuan pembulatan adalah untuk menghasilkan jumlah kesalahan paling sedikit. Saat Anda membulatkan satu nilai, proses itu sederhana dan mudah dan kebanyakan orang memahaminya dengan mudah. Saat Anda membulatkan banyak angka sekaligus, prosesnya menjadi lebih rumit - Anda harus menentukan bagaimana kesalahan akan digabungkan, yaitu apa yang harus diminimalkan.

The jawaban baik sebagai oleh Varun Vohra meminimalkan jumlah kesalahan mutlak, dan itu sangat sederhana untuk menerapkan. Namun ada kasus tepi yang tidak ditangani - apa yang harus menjadi hasil pembulatan 24.25, 23.25, 27.25, 25.25? Salah satu dari itu perlu dibulatkan ke atas bukannya ke bawah. Anda mungkin akan secara sewenang-wenang memilih yang pertama atau terakhir dalam daftar.

Mungkin lebih baik menggunakan kesalahan relatif daripada kesalahan absolut . Pembulatan 23,25 hingga 24 mengubahnya dengan 3,2% sementara pembulatan 27,25 hingga 28 hanya mengubahnya dengan 2,8%. Sekarang ada pemenang yang jelas.

Dimungkinkan untuk mengubah ini lebih jauh. Salah satu teknik yang umum adalah menguadratkan masing-masing kesalahan, sehingga kesalahan besar dihitung secara tidak proporsional lebih dari yang kecil. Saya juga menggunakan pembagi non-linear untuk mendapatkan kesalahan relatif - tampaknya tidak benar bahwa kesalahan pada 1% adalah 99 kali lebih penting daripada kesalahan pada 99%. Dalam kode di bawah ini saya telah menggunakan root kuadrat.

Algoritma lengkap adalah sebagai berikut:

  1. Jumlahkan persentase setelah membulatkan semuanya, dan kurangi dari 100. Ini memberi tahu Anda berapa banyak dari persentase tersebut yang harus dibulatkan sebagai gantinya.
  2. Hasilkan dua skor kesalahan untuk setiap persentase, satu ketika dibulatkan dan satu ketika dibulatkan. Ambil perbedaan di antara keduanya.
  3. Urutkan perbedaan kesalahan yang dihasilkan di atas.
  4. Untuk jumlah persentase yang perlu dibulatkan, ambil item dari daftar yang diurutkan dan tambahkan persentase dibulatkan ke bawah sebesar 1.

Anda mungkin masih memiliki lebih dari satu kombinasi dengan jumlah kesalahan yang sama, misalnya 33.3333333, 33.3333333, 33.3333333. Ini tidak bisa dihindari, dan hasilnya akan sepenuhnya arbitrer. Kode yang saya berikan di bawah ini lebih suka mengumpulkan nilai di sebelah kiri.

Menyatukan semuanya dalam Python terlihat seperti ini.

def error_gen(actual, rounded):
    divisor = sqrt(1.0 if actual < 1.0 else actual)
    return abs(rounded - actual) ** 2 / divisor

def round_to_100(percents):
    if not isclose(sum(percents), 100):
        raise ValueError
    n = len(percents)
    rounded = [int(x) for x in percents]
    up_count = 100 - sum(rounded)
    errors = [(error_gen(percents[i], rounded[i] + 1) - error_gen(percents[i], rounded[i]), i) for i in range(n)]
    rank = sorted(errors)
    for i in range(up_count):
        rounded[rank[i][1]] += 1
    return rounded

>>> round_to_100([13.626332, 47.989636, 9.596008, 28.788024])
[14, 48, 9, 29]
>>> round_to_100([33.3333333, 33.3333333, 33.3333333])
[34, 33, 33]
>>> round_to_100([24.25, 23.25, 27.25, 25.25])
[24, 23, 28, 25]
>>> round_to_100([1.25, 2.25, 3.25, 4.25, 89.0])
[1, 2, 3, 4, 90]

Seperti yang dapat Anda lihat dengan contoh terakhir itu, algoritma ini masih mampu memberikan hasil yang tidak intuitif. Meskipun 89.0 tidak perlu dibulatkan apa pun, salah satu nilai dalam daftar itu perlu dibulatkan; kesalahan relatif terendah dihasilkan dari mengumpulkan nilai besar itu daripada alternatif yang jauh lebih kecil.

Jawaban ini awalnya menganjurkan melalui setiap kombinasi yang mungkin dari putaran ke atas / ke bawah, tetapi seperti yang ditunjukkan dalam komentar metode yang lebih sederhana bekerja lebih baik. Algoritma dan kode mencerminkan penyederhanaan itu.


1
Saya tidak berpikir Anda perlu mempertimbangkan semua kombinasi: proses dalam rangka penurunan drop dalam kesalahan tertimbang dari putaran ke nol ke bulat hingga tak terbatas (cukup banyak hanya memasukkan penimbangan ke dalam jawaban Verun Vohras dan yonilevy ("identik")).
greybeard

@ Chrisbeard kau benar, aku terlalu banyak memikirkan ini. Saya tidak bisa hanya mengurutkan kesalahan karena ada dua kesalahan untuk setiap nilai, tetapi mengambil perbedaan menyelesaikan masalah itu. Saya sudah memperbarui jawabannya.
Mark Ransom

Saya lebih suka selalu memiliki 0% ketika angka aktual adalah 0%. Jadi menambahkan if actual == 0: return 0untuk error_genkarya-karya besar.
Nikolay Baluk

1
apa isclosemetode di awal round_to_100?
toto_tico


7

JANGAN menjumlahkan angka bulat. Anda akan mendapatkan hasil yang tidak akurat. Total dapat dimatikan secara signifikan tergantung pada jumlah istilah dan distribusi bagian fraksional.

Menampilkan angka-angka bulat tetapi menjumlahkan nilai aktual. Tergantung pada bagaimana Anda menyajikan angka-angka, cara aktual untuk melakukan itu akan bervariasi. Dengan begitu kamu bisa

 14
 48
 10
 29
 __
100

Apa pun cara Anda pergi Anda akan memiliki perbedaan. Tidak ada cara dalam contoh Anda untuk menunjukkan angka yang menambahkan hingga 100 tanpa "membulatkan" satu nilai dengan cara yang salah (kesalahan paling sedikit akan berubah 9.596 menjadi 9)

EDIT

Anda harus memilih salah satu dari yang berikut:

  1. Akurasi item
  2. Akurasi jumlah (jika Anda menjumlahkan nilai bulat)
  3. Konsistensi antara item bulat dan jumlah bulat)

Sebagian besar waktu ketika berhadapan dengan persentase # 3 adalah pilihan terbaik karena lebih jelas ketika totalnya sama dengan 101% daripada ketika masing-masing item tidak mencapai 100, dan Anda menjaga setiap item akurat. "Pembulatan" 9.596 hingga 9 tidak akurat menurut saya.

Untuk menjelaskan hal ini, saya terkadang menambahkan catatan kaki yang menjelaskan bahwa nilai-nilai individual dibulatkan dan mungkin tidak berjumlah 100% - siapa pun yang memahami pembulatan harus dapat memahami penjelasan itu.


6
Itu tidak terlalu membantu karena nilai yang dicetak tidak akan bertambah hingga 100. Tujuan dari pertanyaan ini adalah untuk mencegah pengguna dari berpikir bahwa nilai-nilai itu tidak benar, yang dalam hal ini, kebanyakan orang akan lakukan ketika mencari dan membandingkan dengan total .
vvohra87

@VarunVohra membaca edit saya, Anda TIDAK BISA menampilkan angka Anda sehingga mereka menambahkan hingga 100 tanpa "membulatkan" satu dengan lebih dari 0,5.
D Stanley

1
@ Stanley sebenarnya, kecuali satu set di mana semua angka malu 0,5, Anda bisa. Periksa jawaban saya - LRM melakukan hal itu.
vvohra87

3
@VarunVohra Dalam contoh asli LRM akan menghasilkan 14, 48, 9, dan 29 yang akan "membulatkan" 9,596 hingga 9. Jika kita mengalokasikan berdasarkan bilangan bulat LRM akan menjadi yang paling akurat, tetapi masih mengubah satu hasil dengan lebih dari setengah unit.
D Stanley

7

Saya menulis pembantu pembulatan versi C, algoritmenya sama dengan jawaban Varun Vohra , semoga membantu.

public static List<decimal> GetPerfectRounding(List<decimal> original,
    decimal forceSum, int decimals)
{
    var rounded = original.Select(x => Math.Round(x, decimals)).ToList();
    Debug.Assert(Math.Round(forceSum, decimals) == forceSum);
    var delta = forceSum - rounded.Sum();
    if (delta == 0) return rounded;
    var deltaUnit = Convert.ToDecimal(Math.Pow(0.1, decimals)) * Math.Sign(delta);

    List<int> applyDeltaSequence; 
    if (delta < 0)
    {
        applyDeltaSequence = original
            .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
            .OrderBy(a => original[a.index] - rounded[a.index])
            .ThenByDescending(a => a.index)
            .Select(a => a.index).ToList();
    }
    else
    {
        applyDeltaSequence = original
            .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
            .OrderByDescending(a => original[a.index] - rounded[a.index])
            .Select(a => a.index).ToList();
    }

    Enumerable.Repeat(applyDeltaSequence, int.MaxValue)
        .SelectMany(x => x)
        .Take(Convert.ToInt32(delta/deltaUnit))
        .ForEach(index => rounded[index] += deltaUnit);

    return rounded;
}

Itu lulus tes Unit berikut:

[TestMethod]
public void TestPerfectRounding()
{
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 2),
        new List<decimal> {3.33m, 3.34m, 3.33m});

    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.33m, 3.34m, 3.33m}, 10, 1),
        new List<decimal> {3.3m, 3.4m, 3.3m});

    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 1),
        new List<decimal> {3.3m, 3.4m, 3.3m});


    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 13.626332m, 47.989636m, 9.596008m, 28.788024m }, 100, 0),
        new List<decimal> {14, 48, 9, 29});
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 16.666m, 16.666m, 16.666m, 16.666m, 16.666m, 16.666m }, 100, 0),
        new List<decimal> { 17, 17, 17, 17, 16, 16 });
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 33.333m, 33.333m, 33.333m }, 100, 0),
        new List<decimal> { 34, 33, 33 });
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 33.3m, 33.3m, 33.3m, 0.1m }, 100, 0),
        new List<decimal> { 34, 33, 33, 0 });
}

Bagus! memberi saya dasar untuk memulai dengan ... Enumerable tidak memiliki ForEach meskipun saya percaya
Jack0fshad0ws

4

Anda bisa mencoba melacak kesalahan Anda karena pembulatan, dan kemudian pembulatan terhadap gandum jika akumulasi kesalahan lebih besar dari bagian fraksional dari angka saat ini.

13.62 -> 14 (+.38)
47.98 -> 48 (+.02 (+.40 total))
 9.59 -> 10 (+.41 (+.81 total))
28.78 -> 28 (round down because .81 > .78)
------------
        100

Tidak yakin apakah ini akan berfungsi secara umum, tetapi tampaknya berfungsi serupa jika urutannya terbalik:

28.78 -> 29 (+.22)
 9.59 ->  9 (-.37; rounded down because .59 > .22)
47.98 -> 48 (-.35)
13.62 -> 14 (+.03)
------------
        100

Saya yakin ada kasus tepi di mana ini mungkin rusak, tetapi pendekatan apa pun akan setidaknya agak sewenang-wenang karena Anda pada dasarnya memodifikasi data input Anda.


2
Akuntan dan bankir telah menggunakan teknik serupa selama ratusan tahun. "Bawa sisanya" dari satu baris ke yang berikutnya. Mulailah dengan 1/2 dari satu sen di "carry." Tambahkan "carry" ke nilai pertama, dan potong. Sekarang jumlah yang Anda hilangkan dengan memotong, masukkan ke dalam "carry." Lakukan ini sepenuhnya, dan angka-angka bulat akan bertambah hingga total yang diinginkan tepat setiap waktu.
Jeff Grigg

Carolyn Kay menyarankan penerapan ini di Access VB 2007: <code> 'Putaran pengembalian uang menggunakan metode "carry the sisanya" ref1 = rsQry! [Pengembalian Uang $$$] * rsQry! [Nilai Properti] / propValTot ref2 = ref1 + ref5 'Tambahkan sisa yang dibawa, nol untuk memulai ref3 = ref2 * 100' Kalikan dengan 100 menjadi angka integer ref4 = ref3 / 100 'Membagi dengan 100 menjadi angka desimal rsTbl! [Pengembalian Dana $$$] = ref4' Masukkan " sisa "angka bulat di tabel ref5 = ref2 - ref4 'Bawa sisanya baru </code>
Jeff Grigg

2

Saya pernah menulis alat unround, untuk menemukan perturbasi minimal untuk satu set angka untuk mencocokkan suatu gol. Itu adalah masalah yang berbeda, tetapi secara teori seseorang dapat menggunakan ide serupa di sini. Dalam hal ini, kami memiliki serangkaian pilihan.

Jadi untuk elemen pertama, kita bisa membulatkannya hingga 14, atau turun ke 13. Biaya (dalam arti pemrograman bilangan bulat biner) untuk melakukannya lebih sedikit untuk putaran ke atas daripada putaran ke bawah, karena putaran ke bawah mengharuskan kita pindahkan nilai itu ke jarak yang lebih besar. Demikian pula, kita dapat membulatkan setiap angka ke atas atau ke bawah, sehingga ada total 16 pilihan yang harus kita pilih.

  13.626332
  47.989636
   9.596008
+ 28.788024
-----------
 100.000000

Saya biasanya memecahkan masalah umum di MATLAB, di sini menggunakan bintprog, alat pemrograman bilangan bulat biner, tetapi hanya ada beberapa pilihan untuk diuji, jadi cukup mudah dengan loop sederhana untuk menguji masing-masing dari 16 alternatif. Sebagai contoh, anggaplah kita harus melengkapi set ini sebagai:

 Original      Rounded   Absolute error
   13.626           13          0.62633
    47.99           48          0.01036
    9.596           10          0.40399
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.25266

Total kesalahan absolut yang dibuat adalah 1.25266. Ini dapat dikurangi sedikit dengan pembulatan alternatif berikut:

 Original      Rounded   Absolute error
   13.626           14          0.37367
    47.99           48          0.01036
    9.596            9          0.59601
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.19202

Bahkan, ini akan menjadi solusi optimal dalam hal kesalahan absolut. Tentu saja, jika ada 20 istilah, ruang pencarian akan berukuran 2 ^ 20 = 1048576. Untuk 30 atau 40 istilah, ruang tersebut akan memiliki ukuran yang signifikan. Dalam hal ini, Anda perlu menggunakan alat yang dapat mencari ruang secara efisien, mungkin menggunakan cabang dan skema terikat.


Hanya untuk referensi di masa mendatang: algoritme "sisa terbesar" harus meminimalkan kesalahan absolut total menurut metrik Anda (Lihat jawaban @ varunvohra). Buktinya sederhana: anggap saja tidak meminimalkan kesalahan. Maka harus ada beberapa himpunan nilai yang dibulatkan ke bawah yang harus dibulatkan ke atas, dan sebaliknya (kedua himpunan memiliki ukuran yang sama). Tetapi setiap nilai yang dibulatkan lebih jauh dari bilangan bulat berikutnya daripada nilai apa pun yang dibulatkan ke atas (dan vv) sehingga jumlah kesalahan baru harus lebih besar. QED. Namun, itu tidak berfungsi untuk semua metrik kesalahan; algoritma lain diperlukan.
rici

2

Saya pikir yang berikut ini akan mencapai apa yang Anda cari

function func( orig, target ) {

    var i = orig.length, j = 0, total = 0, change, newVals = [], next, factor1, factor2, len = orig.length, marginOfErrors = [];

    // map original values to new array
    while( i-- ) {
        total += newVals[i] = Math.round( orig[i] );
    }

    change = total < target ? 1 : -1;

    while( total !== target ) {

        // Iterate through values and select the one that once changed will introduce
        // the least margin of error in terms of itself. e.g. Incrementing 10 by 1
        // would mean an error of 10% in relation to the value itself.
        for( i = 0; i < len; i++ ) {

            next = i === len - 1 ? 0 : i + 1;

            factor2 = errorFactor( orig[next], newVals[next] + change );
            factor1 = errorFactor( orig[i], newVals[i] + change );

            if(  factor1 > factor2 ) {
                j = next; 
            }
        }

        newVals[j] += change;
        total += change;
    }


    for( i = 0; i < len; i++ ) { marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i]; }

    // Math.round() causes some problems as it is difficult to know at the beginning
    // whether numbers should have been rounded up or down to reduce total margin of error. 
    // This section of code increments and decrements values by 1 to find the number
    // combination with least margin of error.
    for( i = 0; i < len; i++ ) {
        for( j = 0; j < len; j++ ) {
            if( j === i ) continue;

            var roundUpFactor = errorFactor( orig[i], newVals[i] + 1)  + errorFactor( orig[j], newVals[j] - 1 );
            var roundDownFactor = errorFactor( orig[i], newVals[i] - 1) + errorFactor( orig[j], newVals[j] + 1 );
            var sumMargin = marginOfErrors[i] + marginOfErrors[j];

            if( roundUpFactor < sumMargin) { 
                newVals[i] = newVals[i] + 1;
                newVals[j] = newVals[j] - 1;
                marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i];
                marginOfErrors[j] = newVals[j] && Math.abs( orig[j] - newVals[j] ) / orig[j];
            }

            if( roundDownFactor < sumMargin ) { 
                newVals[i] = newVals[i] - 1;
                newVals[j] = newVals[j] + 1;
                marginOfErrors[i] = newVals[i] && Math.abs( orig[i] - newVals[i] ) / orig[i];
                marginOfErrors[j] = newVals[j] && Math.abs( orig[j] - newVals[j] ) / orig[j];
            }

        }
    }

    function errorFactor( oldNum, newNum ) {
        return Math.abs( oldNum - newNum ) / oldNum;
    }

    return newVals;
}


func([16.666, 16.666, 16.666, 16.666, 16.666, 16.666], 100); // => [16, 16, 17, 17, 17, 17]
func([33.333, 33.333, 33.333], 100); // => [34, 33, 33]
func([33.3, 33.3, 33.3, 0.1], 100); // => [34, 33, 33, 0] 
func([13.25, 47.25, 11.25, 28.25], 100 ); // => [13, 48, 11, 28]
func( [25.5, 25.5, 25.5, 23.5], 100 ); // => [25, 25, 26, 24]

Satu hal terakhir, saya menjalankan fungsi menggunakan angka-angka yang awalnya diberikan dalam pertanyaan untuk membandingkan dengan output yang diinginkan

func([13.626332, 47.989636, 9.596008, 28.788024], 100); // => [48, 29, 13, 10]

Ini berbeda dengan apa yang diinginkan pertanyaan => [48, 29, 14, 9]. Saya tidak dapat memahami hal ini sampai saya melihat total margin of error

-------------------------------------------------
| original  | question | % diff | mine | % diff |
-------------------------------------------------
| 13.626332 | 14       | 2.74%  | 13   | 4.5%   |
| 47.989636 | 48       | 0.02%  | 48   | 0.02%  |
| 9.596008  | 9        | 6.2%   | 10   | 4.2%   |
| 28.788024 | 29       | 0.7%   | 29   | 0.7%   |
-------------------------------------------------
| Totals    | 100      | 9.66%  | 100  | 9.43%  |
-------------------------------------------------

Pada dasarnya, hasil dari fungsi saya sebenarnya memperkenalkan jumlah kesalahan paling sedikit.

Biola di sini


itulah yang saya pikirkan, dengan perbedaan bahwa kesalahan harus diukur relatif terhadap nilai (pembulatan 9,8 ke 10 adalah kesalahan yang lebih besar daripada pembulatan dari 19,8 ke 20). Ini bisa dengan mudah dilakukan dengan merefleksikannya dalam semacam callback.
poezn

ini salah untuk [33.33, 33.33, 33.33, 0.1], ia mengembalikan [1, 33, 33, 33] daripada yang lebih akurat [34, 33, 33, 0]
yonilevy

@yonilevy Terima kasih untuk itu. Diperbaiki sekarang
Bruno

belum, untuk [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] ia mengembalikan [15, 17, 17, 17, 17, 17] daripada [16, 16, 17, 17, 17, 17] - lihat saya jawaban
yonilevy

2

Saya tidak yakin apa tingkat akurasi yang Anda butuhkan, tetapi apa yang akan saya lakukan hanyalah menambahkan 1 nangka pertama , nmenjadi langit-langit dari jumlah total desimal. Dalam hal ini 3, jadi saya akan menambahkan 1 ke 3 item pertama dan lantai sisanya. Tentu saja ini tidak super akurat, beberapa angka mungkin dibulatkan ke atas atau ke bawah ketika seharusnya tidak tetapi berfungsi dengan baik dan akan selalu menghasilkan 100%.

Jadi [ 13.626332, 47.989636, 9.596008, 28.788024 ]akan [14, 48, 10, 28]karenaMath.ceil(.626332+.989636+.596008+.788024) == 3

function evenRound( arr ) {
  var decimal = -~arr.map(function( a ){ return a % 1 })
    .reduce(function( a,b ){ return a + b }); // Ceil of total sum of decimals
  for ( var i = 0; i < decimal; ++i ) {
    arr[ i ] = ++arr[ i ]; // compensate error by adding 1 the the first n items
  }
  return arr.map(function( a ){ return ~~a }); // floor all other numbers
}

var nums = evenRound( [ 13.626332, 47.989636, 9.596008, 28.788024 ] );
var total = nums.reduce(function( a,b ){ return a + b }); //=> 100

Anda selalu dapat memberi tahu pengguna bahwa jumlahnya bulat dan mungkin tidak super akurat ...


1

Jika Anda membulatkannya, tidak ada cara yang baik untuk mendapatkannya persis sama dalam semua kasus.

Anda dapat mengambil bagian desimal dari persentase N yang Anda miliki (dalam contoh yang Anda berikan adalah 4).

Tambahkan bagian desimal. Dalam contoh Anda, Anda memiliki total bagian pecahan = 3.

Ceil 3 angka dengan pecahan tertinggi dan lantai sisanya.

(Maaf untuk hasil edit)


1
Sementara itu mungkin memberikan angka yang menambah 100, Anda mungkin akhirnya mengubah 3,9 menjadi 3 dan 25,1 menjadi 26.
RobG

tidak. 3,9 akan menjadi 4 dan 25,1 akan menjadi 25. saya mengatakan untuk menambahkan 3 angka dengan fraksi tertinggi bukan nilai tertinggi.
arunlalam

2
jika ada terlalu banyak pecahan yang berakhir dengan 0,9 mengatakan 9 nilai 9,9% dan satu nilai 10,9 ada satu nilai yang akan berakhir sebagai 9%, 8 10% dan satu 11%.
arunlalam

1

Jika Anda benar-benar harus mengikutinya, sudah ada saran yang sangat bagus di sini (sisanya terbesar, kesalahan relatif paling sedikit, dan sebagainya).

Sudah ada satu alasan bagus untuk tidak membulatkan (Anda akan mendapatkan setidaknya satu nomor yang "terlihat lebih baik" tetapi "salah"), dan bagaimana menyelesaikannya (memperingatkan pembaca Anda) dan itulah yang saya lakukan.

Biarkan saya menambahkan pada bagian nomor "salah".

Misalkan Anda memiliki tiga acara / entitas / ... dengan beberapa persentase yang Anda perkirakan sebagai:

DAY 1
who |  real | app
----|-------|------
  A | 33.34 |  34
  B | 33.33 |  33
  C | 33.33 |  33

Nanti nilai berubah sedikit, menjadi

DAY 2
who |  real | app
----|-------|------
  A | 33.35 |  33
  B | 33.36 |  34
  C | 33.29 |  33

Tabel pertama memiliki masalah yang telah disebutkan memiliki nomor "salah": 33,34 lebih dekat ke 33 daripada ke 34.

Tetapi sekarang Anda memiliki kesalahan yang lebih besar. Membandingkan hari 2 dengan hari 1, nilai persentase riil untuk A meningkat, sebesar 0,01%, tetapi perkiraan menunjukkan penurunan sebesar 1%.

Itu adalah kesalahan kualitatif, mungkin lebih buruk daripada kesalahan kuantitatif awal.

Orang bisa menyusun perkiraan untuk seluruh set tetapi, Anda mungkin harus menerbitkan data pada hari pertama, sehingga Anda tidak akan tahu tentang hari kedua. Jadi, kecuali Anda benar-benar harus memperkirakan, Anda mungkin lebih baik tidak.


siapa pun yang tahu cara membuat tabel yang lebih baik, harap edit atau beri tahu saya bagaimana / di mana
Rolazaro Azeveires

0

periksa apakah ini valid atau tidak sejauh kasus pengujian saya, saya bisa membuatnya bekerja.

katakanlah angka adalah k;

  1. urutkan persentase dengan menurun.
  2. ulangi setiap persentase dari urutan menurun.
  3. hitung persentase k untuk persentase pertama, ambil Math.Ceil dari output.
  4. selanjutnya k = k-1
  5. ulangi sampai semua persentase dikonsumsi.

0

Saya telah menerapkan metode dari jawaban Varun Vohra di sini untuk daftar dan dikte.

import math
import numbers
import operator
import itertools


def round_list_percentages(number_list):
    """
    Takes a list where all values are numbers that add up to 100,
    and rounds them off to integers while still retaining a sum of 100.

    A total value sum that rounds to 100.00 with two decimals is acceptable.
    This ensures that all input where the values are calculated with [fraction]/[total]
    and the sum of all fractions equal the total, should pass.
    """
    # Check input
    if not all(isinstance(i, numbers.Number) for i in number_list):
        raise ValueError('All values of the list must be a number')

    # Generate a key for each value
    key_generator = itertools.count()
    value_dict = {next(key_generator): value for value in number_list}
    return round_dictionary_percentages(value_dict).values()


def round_dictionary_percentages(dictionary):
    """
    Takes a dictionary where all values are numbers that add up to 100,
    and rounds them off to integers while still retaining a sum of 100.

    A total value sum that rounds to 100.00 with two decimals is acceptable.
    This ensures that all input where the values are calculated with [fraction]/[total]
    and the sum of all fractions equal the total, should pass.
    """
    # Check input
    # Only allow numbers
    if not all(isinstance(i, numbers.Number) for i in dictionary.values()):
        raise ValueError('All values of the dictionary must be a number')
    # Make sure the sum is close enough to 100
    # Round value_sum to 2 decimals to avoid floating point representation errors
    value_sum = round(sum(dictionary.values()), 2)
    if not value_sum == 100:
        raise ValueError('The sum of the values must be 100')

    # Initial floored results
    # Does not add up to 100, so we need to add something
    result = {key: int(math.floor(value)) for key, value in dictionary.items()}

    # Remainders for each key
    result_remainders = {key: value % 1 for key, value in dictionary.items()}
    # Keys sorted by remainder (biggest first)
    sorted_keys = [key for key, value in sorted(result_remainders.items(), key=operator.itemgetter(1), reverse=True)]

    # Otherwise add missing values up to 100
    # One cycle is enough, since flooring removes a max value of < 1 per item,
    # i.e. this loop should always break before going through the whole list
    for key in sorted_keys:
        if sum(result.values()) == 100:
            break
        result[key] += 1

    # Return
    return result

0

Berikut ini implementasi Python sederhana dari jawaban @ varun-vohra:

def apportion_pcts(pcts, total):
    proportions = [total * (pct / 100) for pct in pcts]
    apportions = [math.floor(p) for p in proportions]
    remainder = total - sum(apportions)
    remainders = [(i, p - math.floor(p)) for (i, p) in enumerate(proportions)]
    remainders.sort(key=operator.itemgetter(1), reverse=True)
    for (i, _) in itertools.cycle(remainders):
        if remainder == 0:
            break
        else:
            apportions[i] += 1
            remainder -= 1
    return apportions

Anda perlu math, itertools, operator.


0

Bagi mereka yang memiliki persentase dalam Seri panda, berikut adalah implemantasi saya dari metode sisa terbesar (seperti dalam jawaban Varun Vohra ), di mana Anda bahkan dapat memilih desimal yang ingin Anda bulatkan.

import numpy as np

def largestRemainderMethod(pd_series, decimals=1):

    floor_series = ((10**decimals * pd_series).astype(np.int)).apply(np.floor)
    diff = 100 * (10**decimals) - floor_series.sum().astype(np.int)
    series_decimals = pd_series - floor_series / (10**decimals)
    series_sorted_by_decimals = series_decimals.sort_values(ascending=False)

    for i in range(0, len(series_sorted_by_decimals)):
        if i < diff:
            series_sorted_by_decimals.iloc[[i]] = 1
        else:
            series_sorted_by_decimals.iloc[[i]] = 0

    out_series = ((floor_series + series_sorted_by_decimals) / (10**decimals)).sort_values(ascending=False)

    return out_series

-1

Ini adalah kasus pembulatan bankir, alias 'bulat setengah genap'. Ini didukung oleh BigDecimal. Tujuannya adalah untuk memastikan bahwa pembulatan saldo, yaitu tidak menguntungkan bank atau pelanggan.


5
Ini TIDAK memastikan pembulatan saldo keluar - itu hanya mengurangi jumlah kesalahan dengan mendistribusikan setengah pembulatan antara angka genap dan ganjil. Masih ada skenario di mana pembulatan bankir menghasilkan hasil yang tidak akurat.
D Stanley

@Dan Stanley setuju. Saya tidak mengatakan sebaliknya. Saya menyatakan tujuannya . Dengan sangat hati-hati.
Marquis of Lorne

2
Cukup adil - saya salah mengartikan apa yang ingin Anda katakan. Dalam kedua kasus saya tidak berpikir itu menyelesaikan masalah karena menggunakan pembulatan bankir tidak akan mengubah hasil dalam contoh.
D Stanley
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.