<jupyter_start><jupyter_text>![logo.png](image/logo.png) Data Structures You can access this notebook on: [colab](https://colab.com/py/), [github](https://github.com/chisomloius/iLearnPy/), [kaggle](https://kaggle.com/chisomloius/ilearnPy/), [medium](https://medium.com/@chisomloius/ilearnPy/), [web](https://chisomloius.github.io), [zindi](https://zindi.com/@chisomloius/ilearnPy/)  Table of Contents Click on the links to go directly to specific sections on  the notebook.1. [Import Dependencies](dependencies)2. [Tuples](tuples)3. [List](list)4. [Sets](sets)5. [Dictionaries](dictionaries)6. [After Thoughts](after-thoughts)7. [Assignment Link](assignment-link) 8. [About Author](about) 9. [More Info](more-info)Estimated time needed: 50 min----  Python - Let's get you writing some Python Code now!Welcome! This notebook will teach you about the tuples, list, sets and dictionaries in the Python Programming Language. By the end of this lab, you'll know their basic operations in Python, including indexing, slicing, sorting, updating, deleting and so on.      Import Dependencies<jupyter_code>%%expect_exception TypeError<jupyter_output><empty_output><jupyter_text>Tuples Welcome! This notebook will teach you about the tuples in the Python Programming Language. By the end of this lab, you'll know the basics tuple operations in Python, including indexing, slicing and sorting.   About this Dataset  What is in a Tuple In Python, there are different data types: string, integer and float. These data types can all be contained in a tuple as follows:  Now, let us create your first tuple with string, integer and float.<jupyter_code># Create your first tuple

tuple1 = ("disco", 10, 1.2)
tuple1<jupyter_output><empty_output><jupyter_text>The type of variable is a **tuple**.<jupyter_code># Print the type of the tuple you created

type(tuple1)<jupyter_output><empty_output><jupyter_text>Indexing Tuples  Each element of a tuple can be accessed via an index. The following table represents the relationship between the index and the items in the tuple. Each element can be obtained by the name of the tuple followed by a square bracket with the index number:  We can print out each value in the tuple:<jupyter_code># Print the variable on each index

print(tuple1[0])
print(tuple1[1])
print(tuple1[2])<jupyter_output>disco
10
1.2<jupyter_text>We can print out the **type** of each value in the tuple:<jupyter_code># Print the type of value on each index

print(type(tuple1[0]))
print(type(tuple1[1]))
print(type(tuple1[2]))<jupyter_output><class 'str'>
<class 'int'>
<class 'float'><jupyter_text>We can also use negative indexing. We use the same table above with corresponding negative values:  We can obtain the last element as follows (this time we will not use the print statement to display the values):<jupyter_code># Use negative index to get the value of the last element
tuple1[-1]<jupyter_output><empty_output><jupyter_text>We can display the next two elements as follows:<jupyter_code># Use negative index to get the value of the second last element
tuple1[-2]
# Use negative index to get the value of the third last element
tuple1[-3]<jupyter_output><empty_output><jupyter_text>Concatenate Tuples We can concatenate or combine tuples by using the **+** sign:<jupyter_code># Concatenate two tuples

tuple2 = tuple1 + ("hard rock", 10)
tuple2<jupyter_output><empty_output><jupyter_text>We can slice tuples obtaining multiple values as demonstrated by the figure below:   Slicing Tuples We can slice tuples, obtaining new tuples with the corresponding elements:<jupyter_code># Slice from index 0 to index 2
tuple2[0:3]<jupyter_output><empty_output><jupyter_text>We can obtain the last two elements of the tuple:<jupyter_code># Slice from index 3 to index 4
tuple2[3:5]<jupyter_output><empty_output><jupyter_text>We can obtain the length of a tuple using the length command:<jupyter_code># Get the length of tuple
len(tuple2)<jupyter_output><empty_output><jupyter_text>This figure shows the number of elements:   Sorting Tuples  Consider the following tuple:<jupyter_code># A sample tuple
Ratings = (0, 9, 6, 5, 10, 8, 9, 6, 2, 78547, 7402, 90, 1234, 10, 1)<jupyter_output><empty_output><jupyter_text>We can sort the values in a tuple and save it to a new tuple:<jupyter_code># Sort the tuple

RatingsSorted = sorted(Ratings)
RatingsSorted<jupyter_output><empty_output><jupyter_text>Nested Tuple A tuple can contain another tuple as well as other more complex data types. This process is called 'nesting'. Consider the following tuple with several elements:<jupyter_code># Create a nest tuple

NestedT =(1, 2, ("pop", "rock") ,(3,4),("disco",(1,2)))<jupyter_output><empty_output><jupyter_text>Each element in the tuple including other tuples can be obtained via an index as shown in the figure:<jupyter_code># Print element on each index

print("Element 0 of Tuple: ", NestedT[0])
print("Element 1 of Tuple: ", NestedT[1])
print("Element 2 of Tuple: ", NestedT[2])
print("Element 3 of Tuple: ", NestedT[3])
print("Element 4 of Tuple: ", NestedT[4])<jupyter_output>Element 0 of Tuple:  1
Element 1 of Tuple:  2
Element 2 of Tuple:  ('pop', 'rock')
Element 3 of Tuple:  (3, 4)
Element 4 of Tuple:  ('disco', (1, 2))<jupyter_text>We can use the second index to access other tuples as demonstrated in the figure:   We can access the nested tuples :<jupyter_code># Print element on each index, including nest indexes

print("Element 2, 0 of Tuple: ",   NestedT[2][0])
print("Element 2, 1 of Tuple: ",   NestedT[2][1])
print("Element 3, 0 of Tuple: ",   NestedT[3][0])
print("Element 3, 1 of Tuple: ",   NestedT[3][1])
print("Element 4, 0 of Tuple: ",   NestedT[4][0])
print("Element 4, 1 of Tuple: ",   NestedT[4][1])<jupyter_output>Element 2, 0 of Tuple:  pop
Element 2, 1 of Tuple:  rock
Element 3, 0 of Tuple:  3
Element 3, 1 of Tuple:  4
Element 4, 0 of Tuple:  disco
Element 4, 1 of Tuple:  (1, 2)<jupyter_text>We can access strings in the second nested tuples using a third index:<jupyter_code>#print the first element in the first nested tuples

NestedT [2][0][0]
# Print the first element in the second nested tuples

NestedT[2][1][0]
# Print the second element in the second nested tuples

NestedT[2][1][1]<jupyter_output><empty_output><jupyter_text>We can use a tree to visualise the process. Each new index corresponds to a deeper level in the tree:  Similarly, we can access elements nested deeper in the tree with a fourth index:<jupyter_code># Print the first element in the second nested tuples

NestedT[4][1][0]
# Print the second element in the second nested tuples

NestedT[4][1][1]
# Print the second element in the first nested tuples of the fourth index
NestedT [4][0][3]<jupyter_output><empty_output><jupyter_text>The following figure shows the relationship of the tree and the element NestedT[4][1][1]:  While we can retrieve data through indexing (because a `tuple` is ordered), we cannot modify it (because a `tuple` is immutable).<jupyter_code>tuple3 = ('Dylan', 26, 167.6, True)
print(tuple3)<jupyter_output>('Dylan', 26, 167.6, True)<jupyter_text>While we can retrieve data through indexing (because a `tuple` is ordered), we cannot modify it (because a `tuple` is immutable).<jupyter_code>print(tuple3[0])
print(tuple3[1])
print(tuple3[2])
%%expect_exception TypeError

tuple3[2] = 169.3<jupyter_output><empty_output><jupyter_text>Deleting Tuples<jupyter_code>%%expect_exception TypeError

# deletion also fails
del tuple3[-1]<jupyter_output>UsageError: Cell magic `%%expect_exception` not found.<jupyter_text>For clarity enclose tuples with `()`, Python will assume we want a `tuple` if we don't use any symbols to enclose comma separated values as seen below.<jupyter_code>tuple4 = 'Jill', 36, 162.3, True #this is a tuple
print(tuple4)
print(type(tuple4))<jupyter_output>('Jill', 36, 162.3, True)
<class 'tuple'><jupyter_text>This implicit `tuple` comes up most often when working with functions that return multiple outputs. For example, we might have a function that returns the first and last letter of a string.<jupyter_code>def first_last(s):
    return s[0], s[-1]


chars = first_last('hello!')
print(chars)<jupyter_output>('h', '!')<jupyter_text>Tuple Unpacking In such cases, we'll sometimes want to store the multiple outputs in separate variables.<jupyter_code>first_char, last_char = first_last('hello!')

print(first_char)
print(last_char)<jupyter_output>h
!<jupyter_text>This syntax above is called _unpacking_. We can use it with any `tuple`, whether it was returned by a function or not.<jupyter_code>def first_last(s):
    return s[0], s[-1]

first_char, last_char = first_last('computerised!')

print(first_char)
print(last_char)
tuple5 = ('Dylan', 26, 167.6, True)
print(tuple5)

name, age, height, has_dog = tuple5

print(name)
print(age)
print(height)
print(has_dog)<jupyter_output>('Dylan', 26, 167.6, True)
Dylan
26
167.6
True<jupyter_text>Both the Python `list` and `tuple` are ordered and heterogeneous. However, unlike the `list`, the `tuple` is immutable, meaning it cannot be modified after it is created. Therefore, a `list` might be better for representing data that is expected to change over the course of a program, like a to-do list. A `tuple` might be better for representing data that is expected to be fixed, like the responses of an individual subject to a survey. GotchaOne common mistake people make with immutability and especially with tuples is to assume data structures inside the tuple are immutable because the tuple is immutable.  Lets see an example.Even though the tuple itself is immutable, we cannot change the exact objects which it contains, those objects themselves can be mutated if they are mutable.  As with anywhere mutability shows up, this requires the programmer to be careful and not assume data has not been modified in some context.<jupyter_code>tuple6 = tuple([[], 'a'])
print(tuple6)
tuple6[0].append(1)
print(tuple6)
tuple7 = tuple([[], 'a'])
print(tuple7)
tuple7[0].append([23, 90])
print(tuple7)<jupyter_output>([], 'a')
([[23, 90]], 'a')<jupyter_text>Lists Welcome! This notebook will teach you about the lists in the Python Programming Language. By the end of this lab, you'll know the basics list operations in Python, including indexing, other list operations and copy/clone list.  About the Dataset Imagine you received album recommendations from your friends and compiled all of the recommandations into a table, with specific information about each album.The table has one row for each movie and several columns:- **artist** - Name of the artist- **album** - Name of the album- **released_year** - Year the album was released- **length_min_sec** - Length of the album (hours,minutes,seconds)- **genre** - Genre of the album- **music_recording_sales_millions** - Music recording sales (millions in USD) on [SONG://DATABASE](http://www.song-database.com/)- **claimed_sales_millions** - Album's claimed sales (millions in USD) on [SONG://DATABASE](http://www.song-database.com/)- **date_released** - Date on which the album was released- **soundtrack** - Indicates if the album is the movie soundtrack (Y) or (N)- **rating_of_friends** - Indicates the rating from your friends from 1 to 10The dataset can be seen below:      Artist    Album     Released    Length    Genre     Music recording sales (millions)    Claimed sales (millions)    Released    Soundtrack    Rating (friends)        <NAME>    Thriller     1982    00:42:19    Pop, rock, R&B    46    65    30-Nov-82        10.0        AC/DC    Back in Black     1980    00:42:11    Hard rock    26.1    50    25-Jul-80        8.5          Pink Floyd    The Dark Side of the Moon     1973    00:42:49    Progressive rock    24.2    45    01-Mar-73        9.5          Whitney Houston    The Bodyguard     1992    00:57:44    Soundtrack/R&B, soul, pop    26.1    50    25-Jul-80    Y    7.0          Meat Loaf    Bat Out of Hell     1977    00:46:33    Hard rock, progressive rock    20.6    43    21-Oct-77        7.0          Eagles    Their Greatest Hits (1971-1975)     1976    00:43:08    Rock, soft rock, folk rock    32.2    42    17-Feb-76        9.5          Bee Gees    Saturday Night Fever     1977    1:15:54    Disco    20.6    40    15-Nov-77    Y    9.0          Fleetwood Mac    Rumours     1977    00:40:01    Soft rock    27.9    40    04-Feb-77        9.5    Below:|Artist|Album|Relased|Length|Genre|Music recording sales (millions)|Claimed sales (millions)|Released|Soundtrack|Rating (friends)||------|-----|-------|------|-----|-------|-------|-----|-----|----||<NAME>|Thriller|1982|00:42:19|Pop, rock, R&B|46|65|30-Nov-82|b|10.0||<NAME>|Thriller|1982|00:42:19|Pop, rock, R&B|46|65|30-Nov-82|b|9.0||<NAME>|Thriller|1982|00:42:19|Pop, rock, R&B|46|65|30-Nov-82|b|10.0||<NAME>|Thriller|1982|00:42:19|Pop, rock, R&B|46|65|30-Nov-82|b|10.0|  Indexing We are going to take a look at lists in Python. A list is a sequenced collection of different objects such as integers, strings, and other lists as well. The address of each element within a list is called an index. An index is used to access and refer to items within a list.   To create a list, type the list within square brackets [ ], with your content inside the parenthesis and separated by commas. Let’s try it!<jupyter_code># Create a list
L = ["<NAME>", 10.1, 1982, True]
L<jupyter_output><empty_output><jupyter_text>We can use negative and regular indexing with a list :<jupyter_code># Print the elements on each index

print('the same element using negative and positive indexing:\n Postive:',L[0],
'\n Negative:' , L[-3] )
print('the same element using negative and positive indexing:\n Postive:',L[1],
'\n Negative:' , L[-2]  )
print('the same element using negative and positive indexing:\n Postive:',L[2],
'\n Negative:' , L[-1]  )
L[2]<jupyter_output><empty_output><jupyter_text>List Content Lists can contain strings, floats, and integers. We can nest other lists, and we can also nest tuples and other data structures. The same indexing conventions apply for nesting:<jupyter_code># Sample List
["<NAME>", 10.1, 1982, [1, 2], ("A", 1)]<jupyter_output><empty_output><jupyter_text>Python `list` are: 'ordered', 'heterogeneous', and 'mutable'. Because it is heterogeneous and mutable, the `list` is very flexible. We need to be careful about the changes we make to a `list`, because they can be very unpredictable. We could break our code or lose data!  List Operations: Slicing  We can also perform slicing in lists. For example, if we want the last two elements, we use the following command:<jupyter_code># Sample List
List = ["<NAME>", 10.1, 1982, "MJ" ,1]
List<jupyter_output><empty_output><jupyter_text><jupyter_code># List slicing
List[3:5]<jupyter_output><empty_output><jupyter_text>Extend MethodWe can use the method extend to add new elements to the list:<jupyter_code># Use extend to add elements to list
List = ["<NAME>", 10.2]
List.extend(['pop', 10])
List
List = ["<NAME>", 10.2]
List.extend([90])
List<jupyter_output><empty_output><jupyter_text>Another similar method is extendend. If we apply append instead of extend, we add one element to the list:<jupyter_code># Use append to add elements to list
List = [ "<NAME>", 10.2]
List.append(['pop', 10])
List<jupyter_output><empty_output><jupyter_text>Each time we apply a method, the list changes. If we apply extend we add two new elements to the list. The list L is then modified by adding two new elements:<jupyter_code># Use extend to add elements to list
List = [ "<NAME>", 10.2]
List.extend(['pop', 10])
List<jupyter_output><empty_output><jupyter_text>If we append the list  ['a','b'] we have one new element consisting of a nested list:<jupyter_code># Use append to add elements to list
List.append(['a','b'])
List<jupyter_output><empty_output><jupyter_text>As lists are mutable, we can change them. For example, we can change the first element as follows:<jupyter_code># Change the element based on the index
List2 = ["disco", 10, 1.2]
print('Before change:', List2)
List2[0] = 'hard rock'
print('After change:', List2)<jupyter_output>Before change: ['disco', 10, 1.2]
After change: ['hard rock', 10, 1.2]<jupyter_text>Deleting Elements in ListWe can also delete an element of a list using the del command:<jupyter_code># Delete the element based on the index

print('Before change:', List2)
del(List2[0])
print('After change:', List2)<jupyter_output>Before change: ['hard rock', 10, 1.2]
After change: [10, 1.2]<jupyter_text>We can convert a string to a list using split.  For example, the method split translates every group of characters separated by a space into an element in a list:<jupyter_code># Split the string, default is by space

'hard rock'.split()<jupyter_output><empty_output><jupyter_text>We can use the split function to separate strings on a specific character. We pass the character we would like to split on into the argument, which in this case is a comma.  The result is a list, and each element corresponds to a set of characters that have been separated by a comma:<jupyter_code># Split the string by comma

'A,B,C,D'.split(",")<jupyter_output><empty_output><jupyter_text>Copy and Clone List When we set one variable List4 equal to List3; both List3 and List4 are referencing the same list in memory:<jupyter_code># Copy (copy by reference) the list A
List3= ["hard rock", 10, 1.2]
List4 = List3
print('List3:', List3)
print('List4:', List4)<jupyter_output>List3: ['hard rock', 10, 1.2]
List4: ['hard rock', 10, 1.2]<jupyter_text>Initially, the value of the first element in List4 is set as hard rock. If we change the first element in List3 to banana, we get an unexpected side effect.  As List3 and List4 are referencing the same list, if we change list List3, then list List4 also changes. If we check the first element of List4 we get banana instead of hard rock:<jupyter_code># Examine the copy by reference
print('List3[0]:', List3[0])
List3[0] = "banana"
print('List4[0]:', List4[0])<jupyter_output><empty_output><jupyter_text>This is demonstrated in the following figure:   You can clone list  **List3** by using  the following syntax:<jupyter_code># Clone (clone by value) the list A
List4 = List3[:]
List4
List4[0] = "cherry"
List4
List3<jupyter_output><empty_output><jupyter_text>Variable **B** references a new copy or clone of the original list; this is demonstrated in the following figure:  Now if you change A, B will not change:<jupyter_code>print('List4[0]:', List4[0])
List3[0] = "hard rock"
print('List4[0]:', List4[0])<jupyter_output>List4[0]: cherry
List4[0]: cherry<jupyter_text>We see we successfully dealt with both a shorter list  so far.  Collections (or [**containers**](https://stackoverflow.com/questions/11575925/what-exactly-are-containers-in-python-and-what-are-all-the-python-container) as they're known in Python) can be very useful for tackling complex data problems. Python provides several types of containers, which we will explore. Each one has different properties and structure that make them useful for specific tasks. Later in the course, we'll also introduce powerful, highly-structured containers that Python users have invented and shared with the Python community.In the context of data science, we'll often call a collection of data that logically belongs together a **data set**, and the type of variable we use to store it in Python a **data structure**. These terms are meant to emphasize the relationships between the individual pieces of information that creates their meaning as a whole.  Complex List<jupyter_code>grocery_a = 'chicken'
grocery_b = 'onions'
grocery_c = 'rice'
grocery_d = 'peppers'
grocery_e = 'bananas'

grocery_list = ['chicken', 'onions', 'rice', 'peppers', 'bananas']
print(grocery_list)

grocery_list = [grocery_a, grocery_b, grocery_c, grocery_d, grocery_e]
print(grocery_list)<jupyter_output>['chicken', 'onions', 'rice', 'peppers', 'bananas']
['chicken', 'onions', 'rice', 'peppers', 'bananas']<jupyter_text>So far we've worked with `list`s of strings (and `range`), but we're not limited to only that data type.<jupyter_code>int_list = [2, 6, 3049, 18, 37]
float_list = [3.7, 8.2, 178.245, 63.1]
mixed_list = [26, False, 'some words', 1.264]

print(int_list)
print(float_list)
print(mixed_list)<jupyter_output>[2, 6, 3049, 18, 37]
[3.7, 8.2, 178.245, 63.1]
[26, False, 'some words', 1.264]<jupyter_text>We can store any `type` of data in a `list`. We can even put a `list` inside of a `list`.<jupyter_code>list_of_lists = [['a', 'list', 'of', 'words'], [1, 5, 209], [True, True, False]]
print(list_of_lists)<jupyter_output>[['a', 'list', 'of', 'words'], [1, 5, 209], [True, True, False]]<jupyter_text>There are very few restrictions on how we structure a list or what we put in it. This can lead to a very complicated nested structure.<jupyter_code>confusing_list = [[23, 73, 50], 'some words', 12.308, [[False, True], 'more words']]
print(confusing_list)
confusing_list[0]
confusing_list[0][2]
confusing_list[3]<jupyter_output><empty_output><jupyter_text>Again, the Python `list` is _heterogeneous_ because it can hold a collection of mixed objects. This is one of the major defining properties of the Python `list`.You may have also noticed that when we put data into a `list` in particular order, it stays in that order when we `print` or use the `list` in a `for` loop. Because `list` preserves order, we say it is _ordered_. We can use this property to retrieve particular items from a `list` based on their position (or **index**) in the list.<jupyter_code>#again let's use the list
grocery_a = 'chicken'
grocery_b = 'onions'
grocery_c = 'rice'
grocery_d = 'peppers'
grocery_e = 'bananas'

grocery_list = ['chicken', 'onions', 'rice', 'peppers', 'bananas']
print(grocery_list)

grocery_list = [grocery_a, grocery_b, grocery_c, grocery_d, grocery_e]
print(grocery_list)<jupyter_output>['chicken', 'onions', 'rice', 'peppers', 'bananas']
['chicken', 'onions', 'rice', 'peppers', 'bananas']<jupyter_text>Printing `grocery_list[2]` returned the third item in the list: 'rice'. Why did it return the third item if we asked for the item at index 2? Python `list`s are _zero-indexed_.<jupyter_code>print(grocery_list[0])
print(grocery_list[1])
print(grocery_list[2])<jupyter_output>chicken
onions
rice<jupyter_text>'again' only the last line will print. We can also retrieve a _slice_ of items from a list.<jupyter_code>grocery_list[0]
grocery_list[1]
grocery_list[2]
# hint of the list is given as ; ['chicken', 'onions', 'rice', 'peppers', 'bananas']
print(grocery_list[1:4]) #prints the 2th to the element before the 5th element
print(grocery_list[3:]) #prints the 4th to the last element
print(grocery_list[:3]) #print from 1st to the element before the 4th element<jupyter_output>['onions', 'rice', 'peppers']
['peppers', 'bananas']
['chicken', 'onions', 'rice']<jupyter_text>Python also has a negative indexing syntax, allowing us to access the list from the end instead of the beginning. The last element is indexed by -1.<jupyter_code>print(grocery_list[-1])
print(grocery_list[-3:])<jupyter_output>bananas
['rice', 'peppers', 'bananas']<jupyter_text>We can also slice a list using a step-size other than 1. For instance, we can slice every other item of the list, or even reverse the list by making negative steps.<jupyter_code>print(grocery_list[::2])
print(grocery_list[4:1:-1])
print(grocery_list[4:1:-2])<jupyter_output>['chicken', 'rice', 'bananas']
['bananas', 'peppers', 'rice']
['bananas', 'rice']<jupyter_text>We can of course also retrieve information from a list by using a `for` loop.<jupyter_code>for item in grocery_list:
    print('we have ... %s' % item)<jupyter_output>we have ... chicken
we have ... onions
we have ... rice
we have ... peppers
we have ... bananas<jupyter_text>While we'll usuaally use the syntax `for item in list`, sometimes we will combine a `for` loop with indexing. The `range` function (which we used in the last notebook) is useful for this. For example, we can pick out every other item in the list.<jupyter_code>for i in range(0, len(grocery_list), 2):
    print(i, grocery_list[i])<jupyter_output>0 chicken
2 rice
4 bananas<jupyter_text>The `range` function returns a sequence of integers between the first and second argument, using the third argument as the step size. Notice that the upper bound (i.e. second argument) is not included in the output.<jupyter_code>print(range(0, 10, 3)) # starts at 0 and counts by 3 and exclude now
print(range(104, 100, -1)) # starts counting backwward from 104 to 101
print(range(5)) # starts at 0 and counts by 1 by default<jupyter_output>range(0, 10, 3)
range(104, 100, -1)
range(0, 5)<jupyter_text>We can also use indexing/slicing to replace items in the list.<jupyter_code>grocery_list = ['chicken', 'onions', 'rice', 'peppers', 'bananas']
print(grocery_list)
grocery_list[-1] = 'oranges' # replace bananas with oranges
print(grocery_list)
grocery_list[1:3] = ['carrots', 'couscous'] #replace onions and rice with carrots and couscous
print(grocery_list)<jupyter_output>['chicken', 'onions', 'rice', 'peppers', 'bananas']
['chicken', 'onions', 'rice', 'peppers', 'oranges']
['chicken', 'carrots', 'couscous', 'peppers', 'oranges']<jupyter_text>modifying listSince we can modify lists after they are created, we call them _mutable_ (the modifications are called _mutations_). Some Python data types are _immutable_, meaning once they are created they cannot be changed. We'll explore this further as we introduce more data types.Another way we can mutate a `list` is to `append` new items.<jupyter_code>grocery_list = ['chicken', 'onions', 'rice', 'peppers', 'bananas']
print(grocery_list)
grocery_list.append('squash')
print(grocery_list)
grocery_list.append(['bread', 'salt'])
print(grocery_list) # what happened?<jupyter_output>['chicken', 'onions', 'rice', 'peppers', 'bananas']
['chicken', 'onions', 'rice', 'peppers', 'bananas', 'squash']
['chicken', 'onions', 'rice', 'peppers', 'bananas', 'squash', ['bread', 'salt']]<jupyter_text>Since lists can contain lists, we have to be careful about adding multiple items to our list. Instead of `append`, we might want to use `extend`.<jupyter_code>grocery_list = ['chicken', 'onions', 'rice', 'peppers', 'bananas', 'squash']
print(grocery_list)
grocery_list.extend(['bread', 'salt'])
print(grocery_list)<jupyter_output>['chicken', 'onions', 'rice', 'peppers', 'bananas', 'squash']
['chicken', 'onions', 'rice', 'peppers', 'bananas', 'squash', 'bread', 'salt']<jupyter_text>Sorting List Another mutation we can make to a list is to sort it.<jupyter_code>grocery_list.sort()
print(grocery_list)<jupyter_output>['bananas', 'bread', 'chicken', 'onions', 'peppers', 'rice', 'salt', 'squash']<jupyter_text>Sets A Python `set` is also similar to a `list`, except it is unordered. It can store heterogeneous data and it is mutable, but what does it mean to be unordered? The simplest explanation is simply to look at an example. We can create a set by enclosing our data with curly brackets `{}`. Set Content A set is a unique collection of objects in Python. You can denote a set with a curly bracket {}. Python will automatically remove duplicate items:A Python `set` is also similar to a `list`, except it is unordered. It can store heterogeneous data and it is mutable, but what does it mean to be unordered? The simplest explanation is simply to look at an example. We can create a set by enclosing our data with curly brackets `{}`.<jupyter_code># Create a set
set1 = {"pop", "rock", "soul", "hard rock", "rock", "R&B", "rock", "disco"}
set1<jupyter_output><empty_output><jupyter_text>The process of mapping is illustrated in the figure:   You can also  create a set from a list as follows:<jupyter_code># Convert list to set

album_list = [ "<NAME>", "Thriller", 1982, "00:42:19", \
              "Pop, Rock, R&B", 46.0, 65, "30-Nov-82", None, 10.0]
album_set = set(album_list)             
album_set<jupyter_output><empty_output><jupyter_text>Now let us create a set of  genres:<jupyter_code># Convert list to set

music_genres = set(["pop", "pop", "rock", "folk rock", "hard rock", "soul", \
                    "progressive rock", "soft rock", "R&B", "disco"])
music_genres<jupyter_output><empty_output><jupyter_text>Set Operations  Let us go over set operations, as these can be used to change the set. Consider the set A:<jupyter_code># Sample set

A = set(["Thriller", "Back in Black", "AC/DC"])
A<jupyter_output><empty_output><jupyter_text>We can add an element to a set using the add() method:<jupyter_code># Add element to set

A.add("NSYNC")
A<jupyter_output><empty_output><jupyter_text>If we add the same element twice, nothing will happen as there can be no duplicates in a set:<jupyter_code># Try to add duplicate element to the set

A.add("NSYNC")
A<jupyter_output><empty_output><jupyter_text>We can remove an item from a set using the remove method:<jupyter_code># Remove the element from set

A.remove("NSYNC")
A<jupyter_output><empty_output><jupyter_text>We can verify if an element is in the set using the in command:<jupyter_code># Verify if the element is in the set

"AC/DC" in A<jupyter_output><empty_output><jupyter_text>Sets Logic Operations Remember that with sets you can check the difference between sets, as well as the symmetric difference, intersection, and union:  Consider the following two sets:<jupyter_code># Sample Sets

album_set1 = set(["Thriller", 'AC/DC', 'Back in Black'])
album_set2 = set([ "AC/DC", "Back in Black", "The Dark Side of the Moon"])<jupyter_output><empty_output><jupyter_text><jupyter_code># Print two sets

album_set1, album_set2<jupyter_output><empty_output><jupyter_text>As both sets contain AC/DC and Back in Black we represent these common elements with the intersection of two circles.  You can find the intersect of two sets as follow using &:<jupyter_code># Find the intersections

intersection = album_set1 & album_set2
intersection<jupyter_output><empty_output><jupyter_text>You can find all the elements that are only contained in album_set1 using the difference method:<jupyter_code># Find the difference in set1 but not set2

album_set1.difference(album_set2)  
album_set2.difference(album_set1)<jupyter_output><empty_output><jupyter_text>You only need to consider elements in album_set1; all the elements in album_set2, including the intersection, are not included.  The elements in album_set2 but not in album_set1 is given by:<jupyter_code>album_set2.difference(album_set1)<jupyter_output><empty_output><jupyter_text>You can also find the intersection of album_list1 and album_list2, using the intersection method: The union corresponds to all the elements in both sets, which is represented by coloring both circles:   The union is given by:<jupyter_code># Find the union of two sets

album_set1.union(album_set2)<jupyter_output><empty_output><jupyter_text>And you can check if a set is a superset or subset of another set, respectively, like this:<jupyter_code># Check if superset

set(album_set1).issuperset(album_set2)   
# Check if subset

set(album_set2).issubset(album_set1)<jupyter_output><empty_output><jupyter_text>Here is an example where issubset() and issuperset() return true:<jupyter_code># Check if subset
set({"Back in Black", "AC/DC"}).issubset(album_set1) 
# Check if superset
album_set1.issuperset({"Back in Black", "AC/DC"})<jupyter_output><empty_output><jupyter_text>Consider the list A = [1, 2, 2, 1] and set B = set([1, 2, 2, 1]), does sum(A) = sum(B)<jupyter_code># Write your code below and press Shift+Enter to execute
A = [1, 2, 2, 1]  
B = set([1, 2, 2, 1])
print("the sum of A is:", sum(A))
print("the sum of B is:", sum(B))<jupyter_output>the sum of A is: 6
the sum of B is: 3<jupyter_text>Create a new set album_set3 that is the union of album_set1 and album_set2:<jupyter_code># Write your code below and press Shift+Enter to execute

album_set1 = set(["Thriller", 'AC/DC', 'Back in Black'])
album_set2 = set([ "AC/DC", "Back in Black", "The Dark Side of the Moon"])
album_set3 = album_set1.union(album_set2)
album_set3<jupyter_output><empty_output><jupyter_text>Find out if album_set1 is a subset of album_set3:<jupyter_code># Write your code below and press Shift+Enter to execute
album_set1.issubset({"Back in Black", "AC/DC"})<jupyter_output><empty_output><jupyter_text>More set methodsHowever, we can still add and delete items from a set.<jupyter_code>print(example_set)
print(example_set.pop())
print(example_set)

example_set.add('True')
print(example_set)
example_set.update([58.1, 'brown'])
print(example_set)
# Adding an element
example_set.add(10)
example_set
example_set.add(10)
example_set<jupyter_output><empty_output><jupyter_text>The `add` method of a `set` works similarly to the `append` method of a `list`. The `update` method of a `set` works similarly to the `extend` method of a `list`._**Why is `set` useful?**_It seems strange that we might want an _unordered_ data structure. We can't access or modify the data through indexing. How does giving up order benefit us? The answer is that it gives us flexibility about how the data is stored in memory, and that flexibility can make data retrieval much faster.Imagine we have ten boxes and ten piles of money. We put the ten piles of money in the ten boxes. Now say we want to find the box that has \$5.37 in it. We don't know which box this is, so we start with the first box and check. If it isn't in the first box, we move on to the second box. We keep checking boxes until we find it. This might take awhile.![hash_illustration.png](attachment:992d43fb-a799-4ec0-a275-abe1c062f888.png)Now imagine we have the same ten piles of money, but we have 31 boxes. Instead of putting each pile of money into the boxes in order, instead put each pile into a box based on the amount of money in the pile. First we multiply the amount of money by 100, and then take modulus division by 31. This gives the number of the box we should put the pile of money in.<jupyter_code>piles = [2.83, 8.23, 9.38, 10.23, 25.58, 0.42, 5.37, 28.10, 32.14, 7.31]
def hash_function(x):
    return int(x * 100 % 31)

[hash_function(pile) for pile in piles]
def hash_function(x):
    return int(x * 100 % 31)

[hash_function(pile) for pile in piles]
#Now say we want to find the box with \$5.37 in it. We don't have to search through box after box. We can compute:
print(int(5.37 * 100 % 31))<jupyter_output><empty_output><jupyter_text>![hash_illustration.png](attachment:5c826384-08e0-473b-ac2e-37c8a7a21fda.png)Box number 10 contains the \$5.37 pile.This technique of assigning boxes (i.e. memory) based on the object it contains is called **hashing**. It makes searching for data very fast (as we've illustrated), but at the cost of increase memory allocation (we needed more boxes). It also means that we cannot assign an order to the objects as they are stored in memory.Hashing also puts two major restrictions on the `set`. First of all, objects in a `set` must be immutable. If an object were to change, its position in memory would no longer correspond with its **hash**. Secondly, the objects in a `set` must be unique. Identical objects end up with the same hash. Since we can't store multiple objects in the same chunk of memory, we simply discard any duplicates.This second restriction means we can use a `set` to easily determine the unique objects in a `list` or `tuple`. suppose we have 50 boxes with with we want to put in 10 piles of money, we can use the code below<jupyter_code>def hash_function(x):
    return int(x * 100 % 50)

[hash_function(pile) for pile in piles]
(print (int(7.3 *100 % 50)))<jupyter_output><empty_output><jupyter_text>Unique function for set is hashing as displayed below:<jupyter_code>new_list = 'my string'
hash(new_list)
print(set([23, 609, 348, 10, 5, 23, 340, 82]))
print(set(('a', 'b', 'q', 'c', 'c', 'd', 'r', 'a')))
student_a_courses = {'history', 'english', 'biology', 'theatre'}
student_b_courses = {'biology', 'english', 'mathematics', 'computer science'}

print(student_a_courses.intersection(student_b_courses))
print(student_a_courses.union(student_b_courses))
print(student_a_courses.difference(student_b_courses))
print(student_b_courses.difference(student_a_courses))
print(student_a_courses.symmetric_difference(student_b_courses))<jupyter_output>{'biology', 'english'}
{'theatre', 'mathematics', 'history', 'english', 'biology', 'computer science'}
{'theatre', 'history'}
{'computer science', 'mathematics'}
{'theatre', 'computer science', 'mathematics', 'history'}<jupyter_text>Dictionaries What are Dictionaries?  An example of a Dictionary Dict:<jupyter_code># Create the dictionary

Dict = {"key1": 1, "key2": "2", "key3": [3, 3, 3], "key4": (4, 4, 4), ('key5'): 5, (0, 1): 6}
Dict<jupyter_output><empty_output><jupyter_text>The keys can be strings: Keys can also be any immutable object such as a tuple:   Each key is separated from its value by a colon ":".  Commas separate the items, and the whole dictionary is enclosed in curly braces. An empty dictionary without any items is written with just two curly braces, like this  "{}".<jupyter_code># Create a sample dictionary

release_year_dict = {"Thriller": "1982", "Back in Black": "1980", \
                    "The Dark Side of the Moon": "1973", "The Bodyguard": "1992", \
                    "Bat Out of Hell": "1977", "Their Greatest Hits (1971-1975)": "1976", \
                    "Saturday Night Fever": "1977", "Rumours": "1977"}
release_year_dict<jupyter_output><empty_output><jupyter_text>In summary, like a list, a dictionary holds a sequence of elements. Each element is represented by a key and its corresponding value. Dictionaries are created with two curly braces containing keys and values separated by a colon. For every key, there can only be one single value, however,  multiple keys can hold the same value. Keys can only be strings, numbers, or tuples, but values can be any data type. It is helpful to visualize the dictionary as a table, as in the following image. The first column represents the keys, the second column represents the values.  Keys You can retrieve the values based on the names:<jupyter_code># Get value by keys
release_year_dict['Thriller']<jupyter_output><empty_output><jupyter_text>This corresponds to:   Similarly for The Bodyguard<jupyter_code># Get value by key

release_year_dict['The Bodyguard']<jupyter_output><empty_output><jupyter_text>Now let you retrieve the keys of the dictionary using the method release_year_dict():<jupyter_code># Get all the keys in dictionary

release_year_dict.keys()<jupyter_output><empty_output><jupyter_text>You can retrieve the values using the method  values():<jupyter_code># Get all the values in dictionary

release_year_dict.values()<jupyter_output><empty_output><jupyter_text>We can add an entry:<jupyter_code># Append value with key into dictionary

release_year_dict['Graduation'] = '2007'
release_year_dict<jupyter_output><empty_output><jupyter_text>We can delete an entry:<jupyter_code># Delete entries by key

del(release_year_dict['Thriller'])
del(release_year_dict['Graduation'])
release_year_dict<jupyter_output><empty_output><jupyter_text>We can verify if an element is in the dictionary:<jupyter_code># Verify the key is in the dictionary

'The Bodyguard' in release_year_dict<jupyter_output><empty_output><jupyter_text>We can o understand the Python `dict`, let's start again with the Python `list`.<jupyter_code>me = ['chisom', 25, 175.6, 60.1, 'black', 'grey', True]<jupyter_output><empty_output><jupyter_text>This `list` describes me: my name, my age, my height (in centimeters), my weight (in kilograms), my hair color, my eye color, and whether or not I have a dog. We know we can access this information individually by index.<jupyter_code>print('My name is %s' % me[0])
print('I have %s hair' % me[4])<jupyter_output>My name is chisom
I have black hair<jupyter_text>It would be easy to get mixed up about which data is which (for example, which `'brown'` is hair color and which is eye color?), or where I should find it (will age always be at index 1?).A better solution would be a data structure where we could index using meaningful values. For example instead of using `me[0]` to recover `Dylan`, I could use `me['name']`. Instead of hair color being `me[4]`, it could be `me['hair']`. This feature is the central characteristic of the Python `dict`.<jupyter_code>me_dict = {'name': 'Chisom', 'age': 25, 'height': 170.1, 'weight': 60.1, 
           'hair': 'black', 'eyes': 'black', 'has dog': True}

print('My name is %s' % me_dict['name'])
print('I have %s hair' % me_dict['hair'])
print('I will be %d years old next month' % me_dict['age'])
me_dict.values()
me_dict.keys()<jupyter_output><empty_output><jupyter_text>Instead of calling `'name'` and '`hair'` the index, we call them **keys**. Each key is associated with a **value** in a **key-value pair**. We can see the key-value pairs in the `{}` syntax used for creating a `dict`. Each key-value pair is separated by a comma, and within a pair the key and value are separated by a colon `:`.  Exercises1. When might a `dict` be more useful than a `list`?2. Compare the flexibility of a `dict` which contains other `dict` object to that of a multi dimensional array. Answers 1. A 'dict' is more useful when want to reference each elemebt in a list and hence for easy reference we use a 'dict' rather than a list.2. |Dictionary|Multi-dimensional array||---|---||they are very effective in xyz| they are not effective in xyz|  `zip`The `zip` function can be very handy for creating a `dict`. Let's go back to the `list` we made before that contained all the values describing me. We'll make a second `list` containing all the keys we would want for putting these values in a dictionary<jupyter_code>value_list = me
key_list = ['name', 'age', 'height', 'weight', 'hair', 'eyes', 'has dog']

print(value_list)
print(key_list)<jupyter_output>['chisom', 25, 175.6, 60.1, 'black', 'grey', True]
['name', 'age', 'height', 'weight', 'hair', 'eyes', 'has dog']<jupyter_text>Currently we have two lists: one of values and one of keys. They have no relationship to each other within Python, but we can see that they belong together logically. How do we combine them in Python? By using the `zip` function.<jupyter_code>key_value_pairs = list(zip(key_list, value_list))
print(key_value_pairs)<jupyter_output>[('name', 'chisom'), ('age', 25), ('height', 175.6), ('weight', 60.1), ('hair', 'black'), ('eyes', 'grey'), ('has dog', True)]<jupyter_text>We now have a list of tuples. We interpret the first element of each tuple as a key, and the second element as a value. We can turn this list of tuples directly into a `dict`.<jupyter_code>me_dict = dict(key_value_pairs)
print(me_dict)<jupyter_output>{'name': 'chisom', 'age': 25, 'height': 175.6, 'weight': 60.1, 'hair': 'black', 'eyes': 'grey', 'has dog': True}<jupyter_text>You may have noticed that even though our list of tuples began with `('name', 'Dylan')`, when we printed `me_dict` it started with `'eyes': 'brown'`. If you guessed this means that a `dict` is unordered, you are correct! The keys are hashed to assign key-value pairs to memory. Therefore, keys must be immutable and unique, similar to the elements of a `set`. However, values don't have these restrictions.  Some ways we can't write a 'dict'<jupyter_code>%%expect_exception TypeError

# this doesn't work
invalid_dict = {[1, 5]: 'a', 5: 23}<jupyter_output>UsageError: Cell magic `%%expect_exception` not found.<jupyter_text>The `dict` is also mutable. We can add new key-value pairs by simple assignment.<jupyter_code>print(me_dict)
me_dict['favorite book'] =  'The Little Prince'
print(me_dict)<jupyter_output><empty_output><jupyter_text>We can also use `update`, similar to the way we used it for a `set`, except now with key-value pairs.<jupyter_code>print(me_dict)
me_dict.update({'favorite color': 'orange', 'siblings': 3, 'nieces/nephews': 75})
print(me_dict)<jupyter_output>{'name': 'chisom', 'age': 25, 'height': 175.6, 'weight': 60.1, 'hair': 'black', 'eyes': 'grey', 'has dog': True}
{'name': 'chisom', 'age': 25, 'height': 175.6, 'weight': 60.1, 'hair': 'black', 'eyes': 'grey', 'has dog': True, 'favorite color': 'orange', 'siblings': 3, 'nieces/nephews': 75}<jupyter_text>We can replace or delete key-value pairs from the `dict`.<jupyter_code>del me_dict['favorite book']
print(me_dict)
print(me_dict.pop('siblings'))
print(me_dict)<jupyter_output><empty_output><jupyter_text>Because the `dict` uses hashing, searching it is very fast (as with `set`). Sometimes dictionaries are referred to as **lookup tables** or **hash tables**. It is incredibly useful for referencing data through meaningful keys. While the data in a `dict` is unordered, it remains organized by the keys. We can retrieve a list of the keys and values directly, or as key-value pairs, using the appropriate methods of `dict`.<jupyter_code>print(me_dict.keys())
print(me_dict.values())
print(me_dict.items())<jupyter_output><empty_output><jupyter_text>Switching data structuresEach of the containers we've introduced has different properties and characteristics. Sometimes we will want to change one data structure into another to take advantage of these differences. We've already seen some methods for transforming a `dict` into a `list` of `tuple`s or vice versa. We can easily transform between `list`, `tuple`, and `set`.m<jupyter_code>example_list = ['a', 'b', 23, 10, True, 'a', 10]
example_tuple = tuple(example_list)
example_set = set(example_tuple)
example_list = list(example_set)

print(example_tuple)
print(example_set)
print(example_list) # note we lost the duplicates because of set<jupyter_output>('a', 'b', 23, 10, True, 'a', 10)
{'a', True, 10, 'b', 23}
['a', True, 10, 'b', 23]<jupyter_text>SearchWe discussed the idea of searching for data in our data structures when describing what makes `set` (and `dict`) so special. What does search look like in Python? We search for data using the keyword `in`.<jupyter_code>print(example_list)
print('a' in example_list)
print('c' in example_list)<jupyter_output>['a', True, 10, 'b', 23]
True
False<jupyter_text>When dealing with a `dict`, we can search keys, but not values.<jupyter_code>print(me_dict)
print('hair' in me_dict)
print('has cat' in me_dict)
print('brown' in me_dict)<jupyter_output>{'name': 'chisom', 'age': 25, 'height': 175.6, 'weight': 60.1, 'hair': 'black', 'eyes': 'grey', 'has dog': True}
True
False
False<jupyter_text>Searching for keys is important in dictionaries so that we don't accidentally try to retrieve a key-value pair that doesn't exist.<jupyter_code>me_dict.update({'has cat':'ukra'})
if 'has dog' in me_dict:
    print('Has dog: %s' % me_dict['has dog'])
else:
    print(None)

if 'has cat' in me_dict:
    print('Has cat: %s' % me_dict['has cat'])
else:
    print(None)
# can use get method for same results

print('Has dog: %s' % me_dict.get('has dog'))
print('Has cat: %s' % me_dict.get('has cat'))<jupyter_output><empty_output><jupyter_text>SortingSince a `tuple` is immutable, can we sort it? Or is that a mutation? What would it mean to sort a `set` or a `dict`, which has no order?Out of the data structures we've studied so far, only `list` has a `sort` method. However, Python also has a `sorted` function, which will create a sorted `list` out of other data structures. By default `sorted` applied to a `dict` makes a `list` of sorted keys. We must use the `items` method if we want our output to be key-value pairs.<jupyter_code>print(sorted(map(str, example_tuple)))
print(sorted(map(str, example_set)))
print(sorted(me_dict.items()))
print(sorted(me_dict))<jupyter_output>['10', '10', '23', 'True', 'a', 'a', 'b']
['10', '23', 'True', 'a', 'b']
[('age', 25), ('eyes', 'grey'), ('favorite color', 'orange'), ('hair', 'black'), ('has cat', 'ukra'), ('has dog', True), ('height', 175.6), ('name', 'chisom'), ('nieces/nephews', 75), ('siblings', 3), ('weight', 60.1)]
['age', 'eyes', 'favorite color', 'hair', 'has cat', 'has dog', 'height', 'name', 'nieces/nephews', 'siblings', 'weight']<jupyter_text>IterationAs we've seen in some examples already, it will often be useful to iterate through a data structure, whether to execute some task based on the information contained or to transform or analyze a data set. We will most often use `for` loops to iterate over data structures. With a `list`, `tuple`, or `set` the elements of the container are returned one after another. With a `dict` things are a little more complicated: do we want to iterate over keys, values, or key-value pairs?<jupyter_code># by default we iterate over keys of a dict
for f in me_dict:
    print(f)
# to iterate over values...
for y in me_dict.values():
    print(y)
# or to iterate over key-value pairs...
for k, v in me_dict.items():
    print('%s:%s' % (k, v))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11571416857986225, 'fraction_numerical': 0.025869137422105608, 'mean_word_length': 5.434318121867581, 'pattern_counts': {'":': 13, '<': 408, '<?xml version=': 0, '>': 408, 'https://': 7, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '108', 'dir': 'jupyter-structured-clean-dedup', 'id': '34855', 'license': 'MIT', 'n_tokens_mistral': 16660, 'n_tokens_neox': 15545, 'n_words': 6085, 'path': 'iLearn/py/practice/1-basic/notes/2.Data_Types.ipynb', 'repo_name': 'chisomloius/iLearnPy-', 'size': '48901'}