Search
Extra figure

Alternative Figure 3:

To be edited...

# Import modules 
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from scipy.stats import hypergeom
from scipy.stats import binned_statistic as binsta
from scipy.special import logsumexp
from util import *
import palettable as pal
clrx = pal.cartocolors.qualitative.Prism_10.mpl_colors
clr = tuple(x for n,x in enumerate(clrx) if n in [1,2,4,5,6])
clr2 = pal.cartocolors.sequential.agSunset_7.mpl_colors
import matplotlib.gridspec as gridspec
import matplotlib.patches as patches

import plotly.graph_objects as go
import plotly.tools as tls
from plotly.offline import plot, iplot, init_notebook_mode
from IPython.core.display import display, HTML
init_notebook_mode(connected = True)
config={'showLink': False, 'displayModeBar': False}

# CCP, the Coupon Collector's Problem
def ccp_sample(c,pool=60):
    return len(set(np.random.choice(pool,c)))

# Draw overlap
def nab_sample(s,na,nb,pool=60):
    sa = np.random.hypergeometric(s,pool-s,na)
    nab = np.random.hypergeometric(sa,pool-sa,nb)
    return nab

# Overlap between two PCRs of depth c and overlap s
def pcr_sample(c,s):
    na = ccp_sample(c)
    nb = ccp_sample(c)
    return nab_sample(s,na,nb),na,nb

# Draw na and nb samples from two populations of size pool_a and pool_b, with true overlap s
# and return empirical overlap between na and nb
# note that this is basically the same as nab_sample, but with two different size pools!
def nab_sample_unequal(s,na,nb,pool_a,pool_b):
    sa = np.random.hypergeometric(s,pool_a-s,na)
    nab = np.random.hypergeometric(sa,pool_b-sa,nb)
    return nab


def p_ccp(c, pool=60):
    p = np.zeros([c+1,pool+1])
    p[0,0] = 1;
    for row in range(1,c+1):
        for k in range(1,np.min([row+2,pool+1])):
            p[row,k] = p[row-1,k]*k/pool + p[row-1,k-1]*(1-(k-1)/pool)
    return p[-1,:]

def p_overlap(na,nb,nab,pool=60):
    p_s = np.zeros(pool+1)
    # reference: hypergeom.pmf(outcome, Total, hits, Draws, loc=0)
    for s in np.arange(pool+1):
        # p_sa is the probability that we'd get sa from the overlap (s), just in na draws of a
        p_sa = hypergeom.pmf(np.arange(pool+1),pool,s,na)
        # p_nab_given_sa is the probability of getting that nab, given sa
        p_nab_given_sa = hypergeom.pmf(nab,pool,np.arange(pool+1),nb)
        p_s[s] = np.dot(p_sa,p_nab_given_sa)
    return p_s/np.sum(p_s)

def e_overlap(na,nb,nab,pool=60):
    p_s = p_overlap(na,nb,nab,pool=pool)
    return np.dot(np.arange(pool+1),p_s)


def credible_interval(na,nb,nab,pct=90,pool=60):
    p_s = p_overlap(na,nb,nab,pool=pool)
    cdf = np.cumsum(p_s)
    ccdf = np.flipud(np.cumsum(np.flipud(p_s)))
    # adjust for fractions vs percents; put everything as a fraction
    if pct > 1:
        pct = pct/100
    cutoff = (1-pct)/2
    # get the lower bound. 
    # it's the first index at which cdf ≥ cutoff
    try:
        lower = np.where(cdf >= cutoff)[0][0]
    except IndexError:
        lower = 0
    # get the upper bound
    # it's the first index at which ccdf ≥ 0.05
    try:
        upper = np.where(ccdf >= cutoff)[0][-1]
    except IndexError:
        upper=pool
    expectation = np.dot(np.arange(pool+1),p_s)
    # Sanity and indexing check: uncomment this line to see true tail probability ≤ 0.05
    # print([cdf[lower-1],(1-ccdf[upper+1])])
    return lower,expectation,upper


