Statistiques: combinaisons en Python

122

Je dois calculer combinatoires (nCr) en Python mais ne peut pas trouver la fonction de le faire dans math, numpyou les stat bibliothèques. Quelque chose comme une fonction du type:

comb = calculate_combinations(n, r)

J'ai besoin du nombre de combinaisons possibles, pas des combinaisons réelles, donc itertools.combinationscela ne m'intéresse pas.

Enfin, je veux éviter d'utiliser des factorielles, car les nombres pour lesquels je vais calculer les combinaisons peuvent devenir trop gros et les factorielles vont être monstrueuses.

Cela semble être une question VRAIMENT facile à répondre, mais je suis noyé dans des questions sur la génération de toutes les combinaisons réelles, ce qui n'est pas ce que je veux.

Morlock
la source

Réponses:

121

Voir scipy.special.comb (scipy.misc.comb dans les anciennes versions de scipy). Quand exactest False, il utilise la fonction gammaln pour obtenir une bonne précision sans prendre beaucoup de temps. Dans le cas exact, il renvoie un entier de précision arbitraire, dont le calcul peut prendre beaucoup de temps.

Jouni K. Seppänen
la source
5
scipy.misc.combest obsolète au profit de la scipy.special.combversion depuis 0.10.0.
Dilawar
120

Pourquoi ne pas l'écrire vous-même? C'est un one-liner ou tel:

from operator import mul    # or mul=lambda x,y:x*y
from fractions import Fraction

def nCk(n,k): 
  return int( reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1) )

Test - impression du triangle de Pascal:

>>> for n in range(17):
...     print ' '.join('%5d'%nCk(n,k) for k in range(n+1)).center(100)
...     
                                                   1                                                
                                                1     1                                             
                                             1     2     1                                          
                                          1     3     3     1                                       
                                       1     4     6     4     1                                    
                                    1     5    10    10     5     1                                 
                                 1     6    15    20    15     6     1                              
                              1     7    21    35    35    21     7     1                           
                           1     8    28    56    70    56    28     8     1                        
                        1     9    36    84   126   126    84    36     9     1                     
                     1    10    45   120   210   252   210   120    45    10     1                  
                  1    11    55   165   330   462   462   330   165    55    11     1               
               1    12    66   220   495   792   924   792   495   220    66    12     1            
            1    13    78   286   715  1287  1716  1716  1287   715   286    78    13     1         
         1    14    91   364  1001  2002  3003  3432  3003  2002  1001   364    91    14     1      
      1    15   105   455  1365  3003  5005  6435  6435  5005  3003  1365   455   105    15     1   
    1    16   120   560  1820  4368  8008 11440 12870 11440  8008  4368  1820   560   120    16     1
>>> 

PS. modifié pour remplacer int(round(reduce(mul, (float(n-i)/(i+1) for i in range(k)), 1))) par int(reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1))afin qu'il ne se trompe pas pour le grand N / K

Nas Banov
la source
26
+1 pour avoir suggéré d'écrire quelque chose de simple, pour utiliser réduire et pour la démo sympa avec pascal triangle
jon_darkstar
6
-1 parce que cette réponse est fausse: print factorielle (54) / (factorielle (54 - 27)) / factorielle (27) == nCk (54, 27) donne False.
robert king
3
@robertking - Ok, vous étiez à la fois mesquine et techniquement correcte. Ce que j'ai fait était destiné à illustrer comment écrire sa propre fonction; Je savais que ce n'était pas précis pour N et K assez grands en raison de la précision en virgule flottante. Mais nous pouvons résoudre ce problème - voir ci-dessus, maintenant il ne devrait pas se tromper pour les grands nombres
Nas Banov
9
Ce serait probablement rapide dans Haskell, mais pas en Python malheureusement. C'est en fait assez lent par rapport à la plupart des autres réponses, par exemple @Alex Martelli, JF Sebastian et la mienne.
Todd Owen
9
Pour Python 3, je devais aussi from functools import reduce.
Velizar Hristov
52

