0
\$\begingroup\$

Preface: Before anyone suggest using pandas, no I don't want to use pandas, only pure python please.

The concept of my code is quite simple, given some list of strings in a text file formatted as such (all the same size):

#sample_test.txt
------dooadb-----
asdoutdocnbmasdgd
--sdfhteoadb-----
-----asdfhwi-----
----ssdweibb-----

Find how frequently an element occurs within each position of each string (i.e. the first column would be [-,a,-,-,-]) using the shortest string as the bounds (i.e. ignoring the '-'). Then plot each element, x being the position, y being the frequency of occurrence. With the size being proportional to the how frequently the element occurs.

As an example: The shortest string is the very first line 'dooadb', thus the very first "column" to compare would be ['d','d','t','s','d'] . Within this column, d occurs 3x, t and s occur once. So for the plot, in position 0, d would be at the top, being the biggest letter. t and s would be lower (overlapping one another since they have the same occurrence)

What I've done in the code below is 3 functions. First I find the index of the shortest string to use as the bounds. Then I create a list of dictionaries, where the key is the letter and the value the frequency it occurs. Finally, I plot this using a scatter plot.

There are a couple of issues however:

  1. I think this code is redundant, bloated, and ugly. I think using my counts and loops in the find_bounds() function is...well there should be a cleaner better way to do this than that.
  2. I think using a dictionary is the best way to go about this, it contains all the info I need for my plot. However, I feel like I'm not quite utilizing it properly. I'm thinking there should be a way to simply plot the dictionary as is, without breaking it down within a loop to its keys and values. Furthermore, I have a 2nd loop because keys with the same values will result in overlap, to resolve this I create a new dict with the key now being the value, and the label being a combination of the duplicates. This way they are plotted side by side. However, I wonder if there is a cleaner way to do this that doesn't involve looping through my dicts and creating a new dict (i.e. I just feel like I'm utilizing my dicts wrong).
  3. Finally, while the plot works, it doesn't look good. The annotate means the labels will be to the side of the dot (i.e. not aligned to the x_axis), whereas I'd like it to actually replace the dot (but using annotate by itself doesn't work, you need to first plot the data than annotate it). This is also why I make the size of the dot super small, since I don't even want to see it, I just want the label. The label size also is arbitrary. If I set it to just the frequency of occurrence, its too small, but I also have to be careful not to set the small ones too big, because then the bigger ones will be too large. Ideally, I feel like normalizing all the values (in terms of size) to each other then expanding them is key. This way they are resized proportionally (i.e. if one letter occurs 2x and another 4x, the 4x letter should be twice as large as the 2x, but both as still legible). Additionally, the way the label works, it's not treated like a data point, so the bounds of the plot don't resize to fit it. Therefore, as it stands the bigger letters go outside of the bounds of the plot so it looks ugly.

In short, the code works, but the output plot is ugly, I think the code is poorly written, and I feel like I'm not utilizing the dictionary properly.

import matplotlib.pyplot as plt

def find_bounds():
    """find bounds of the shortest string"""
    bounds=[]
    temp_bounds=[]
    with open('test_samples.txt') as file:
        for lines in file:
            count=0
            counter=0
            for elements in lines:
                if elements == '-':
                    count+=1
                    if counter > 1:
                        counter-=1
                        temp_bounds.append(counter)
                        break
                else:
                    counter+=1
                    if counter == 1:
                        temp_bounds.append(count)
            if bounds == []:
                bounds.append(temp_bounds[0])
                bounds.append(temp_bounds[1])
                continue
            if temp_bounds[1] < bounds[1]:
                bounds.clear()
                bounds.append(temp_bounds[0])
                bounds.append(temp_bounds[1])
            temp_bounds.clear()
    bounds[1]=bounds[0]+bounds[1]
    return bounds

def find_frequency():
    """Using the bounds above, find the occurence frequency of each letter in it's respective column"""
    bounds=find_bounds()
    start=bounds[0]
    end=bounds[1]
    sequences=[]
    list_of_frequencies=[]
    with open('test_samples.txt') as file:
        for lines in file:
            sequences.append(list(lines[start:end+1]))
    for x in range(len(sequences[0])):
        frequence_counts={}
        for entry in [item[x] for item in sequences]:
            if entry in frequence_counts:
                frequence_counts[entry] += 1
            else:
                frequence_counts.update({entry: 1})
        list_of_frequencies.append([frequence_counts])
    return list_of_frequencies


def plotting():
    """Plot the letters, with their size being proportional to their occurence frequency, and the y value being the occurence frequency. Letters that have the same occurence frequency should be plotted side by side so as to not overlap"""
    plot_frequency_list=find_frequency()
    for x,dicts in enumerate(plot_frequency_list):
        duplicate_dict={}
        for next_dict in dicts:
            for dict_values,labels in zip(next_dict.values(),next_dict.keys()):
                if dict_values in duplicate_dict:
                    duplicate_dict[dict_values] += labels
                else:
                    duplicate_dict.update({dict_values: labels})
            for dict_values,labels in zip(next_dict.values(),next_dict.keys()):
                plt.scatter(x,dict_values,s=dict_values/1000000000000)
                plt.annotate(duplicate_dict[dict_values],(x,dict_values),fontsize=dict_values**3+10)
    plt.show()

plotting()
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

You can calculate frequencies by making use of collections.Counter:

from collections import Counter

column_freqs: list[Counter] = []
# zipping the rows together forms an iterator of columns
for col in zip(*sample_test.readlines()):
    # no need to do anything fancy to pre-calculate the boundaries,
    # just filter out columns that aren't fully letters
    if "-" in col:
        continue
    column_freqs.append(Counter(col))

For the plotting, IMO it takes a lot of work to make matplotlib look pretty. The easiest thing to do here is just pad the axes limits so that your text doesn't overflow. Also, you can simplify the plotting a bit by just using plt.text.

fig = plt.figure()
ax = fig.add_subplot()
# add some padding around the edges
ax.axis(
    [
        -0.1,
        len(column_freqs) * 0.9,
        0.9,
        max(max(f.values()) for f in column_freqs) * 1.1,
    ]
)
for index, freqs in enumerate(column_freqs):
    duplicate_dict = {}
    for label, count in freqs.items():
        if count in duplicate_dict:
            duplicate_dict[count] += label
        else:
            duplicate_dict.update({count: label})
    for count, label in duplicate_dict.items():
        # use ax.text directly and modify fontsize to scale the text
        ax.text(
            index,
            count,
            label,
            fontsize=count * 10,
            verticalalignment="center",
            horizontalalignment="left",
        )
plt.show()
\$\endgroup\$
6
  • \$\begingroup\$ Thank you! This is much cleaner than what I was doing. Although, there are 2 questions that I have: 1) What does the * do in the readfile. Normally you read everthing within a line, but this is reading every element. In short, I don't exactly understand how you generate that column via zip(*(readlines)) 2) I didn't know about the .text, this is exactly what I was originally trying to find which I had to resort to annotate. Thank you! However, I noticed if I remove the extra padding you make (i.e. where you define the bounds for x and y in the plot), the y-axis is just 0-1, it doesn't autosiz \$\endgroup\$
    – samman
    Nov 4, 2022 at 23:33
  • \$\begingroup\$ Forgot to also mention one other thing, I'm a bit confused by this: column_freqs: list[Counter] = []. The script works if column_freqs is just a standard list. column_freq=[] I don't know what the rest of : list[Counter] is doing \$\endgroup\$
    – samman
    Nov 4, 2022 at 23:39
  • 1
    \$\begingroup\$ @samman : list[Counter] is a type hint. \$\endgroup\$ Nov 5, 2022 at 0:42
  • 1
    \$\begingroup\$ @samman * is an operator for iterable unpacking. It turns sample_test.readlines() into individual arguments separated by commas. \$\endgroup\$ Nov 5, 2022 at 0:47
  • \$\begingroup\$ @samman you're correct, it doesn't autosize the axes. I'm not super familiar with matplotlib's handling of text/image plotting, but I don't think there's an automatic way to handle it \$\endgroup\$
    – onepan
    Nov 7, 2022 at 6:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.