Статистика: комбінації в Python


122

Мені потрібно обчислити combinatorials (NCR) в Python , але не може знайти функцію , щоб зробити це в math, numpyабо stat бібліотеках. Щось на зразок функції типу:

comb = calculate_combinations(n, r)

Мені потрібна кількість можливих комбінацій, а не фактичні комбінації, тому itertools.combinationsмене не цікавить.

Нарешті, я хочу уникати використання фабрикантів, оскільки число, для якого я буду обчислювати комбінації, може стати занадто великим, а фактичні факти будуть жахливими.

Це здається дійсно легким для відповіді на питання, однак мене занурюють питання щодо генерування всіх фактичних комбінацій, а це не те, чого я хочу.

Відповіді:


121

Дивіться scipy.special.comb (scipy.misc.comb у старих версіях scipy). Коли exactFalse, він використовує функцію gammaln, щоб отримати хорошу точність, не забираючи багато часу. У такому випадку він повертає довільну точність цілого числа, для обчислення якого може знадобитися багато часу.


5
scipy.misc.combзастаріла на користь scipy.special.combверсії з моменту 0.10.0.
Ділавар

120

Чому б не написати це самостійно? Це однолінійний або такий:

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

Тест - друк трикутника Паскаля:

>>> 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. відредагований замінити int(round(reduce(mul, (float(n-i)/(i+1) for i in range(k)), 1))) з int(reduce(mul, (Fraction(n-i, i+1) for i in range(k)), 1))так буде не помиляється для великого N / K


26
+1 за те, що пропонують написати щось просте, для використання скорочення та для прикольної демонстрації з
паскальним

6
-1 тому, що ця відповідь невірна: друк факторіал (54) / (факторіал (54 - 27)) / фактор (27) == nCk (54, 27) дає помилковий.
Роберт Кінг

3
@robertking - Добре, ви були і дрібними, і технічно правильними. Те, що я робив, малося на увазі як ілюстрація того, як написати власну функцію; Я знав, що це не точно для досить великих N і K через точність плаваючої точки. Але ми можемо це виправити - дивіться вище, тепер це не повинно помилятися у великих числах
Нас Банов

9
Це, мабуть, буде швидко в Haskell, але не на жаль, Python. Це насправді досить повільно порівняно з багатьма іншими відповідями, наприклад @Alex Martelli, JF Sebastian і моєю власною.
Тодд Оуен

9
Для Python 3 я також повинен був from functools import reduce.
Велізар Христов

52

