Statistiche: combinazioni in Python


122

Ho bisogno di calcolare combinatorials (nCr) in Python, ma non riesco a trovare la funzione di farlo in math, numpyo stat librerie. Qualcosa di simile a una funzione del tipo:

comb = calculate_combinations(n, r)

Ho bisogno del numero di combinazioni possibili, non delle combinazioni effettive, quindi itertools.combinationsnon mi interessa.

Infine, voglio evitare di usare i fattoriali, poiché i numeri per i quali calcolerò le combinazioni possono diventare troppo grandi e i fattoriali saranno mostruosi.

Sembra una domanda DAVVERO facile da rispondere, tuttavia sono sommerso dalle domande sulla generazione di tutte le combinazioni effettive, che non è quello che voglio.

Risposte:


121

Vedi scipy.special.comb (scipy.misc.comb nelle versioni precedenti di scipy). Quando exactè False, utilizza la funzione gammaln per ottenere una buona precisione senza impiegare molto tempo. Nel caso esatto restituisce un numero intero con precisione arbitraria, che potrebbe richiedere molto tempo per il calcolo.


5
scipy.misc.combè deprecato a favore di scipy.special.combdalla versione 0.10.0.
Dilawar

120

Perché non scriverlo da solo? È una battuta o simile:

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 - stampa del triangolo di 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. modificato per sostituire int(round(reduce(mul, (float(n-i)/(i+1) for i in range(k)), 1))) con in int(reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1))modo che non sbagli per grandi N / K


26
+1 per aver suggerito di scrivere qualcosa di semplice, per l'utilizzo di reduce e per la bella demo con il triangolo pascal
jon_darkstar

6
-1 perché questa risposta è sbagliata: print factorial (54) / (factorial (54 - 27)) / factorial (27) == nCk (54, 27) dà False.
robert king,

3
@ robertking - Ok, eri meschino e tecnicamente corretto. Quello che ho fatto era inteso come illustrazione di come scrivere la propria funzione; Sapevo che non è accurato per N e K abbastanza grandi a causa della precisione in virgola mobile. Ma possiamo aggiustarlo - vedi sopra, ora non dovrebbe sbagliare per i grandi numeri
Nas Banov

9
Questo probabilmente sarebbe veloce in Haskell, ma sfortunatamente non in Python. In realtà è piuttosto lento rispetto a molte altre risposte, ad esempio @Alex Martelli, JF Sebastian e la mia.
Todd Owen

9
Per Python 3, ho dovuto anche from functools import reduce.
Velizar Hristov

52

