Estadísticas: combinaciones en Python

122

Necesito calcular combinatorias (nCr) en Python, pero no puedo encontrar la función de hacer eso en math, numpyo stat bibliotecas. Algo así como una función del tipo:

comb = calculate_combinations(n, r)

Necesito el número de combinaciones posibles, no las combinaciones reales, por lo itertools.combinationsque no me interesa.

Finalmente, quiero evitar el uso de factoriales, ya que los números para los que calcularé las combinaciones pueden ser demasiado grandes y los factoriales serán monstruosos.

Esto parece una pregunta REALMENTE fácil de responder, sin embargo, me estoy ahogando en preguntas sobre la generación de todas las combinaciones reales, que no es lo que quiero.

Morlock
fuente

Respuestas:

121

Consulte scipy.special.comb (scipy.misc.comb en versiones anteriores de scipy). Cuando exactes falso, utiliza la función gammaln para obtener una buena precisión sin tomar mucho tiempo. En el caso exacto, devuelve un número entero de precisión arbitraria, que puede tardar mucho tiempo en calcularse.

Jouni K. Seppänen
fuente
55
scipy.misc.combestá en desuso a favor de scipy.special.combdesde la versión 0.10.0.
Dilawar
120

¿Por qué no lo escribes tú mismo? Es de una sola línea o tal:

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

Prueba - imprimir el triángulo 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
>>> 

PD. editado para reemplazar int(round(reduce(mul, (float(n-i)/(i+1) for i in range(k)), 1))) con int(reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1))lo que no lo hará err de gran N / K

Nas Banov
fuente
26
+1 por sugerir escribir algo simple, por usar reduce y por la demostración genial con triángulo pascal
jon_darkstar
66
-1 porque esta respuesta es incorrecta: print factorial (54) / (factorial (54 - 27)) / factorial (27) == nCk (54, 27) da False.
Robert King
3
@robertking - Ok, ambos eran mezquinos y técnicamente correctos. Lo que hice fue una ilustración de cómo escribir la propia función; Sabía que no es preciso para N y K lo suficientemente grandes debido a la precisión de coma flotante. Pero podemos arreglar eso, ver arriba, ahora no debería equivocarse con grandes números
Nas Banov
9
Esto probablemente sería rápido en Haskell, pero desafortunadamente no en Python. En realidad, es bastante lento en comparación con muchas de las otras respuestas, por ejemplo, @Alex Martelli, JF Sebastian y la mía.
Todd Owen
9
Para Python 3, tuve que hacerlo también from functools import reduce.
Velizar Hristov
52

Una búsqueda rápida en el código de google da (usa la fórmula de la respuesta 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()es 10 veces más rápido (probado en todos los pares 0 <= (n, k) <1e3) que scipy.misc.comb()si necesita una respuesta exacta.

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
fuente
Una buena solución que no requiere ningún paquete
Edward Newell
2
FYI: La fórmula mencionada está aquí: en.wikipedia.org/wiki/…
jmiserez
¡Esta choosefunción debería tener muchos más votos positivos! Python 3.8 tiene math.comb, pero tuve que usar Python 3.6 para un desafío y ninguna implementación dio resultados exactos para enteros muy grandes. Este lo hace y lo hace rápido!
Reconn
42

Si quieres resultados exactos y velocidad, prueba gmpy : gmpy.combdebe hacer exactamente lo que pides y es bastante rápido (por supuesto, como gmpyautor original, soy parcial ;-).

Alex Martelli
fuente
66
De hecho, gmpy2.comb()es 10 veces más rápido que choose()mi respuesta para el código: ¿ for k, n in itertools.combinations(range(1000), 2): f(n,k)dónde f()está gmpy2.comb()o choose()en Python 3.
Jfs
Puesto que usted es el autor del paquete, voy a dejar que te arregle el enlace roto para que apunte al lugar correcto ....
SeldomNeedy
@SeldomNeedy, el enlace a code.google.com es un lugar correcto (aunque el sitio está en modo de archivo ahora). Por supuesto, a partir de ahí, es fácil encontrar la ubicación de github, github.com/aleaxit/gmpy , y la de PyPI, pypi.python.org/pypi/gmpy2 , ya que enlaza con ambas. -)
Alex Martelli
@AlexMartelli Perdón por la confusión. La página muestra un 404 si javascript ha sido (selectivamente) deshabilitado. ¿Supongo que eso es para desalentar a los AI no autorizados a incorporar fuentes archivadas de Google Code Project con tanta facilidad?
SeldomNeedy
28