Швидкий пошук коду Google дає (він використовує формулу з відповіді @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 разів швидше (тестується на всіх парах <<((n, k) <1e3), ніж scipy.misc.comb()якщо вам потрібна точна відповідь.

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

Гарне рішення, яке не вимагає ніякого кг
Едвард Ньюелл

2
FYI: Згадана формула знаходиться тут: en.wikipedia.org/wiki/…
jmiserez

Ця chooseфункція повинна мати набагато більше голосів! У Python 3.8 є math.comb, але мені довелося використовувати Python 3.6 для виклику, і жодна реалізація не дала точних результатів для дуже великих цілих чисел. Це робить і робить це швидко!
відновіться

42

Якщо ви хочете точні результати і швидкість, спробуйте gmpy - gmpy.combповинні робити те , що ви просите, і це досить швидко (звичайно, як gmpy«s оригінальний автор, я маю в зміщена ;-).


6
Дійсно, gmpy2.comb()в 10 разів швидше , ніж choose()від моєї відповіді на код: for k, n in itertools.combinations(range(1000), 2): f(n,k)де f()знаходиться або gmpy2.comb()або choose()на Python 3.
JFS

Оскільки ви автор пакету, я дозволю вам виправити пошкоджене посилання, щоб воно вказувало на потрібне місце ....
SeldomNeedy

@SeldomNeedy, посилання на code.google.com - це одне правильне місце (хоча сайт зараз знаходиться в архівному режимі). Звичайно, звідти легко знайти місце розташування github, github.com/aleaxit/gmpy та PyPI, pypi.python.org/pypi/gmpy2 , оскільки воно посилається на обидва! -)
Алекс Мартеллі,

@AlexMartelli Вибачте за плутанину. Сторінка відображає 404, якщо javascript (вибірково) відключений. Я думаю, що це відмовить шахрайським ШІ досить легко легко включати в архіви джерела Google Code Project?
SeldomNeedy

28

Якщо ви хочете точного результату, використовуйте sympy.binomial. Здається, це найшвидший метод, руки вниз.

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

Буквальний переклад математичного визначення є цілком адекватним у багатьох випадках (пам'ятаючи, що Python автоматично використовуватиме арифметику великої кількості):

from math import factorial

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

Для деяких тестів, які я перевірив (наприклад, n = 1000 r = 500), це було в 10 разів швидше, ніж один лайнер, reduceзапропонований в іншій (на даний момент найвищий голос) відповіді. З іншого боку, це виходить у виконанні фрагментом, який надає @JF Себастьян.


11

Починаючи Python 3.8, стандартна бібліотека тепер включає math.combфункцію для обчислення біноміального коефіцієнта:

math.comb (n, k)

яка кількість способів вибирати k елементів із n елементів без повторення
n! / (k! (n - k)!):

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

10

Ось ще одна альтернатива. Цей спочатку був написаний на C ++, тому його можна підтримувати на C ++ для цілого числа з кінцевою точністю (наприклад, __int64). Перевага полягає в тому, що (1) вона включає лише цілі операції, і (2) вона уникає роздуття цілого значення, роблячи послідовні пари множення та ділення. Я перевірив результат за допомогою трикутника Паскаля Наса Банова, він отримує правильну відповідь:

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

Обгрунтування: Щоб мінімізувати число множень і ділення, ми перепишемо вираз як

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

Щоб максимально уникнути переповнення множин, ми будемо оцінювати у наступному порядку STRICT зліва направо:

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

Ми можемо показати, що ціла арифмічна, керована в цьому порядку, є точною (тобто немає помилки округлення).


5

За допомогою динамічного програмування часова складність становить is (n * m), а складність простору Θ (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

Якщо ваша програма має верхню межу n(скажімо n <= N) і потребує багаторазового обчислення nCr (краще за >> Nраз), використання lru_cache може підвищити ефективність роботи:

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)

Конструювання кеша (який робиться неявно) займає O(N^2)час. Будь-які наступні дзвінки на номер nCrповернуться O(1).


4

Ви можете написати 2 прості функції, які фактично виявляються приблизно в 5-8 разів швидшими, ніж використання scipy.special.comb . Насправді, вам не потрібно імпортувати зайвих пакетів, і ця функція досить легко читається. Трюк полягає у використанні мемунізації для зберігання раніше обчислених значень та з використанням визначення 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))

Якщо ми порівняємо часи

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

На сьогоднішній день у фунікулерах під назвою lru_cache є декоратор пам'яті, який може спростити ваш код?
дементований їжак


2

Використовуючи лише стандартну бібліотеку, що поширюється з Python :

import itertools

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

3
Я не думаю, що його складність у часі (і використання пам'яті) є прийнятною.
xmcp

2

Пряма формула виробляє великі цілі числа, коли n більше 20.

Отже, ще одна відповідь:

from math import factorial

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

короткий, точний та ефективний, оскільки це дозволяє уникнути великих цілих чисел пітона, дотримуючись довгі.

Він більш точний і швидший у порівнянні з 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

Це неправильно! Якщо n == r, результат повинен бути 1. Цей код повертає 0.
reyammer

Точніше, це має бути range(n-r+1, n+1)замість range(n-r,n+1).
reyammer

1

Це @ killerT2333 код за допомогою вбудованого декоратора запам'ятовування.

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

Ось ефективний алгоритм для вас

for i = 1.....r

   p = p * ( n - i ) / i

print(p)

Наприклад, nCr (30,7) = факт (30) / (факт (7) * факт (23)) = (30 * 29 * 28 * 27 * 26 * 25 * 24) / (1 * 2 * 3 * 4 * 5 * 6 * 7)

Тому просто запустіть цикл від 1 до r, щоб отримати результат.


0

Це, мабуть, так швидко, як ви можете зробити це в чистому пітоні для досить великих входів:

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.