Comment obtenir des pourcentages arrondis jusqu'à 100%

193

Considérez les quatre pourcentages ci-dessous, représentés sous forme de floatnombres:

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

J'ai besoin de représenter ces pourcentages sous forme de nombres entiers. Si j'utilise simplement Math.round(), je me retrouve avec un total de 101%.

14 + 48 + 10 + 29 = 101

Si j'utilise parseInt(), je me retrouve avec un total de 97%.

13 + 47 + 9 + 28 = 97

Qu'est-ce qu'un bon algorithme pour représenter n'importe quel nombre de pourcentages sous forme de nombres entiers tout en conservant un total de 100%?


Edit : Après avoir lu certains des commentaires et des réponses, il existe clairement de nombreuses façons de résoudre ce problème.

Dans mon esprit, pour rester fidèle aux chiffres, le "bon" résultat est celui qui minimise l'erreur globale, définie par la quantité d'erreurs d'arrondi introduite par rapport à la valeur réelle:

        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)

En cas d'égalité (3.33, 3.33, 3.33), une décision arbitraire peut être prise (par exemple 3, 4, 3).

poezn
la source
21
Supposons que vous ayez 3.33, 3.33 et 3.33. Lequel ferez-vous 4?
RobG
3
Exactement. La question incarne une contradiction dans les termes.
Marquis de Lorne
4
C'est un scénario très courant dans les rapports - comment afficher un «total» de valeurs décimales qui ne correspond pas toujours à la somme des valeurs affichées.
D Stanley
1
Quel est le «bon» résultat dans votre exemple? Cela peut résoudre les désaccords sur la «meilleure» solution.
D Stanley

Réponses:

35

Étant donné qu'aucune des réponses ici ne semble le résoudre correctement, voici ma version semi-obscurcie utilisant des 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]
yonilevy
la source
6
Corrigez-moi si je me trompe, mais n'est-ce pas une implémentation de l'algorithme proposé par ma réponse? (Ne pas effacer sur underscorejs)
vvohra87
@VarunVohra désolé, je n'ai pas remarqué cela jusqu'à présent, oui, il semble que votre algorithme est le même :) Je ne sais pas pourquoi mon message est la réponse acceptée, le code obscurci était juste pour le lolz ...
yonilevy
@yonilevy J'ai supprimé mon commentaire; Je n'avais tout simplement pas réalisé qu'il était censé renvoyer une liste triée. Je m'excuse!
Zack Burt
2
Il y a un problème avec cette fonction lorsque le dernier élément est 0 et les précédents ajoutent à 100. Par exemple [52.6813880126183, 5.941114616193481, 24.55310199789695, 8.780231335436383, 8.04416403785489, 0]. Le dernier renvoie logiquement -1. J'ai pensé à la solution suivante très rapidement mais il y a probablement quelque chose de mieux: jsfiddle.net/0o75bw43/1
Cruclax
1
@Cruclax, il montre tous les 1 lorsque toutes les entrées sont nulles dans le tableau d'entrée
tony.0919
159

Il existe de nombreuses façons de procéder, à condition que vous ne soyez pas préoccupé par la dépendance aux données décimales d'origine.

La première méthode et peut-être la plus populaire serait la méthode du plus grand reste

Ce qui est fondamentalement:

  1. Arrondir tout vers le bas
  2. Obtenir la différence en somme et 100
  3. Distribuer la différence en ajoutant 1 aux éléments par ordre décroissant de leurs parties décimales

Dans votre cas, cela ressemblerait à ceci:

13.626332%
47.989636%
 9.596008%
28.788024%

Si vous prenez les parties entières, vous obtenez

13
47
 9
28

ce qui fait 97, et vous voulez en ajouter trois de plus. Maintenant, vous regardez les parties décimales, qui sont

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

et prenez les plus gros jusqu'à ce que le total atteigne 100. Vous obtiendrez donc:

14
48
 9
29

Sinon, vous pouvez simplement choisir d'afficher une décimale au lieu de valeurs entières. Les nombres seraient donc 48,3 et 23,9, etc. Cela réduirait considérablement la variance de 100.