Une recherche rapide sur le code google donne (elle utilise la formule de la réponse de @Mark Byers ):

def choose(n, k):
    """
    A fast way to calculate binomial coefficients by Andrew Dalke (contrib).
    """
    if 0 <= k <= n:
        ntok = 1
        ktok = 1
        for t in xrange(1, min(k, n - k) + 1):
            ntok *= n
            ktok *= t
            n -= 1
        return ntok // ktok
    else:
        return 0

choose()est 10 fois plus rapide (testé sur toutes les paires 0 <= (n, k) <1e3) que scipy.misc.comb()si vous avez besoin d'une réponse exacte.

def comb(N,k): # from scipy.comb(), but MODIFIED!
    if (k > N) or (N < 0) or (k < 0):
        return 0L
    N,k = map(long,(N,k))
    top = N
    val = 1L
    while (top > (N-k)):
        val *= top
        top -= 1
    n = 1L
    while (n < k+1L):
        val /= n
        n += 1
    return val
jfs
la source
Une belle solution qui ne nécessite aucun paquet
Edward Newell
2
FYI: La formule mentionnée est ici: en.wikipedia.org/wiki/…
jmiserez
Cette choosefonction devrait avoir beaucoup plus de votes positifs! Python 3.8 a math.comb, mais j'ai dû utiliser Python 3.6 pour un défi et aucune implémentation n'a donné de résultats exacts pour de très grands entiers. Celui-ci le fait et le fait vite!
reconnaître le
42

Si vous voulez des résultats et une vitesse exacts , essayez gmpy - gmpy.combdevrait faire exactement ce que vous demandez, et c'est assez rapide (bien sûr, en tant gmpyqu'auteur original, je suis partial ;-).

Alex Martelli
la source
6
En effet, gmpy2.comb()c'est 10 fois plus rapide que d' choose()après ma réponse pour le code: for k, n in itertools.combinations(range(1000), 2): f(n,k)f()est l'un gmpy2.comb()ou l' autre ou choose()sur Python 3.
jfs
Puisque vous êtes l'auteur du paquet, je vous laisse vous fixer le lien cassé afin qu'il pointe au bon endroit ....
SeldomNeedy
@SeldomNeedy, le lien vers code.google.com est un bon endroit (bien que le site soit maintenant en mode archivage). Bien sûr, à partir de là, il est facile de trouver l'emplacement github, github.com/aleaxit/gmpy , et celui de PyPI, pypi.python.org/pypi/gmpy2 , car il renvoie aux deux! -)
Alex Martelli
@AlexMartelli Désolé pour la confusion. La page affiche un 404 si javascript a été (sélectivement) désactivé. Je suppose que cela décourage les IA malhonnêtes d'incorporer assez facilement les sources archivées de Google Code Project?
SeldomNeedy
28

Si vous voulez un résultat exact, utilisez sympy.binomial. Cela semble être la méthode la plus rapide, haut la main.

x = 1000000
y = 234050

%timeit scipy.misc.comb(x, y, exact=True)
1 loops, best of 3: 1min 27s per loop

%timeit gmpy.comb(x, y)
1 loops, best of 3: 1.97 s per loop

%timeit int(sympy.binomial(x, y))
100000 loops, best of 3: 5.06 µs per loop
Jim Garrison
la source
22

