<jupyter_start><jupyter_text>Lecture 5: LoopsCSCI 1360: Foundations for Informatics and Analytics  Overview and ObjectivesIn this lecture, we'll go over the basics of looping in Python. By the end of this lecture, you should be able to  - Perform basic arithmetic operations using arbitrary-length collections - Use "unpacking" as a shortcut for iterating through dictionaries - Describe the differences between the separate kinds of loops  Part 1: `for` Loops Looping, like lists, is a critical component in programming and data science. When we're training models on data, we'll need to loop over each data point, examining it in turn and adjusting our model accordingly *regardless of how many data points there are*. This kind of repetitive task is ideal for looping. Let's define for ourselves the following list:<jupyter_code>ages = [21, 22, 19, 19, 22, 21, 22, 31]<jupyter_output><empty_output><jupyter_text>This is a list containing the ages of some group of students, and we want to compute the average. How do we compute averages? We know an average is some *total quantity* divided by *number of elements*. Well, the latter is easy enough to compute:<jupyter_code>number_of_elements = len(ages)
print(number_of_elements)<jupyter_output>8<jupyter_text>The total quantity is a bit trickier. You could certainly sum them all manually--<jupyter_code>age_sum = ages[0] + ages[1] + ages[2] # + ... and so on<jupyter_output><empty_output><jupyter_text>...but that seems really, really tedious. Plus, how do you even know how many elements your list has?  Loop structure The structure itself is pretty simple:  - some collection of "things" to iterate over  - a placeholder for the current "thing"  - a chunk of code describing what to do with the current "thing" Let's start simple: looping through a list, printing out each item one at a time.<jupyter_code>for N in [2, 5, 7, 9]:  # Header
    print(N)            # Body<jupyter_output>2
5
7
9<jupyter_text>There are two main parts to the loop: the **header** and the **body**.  - The **header** contains 1) the collection we're iterating over (in this example, the list), and 2) the "placeholder" we're using to hold the current value (in this example, `N`).  - The **body** is the chunk of code under the header (indented!) that executes on each iteration. Back, then, to computing an average:<jupyter_code>age_sum = 0
ages = [21, 22, 19, 19, 22, 21, 22, 31]

for age in ages:
    age_sum += age

avg = age_sum / number_of_elements  # Compute the average using the formula we know and love!
print("Average age: {:.2f}".format(avg))<jupyter_output>Average age: 22.12<jupyter_text>You can loop through sets and tuples the same way.<jupyter_code>s = set([1, 1, 2, 3, 5])
for item in s:
    print(item)
t = tuple([1, 1, 2, 3, 5])
for item in t:
    print(item)<jupyter_output>1
1
2
3
5<jupyter_text>Iterators The unifying theme with all these collections you can loop through is that they're all examples of *iterators*. Easily the most common iterator you'll use (aside from lists, sets, and tuples) is the `range` function:<jupyter_code>for i in range(10):
    print(i, end = " ")  # Prints everything on 1 line.<jupyter_output>0 1 2 3 4 5 6 7 8 9<jupyter_text>Note, again, that the range of numbers goes from 0 (inclusive) to the specified end (exclusive)! The critical point is that the argument to `range` specifies the *length* of the returned iterator. A few more examples of `range` before we get back to loops:<jupyter_code>for i in range(5):  # One argument: specifies the "end"
    print(i, end = " ")
for i in range(5, 10):  # Two arguments: first is "start" (inclusive), second is "end" (exclusive)
    print(i, end = " ")
for i in range(0, 10, 2):  # Three arguments: start, end, and increment
    print(i, end = " ")<jupyter_output>0 2 4 6 8<jupyter_text>**IMPORTANT: INDENTATION MATTERS** You'll notice in these loops that the *loop body* is distinctly indented relative to the *loop header*. This is intentional and is indeed how it works! If you fail to indent the body of the loop, Python will complain:<jupyter_code>some_list = [3.14159, "random stuff", 4200]