vvohra87
la source
5
Cette «colonne de caractéristiques» sur le site Web de l'American Mathematical Society - Apportionment II: Apportionment Systems - décrit plusieurs méthodes de «répartition» similaires.
Kenny Evitt
1
Cela ressemble presque à un copier-coller de ma réponse ici stackoverflow.com/questions/5227215/… .
sawa
Notez que, contrairement à votre commentaire sur la réponse de @DStanley, dans votre réponse, 9,596008% a été arrondi à 9%, ce qui représente plus de 0,5% de différence. Encore une bonne réponse, cependant.
Rolazaro Azeveires
33

La "meilleure" façon de faire ceci (citée puisque "meilleur" est un terme subjectif) est probablement de garder un compte courant (non intégral) de votre position et d'arrondir cette valeur.

Ensuite, utilisez-le avec l'historique pour déterminer la valeur à utiliser. Par exemple, en utilisant les valeurs que vous avez données:

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

À chaque étape, vous n'arrondissez pas le nombre lui-même. Au lieu de cela, vous arrondissez la valeur accumulée et calculez le meilleur entier qui atteint cette valeur à partir de la ligne de base précédente - cette ligne de base est la valeur cumulative (arrondie) de la ligne précédente.

Cela fonctionne parce que vous ne perdez pas d' informations à chaque étape, mais utilisez plutôt les informations de manière plus intelligente. Les valeurs arrondies `` correctes '' se trouvent dans la dernière colonne et vous pouvez voir qu'elles totalisent 100.

Vous pouvez voir la différence entre cela et arrondir aveuglément chaque valeur, dans la troisième valeur ci-dessus. Alors 9.596008que normalement arrondirait à 10, le cumul est 71.211976correctement arrondi à 71- cela signifie qu'il suffit 9d'ajouter à la ligne de base précédente de 62.


Cela fonctionne également pour une séquence "problématique" comme trois valeurs approximatives , où l' une d'elles doit être arrondie: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
paxdiablo
la source
1
La seconde approche résout ces deux problèmes. Le premier donne 26, 25, 26, 23, le second 1, 0, 1, 0, 1, 0, ....
paxdiablo
Cette approche fonctionne également bien pour arrondir les petits nombres car elle empêche le nombre négatif de pécher la sortie
Jonty5817
19

Le but de l'arrondi est de générer le moins d'erreur possible. Lorsque vous arrondissez une valeur unique, ce processus est simple et direct et la plupart des gens le comprennent facilement. Lorsque vous arrondissez plusieurs nombres en même temps, le processus devient plus délicat - vous devez définir comment les erreurs vont se combiner, c'est-à-dire ce qui doit être minimisé.

La réponse bien votée de Varun Vohra minimise la somme des erreurs absolues et est très simple à mettre en œuvre. Cependant, il y a des cas extrêmes qu'il ne gère pas - quel devrait être le résultat de l'arrondissement 24.25, 23.25, 27.25, 25.25? L'un de ces éléments doit être arrondi à la hausse plutôt qu'à la baisse. Vous choisiriez probablement arbitrairement le premier ou le dernier de la liste.

Il vaut peut-être mieux utiliser l' erreur relative au lieu de l' erreur absolue . Arrondir 23,25 à 24 le change de 3,2% tandis que arrondir 27,25 à 28 ne le change que de 2,8%. Maintenant, il y a un gagnant clair.

Il est possible de peaufiner cela encore plus. Une technique courante consiste à mettre au carré chaque erreur, de sorte que les grandes erreurs comptent disproportionnellement plus que les petites. J'utiliserais également un diviseur non linéaire pour obtenir l'erreur relative - il ne semble pas juste qu'une erreur à 1% soit 99 fois plus importante qu'une erreur à 99%. Dans le code ci-dessous, j'ai utilisé la racine carrée.