def p_nab_given_c(s,c,pool=60):
    pna = p_ccp(c)
    pnb = p_ccp(c)
    nas = np.arange(1,len(pna))
    nbs = np.arange(1,len(pnb))
    p_gen = np.zeros([pool+1,pool+1,pool+1])
    for na in nas:
        p_sa = hypergeom.pmf(np.arange(pool+1),pool,s,na)
        for nb in nbs:
            pna_pnb = pna[na] * pnb[nb]
            for nab in range(0,np.minimum(na,nb)):
                p_nab_given_sa = hypergeom.pmf(nab,pool,np.arange(pool+1),nb)
                p_nab_given_s = np.dot(p_sa,p_nab_given_sa)
                p_gen[na,nb,nab] = p_nab_given_s * pna_pnb
    return p_gen

def p_shat_given_sc(s,c,shat,pool=60):
    masses = p_nab_given_c(s,c,pool=pool)
    if np.sum(masses)<0.99:
        print('Swapping to Monte Carlo')
        return p_shat_given_sc_montecarlo(s,c,shat,pool=pool)
    hist = binsta(np.ravel(shat),np.ravel(masses),statistic='sum',bins=(np.arange(pool+2)-0.5))
    return hist

def p_shat_given_sc_montecarlo(s,c,shat,pool=60,n_mc=int(1e5)):
    masses = np.zeros([pool+1,pool+1,pool+1])
    for ii in range(n_mc):
        nab,na,nb = pcr_sample(c,s)
        masses[na,nb,nab] += 1
    hist = binsta(np.ravel(shat),np.ravel(masses/n_mc),statistic='sum',bins=(np.arange(pool+2)-0.5))
    return hist

def compute_all_estimates(pool=60):
    shat = np.zeros([pool+1,pool+1,pool+1])
    for na in range(1,pool+1):
        for nb in range(1,pool+1):
            for nab in range(0,np.minimum(na+1,nb+1)):
                shat[na,nb,nab] = e_overlap(na,nb,nab,pool=pool)
    return shat

def p_overlap_unequal(na,nb,nab,pool_a,pool_b):
    # all loops are in terms of pool_a, which is assumed to be ≤ pool_b. 
    p_s = np.zeros(pool_a+1)
    # reference: hypergeom.pmf(outcome, Total, hits, Draws, loc=0)
    for s in np.arange(pool_a+1):
        # p_sa is the probability that we'd get sa from the overlap (s), just in na draws of a
        p_sa = hypergeom.pmf(np.arange(pool_a+1),pool_a,s,na)
        # p_nab_given_sa is the probability of getting that nab, given sa
        p_nab_given_sa = hypergeom.pmf(nab,pool_b,np.arange(pool_a+1),nb)
        p_s[s] = np.dot(p_sa,p_nab_given_sa)
    return p_s/np.sum(p_s)

def e_overlap_unequal(na,nb,nab,pool_a,pool_b):
    # TODO. Code expects that pool_b > pool_a...
    p_s = p_overlap_unequal(na,nb,nab,pool_a,pool_b)
    return np.dot(np.arange(pool_a+1),p_s)

def incremental_sample_nab(s,na_min=30,na_max=50):    
    # total repertoire size
    pool = 60
    
    # preallocate the array to be returned
    nabs = np.zeros(pool+1)

    # represent the items in the set by ints from 0 to 60, and from 60-s to 120-s
    # In this way, these sets will share s items
    repertoire_a = np.arange(60)
    repertoire_b = np.arange(60-s,120-s)

    # Create random samples from each repertoire
    sampled_a = np.random.choice(repertoire_a,size=na_min,replace=False)
    sampled_b = np.random.choice(repertoire_b,size=na_min,replace=False)

    # Set Diff to get the items that WEREN'T sampled
    unsampled_a = np.setdiff1d(repertoire_a, sampled_a)
    unsampled_b = np.setdiff1d(repertoire_b, sampled_b)

    # Compute the length of the intersection
    nabs[na_min] = len(np.intersect1d(sampled_a,sampled_b,assume_unique=True))

    # Now shuffle the unsampled items so that choosing the last item
    # from each array is equivalent to drawing from the ordered array at random
    np.random.shuffle(unsampled_a) #inplace shuffle
    np.random.shuffle(unsampled_b) #inplace shuffle

    # Iterate over values of na from 31 up to 50, incrementally gaining one more
    # sample each time.
    for na in np.arange(na_min+1,na_max+1):
        # Sample 1 more from a
        sampled_a = np.append(sampled_a,unsampled_a[-1])
        unsampled_a = np.delete(unsampled_a,-1)

        # Sample 1 more from b
        sampled_b = np.append(sampled_b,unsampled_b[-1])
        unsampled_b = np.delete(unsampled_b,-1)

        # Compute new intersection size
        nabs[na] = len(np.intersect1d(sampled_a,sampled_b,assume_unique=True))

    return nabs