Una rapida ricerca sul codice google dà (utilizza la formula dalla risposta di @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()è 10 volte più veloce (testato su tutte le coppie 0 <= (n, k) <1e3) che scipy.misc.comb()se avessi bisogno di una risposta esatta.

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

Una bella soluzione che non richiede alcun pacchetto
Edward Newell

2
FYI: La formula menzionata è qui: en.wikipedia.org/wiki/…
jmiserez

Questa choosefunzione dovrebbe avere molti più voti positivi! Python 3.8 ha math.comb, ma ho dovuto usare Python 3.6 per una sfida e nessuna implementazione ha dato risultati esatti per interi molto grandi. Questo lo fa e lo fa velocemente!
riconnessione il

42

Se vuoi risultati esatti e velocità, prova gmpy - gmpy.combdovrebbe fare esattamente quello che chiedi, ed è abbastanza veloce (ovviamente, come gmpyl'autore originale, sono di parte ;-).


6
Infatti, gmpy2.comb()è 10 volte più veloce rispetto choose()dalla mia risposta per il codice: for k, n in itertools.combinations(range(1000), 2): f(n,k)in cui f()o è gmpy2.comb()o choose()su Python 3.
JFS

Dato che sei l'autore del pacchetto, ti permetterò di aggiustare il collegamento interrotto in modo che punti al posto giusto ....
Raramente

@SeldomNeedy, il collegamento a code.google.com è un posto giusto (anche se il sito è ora in modalità di archiviazione). Ovviamente da lì è facile trovare la posizione github, github.com/aleaxit/gmpy , e quella PyPI, pypi.python.org/pypi/gmpy2 , poiché si collega a entrambi! -)
Alex Martelli

@AlexMartelli Scusa per la confusione. La pagina visualizza un 404 se javascript è stato (selettivamente) disabilitato. Immagino che sia per scoraggiare le intelligenze artificiali canaglia dall'incorporare le fonti archiviate di Google Code Project abbastanza facilmente?
rado

28

Se vuoi un risultato esatto, usa sympy.binomial. Sembra essere il metodo più veloce, senza dubbio.

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

22

Una traduzione letterale della definizione matematica è abbastanza adeguata in molti casi (ricordando che Python userà automaticamente l'aritmetica dei grandi numeri):

from math import factorial

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

Per alcuni input che ho testato (ad esempio n = 1000 r = 500) questo era più di 10 volte più veloce di quello reducesuggerito in un'altra risposta (attualmente con il voto più alto). D'altra parte, è superato dallo snippit fornito da @JF Sebastian.


11

Iniziando Python 3.8, la libreria standard ora include la math.combfunzione per calcolare il coefficiente binomiale:

math.comb (n, k)

che è il numero di modi per scegliere k elementi da n elementi senza ripetizione
n! / (k! (n - k)!):

import math
math.comb(10, 5) # 252

10

Ecco un'altra alternativa. Questo è stato originariamente scritto in C ++, quindi può essere backportato in C ++ per un numero intero a precisione finita (ad esempio __int64). Il vantaggio è che (1) coinvolge solo operazioni su interi e (2) evita di gonfiare il valore intero eseguendo coppie successive di moltiplicazione e divisione. Ho testato il risultato con il triangolo Pascal di Nas Banov, ottiene la risposta corretta:

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

Motivazione: per ridurre al minimo il numero di moltiplicazioni e divisioni, riscriviamo l'espressione come

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

Per evitare il più possibile l'overflow della moltiplicazione, valuteremo nel seguente ordine RIGOROSO, da sinistra a destra:

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

Possiamo mostrare che l'aritmatica dei numeri interi operata in questo ordine è esatta (cioè nessun errore di arrotondamento).


5

Usando la programmazione dinamica, la complessità temporale è Θ (n * m) e la complessità spaziale Θ (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]

4

Se il tuo programma ha un limite superiore a n(diciamo n <= N) e deve calcolare ripetutamente nCr (preferibilmente per >> Nvolte), l'uso di lru_cache può darti un enorme aumento delle prestazioni:

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 costruzione della cache (che viene eseguita implicitamente) richiede O(N^2)tempo. Eventuali chiamate successive a nCrtorneranno in O(1).


4

Puoi scrivere 2 semplici funzioni che in realtà risultano essere circa 5-8 volte più veloci rispetto all'utilizzo di scipy.special.comb . In effetti, non è necessario importare pacchetti aggiuntivi e la funzione è abbastanza facilmente leggibile. Il trucco è usare la memoizzazione per memorizzare valori calcolati in precedenza e utilizzare la definizione di 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))

Se confrontiamo i tempi

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

In questi giorni c'è un decoratore memoize in functools chiamato lru_cache che potrebbe semplificare il tuo codice?
riccio demente

2

È abbastanza facile con sympy.

import sympy

comb = sympy.binomial(n, r)

2

Utilizzando solo la libreria standard distribuita con Python :

import itertools

def nCk(n, k):
    return len(list(itertools.combinations(range(n), k)))

3
non penso che la sua complessità temporale (e l'utilizzo della memoria) sia accettabile.
xmcp

2

La formula diretta produce numeri interi grandi quando n è maggiore di 20.

Quindi, ancora un'altra risposta:

from math import factorial

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

breve, preciso ed efficiente perché questo evita i grandi interi di Python attenendosi ai lunghi.

È più preciso e veloce se confrontato 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

Questo è sbagliato! Se n == r, il risultato dovrebbe essere 1. Questo codice restituisce 0.
reyammer

Più precisamente, dovrebbe essere range(n-r+1, n+1)invece di range(n-r,n+1).
Reyammer

1

Questo è il codice @ killerT2333 che utilizza il decoratore di memoizzazione incorporato.

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

1

Ecco un algoritmo efficiente per te

for i = 1.....r

   p = p * ( n - i ) / i

print(p)

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

Quindi basta eseguire il ciclo da 1 a r per ottenere il risultato.


0

Probabilmente è il più veloce possibile in puro Python per input ragionevolmente grandi:

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

0

Questa funzione è molto ottimizzata.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.