L'algorithme complet est le suivant:

  1. Additionnez les pourcentages après les avoir tous arrondis et soustrayez de 100. Cela vous indique combien de ces pourcentages doivent être arrondis à la place.
  2. Générez deux scores d'erreur pour chaque pourcentage, l'un lorsqu'il est arrondi vers le bas et l'autre lorsqu'il est arrondi vers le haut. Faites la différence entre les deux.
  3. Triez les différences d'erreur produites ci-dessus.
  4. Pour le nombre de pourcentages qui doivent être arrondis, prenez un élément de la liste triée et incrémentez le pourcentage arrondi de 1.

Vous pouvez toujours avoir plus d'une combinaison avec la même somme d'erreurs, par exemple 33.3333333, 33.3333333, 33.3333333. Cela est inévitable et le résultat sera complètement arbitraire. Le code que je donne ci-dessous préfère arrondir les valeurs à gauche.

Tout rassembler en Python ressemble à ceci.

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]

Comme vous pouvez le voir avec ce dernier exemple, cet algorithme est toujours capable de fournir des résultats non intuitifs. Même si 89,0 ne nécessite aucun arrondi, l'une des valeurs de cette liste a dû être arrondie; l'erreur relative la plus faible résulte de l'arrondissement de cette grande valeur plutôt que des alternatives beaucoup plus petites.

Cette réponse recommandait à l'origine de passer par toutes les combinaisons possibles d'arrondi vers le haut / arrondi vers le bas, mais comme indiqué dans les commentaires, une méthode plus simple fonctionne mieux. L'algorithme et le code reflètent cette simplification.

Mark Ransom
la source
1
Je ne pense pas que vous ayez besoin de considérer toutes les combinaisons: processus par ordre décroissant de baisse d'erreur pondérée allant d' arrondi à zéro à arrondi à l'infini (à peu près simplement introduire la pesée dans les réponses de Verun Vohras et de yonilevy ("identiques")).
greybeard
@greybeard vous avez raison, j'y réfléchissais trop. Je ne pouvais pas simplement trier l'erreur car il y avait deux erreurs pour chaque valeur, mais prendre la différence a résolu ce problème. J'ai mis à jour la réponse.
Mark Ransom
Je préfère toujours avoir 0% lorsque le nombre réel est de 0%. Ainsi , l' ajout if actual == 0: return 0de error_gengrandes œuvres.
Nikolay Baluk
1
quelle est la iscloseméthode au début de round_to_100?
toto_tico
2
@toto_tico stackoverflow.com/questions/5595425/…
Mark Ransom
7

NE additionnez PAS les nombres arrondis. Vous allez avoir des résultats inexacts. Le total pourrait être considérablement réduit en fonction du nombre de termes et de la distribution des parties fractionnaires.

Affichez les nombres arrondis mais additionnez les valeurs réelles. Selon la façon dont vous présentez les chiffres, la manière réelle de procéder varie. De cette façon vous obtenez

 14
 48
 dix
 29
 __
100

De toute façon, vous allez avoir des divergences. Il n'y a aucun moyen dans votre exemple d'afficher des nombres qui totalisent 100 sans "arrondir" une valeur dans le mauvais sens (la moindre erreur serait de changer 9,596 en 9)

ÉDITER

Vous devez choisir entre l'une des options suivantes:

  1. Précision des articles
  2. Précision de la somme (si vous additionnez des valeurs arrondies)
  3. Cohérence entre les éléments arrondis et la somme arrondie)

La plupart du temps, traiter des pourcentages n ° 3 est la meilleure option, car elle est plus évidente lorsque le total est égal à 101% que lorsque les éléments individuels ne totalisent pas 100, et vous gardez les éléments individuels précis. «Arrondir» 9,596 à 9 est à mon avis inexact.

Pour expliquer cela, j'ajoute parfois une note de bas de page qui explique que les valeurs individuelles sont arrondies et peuvent ne pas totaliser 100% - toute personne qui comprend l'arrondi devrait être en mesure de comprendre cette explication.

