TODO: The numerical/data-specific answer awaits the original SerologyData.csv dataset. The file was not supplied; no numerical output is fabricated.
TODO: The numerical/data-specific answer awaits the original SerologyData.csv dataset. The file was not supplied; no numerical output is fabricated.
TODO: The numerical/data-specific answer awaits the original SerologyData.csv dataset. The file was not supplied; no numerical output is fabricated.
Recompute the root conditional likelihoods at the fitted rate, then multiply by the uniform root prior and normalize. The factors cancel between numerator and denominator:
prob = initialise_prob(leaves)
felsenstein(prob, mu_max, len(leaves), left, right, left_time, right_time)
root_posterior = prob[-1] / prob[-1].sum()
print(dict(zip("ACGT", root_posterior)))
At the refined optimum, the posterior distribution is
Thus G is the most probable ancestral base, but it is not inferred with certainty. This conditions on the fitted rate and does not additionally average over uncertainty in that rate.
Evaluate the log-likelihood function on the specified interval, retaining the rate with the largest value. A grid spacing of finds ; refining locally avoids relying on a coarse-grid location when reporting three significant figures:
def evaluate(mu):
    p = initialise_prob(leaves)
    return felsenstein(p, mu, len(leaves), left, right, left_time, right_time)

coarse = np.linspace(.1, 1., 901)
centre = coarse[np.argmax([evaluate(mu) for mu in coarse])]
fine = np.linspace(max(.1, centre-.001), min(1., centre+.001), 2001)
mu_max = float(fine[np.argmax([evaluate(mu) for mu in fine])])
print(f"mu = {mu_max:.3g}; log-likelihood = {evaluate(mu_max):.8f}")
The maximum-likelihood estimate is to three significant figures, with refined value about and log-likelihood . Under the alternative per-replacement convention this same fitted transition matrix has ; these are equivalent parameterizations, not competing fitted trees.
For node with children , the phylogenetic pruning algorithm combines the two conditionally independent descendant subtrees:
The likelihood function is under a uniform ancestral prior. In the supplied numbering, children have smaller indices than their parents, so increasing node order is a valid bottom-up traversal:
def felsenstein(prob, mu, L, left, right, left_time, right_time):
    # All four entries are overwritten at every internal node.
    for node in range(L, 2*L-1):
        Tl = np.array([[jukes(a,b,left_time[node],mu)
                        for b in range(4)] for a in range(4)])
        Tr = np.array([[jukes(a,b,right_time[node],mu)
                        for b in range(4)] for a in range(4)])
        prob[node] = (Tl @ prob[left[node]]) * (Tr @ prob[right[node]])
    return float(np.log(prob[-1].mean()))

print(felsenstein(prob, .5, len(leaves), left, right, left_time, right_time))
At , the log-likelihood is . Internal rows must remain unnormalized in this implementation, because their magnitudes contribute to the tree likelihood function; normalization without retaining the removed factors would lose that information. The final root normalization required for a posterior probability is a separate step in part (e).
Initialize all eight leaves, not seven: every observed base contributes a one-hot vector in the order A, C, G, T. The reference to leaf rows in the prose is inconsistent with its eight-leaf tree.
def initialise_prob(leafList):
    alphabet = {base: j for j, base in enumerate("ACGT")}
    result = np.zeros((2*len(leafList)-1, 4), dtype=float)
    for i, base in enumerate(leafList):
        result[i, alphabet[base]] = 1.
    return result

leaves = ["G", "A", "T", "G", "G", "G", "C", "G"]
prob = initialise_prob(leaves)
print(prob[:len(leaves)])
The initialized leaf rows are:
0 0 1 0
1 0 0 0
0 0 0 1
0 0 1 0
0 0 1 0
0 0 1 0
0 1 0 0
0 0 1 0
The remaining seven internal-node rows initially contain zeros. Their eventual entries are conditional likelihoods of descendant observations given the ancestral base, rather than normalized posterior probabilities of that ancestral base.
Take to be the total instantaneous mutation rate away from a base, so each specified alternative has rate . Under the Jukes-Cantor model, the uniform component is stationary and the three contrast components decay as , giving
An implementation is:
def jukes(a, b, t, mu):
    decay = np.exp(-4*mu*t/3)
    return .25 + .75*decay if a == b else .25*(1-decay)

