<jupyter_start><jupyter_text>This is an interactive land probability calculator for Magic the Gathering.To start the interactive calculator, select"Cell""Run All"from above menu and then scroll to the bottom see the interactive probability calculator.You can specify with interactive sliders the number of lands and nonlands in the deck, and you'll get a clearly explained output showing you the probability after not just the first draw, but also after each mulligan. So far as I know, the only way to get the interactive slider is by running this notebook (static view doesn't get the slider).If you want to skip the probability explanations, then simply scroll to the bottom and see how probabilities vary as you adjust land counts and deck size with the sliders.This was a fun way to help me recall probability theory (DeGroot and Schervish, chapter 1), practice probability calculation in Python, learn how to use iPython Notebook interactively, and at the same have a helpful Magic: The Gathering tool.<NAME>Originally done in Dec 2014 as iPython notebookAdapted to Jupyter April 2019 (and made some minor explanation corrections)<jupyter_code>import ipywidgets as widgets
from sympy import binomial as combos
from IPython.display import display, HTML
# Ignore this cell (unless you intend to create your own notebooks with messy display output)

# At first all this code was integrated into land_probability() function. It was fewer
# lines of code but harder to read. This HTML and iPython Notebook messiness is not
# important to calculating probability. By pulling out the messy display code, it is
# easier to focus on and understand the probability aspects of land_probability().

# This can be re-used in other iPython/Jupyter Notebooks, simplifing HTML output for interact()

def title_HTML(title):
    return '<h3>' + title + '</h3>'

def intro_HTML(*args):
    lands, cards, drawn_cards, mulligans_so_far = args
    slider_inputs = {'lands: ': lands,
                     'cards: ': cards,
                     'number of cards drawn: ': drawn_cards,
                     'mulligans so far: ': mulligans_so_far}
    s = ''
    for key in slider_inputs:
        s += '<br>' + key + str(slider_inputs[key])
    return s

def start_HTML_table(headers):
    s = '<br><table>\n<tr>'
    for header in headers:
        s += '<th>' + header + '</th>'
    s += '</tr>'
    return s

def row_of_HTML_table(x_int, scipy_float):
    '''
    Args:
      x_int: integer
      scipy_float: float (scipy or regular)
    '''
    return '<tr><td>{0:.>6}</td><td>{1:.2%}</td></tr>'.format(x_int, float(scipy_float))
    # must coerce scipy float type into regular float type for format to work right
    