D Stanley
la source
6
Ce n'est pas très utile car les valeurs imprimées ne totalisent pas 100. Le but de la question était d'empêcher les utilisateurs de penser que les valeurs sont incorrectes, ce que dans ce cas, la plupart des gens feraient en regardant et en comparant le total .
vvohra87
@VarunVohra lisez ma modification, vous NE POUVEZ PAS afficher vos nombres de telle sorte qu'ils totalisent 100 sans "arrondir" un de plus de 0,5.
D Stanley
1
@DStanley en fait, à l'exception d'un ensemble où tous les nombres sont inférieurs à 0,5, vous pouvez. Vérifiez ma réponse - LRM fait exactement cela.
vvohra87
3
@VarunVohra Dans le LRM exemple original donné 14, 48, 9 et 29 qui « rond » 9,596 à 9. Si nous allocation en fonction des nombres entiers LRM sera le plus précis, mais il change encore un résultat de plus d'une demi-unité.
D Stanley
7

J'ai écrit un assistant d'arrondi de la version C #, l'algorithme est le même que la réponse de Varun Vohra , j'espère que cela aide.

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

Il réussit le test unitaire suivant:

[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 });
}
Bruce
la source
Agréable! m'a donné une base au sol pour commencer .. Enumerable n'a pas ForEach bien que je crois
Jack0fshad0ws
4

Vous pouvez essayer de garder une trace de votre erreur due à l'arrondissement, puis arrondir dans le sens contraire du grain si l'erreur accumulée est supérieure à la partie fractionnaire du nombre actuel.

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

Je ne sais pas si cela fonctionnerait en général, mais cela semble fonctionner de la même manière si l'ordre est inversé:

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

Je suis sûr qu'il y a des cas extrêmes où cela pourrait échouer, mais toute approche sera au moins quelque peu arbitraire puisque vous modifiez fondamentalement vos données d'entrée.

atkretsch
la source
2
Les comptables et les banquiers utilisent une technique similaire depuis des centaines d'années. «Portez le reste» d'une rangée à l'autre. Commencez avec 1/2 d'un cent dans le «report». Ajoutez le «report» à la première valeur et tronquez. Maintenant, le montant que vous avez perdu en tronquant, mettez-le dans le «report». Faites-le tout en bas et les nombres arrondis s'ajouteront exactement au total souhaité à chaque fois.
Jeff Grigg
Carolyn Kay a suggéré cette implémentation dans Access VB 2007: <code> 'Remboursement rond en dollars en utilisant la méthode "carry the restder" ref1 = rsQry! [Refund Paid $$$] * rsQry! [Property Value] / propValTot ref2 = ref1 + ref5 'Ajouter le reste reporté, zéro pour démarrer ref3 = ref2 * 100' Multiplier par 100 en un nombre entier ref4 = ref3 / 100 'Diviser par 100 en un nombre décimal rsTbl! [Remboursement payé $$$] = ref4' Mettez le " reste "nombre arrondi dans le tableau ref5 = ref2 - ref4 'Portez le nouveau reste </code>
Jeff Grigg
2

Une fois, j'ai écrit un outil non arrondi, pour trouver la perturbation minimale d'un ensemble de nombres correspondant à un objectif. C'était un problème différent, mais on pourrait en théorie utiliser une idée similaire ici. Dans ce cas, nous avons un ensemble de choix.

Ainsi, pour le premier élément, nous pouvons l'arrondir à 14 ou à 13. Le coût (au sens de la programmation en nombres entiers binaires) est moindre pour l'arrondi vers le haut que pour l'arrondi vers le bas, car l'arrondi vers le bas nécessite que nous déplacez cette valeur sur une plus grande distance. De même, nous pouvons arrondir chaque nombre à la hausse ou à la baisse, il y a donc un total de 16 choix parmi lesquels nous devons choisir.

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

Je résoudrais normalement le problème général dans MATLAB, ici en utilisant bintprog, un outil de programmation d'entiers binaires, mais il n'y a que quelques choix à tester, il est donc assez facile avec des boucles simples de tester chacune des 16 alternatives. Par exemple, supposons que nous arrondissions cet ensemble comme suit:

 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