print(f"{jukes(2,2,1,.5):.3f} {jukes(2,3,1,.5):.3f}")
The two probabilities are and . The probabilities in any row sum to one, at the transition matrix is the identity, and at long times every entry tends to .
The Jukes-Cantor model is also parameterized by a rate for each specified replacement, in which case the exponential is and . The total-rate convention is used consistently below; it yields an optimum within the stated search interval, whereas the corresponding per-replacement rate is about .
Columns 22–28 form one run between alignment columns 21 and 29, corresponding to match states and . A self-repeating insertion state can emit a variable number of residues, so these seven alignment columns require one populated insertion state, not seven successive mandatory states.
Pool all nongap residues in this run to estimate its emission probabilities:
insert_columns = np.flatnonzero(~match_mask)
runs = np.split(insert_columns, np.flatnonzero(np.diff(insert_columns) > 1) + 1)
insert_emissions = []
for run in runs:
    if len(run):
        counts, n = emission_counts(A[:, run].ravel())
        insert_emissions.append({aa: count/n for aa, count in counts.items()})
        print(run+1, n, insert_emissions[-1])
There are 24 emitted residues, giving
All other amino acids have probability zero in this empirical emission probabilities. A conventional full architecture may provide an optional insertion state at every boundary, including the two ends, giving possible insertion positions; only one has an observed insertion here and is required by this alignment's populated insertion region.
For each alignment column, ignore gaps, form the empirical distribution of its amino acids, and calculate its Shannon entropy in bits. The requested alignment column information content is
Thus it is the relative entropy from the column distribution to the uniform distribution on twenty amino acids, not the ordinary Shannon entropy itself. A column containing one residue type has the maximum value bits.
information = []
for j in range(A.shape[1]):
    counts, n = emission_counts(A[:, j])
    p = np.array(list(counts.values()), dtype=float) / n
    H = -np.sum(p * np.log2(p))
    information.append(float(np.log2(20) - H))
print(np.round(information, 4))
The values for all 79 alignment columns are as follows, in bits. Each line lists its one-based column interval followed by the corresponding values in order:
01–10: 1.9025, 1.5964, 1.9025, 3.8187, 1.9025, 1.5964, 1.9025, 3.5577, 1.9025, 1.9025
11–20: 2.4420, 3.3355, 1.5964, 4.3219, 3.8187, 1.6803, 3.0975, 3.3355, 3.0975, 2.4420
21–30: 2.5692, 2.8219, 3.5106, 3.3219, 4.3219, 4.3219, 2.8000, 2.0703, 2.6574, 1.8003
31–40: 2.1938, 1.9163, 3.7784, 3.2606, 1.6803, 1.6803, 2.6642, 2.5692, 2.8753, 2.2631
41–50: 2.8753, 3.3355, 1.6803, 2.9703, 3.3355, 2.7914, 2.2086, 2.5692, 4.3219, 1.3742
51–60: 2.2631, 4.3219, 4.3219, 3.8187, 4.3219, 3.5577, 2.8753, 1.9025, 2.7914, 2.0409
61–70: 3.3355, 2.7370, 2.4309, 2.4309, 2.5692, 2.0409, 2.5692, 3.3355, 2.9298, 3.5577
71–79: 1.9025, 1.8187, 4.3219, 2.2631, 3.8187, 2.4309, 1.9025, 1.9025, 1.9025
A high score indicates conservation among the observed nongap letters, but does not itself imply low gap fraction. For example, an insertion column represented by only one observed nongap residue has maximal information by this calculation; its tiny sample size still makes that estimate uncertain.
For each match state, count the nongap amino acids and divide by the number of nongap observations in that column. A gap is not one of the twenty emitted amino acids; including it in the denominator would make the emission probabilities sum to less than one:
def emission_counts(column):
    counts = Counter(str(x) for x in column if x != ".")
    n = sum(counts.values())
    if n == 0:
        raise ValueError("an all-gap column has no empirical emission distribution")
    return counts, n

match_emissions = {}
for state, j in enumerate(match_columns, 1):
    counts, n = emission_counts(A[:, j])
    match_emissions[state] = {aa: count/n for aa, count in counts.items()}
    print(state, j+1, match_emissions[state])