def finish_HTML_table(s, min_land, max_land, drawn_cards, mulligans_so_far,
                      below_land_prob, above_land_prob, target_land_prob, out_of_range):
    cumulative_p = 1
    for p in out_of_range[:-1]:
        cumulative_p *= p
    s += '</table><br>'
    s += '{0:.1%} chance of between {1} and {2} lands in this {3} card draw.<br>'.format(float(target_land_prob), min_land, max_land, drawn_cards)
    s += '{0:.1%} chance that number of lands will be outside this range.<br><br>'.format(float(1 - target_land_prob))
    s += '{0:.2%} chance that number of lands will be too low after {1} mulligan(s).<br>'.format(float(below_land_prob * cumulative_p), mulligans_so_far)
    s += '{0:.2%} chance that number of lands will be too high after {1} mulligan(s).<br><br>'.format(float(above_land_prob * cumulative_p), mulligans_so_far)
    return s<jupyter_output><empty_output><jupyter_text>In mathematical notation, the probability of exactly 2 land cards appearing in a 7 card draw from a 60 card deck with 20 lands is:$$\frac{\binom{40}{5}\binom{20}{2}}{60 \choose 7} = \frac{\frac{40!}{5!35!}\frac{20!}{2!18!}}{\frac{60!}{7!53!}} = .3237$$The denominator in all probability calculation represents the sample space (out of how many possibilities . . .).  In this case, it is the number of subsets of 7 cards possible out of 60 cards. This is exactly the definition of a combination. Plugging into the combination formula, using 60 choose 7.$${60 \choose 7} = \frac{60!}{7!53!}$$The first part of the numerator represents the number of nonland combinations possible. There are 5 nonlands in this hand, drawn from 40 nonlands. So the number of subsets of 5 cards possible out of 40 cards is:$${40 \choose 5} = \frac{40!}{5!35!}$$Similarly, the second part of the numerator is the number of land combinations possible when 2 lands are drawn from 20 lands:$${20 \choose 2} = \frac{20!}{2!18!}$$These two parts of the numerator are multiplied, because each single combination of nonlands can be combined with all the combinations of lands. The general form of this is the "multiplication rule" which states:> When all the outcomes in each part of an experiment can occur regardless of which outcomes have occured in the other parts, then the parts are multipled together.<jupyter_code>def land_probability(lands, cards, drawn_cards,
                     min_land, max_land, mulligans=0):
    out_of_range = []
    nonlands = cards - lands
    text = title_HTML("Probabilities for Lands Drawn:")
    for mulligans_so_far in range(mulligans+1):
        text += intro_HTML(lands, cards, drawn_cards - mulligans_so_far, mulligans_so_far)
        text += start_HTML_table(['Lands', 'Probability'])
        target_land_prob, below_land_prob, above_land_prob = 0, 0, 0
        for drawn_lands in range(0, drawn_cards + 1 - mulligans_so_far):
            p = (combos(nonlands, drawn_cards - drawn_lands - mulligans_so_far)
                 * combos(lands, drawn_lands)
                 / float(combos(cards, drawn_cards - mulligans_so_far)))
            if drawn_lands < min_land:
                below_land_prob += p
            elif drawn_lands > max_land:
                above_land_prob += p
            else:
                target_land_prob +=p
            text += row_of_HTML_table(drawn_lands, p)
        out_of_range.append(below_land_prob + above_land_prob)
        text = finish_HTML_table(text, min_land, max_land, drawn_cards - mulligans_so_far, mulligans_so_far,
                                 below_land_prob, above_land_prob, target_land_prob, out_of_range)
    display(HTML(text))<jupyter_output><empty_output><jupyter_text>Magic the Gathering intro packs recommend the following defaults for land construction:* 60 card deck* 24 lands in the deckIf you follow the recommended defaults, you can see that there is better than an 85% chance of starting with at least 2 lands when drawing your initial 7 card hand.It can be useful to think of land tappable creatures as lands for the purposes of this calculation. For example, if you have 20 lands and 4 Mystic Elves, that is essentially equivalent to 24 lands (well, not really, because there are many more ways to lose a Mystic Elf than a land . . . but it's still useful to think of your starting hand this way as you're not likely to lose a Mystic Elf on turn 1).You may want to consider mulligans in your calculation. Move the mulligan slider to "1" and you'll see the probabilities for the subsequent 6 card draw, as well as the chances of getting below or above your desired range of lands to start with. Move it to "2" and you'll see it go down to a 5 card draw (2nd mulligan).I assume for this analysis that mulligans automatically occur when below minimum required land or above maximum required land, but not otherwise. Therefore, the probability, p, of having too little land after 1 or 2 draws (2nd draw only if 1st draw outside of target range) is:p = p1(below) * (p0(below) + p0(above))where* p0 represents probability in a 7 card draw (zero mulligans)* p1 represents probability in a 6 card draw (1 mulligan)* p0(below) represents probability that the number of cards drawn is below the required minimum<jupyter_code>i = widgets.interact(land_probability,
             lands = widgets.IntSlider(min = 1, max = 50, step = 1, value = 24), 
             cards = widgets.IntSlider(min = 1, max = 200, step = 1, value = 60), 
             drawn_cards = widgets.IntSlider(min = 0, max = 20, step = 1, value = 7),
             min_land = widgets.IntSlider(min = 0, max = 3, step = 1, value = 2),
             max_land = widgets.IntSlider(min = 3, max = 7, step = 1, value = 5),
             mulligans = widgets.IntSlider(min = 0, max = 3, step = 1, value = 2)
             )<jupyter_output><empty_output>