L'erreur absolue totale commise est de 1,25266. Il peut être légèrement réduit par l'arrondi alternatif suivant:

 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

En fait, ce sera la solution optimale en termes d'erreur absolue. Bien sûr, s'il y avait 20 termes, l'espace de recherche sera de taille 2 ^ 20 = 1048576. Pour 30 ou 40 termes, cet espace sera de taille significative. Dans ce cas, vous devrez utiliser un outil capable de rechercher efficacement l'espace, peut-être en utilisant un schéma de branche et lié.


la source
Juste pour référence future: l'algorithme du "plus grand reste" doit minimiser l'erreur absolue totale en fonction de votre métrique (voir la réponse de @ varunvohra). La preuve est simple: supposons que cela ne minimise pas l'erreur. Ensuite, il doit y avoir un ensemble de valeurs qu'il arrondit vers le bas qui doit être arrondi vers le haut, et vice versa (les deux ensembles sont de la même taille). Mais chaque valeur arrondie est plus éloignée de l'entier suivant que toute valeur arrondie supérieure (et vv), donc le nouveau montant d'erreur doit être supérieur. QED. Cependant, cela ne fonctionne pas pour toutes les mesures d'erreur; d'autres algorithmes sont nécessaires.
rici
2

Je pense que ce qui suit réalisera ce que vous recherchez

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]

Une dernière chose, j'ai exécuté la fonction en utilisant les nombres initialement donnés dans la question pour comparer à la sortie souhaitée

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

C'était différent de ce que voulait la question => [48, 29, 14, 9]. Je ne pouvais pas comprendre cela avant d'avoir examiné la marge d'erreur totale

-------------------------------------------------
| 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%  |
-------------------------------------------------

Essentiellement, le résultat de ma fonction introduit en fait le moins d'erreur possible.

Violon ici