# Calculate arrays for steps
shat = np.load('shat_60.npy')
planted_new = []
recovered_new = []
recovered_pts_new = []
pool=60
reps=4

# Temps
x1 = 30
# Calculate list for 30-50 steps 
for i in range(0, 7, 1):
    recovered = np.zeros([6,reps,pool+1])
    planted = np.zeros([6,reps,pool+1])
    recovered_pts = np.zeros([6,reps,pool+1])
    
    # Calculate for 30-32, 33-35, 36-38, 39-41, 42-44, 45-47, 48-50
    nas = [x1, x1+1, x1+2]
    for idxa,na in enumerate(nas):
        nb = na
        for s in np.arange(0,pool+1,1):
            for rep in range(3):
                # ------------------------- CHECK ME OUT -----------------------------
                # ??????????? Change code to no longer parameterized na,nb ???????????
                nabs = incremental_sample_nab(s)
                # ??????????? nabs[na] is the number of items shared between the two sets when na samples are drawn from each ???????????
                nab = nabs[na]
                planted[idxa,rep,s] = s
                recovered[idxa,rep,s] = e_overlap(na,nb,nab)
                recovered_pts[idxa,rep,s] = 2*nab/(na+nb)
                
    # Flatten arrays 
    for index in range(3):
        planted_new.append( planted[index,:,:].flatten() )
        recovered_new.append( recovered[index,:,:].flatten() )
        recovered_pts_new.append( recovered_pts[index,:,:].flatten() )
        
    # Enlarge the sample size
    x1 = x1 + 3
# Build and show figure 
z = np.zeros(60)
d = np.arange(1,60)
data = []

trace0 = go.Scatter(x = d , 
                    y = d, 
                    line = dict(width=1, color = 'rgb(0,0,0)'), 
                    name = 'Reference', 
                    showlegend = False,
                    hoverinfo = 'skip')

for index in range(len(planted_new)):
    
    trace1 = go.Scatter(x=planted_new[index], 
                        y=recovered_new[index],
                        mode='markers',
                        name = 'BRO',
                        visible = False,
                        marker=dict(color='rgb(255,84,0)'),
                        hovertemplate = "%{y: .3f}")
    
    trace2 = go.Scatter(x=planted_new[index], 
                        y=pool*recovered_pts_new[index],
                        mode='markers',
                        name = '    S',
                        visible = False,
                        marker_symbol = "x",
                        marker=dict(color='rgba(0,171,255, 1)'),
                        hovertemplate = "%{y: .3f}")
    
    data.append(trace1)
    data.append(trace2)

data.append(trace0)

# Toggle first trace to be visible
data[0]['visible'] = True
data[1]['visible'] = True


# Create steps and slider
steps = []
for i in range(0, len(planted_new)):
    step = dict(
        method = 'restyle',  
        args = ['visible', [False]*43],
        label = str(i+30)) 
    
    step['args'][1][i*2] = True 
    step['args'][1][i*2+1] = True 
    step['args'][1][-1] = True
    steps.append(step)

sliders = [dict(
    active = 0,
    currentvalue = {'prefix':
                    "<i>Sample size</i>: <b>"},
    pad = {"t": 80, "b": 10},
    steps = steps
)]

# Setup the layout of the figure
layout = go.Layout(plot_bgcolor='rgb(255,255,255)',
                   xaxis_title="True overlap s",
                   yaxis_title='Estimated overlap \u015D', 
                   yaxis=dict(showspikes = True),
                   legend=dict(x=.85, 
                               y=.1, 
                               bordercolor="Gray",
                               borderwidth=1),
                   width = 650, 
                   height = 680,
                   hovermode = 'x unified',
                   sliders = sliders,
                   font = dict(size = 13),
                   margin=go.layout.Margin(l=50,
                                         r=50,
                                         b=60,
                                         t=35))

# Show plot
fig_enhanced = dict(data=data, layout=layout)
plot(fig_enhanced, filename = 'fig3_enhanced.html', config = config)
display(HTML('fig3_enhanced.html'))