Si quieres un resultado exacto, úsalo sympy.binomial. Parece ser el método más rápido, sin duda.

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
fuente
22

Una traducción literal de la definición matemática es bastante adecuada en muchos casos (recordando que Python usará automáticamente aritmética de números grandes):

from math import factorial

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

Para algunas entradas que probé (p. Ej., N = 1000 r = 500), esto fue más de 10 veces más rápido que el revestimiento reducesugerido en otra respuesta (actualmente más votada). Por otro lado, es superado por el fragmento proporcionado por @JF Sebastian.

Todd Owen
fuente
11

Comenzando Python 3.8, la biblioteca estándar ahora incluye la math.combfunción para calcular el coeficiente binomial:

math.comb (n, k)

cuál es la cantidad de formas de elegir k elementos de n elementos sin repetición
n! / (k! (n - k)!):

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

Aquí hay otra alternativa. Este se escribió originalmente en C ++, por lo que se puede transferir a C ++ para un entero de precisión finita (por ejemplo, __int64). La ventaja es que (1) involucra solo operaciones enteras y (2) evita hinchar el valor entero haciendo pares sucesivos de multiplicación y división. He probado el resultado con el triángulo Pascal de Nas Banov, obtiene la respuesta correcta:

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

Justificación: para minimizar el número de multiplicaciones y divisiones, reescribimos la expresión como

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

Para evitar el desbordamiento de multiplicación tanto como sea posible, evaluaremos en el siguiente orden ESTRICTO, de izquierda a derecha:

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

Podemos mostrar que la aritmética de enteros operada en este orden es exacta (es decir, sin error de redondeo).

Wirawan Purwanto
fuente
5

Mediante la programación dinámica, la complejidad del tiempo es Θ (n * m) y la complejidad del espacio Θ (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
fuente
4

Si su programa tiene un límite superior para n(digamos n <= N) y necesita calcular repetidamente nCr (preferiblemente por >> Nveces), el uso de lru_cache puede brindarle un gran aumento de rendimiento:

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 construcción de la memoria caché (que se hace implícitamente) lleva O(N^2)tiempo. Cualquier llamada posterior a nCrregresará O(1).

yzn-pku
fuente
4

Puede escribir 2 funciones simples que en realidad resultan ser aproximadamente 5-8 veces más rápidas que usar scipy.special.comb . De hecho, no necesita importar ningún paquete adicional, y la función es bastante fácil de leer. El truco consiste en utilizar la memorización para almacenar valores calculados previamente y utilizar la definición 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 comparamos tiempos

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
fuente
En estos días hay un decorador de memoria en functools llamado lru_cache que podría simplificar su código.
erizo demente
2

Es bastante fácil con sympy.

import sympy

comb = sympy.binomial(n, r)
Poli
fuente
2

Usando solo la biblioteca estándar distribuida con Python :

import itertools

def nCk(n, k):
    return len(list(itertools.combinations(range(n), k)))
MarianD
fuente
3
No creo que su complejidad de tiempo (y uso de memoria) sea aceptable.
xmcp
2

La fórmula directa produce enteros grandes cuando n es mayor que 20.

Entonces, otra respuesta más:

from math import factorial

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

corto, preciso y eficiente porque esto evita los grandes enteros de Python al quedarse con largos.

Es más preciso y más rápido cuando se compara con 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
fuente
¡Esto está mal! Si n == r, el resultado debería ser 1. Este código devuelve 0.
reyammer
Más precisamente, debería ser en range(n-r+1, n+1)lugar de range(n-r,n+1).
reyammer
1

Este es el código @ killerT2333 que usa el decorador de memoria incorporado.

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))
erizo demente
fuente
1

Aquí hay un algoritmo eficiente para ti

for i = 1.....r

   p = p * ( n - i ) / i

print(p)

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

Entonces, simplemente ejecute el ciclo de 1 a r puede obtener el resultado.

kta
fuente
0

Probablemente sea lo más rápido que pueda hacerlo en Python puro para entradas razonablemente grandes:

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
fuente
0

Esta función está muy optimizada.

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
fuente