Bruno
la source
c'est à peu près ce que j'avais à l'esprit, à la différence que l'erreur doit être mesurée par rapport à la valeur (arrondir 9,8 à 10 est une erreur plus importante que d'arrondir de 19,8 à 20). Cela pourrait être facilement fait en le reflétant dans le rappel de tri, cependant.
poezn
c'est faux pour [33.33, 33.33, 33.33, 0.1], il renvoie [1, 33, 33, 33] plutôt que le plus précis [34, 33, 33, 0]
yonilevy
@yonilevy Merci pour cela. Corrigé maintenant.
Bruno
pas encore, pour [16.666, 16.666, 16.666, 16.666, 16.666, 16.666] il renvoie [15, 17, 17, 17, 17, 17] plutôt que [16, 16, 17, 17, 17, 17] - voir mon réponse
yonilevy
2

Je ne suis pas sûr du niveau de précision dont vous avez besoin, mais ce que je ferais, c'est simplement ajouter 1 les premiers nnombres, nsoit le plafond de la somme totale des décimales. Dans ce cas 3, j'ajouterais donc 1 aux 3 premiers éléments et j'appliquerais le reste. Bien sûr, ce n'est pas très précis, certains nombres peuvent être arrondis à la hausse ou à la baisse alors que cela ne devrait pas, mais cela fonctionne bien et donnera toujours 100%.

Ce [ 13.626332, 47.989636, 9.596008, 28.788024 ]serait aussi [14, 48, 10, 28]parce queMath.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

Vous pouvez toujours informer les utilisateurs que les nombres sont arrondis et peuvent ne pas être très précis ...

elclans
la source
1

Si vous l'arrondissez, il n'y a pas de bon moyen d'obtenir exactement la même chose dans tous les cas.

Vous pouvez prendre la partie décimale des N pourcentages que vous avez (dans l'exemple que vous avez donné, c'est 4).

Ajoutez les parties décimales. Dans votre exemple, vous avez un total de partie fractionnaire = 3.

Placer les 3 nombres avec les fractions les plus élevées et plancher le reste.

(Désolé pour les modifications)

arunlalam
la source
1
Bien que cela puisse fournir des nombres qui s'ajoutent à 100, vous pouvez finir par transformer 3,9 en 3 et 25,1 en 26.
RobG
non. 3.9 sera 4 et 25.1 sera 25. J'ai dit de plafonner les 3 nombres avec les fractions les plus élevées et non la valeur la plus élevée.
arunlalam
2
s'il y a beaucoup trop de fractions se terminant par .9, disons 9 valeurs de 9,9% et une valeur de 10,9, il y a une valeur qui finira par 9%, 8 comme 10% et une comme 11%.
arunlalam
1

Si vous devez vraiment les arrondir, il y a déjà de très bonnes suggestions ici (reste le plus important, erreur relative la moins élevée, etc.).

Il y a aussi déjà une bonne raison de ne pas arrondir (vous obtiendrez au moins un numéro qui "semble mieux" mais qui est "faux"), et comment résoudre cela (prévenez vos lecteurs) et c'est ce que je fais.

Permettez-moi d'ajouter la «mauvaise» partie du numéro.

Supposons que vous ayez trois événements / entités / ... avec des pourcentages que vous approximez comme:

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

Plus tard, les valeurs changent légèrement pour devenir

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

Le premier tableau a le problème déjà mentionné d'avoir un "faux" numéro: 33,34 est plus proche de 33 que de 34.

Mais maintenant, vous avez une erreur plus grave. En comparant le jour 2 au jour 1, la valeur réelle en pourcentage de A a augmenté de 0,01%, mais l'approximation montre une diminution de 1%.

C'est une erreur qualitative, probablement bien pire que l'erreur quantitative initiale.

On pourrait imaginer une approximation pour l'ensemble complet, mais vous devrez peut-être publier des données le premier jour, vous ne saurez donc pas le jour deux. Donc, à moins que vous ne deviez vraiment, vraiment, vous rapprocher, vous feriez probablement mieux de ne pas le faire.

Rolazaro Azeveires
la source
toute personne sachant comment créer de meilleures tables s'il vous plaît éditer ou me dire comment / où
Rolazaro Azeveires
0

vérifier si cela est valide ou pas dans la mesure où mes cas de test, je suis capable de faire fonctionner cela.

disons que le nombre est k;

  1. trier le pourcentage par ordre décroissant.
  2. itérer sur chaque pourcentage par ordre décroissant.
  3. calculer le pourcentage de k pour le premier pourcentage prendre Math.Ceil de sortie.
  4. k suivant = k-1
  5. itérer jusqu'à ce que tout le pourcentage soit consommé.
relâché
la source
0

J'ai implémenté la méthode de la réponse de Varun Vohra ici pour les listes et les dictionnaires.

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
beruic
la source
0

Voici une implémentation Python plus simple de la réponse @ 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

Vous avez besoin math, itertools, operator.

CMCDragonkai
la source
0

Pour ceux qui ont les pourcentages dans une série de pandas, voici mon implémentation de la méthode du plus grand reste (comme dans la réponse de Varun Vohra ), où vous pouvez même sélectionner les décimales auxquelles vous voulez arrondir.

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
maxi.marufo
la source
-1

C'est un cas pour l'arrondi des banquiers, alias «rond demi-pair». Il est pris en charge par BigDecimal. Son but est de s'assurer que l'arrondi s'équilibre, c'est-à-dire ne favorise ni la banque ni le client.

Marquis de Lorne
la source
5
Il ne garantit PAS que l'arrondi s'équilibre - il réduit simplement la quantité d'erreur en répartissant les demi-arrondis entre les nombres pairs et impairs. Il existe encore des scénarios où les arrondis des banquiers produisent des résultats inexacts.
D Stanley
@DStanley D'accord. Je n'ai pas dit le contraire. J'ai énoncé son but . Très soigneusement.
Marquis de Lorne
2
Assez juste - j'ai mal interprété ce que vous essayiez de dire. Dans les deux cas, je ne pense pas que cela résout le problème car l'utilisation de l'arrondissement des banquiers ne changera pas les résultats de l'exemple.
D Stanley