The table gives an exact compact representation of every emission probability: divide each displayed count by the denominator in its row; all unlisted amino acids have probability zero. The state number follows the order of match states, while the column refers to the original multiple sequence alignment.
StateAlignment columnNongap counts
119I:1, K:1, R:1, S:2, V:3, Y:1
229D:1, E:2, G:1, I:1, L:1, Q:2, S:1
339E:2, I:1, K:1, Q:1, R:3, T:1
449L:8, M:1
559E:3, G:1, L:2, N:1, Q:1, S:1
669A:1, D:2, G:1, K:1, N:1, S:2, T:1
779A:1, E:3, I:2, L:1, M:1, V:1
889I:2, L:7
999D:3, E:1, K:1, L:2, N:1, Q:1
10109D:1, E:3, G:1, N:1, P:1, S:2
11119A:5, D:1, G:1, P:1, T:1
12129I:1, L:7, Y:1
13139E:1, F:1, G:1, H:2, M:1, Q:2, T:1
14149F:9
15159F:1, L:8
16169E:1, I:1, L:1, N:3, Q:1, R:1, Y:1
17179D:6, E:2, S:1
18189C:7, F:1, S:1
19199K:1, N:6, R:2
20209E:1, F:1, I:5, L:1, V:1
21219K:3, P:1, Q:1, S:4
22297K:4, P:1, R:1, S:1
23307E:2, F:1, G:1, I:1, K:1, V:1
24317D:3, E:1, G:1, K:1, S:1
25328D:1, E:1, G:1, K:1, N:1, R:3
26338D:7, N:1
27348A:1, P:6, S:1
28359C:1, H:1, I:1, K:1, M:1, R:1, T:3
29369E:1, F:1, G:1, L:1, Q:3, W:1, Y:1
30379I:5, L:1, T:2, Y:1
31389A:1, G:1, P:3, S:4
32399I:1, K:6, L:1, R:1
33409F:1, N:1, Q:4, T:2, V:1
34419A:1, F:1, I:1, V:6
35429I:1, L:7, V:1
36439A:1, C:1, E:1, N:1, R:1, S:3, V:1
37449D:3, E:5, Q:1
38459A:1, C:7, I:1
39469K:2, L:3, R:4
40479A:3, E:1, L:1, N:3, T:1
41489I:4, L:1, T:1, V:3
42499L:9
43509D:1, E:1, H:1, Q:1, S:1, T:1, V:1, Y:2
44519E:1, K:1, Q:2, R:1, V:4
45529L:9
46539G:9
47549P:8, T:1
48559W:9
49569A:2, C:7
50579A:6, C:1, L:1, T:1
51589A:1, D:3, F:1, S:2, V:1, Y:1
52599K:4, R:2, S:3
53609A:2, F:1, I:2, L:2, V:2
54619A:7, G:1, Y:1
55629E:3, G:3, Q:3
56639I:3, M:3, T:1, Y:2
57649F:3, L:2, M:3, N:1
58659H:1, I:3, L:1, V:4
59669K:1, M:1, P:1, R:4, S:1, T:1
60679D:1, E:3, Q:4, Y:1
61689I:1, L:7, M:1
62699D:1, E:4, Q:4
63709K:7, R:2
64719A:1, I:2, M:1, N:1, R:1, Y:3
65729A:1, C:1, D:1, E:2, I:2, V:2
66739K:9
67749H:4, L:1, Q:2, S:1, V:1
68759E:8, Q:1
69769Q:3, R:1, S:2, T:3
70779C:2, E:3, G:1, R:1, S:1, V:1
71789A:1, D:2, E:3, I:1, K:1, Y:1
72799I:2, L:3, M:1, N:1, Q:1, V:1
These are unregularized maximum-likelihood estimates of the emission probabilities. No additional artificial observations are added. Although only emissions are requested here, a full profile hidden Markov model would also require transitions and deletion mechanisms to represent the gaps that remain in some match columns.
Convert the supplied multiple sequence alignment to an ordinary array so that reducing over rows returns one value per column. With nine sequences, fewer than 30% gaps means at most two gaps in a column:
from collections import Counter
A = np.asarray(msa_matrix)
gap_fraction = np.mean(A == ".", axis=0)
match_mask = gap_fraction < .30
match_columns = np.flatnonzero(match_mask)
print(match_columns + 1)  # human-readable, one-based alignment columns
There are 72 match states: alignment columns 1–21 and 29–79, using one-based numbering. The seven remaining columns, 22–28, form one contiguous insertion region. The strict inequality is applied to the actual fraction, rather than to rounded percentages.
The maximum-scoring cell records the inclusive end position in each sequence. During traceback, moving left consumes a reference residue, moving up consumes a query residue, and moving diagonally consumes both; when traceback stops, the remaining prefix lengths are one less than the corresponding one-based starts. Consequently i+1 and j+1 are the correct starts returned in part (a), without counting any inserted gaps as sequence residues.
The following report joins these coordinates to the alignments and sequence identity/sequence similarity values already calculated:
for name, a in alignments.items():
    print(f"{name}: query length={a['query_length']}, "
          f"reference length={a['reference_length']}, "
          f"alignment columns={a['alignment_length']}, score={a['score']}")
    print(f"Query {a['query_start']} {a['query']} {a['query_end']}")
    print(f"Ref   {a['reference_start']} {a['reference']} {a['reference_end']}")
    print(f"Identity {a['identity_percent']:.2f}%; "
          f"similarity {a['similarity_percent']:.2f}%; "
          f"gaps {a['gap_percent']:.2f}%")
