Section 6. More Dictionaries & Drawing


Section materials curated by Juliette Woodrow, drawing upon materials from previous quarters.

This week in section you are going to get more practice with dictionaries and drawing!

Pycharm Project

Dictionaries

Recipes

Inspired by an unhealthy amount of Netflix and quarantine cooking, Parth and Peter are opening up a bakery. To try and keep costs low, they want to automate as much of their inventory tracking as possible.

Thanks to 106A, they think they can use dictionaries to help.

All of the ingredients they have available will be stored in a pantry dictionary, whose keys are ingredients and values are weights (Of course they're using the metric system), like so:

pantry = {
    'flour': 400,
    'sugar': 300,
    'salt': 10,
    'chocolate': 150
}
      

Each recipe is also stored in a dictionary, like the following uninspiring concoction:

recipe = {
  'flour': 200,
  'salt': 2.5
}

(As you can tell from the recipe above, it doesn't look like their bakery is going to do very well, but that's not important right now.)

Recipes and the pantry are first stored as files, where each key value pair is on a different line, separated by ':: '. The pantry list and recipe above would look like:

pantry.txt

flour:: 400
sugar:: 300
salt:: 10
chocolate:: 150     

recipe.txt

flour:: 200
salt:: 2.5    

Our goal is to write a few functions to help Parth and Peter run their bakery. Begin by implementing the following function:

def read_dict_from_file(filename):
    """
    Takes in the name of a file containing a recipe or 
    pantry list and reads it into a dictionary.
    
    An example doctest using the file above:
    >>> read_dict_from_file('recipe.txt')
    {'flour': 200, 'salt': 2.5}
    """

Once you've developed the infrastructure to construct recipe and pantry list dictionaries, implement the following functions:

def can_make(recipe, pantry):
    """
    Given the contents of the pantry, returns a boolean indicating 
    whether or not it is possible to follow the recipe. Note that 
    the parameters to this function are dictionaries, and not 
    filenames. The pantry should not be modified in this function
    """
    pass

def make_recipe(recipe, pantry):
    """
    Given a recipe and a pantry with enough ingredients to make the recipe, 
    modify the contents of the pantry to remove as many quantities as the 
    recipe requires. You may modify the pantry in place, but return the modified 
    pantry in order to test the output using doctests. 

    # using the recipe and pantry defined above
    >>> make_recipe(recipe, pantry)
    {'flour': 200, 'sugar': 300, 'salt': 7.5, 'chocolate': 150}
    """
    pass

Note that make_recipe assumes the pantry is sufficient for the recipe but this is not necessarily always the case; thus, every call to make_recipe will need to be guarded by a call to can_make_recipe.

Finally, implement a main function, which first reads in a pantry file from a user and then continuously asks for recipe filenames from the user and either prints an error message if the pantry does not have sufficient ingredients, or removes the ingredients from the pantry if it does exist, printing the pantry afterwards.

A sample run of the program is below, which repeatedly tries to make the recipe above in the vain hope that someone will actually want to eat it. Note that your program should be able to accept any valid recipe filename, though.

$ python3 pantry_manager.py 
Enter pantry filename: pantry.txt
What recipe should we bake next (Press enter to quit.)? recipe.txt
You can make that recipe! Your pantry now looks like this:
{'flour': 200, 'sugar': 300, 'salt': 7.5, 'chocolate': 150}

What recipe should we bake next (Press enter to quit.)? recipe.txt
You can make that recipe! Your pantry now looks like this:
{'flour': 0, 'sugar': 300, 'salt': 5, 'chocolate': 150}

What recipe should we bake next (Press enter to quit.)? recipe.txt
You can't make that recipe. 

What recipe should we bake next (Press enter to quit.)? User presses enter immediately
def read_dict_from_file(filename):
    """
    Takes in the name of a file containing a recipe or
    pantry list and reads it into a dictionary.
​
    An example doctest using the file above:
    >>> read_dict_from_file('recipe.txt')
    {'flour': 200, 'salt': 2.5}
    """
    # Create an empty dictionary
    d = {}
​
    with open(filename) as f:
        # For each line in the file
        for line in f:
            # Split this line into a list at '::'
            key_val = line.split(":: ")
            # Add key:value pair of ingredient:weight to our dictionary
            d[key_val[0]] = float(key_val[1])
​
    return d
​
​
def can_make(recipe, pantry):
    """
    Given the contents of the pantry, returns a boolean indicating
    whether or not it is possible to follow the recipe. Note that
    the parameters to this function are dictionaries, and not
    filenames. The pantry should not be modified in this function
    """
    # For each key in the recipe dictionary
    for ingredient in recipe:
        # Check if amount of ingredient in pantry is less than amount needed for recipe
        if pantry.get(ingredient, 0) < recipe[ingredient]:
            return False
    # Return True after checking every ingredient
    return True
​
​
def make_recipe(recipe, pantry):
    """
    Given a recipe and a pantry with enough ingredients to make the recipe,
    modify the contents of the pantry to remove as many quantities as the
    recipe requires. You may modify the pantry in place, but return the modified
    pantry in order to test the output using doctests.
​
    # using the recipe and pantry defined above
    >>> make_recipe(recipe, pantry)
    {'flour': 200, 'sugar': 300, 'salt': 7.5, 'chocolate': 150}
    """
    # For each value in recipe dictionary
    for ingredient in recipe:
        # Subtract the needed amount from this ingredient in pantry
        pantry[ingredient] -= recipe[ingredient]
    return pantry
​
​
def main():
    pantry = read_dict_from_file(PANTRY_FILENAME)
    while True:
        recipe_filename = input(
            "What recipe should we bake next (Press enter to quit.)? ")
        if recipe_filename == "":
            break
        recipe = read_dict_from_file(recipe_filename)
        if can_make(recipe, pantry):
          make_recipe(recipe, pantry)
          print("You can make that recipe! Your pantry now looks like this:")
          print(pantry)
        else:
          print("You can't make that recipe.")

Cryptography

Cryptography is the study of techniques for communicating messages secretly. Imagine that Alice and Bob want to send messages to each other, but Eve can snoop on the messages they're sending and read them. Alice wants to figure out way to "encrypt" her messages so that if Eve reads the message, she won't be able to understand it, but Bob will be able to "decrypt" the message. We're going to write a program to help Alice and Bob do this.

Alice decides that she'll replace every letter in her original message with a different letter. She's defined a variable in the program called ENCRYPTION_DICT that keeps track of these associations:

      
ENCRYPTION_DICT = {
 'A': 'T',
 'B': 'H',
 'C': 'E',
 'D': 'Q',
 'E': 'U',
 'F': 'I',
 'G': 'C',
 'H': 'K',
 ...
} 

Alice and Bob exchanged this dictionary, so they both know that this is the strategy, but Eve doesn't know that! Note that in order to avoid ambiguity, the values in this dictionary are unique (that is, each letter is a value in the dictionary exactly once).

Encryption

To start us off, implement the following function:

def encrypt(plaintext):
    """
    Takes in plaintext as an input and returns 'ciphertext': the result 
    of substituting each letter in the plaintext by its corresponding 
    encrypted character in ENCRYPTION_DICT. 

    The plaintext comprises entirely of uppercase letters and non-alphabetic 
    characters like punctuation. Non-alphabetic characters needn't be encrypted, 
    but rather should appear in the plaintext in their original form. 

    >>> encrypt("HEY, HOW'S IT GOING?")
    "KUD, KXZ'S BV CXBFC?"
    >>> encrypt("I LOVE CS 106A!")
    'B WXLU ES 106T!'
    >>> encrypt("UNICORNS ARE THE MOST BEAUTIFUL ANIMALS IN EXISTENCE")
    'AFBEXPFS TPU VKU NXSV HUTAVBIAW TFBNTWS BF UYBSVUFEU'
    """

Decryption

Now that Bob has received Alice's encrypted message, he needs to "decrypt" it, or convert it back to the original message. To help him do so, implement the following function:

def decrypt(ciphertext):
    """
    Uses ENCRYPTION_DICT to decrypt each of the alphabetic characters of
    ciphertext.

    >>> decrypt("KUD, KXZ'S BV CXBFC?")
    "HEY, HOW'S IT GOING?"
    >>> decrypt('B WXLU ES 106T!')
    'I LOVE CS 106A!'
    >>> decrypt('AFBEXPFS TPU VKU NXSV HUTAVBIAW TFBNTWS BF UYBSVUFEU')
    'UNICORNS ARE THE MOST BEAUTIFUL ANIMALS IN EXISTENCE'
    """

Note that in order to successfully decrypt a message, you need the 'reverse' of ENCRYPTION_DICT: rather than associating plaintext characters with their encrypted counterparts, we need to go the other way.

We suggest writing a function reverse_encryption_dict to decompose out this problem. In class, we saw an example of this wherein a reversed dictionary associated keys with lists of values, because multiple keys can share the same value. Note that this doesn't apply to the case of ENCRYPTION_DICT, because each character is guaranteed to have a unique character. How does this affect how you implement reverse_encryption_dict?

This is an interesting encryption scheme (known formally as a substitution cipher), but unfortunately isn't very secure. What issues do you see with it?

def encrypt(plaintext):
    """
    Takes in plaintext as an input and returns 'ciphertext': the result
    of substituting each letter in the plaintext by its corresponding
    encrypted character in ENCRYPTION_DICT.
​
    The plaintext comprises entirely of uppercase letters and non-alphabetic
    characters like punctuation. Non-alphabetic characters needn't be encrypted,
    but rather should appear in the plaintext in their original form.
    """
    output = ""
​
    # For each character in the unencrypted message
    for ch in plaintext:
        # If this is a character we have an encryption for, add the encryption
        if ch in ENCRYPTION_DICT:
            output += ENCRYPTION_DICT[ch]
        # Otherwise, add the character in its original form
        else:
            output += ch
​
    return output
​
​
def reverse_encryption_dict():
    """
    This helper function returns a dictionary that is the reverse of
    ENCRYPTION_DICT (keys and values are swapped).
    """
    # Create a new dictionary
    decryption_dict = {}
​
    # For each plaintext character key in the encryption dictionary
    for plaintext_char in ENCRYPTION_DICT:
        # Get the encrypted character value for this key
        encrypted_char = ENCRYPTION_DICT[plaintext_char]
        # In the new dict, add the plaintext character as the value of our encrypted key
        decryption_dict[encrypted_char] = plaintext_char
​
    return decryption_dict
​
​
def decrypt(ciphertext):
    """
    Uses ENCRYPTION_DICT to decrypt each of the alphabetic characters of
    ciphertext.
    """
    decryption_dict = reverse_encryption_dict()
    output = ""
​
    # For each character in the encrypted word
    for ch in ciphertext:
        # If this character can be decrypted, add its decryption to output string
        if ch in decryption_dict:
            output += decryption_dict[ch]
        # Otherwise, add the original character to the output string
        else:
            output += ch
​
    return output

Visualizing Big Tweets

In last week's section, you implemented a program that constructs what we refer to as a user tags dictionary. This dictionary maps twitter handles to dictionaries which map hashtags to frequencies of usage. For example, suppose we have the following user tags dictionary:

{'@kanyewest': {'yeezy': 50, 'chicago': 20, 'pablo': 30}}

This dictionary illustrates the unlikely scenario in which @kanyewest is the only user of twitter (since that is the only key in the outer dictionary) and that he has tweeted about yeezy, chicago and pablo 50, 20 and 30 times respectively.

Our goal is to implement a program to visualize this data for us, by showing the relative usage of each hashtag by a user. For the dictionary associated with @kanyewest, this visualization would look like this:

In order to produce this visualization, you should implement the following function:

def visualize_trends(canvas, user_tags, user_name):
    """
    Draws a visualization of the top 10 hashtags used 
    by the user whose handle is user_name. 
    """

As you write this function, keep in mind the following details:

  • You need not worry about producing a user_tags dictionary. The starter code loads this dictionary in from a file and passes it into your function.
  • In order to keep the bars a reasonable width, you need only plot the ten most frequently used hashtags (or fewer, if the user doesn't use many hashtags). We've provided a function get_top_tags, which accepts as a parameter a single user's hashtag frequency dictionary, and returns a frequency dictionary of just the TOP_N most used hashtags, where TOP_N is a constant defined to be equal to 10.
  • All the bars should be equal width.
  • All the bars should have a height that is proportional to their hashtag's usage. For example, in the example above, 20% of @kanyewest's tweets used the chicago hashtag, and so the bar for chicago is 20% the height of the canvas. Restated, the height of all the bars combined should equal CANVAS_HEIGHT.

The design and decomposition of this function is up to you, but we suggest the following milestones:

  1. Implement a function that-given the dimensions and coordinates of a bar, as well as the corresponding hasthag string-draws a bar and label at the top of the bar. We've provided some constants in the starter code that will be helpful here.
  2. Once you have the functionality to draw a single bar working, produce a dictionary of just the top ten hashtags for the user using the get_top_tags function. For each hashtag, plot a bar with the correct position and size.
CANVAS_WIDTH = 1000
CANVAS_HEIGHT = 600
TEXT_TOP_OFFSET = 10
TEXT_SIDE_OFFSET = 3

def draw_bar(canvas, tag_label, left_x, top_y, right_x, bottom_y):
    """
    Given top-left and bottom-right coordinates for a bar and a label, 
    draw the bar
    """ 
    canvas.create_rectangle(left_x, top_y, right_x, bottom_y, fill="light blue", outline="blue")
    canvas.create_text(left_x + TEXT_SIDE_OFFSET, top_y+ TEXT_TOP_OFFSET, anchor='w',
        font=('Calibri', '10'),
        fill='Blue',
        text=tag_label)


def visualize_trends(canvas, user_tags, user_name): 
    if user_name not in user_tags:
        return 
    
    tags = get_top_tags(user_tags[username]) 
    
    totalNum = sum(tags.values()) #grab the total number of hashtags they used 
    tags_list = list(tags.keys()) #to grab all the tags we need and store in a list
    space = CANVAS_WIDTH/len(tags_list)
    
    for i in range(len(tags_list)): 
        tag = tags_list[i]
        count = tags[tag]
        height = CANVAS_HEIGHT * (count/ totalNum)
        x1 = space *i
        y1 = CANVAS_HEIGHT - height
        x2 = space*i + space 
        y2 = CANVAS_HEIGHT -1 
        tag_label = tag + " " + str(count)


        draw_bar(canvas, tag_label, x1, y1, x2, y2)