Une traduction littérale de la définition mathématique est tout à fait adéquate dans de nombreux cas (en se rappelant que Python utilisera automatiquement l'arithmétique des grands nombres):

from math import factorial

def calculate_combinations(n, r):
    return factorial(n) // factorial(r) // factorial(n-r)

Pour certaines entrées que j'ai testées (par exemple n = 1000 r = 500), c'était plus de 10 fois plus rapide que la ligne reducesuggérée dans une autre réponse (actuellement la plus votée). En revanche, il est surpassé par l'extrait de code fourni par @JF Sebastian.

Todd Owen
la source
11

Au départ Python 3.8, la bibliothèque standard comprend désormais la math.combfonction de calcul du coefficient binomial:

math.comb (n, k)

qui est le nombre de façons de choisir k éléments parmi n éléments sans répétition
n! / (k! (n - k)!):

import math
math.comb(10, 5) # 252
Xavier Guihot
la source
10

Voici une autre alternative. Celui-ci a été écrit à l'origine en C ++, il peut donc être rétroporté en C ++ pour un entier de précision finie (par exemple __int64). L'avantage est (1) qu'il n'implique que des opérations entières, et (2) il évite de gonfler la valeur entière en faisant des paires successives de multiplication et de division. J'ai testé le résultat avec le triangle Pascal de Nas Banov, il obtient la bonne réponse:

def choose(n,r):
  """Computes n! / (r! (n-r)!) exactly. Returns a python long int."""
  assert n >= 0
  assert 0 <= r <= n

  c = 1L
  denom = 1
  for (num,denom) in zip(xrange(n,n-r,-1), xrange(1,r+1,1)):
    c = (c * num) // denom
  return c

Justification: pour minimiser le nombre de multiplications et de divisions, nous réécrivons l'expression comme

    n!      n(n-1)...(n-r+1)
--------- = ----------------
 r!(n-r)!          r!

Pour éviter autant que possible le débordement de multiplication, nous évaluerons dans l'ordre STRICT suivant, de gauche à droite:

n / 1 * (n-1) / 2 * (n-2) / 3 * ... * (n-r+1) / r

Nous pouvons montrer que l'arithmatique entière opérée dans cet ordre est exacte (c'est-à-dire pas d'erreur d'arrondi).

Wirawan Purwanto
la source
5

En utilisant la programmation dynamique, la complexité temporelle est Θ (n * m) et la complexité spatiale Θ (m):

def binomial(n, k):
""" (int, int) -> int

         | c(n-1, k-1) + c(n-1, k), if 0 < k < n
c(n,k) = | 1                      , if n = k
         | 1                      , if k = 0

Precondition: n > k

>>> binomial(9, 2)
36
"""

c = [0] * (n + 1)
c[0] = 1
for i in range(1, n + 1):
    c[i] = 1
    j = i - 1
    while j > 0:
        c[j] += c[j - 1]
        j -= 1

return c[k]
pantelis300
la source
4

Si votre programme a une limite supérieure à n(par exemple n <= N) et doit calculer à plusieurs reprises nCr (de préférence pour >> Nfois), l'utilisation de lru_cache peut vous donner un énorme gain de performances:

from functools import lru_cache

@lru_cache(maxsize=None)
def nCr(n, r):
    return 1 if r == 0 or r == n else nCr(n - 1, r - 1) + nCr(n - 1, r)

La construction du cache (ce qui est fait implicitement) prend du O(N^2)temps. Tous les appels ultérieurs à nCrretourneront O(1).

yzn-pku
la source
4

Vous pouvez écrire 2 fonctions simples qui s'avèrent être environ 5 à 8 fois plus rapides qu'en utilisant scipy.special.comb . En fait, vous n'avez pas besoin d'importer de packages supplémentaires et la fonction est assez facilement lisible. L'astuce consiste à utiliser la mémorisation pour stocker les valeurs précédemment calculées et à utiliser la définition de nCr

# create a memoization dictionary
memo = {}
def factorial(n):
    """
    Calculate the factorial of an input using memoization
    :param n: int
    :rtype value: int
    """
    if n in [1,0]:
        return 1
    if n in memo:
        return memo[n]
    value = n*factorial(n-1)
    memo[n] = value
    return value

def ncr(n, k):
    """
    Choose k elements from a set of n elements - n must be larger than or equal to k
    :param n: int
    :param k: int
    :rtype: int
    """
    return factorial(n)/(factorial(k)*factorial(n-k))