for item in some_list:
print(item)<jupyter_output><empty_output><jupyter_text>With loops, whitespace in Python *really* starts to matter. If you want many things to happen inside of a loop, you'll need to indent every line! Let's say in some future homework assignment, I ask you to write a loop computing the squares of the numbers 1-10. How would you do it? Well, you could manually write it out, I suppose...<jupyter_code>squares = [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]<jupyter_output><empty_output><jupyter_text>...but that's awfully boring. Instead, let's use the `range` function we were just discussing:<jupyter_code>squares = []  # Empty list for all our squares

for num in range(10):
    squared_number = num ** 2  # Exponent operation!
    squares.append(squared_number)  # Add to our list.

print(squares)<jupyter_output>[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]<jupyter_text>Looping through dictionaries This gets its own subsection because it pulls together pretty much all the concepts we've discussed so far: lists, tuples, dictionaries, and looping. Let's start by defining a dictionary. In this case, we'll set up a dictionary that maps people to their favorite programming language.<jupyter_code>favorite_languages = {
    'jen': 'python',
    'sarah': 'c',
    'edward': 'ruby',
    'shannon': 'python'
}
# Notice the indentation, if you decide to define a dictionary this way!<jupyter_output><empty_output><jupyter_text>Remember the super-useful methods for iterating through dictionaries? `keys` gives you a list of all the keys, `values` a list of all the values, and `items` a list of *tuples* of the key-value pairs. Here's the loop:<jupyter_code>for key, value in favorite_languages.items():  # 1
    print("{} prefers {}.".format(key, value))<jupyter_output>sarah prefers c.
shannon prefers python.
jen prefers python.
edward prefers ruby.<jupyter_text>1: Notice how `key, value` are just out there floating! This is called *unpacking* and is a very useful technique in Python. If I have a list of a few items, and (critically) *I know how many items there are*, I can do this<jupyter_code>some_list = ['a', 'b']
a, b = some_list<jupyter_output><empty_output><jupyter_text>instead of this<jupyter_code>some_list = ['a', 'b']
a = some_list[0]
b = some_list[1]<jupyter_output><empty_output><jupyter_text>In the same vein, I could have just as easily written the loop like this:<jupyter_code>for keyvalue in favorite_languages.items():  # 1
    key = keyvalue[0]
    value = keyvalue[1]
    print("{} prefers {}.".format(key, value)) # 2<jupyter_output>sarah prefers c.
shannon prefers python.
jen prefers python.
edward prefers ruby.<jupyter_text>and indeed, if that is easier for you to understand, by all means do it! This is to illustrate all the concepts at play at once:  - the loop header iterates through a list provided by `favorite_languages.items()` - each iteration, `items()` provides a tuple: a key-value pair from the dictionary - we can "unpack" these variables using shorthand, but it's also perfectly valid to do it the "regular" way That's pretty much `for` loops! What about the case where you don't know ahead of time how many iterations your loop will take?  Part 2: `while` Loops "While" loops go back yet again to the concept of boolean logic we introduced in an earlier lecture: loop until some condition is reached. The structure here is a little different than `for` loops. Instead of explicitly looping over an iterator, you'll set some condition that evaluates to either `True` or `False`; as long as the condition is `True`, Python executes another loop.<jupyter_code>x = 10

while x < 15:
    print(x, end = " ")
    x += 1<jupyter_output>10 11 12 13 14<jupyter_text>`x < 15` is a boolean statement: it is either `True` or `False`, depending on the value of `x`. Initially, this number is 10, which is certainly `< 15`, so the loop executes. 10 is printed, `x` is incremented, and the condition is checked again. A potential downside of `while` loops: **forgetting to update the condition inside the loop.** It's easy to take for granted; `for` loops implicitly handle this for us!<jupyter_code>for i in range(10, 15):
    print(i, end = " ")
    # No update needed!<jupyter_output>10 11 12 13 14
{'fraction_non_alphanumeric': 0.09140811455847256, 'fraction_numerical': 0.023389021479713605, 'mean_word_length': 4.7247267759562845, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 62, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 5, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '216613', 'license': 'MIT', 'n_tokens_mistral': 2756, 'n_tokens_neox': 2500, 'n_words': 1175, 'path': 'lectures/L5.ipynb', 'repo_name': 'eds-uga/csci1360-fa16', 'size': '8380'}