All reference sequences have length 508. The following are one-based inclusive coordinates; the aligned strings and all percentages are given in parts (a) and (b).
QueryQuery lengthQuery intervalReference intervalColumnsScore
baboon651–65241–30666314
squirrel monkey561–5661–11658278
dingo661–661–6671301
goldfish632–63248–31572190
For example, the goldfish local sequence alignment excludes its first query residue and aligns residues 2–63. As a check, removing gaps from each aligned string gives exactly the sequence slice determined by its reported interval.
Use the number of alignment columns, including gap columns, as the denominator. A column contributes to sequence identity when its two nongap letters are equal, to sequence similarity when its two nongap letters have a strictly positive BLOSUM62 score, and to the gap fraction when either letter is a gap. A zero substitution score is not counted as similar.
The implementation in part (a) computes each numerator directly during its final pass through the aligned strings:
for name, a in alignments.items():
    print(name, a["alignment_length"], a["identities"],
          a["similarities"], a["gaps"])
    print(a["identity_percent"], a["similarity_percent"], a["gap_percent"])
QueryColumnsIdenticalSimilarGap columns
baboon6663 (95.45%)65 (98.48%)1 (1.52%)
squirrel monkey5849 (84.48%)50 (86.21%)4 (6.90%)
dingo7157 (80.28%)59 (83.10%)10 (14.08%)
goldfish7234 (47.22%)50 (69.44%)14 (19.44%)
Identical columns also count as similar for these data, so these percentages are overlapping summaries rather than three categories summing to 100%.
Use the supplied BLOSUM62 entry for every diagonal move in the Smith-Waterman algorithm, while retaining the stipulated linear gap penalty of . If and are the current amino acids, the dynamic programming recurrence is
The zero option permits a new local start. The following original implementation also records the traceback coordinates needed in part (c); its tie ordering is left, up, diagonal, and an equal overall maximum replaces the previous best cell, as in the supplied convention:
def local_alignment(query,reference,alphabet,scores,gap=-1):
    lookup={aa:i for i,aa in enumerate(alphabet)}
    table=np.asarray(scores,dtype=int)
    height,width=len(query)+1,len(reference)+1
    H=np.zeros((height,width),dtype=int)
    back=np.zeros((height,width),dtype=np.int8)
    best=(0,0,0)
    for i,a in enumerate(query,1):
        for j,b in enumerate(reference,1):
            # Order implements the printed LEFT, UP, DIAGONAL tie priority.
            candidates=(0,H[i,j-1]+gap,H[i-1,j]+gap,
                        H[i-1,j-1]+int(table[lookup[a],lookup[b]]))
            value=max(candidates);H[i,j]=value
            back[i,j]=0 if value==0 else candidates.index(value)
            if value>=best[0]:best=(value,i,j)
    score,i,j=best;query_end,ref_end=i,j
    aq=[];ar=[]
    while H[i,j]>0:
        step=back[i,j]
        if step==1:aq.append('-');ar.append(reference[j-1]);j-=1
        elif step==2:aq.append(query[i-1]);ar.append('-');i-=1
        elif step==3:aq.append(query[i-1]);ar.append(reference[j-1]);i-=1;j-=1
    qa=''.join(reversed(aq));ra=''.join(reversed(ar));length=len(qa)
    identities=sum(a==b and a!='-' for a,b in zip(qa,ra))
    similarities=sum(a!='-' and b!='-' and table[lookup[a],lookup[b]]>0 for a,b in zip(qa,ra))
    gaps=sum(a=='-' or b=='-' for a,b in zip(qa,ra))
    return dict(query=qa,reference=ra,score=int(score),query_start=i+1,query_end=query_end,
                reference_start=j+1,reference_end=ref_end,query_length=len(query),reference_length=len(reference),
                alignment_length=length,identities=int(identities),similarities=int(similarities),gaps=int(gaps),
                identity_percent=100*identities/length if length else 0.,
                similarity_percent=100*similarities/length if length else 0.,gap_percent=100*gaps/length if length else 0.)

