The Molecule Report

An easy Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.

Domain
Python
Difficulty
easy
Seniority
L4
Asked in
Invitae

Problem

An analyst stopped trusting the read-quality report once contaminated reads, carrying stray characters among the bases A, C, G and T, started slipping through it. Rebuild the report for each sequence: whether it is clean, its GC content as a percentage of the full read length, how often each of the four bases occurs, and its most frequent pair of consecutive bases. When pairs tie for most frequent, the pair that sorts last alphabetically wins.

Four letters. A lot of math hidden in the sequence.

Example 1

Input:

sequence = "ATGCATGC"

Output:

{"is_valid":true,"gc_content":50,"nucleotide_counts":{"A":2,"C":2,"G":2,"T":2},"most_common_dinucleotide":"TG"}

Example 2

Input:

sequence = "AAAT"

Output:

{"is_valid":true,"gc_content":0,"nucleotide_counts":{"A":3,"C":0,"G":0,"T":1},"most_common_dinucleotide":"AA"}

Worked solution and explanation

What this really is

Under the lab-report costume, this is a single-pass aggregation with a nasty tie-break. Anyone can tally four bases. What separates candidates is the dinucleotide. If you reach for Counter(pairs).most_common(1), you will confidently return the WRONG pair whenever two pairs tie, because most_common breaks ties by first appearance, not alphabetically. In the sample read 'ATGCATGC', the pairs 'AT', 'TG' and 'GC' each appear twice. The contract wants the alphabetically largest, 'TG', not whichever one you happened to see first. Add the empty-read division by zero, the short read that returns '' rather than None, and the count dict that always carries four keys, and the 'easy' problem has three more quiet places to get it wrong.


Break down the requirements

Step 1: Count each nucleotide base

Walk the string once and tally A, C, G and T into a nucleotide_counts dict seeded with all four keys at 0, so absent bases still report 0.

Step 2: Compute GC content

GC content is (count_G + count_C) / len(sequence) * 100, over the full read length including stray characters. Guard the empty sequence against division by zero by returning 0.0.

Step 3: Find the most common dinucleotide

In the same pass, tally every consecutive 2-character pair and find the highest frequency. On a tie, return the alphabetically largest pair: take max(...) over the tied pairs, not the first one seen. Return '' (the empty string, not None) if the sequence has fewer than 2 characters.

Step 4: Validate the sequence

Flip is_valid to False the moment you see any character outside A, C, G, T. Do this per character rather than trusting a length comparison after the fact.

Step 5: Build the response dict

Assemble the four results into a dict with exactly the keys is_valid, gc_content, nucleotide_counts and most_common_dinucleotide.


The solution

Single pass with multi-metric aggregation
def analyze_dna_sequence(sequence):
    nucleotide_counts = {'A': 0, 'C': 0, 'G': 0, 'T': 0}
    is_valid = True
    pair_counts = {}
    n = len(sequence)
    for i in range(n):
        ch = sequence[i]
        if ch in nucleotide_counts:
            nucleotide_counts[ch] += 1
        else:
            is_valid = False
        if i < n - 1:
            pair = sequence[i:i + 2]
            pair_counts[pair] = pair_counts.get(pair, 0) + 1
    gc_content = 0.0
    if n > 0:
        gc_content = (nucleotide_counts['G'] + nucleotide_counts['C']) / n * 100
    most_common_dinucleotide = ''
    if pair_counts:
        max_count = max(pair_counts.values())
        most_common_dinucleotide = max(
            pair for pair, count in pair_counts.items() if count == max_count
        )
    return {
        'is_valid': is_valid,
        'gc_content': gc_content,
        'nucleotide_counts': nucleotide_counts,
        'most_common_dinucleotide': most_common_dinucleotide,
    }

Common follow-up questions