Si nous comparons les temps

from scipy.special import comb
%timeit comb(100,48)
>>> 100000 loops, best of 3: 6.78 µs per loop

%timeit ncr(100,48)
>>> 1000000 loops, best of 3: 1.39 µs per loop
PyRsquared
la source
De nos jours, il y a un décorateur de mémorisation dans functools appelé lru_cache qui pourrait simplifier votre code?
hérisson dément
2

C'est assez facile avec sympy.

import sympy

comb = sympy.binomial(n, r)
Policier
la source
2

En utilisant uniquement la bibliothèque standard distribuée avec Python :

import itertools

def nCk(n, k):
    return len(list(itertools.combinations(range(n), k)))
MarianD
la source
3
Je ne pense pas que sa complexité temporelle (et son utilisation de la mémoire) soit acceptable.
xmcp
2

La formule directe produit de grands entiers lorsque n est supérieur à 20.

Alors, encore une autre réponse:

from math import factorial

reduce(long.__mul__, range(n-r+1, n+1), 1L) // factorial(r)

court, précis et efficace car cela évite les grands entiers python en s'en tenant aux longs.

Il est plus précis et plus rapide par rapport à scipy.special.comb:

 >>> from scipy.special import comb
 >>> nCr = lambda n,r: reduce(long.__mul__, range(n-r+1, n+1), 1L) // factorial(r)
 >>> comb(128,20)
 1.1965669823265365e+23
 >>> nCr(128,20)
 119656698232656998274400L  # accurate, no loss
 >>> from timeit import timeit
 >>> timeit(lambda: comb(n,r))
 8.231969118118286
 >>> timeit(lambda: nCr(128, 20))
 3.885951042175293
olivecoder
la source
C'est faux! Si n == r, le résultat doit être 1. Ce code renvoie 0.
reyammer
Plus précisément, il devrait être range(n-r+1, n+1)au lieu de range(n-r,n+1).
reyammer
1

Il s'agit du code @ killerT2333 utilisant le décorateur de mémorisation intégré.

from functools import lru_cache

@lru_cache()
def factorial(n):
    """
    Calculate the factorial of an input using memoization
    :param n: int
    :rtype value: int
    """
    return 1 if n in (1, 0) else n * factorial(n-1)

@lru_cache()
def ncr(n, k):
    """
    Choose k elements from a set of n elements,
    n must be greater than or equal to k.
    :param n: int
    :param k: int
    :rtype: int
    """
    return factorial(n) / (factorial(k) * factorial(n - k))

print(ncr(6, 3))
hérisson dément
la source
1

Voici un algorithme efficace pour vous

for i = 1.....r

   p = p * ( n - i ) / i

print(p)

Par exemple nCr (30,7) = fact (30) / (fact (7) * fact (23)) = (30 * 29 * 28 * 27 * 26 * 25 * 24) / (1 * 2 * 3 * 4 * 5 * 6 * 7)

Donc, exécutez simplement la boucle de 1 à r pour obtenir le résultat.

kta
la source
0

C'est probablement aussi rapide que vous pouvez le faire en python pur pour des entrées raisonnablement volumineuses:

def choose(n, k):
    if k == n: return 1
    if k > n: return 0
    d, q = max(k, n-k), min(k, n-k)
    num =  1
    for n in xrange(d+1, n+1): num *= n
    denom = 1
    for d in xrange(1, q+1): denom *= d
    return num / denom
Rabih Kodeih
la source
0

Cette fonction est très optimisée.

def nCk(n,k):
    m=0
    if k==0:
        m=1
    if k==1:
        m=n
    if k>=2:
        num,dem,op1,op2=1,1,k,n
        while(op1>=1):
            num*=op2
            dem*=op1
            op1-=1
            op2-=1
        m=num//dem
    return m
Santiago Coca Rojas
la source