queries = {"baboon": baboon, "squirrel_monkey": squirrel_monkey,
           "dingo": dingo, "goldfish": goldfish}
alignments = {
    name: local_alignment(seq, reference_epo_receptor_human, amino_acids, bmat)
    for name, seq in queries.items()
}
for name, result in alignments.items():
    print(name, "score", result["score"])
    print(result["query"])
    print(result["reference"])
The resulting local sequence alignments, with the query above the reference, are:
baboon; score 314
SLLTPSDLDPLILTLSLILVLIVVLLTVLALLSHRRALKQKIWPGIPSPESEFEGLFTTHKG-FQL
SLLTPSDLDPLILTLSLILVVILVLLTVLALLSHRRALKQKIWPGIPSPESEFEGLFTTHKGNFQL
squirrel monkey; score 278
VCFWEEAASAGMGPDNYSFSYQL-DNEPWKR-CRLHQTPTARGVVRFWCSLPRADTSS
VCFWEEAASAGVGPGNYSFSYQLED-EPWK-LCRLHQAPTARGAVRFWCSLPTADTSS
dingo; score 301
MNHLW--THLWPG-VGSLCLLLAGAAWASLPKP-L-DPKFESKAALLAARAPEELLCFTERLEDLVCFWEE
MDHL-GAS-LWP-QVGSLCLLLAGAAWAP-P-PNLPDPKFESKAALLAARGPEELLCFTERLEDLVCFWEE
goldfish; score 190
L--LVLSIGLILC--L-LSLTVI-L-SHQKFLLK-KLWPDIPTPEHKFPGLFTVYKGDFKE-WMSH-NNGSM
LDPLILTLSLILVVILVL-LTVLALLSHRR-ALKQKIWPGIPSPESEFEGLFTTHKGNF-QLWL-YQNDGCL
The several short gaps are a consequence of the specified inexpensive linear gap penalty; substituting a different default or an affine gap penalty would solve a different scoring problem.
TODO: The numerical/data-specific answer awaits the original clustering_dataset.csv dataset. The file was not supplied; no numerical output is fabricated.
TODO: The numerical/data-specific answer awaits the original clustering_dataset.csv dataset. The file was not supplied; no numerical output is fabricated.
TODO: The numerical/data-specific answer awaits the original clustering_dataset.csv dataset. The file was not supplied; no numerical output is fabricated.
TODO: The numerical/data-specific answer awaits the original clustering_dataset.csv dataset. The file was not supplied; no numerical output is fabricated.
Starting with the identity matrix, multiply by once per iteration and test the resulting power. Using integer objects keeps the calculation exact and avoids fixed-width integer overflow accidentally turning a nonzero power into zero:
def kzero(matrix,kmax=100):
    a=np.asarray(matrix,dtype=object)
    if a.ndim!=2 or a.shape[0]!=a.shape[1]:raise ValueError('square matrix required')
    power=np.eye(len(a),dtype=object)
    for k in range(1,kmax+1):
        power=power@a
        if np.equal(power,0).all():return k
    return None

print([kzero(rand_triu(10)) for _ in range(5)])
Continuing the random-number stream from the displayed example gives . These are the least positive zero powers, rather than the number of nonzero powers; in particular, a zero matrix has nilpotency index one.
For a strictly upper triangular matrix, a nonzero entry of can only lie at least positions above the diagonal. Thus , and every one of these examples must return an integer at most ten. The code interprets “up to” as including kmax; if the final strict inequality in the question is intended instead, replace range(1,kmax+1) by range(1,kmax).

There are unlisted articles, also show them or only show them.