Статистика и открытые данные

И. В. Щуров, НИУ ВШЭ

Авторы задач в подборке: А. Зотов, И. Щуров.

На странице курса находятся другие материалы.

Домашнее задание №7

За разные задачи можно получить разное число баллов. Если не указано обратное, задача весит 1 балл. Максимум за ДЗ можно набрать 14 баллов. Вы можете решить больше задач, чем требуется, чтобы потренироваться.

Для предварительной проверки задания нужно сделать следующее:

  1. Скачать данный ipynb-файл на свой компьютер, открыть его в IPython Notebook/Jupyter.
  2. Активировать тесты (см. ниже).
  3. Запустить ячейку, в которую вы вставили код с решением.
  4. Запустить следующую ячейку (в ней содержится тест). Если запуск ячейки с тестом не приводит к появлению ошибки (assertion), значит, всё в порядке, задача решена. Если приводит к появлению ошибки, значит, тест не пройден и нужно искать ошибку.

Внимание! Если в какой-то момент забыть ввести входные данные и перейти на следующую ячейку, есть риск, что Notebook перестанет откликаться. В этом случае надо перезапустить ядро: Kernel → Restart. При этом потеряются все значения переменных, но сам код останется.

Чтобы сдать ДЗ, его надо загрузить в nbgr-x в виде ipynb-файла. Получить ipynb-файл можно, выбрав в Jupyter пункт меню File → Download as... → IPython Notebook (.ipynb).

Во всех задачах словом «датафрейм» обозначается объект типа pd.dataframe. Вы должны пользоваться методами pandas, чтобы получить баллы за задачи. Везде, где не оговорено обратное, запрещается пользоваться циклами и list comprehensions!

In [ ]:
import pandas as pd #Поехали!
import numpy as np

from pandas.testing import assert_frame_equal

Задача 1 (2 балла)

Напишите функцию make_panel(df), которая получает на вход датафрейм, столбцами в котором являются годы, строками — названия фирм, а значениями в ячейках — некая числовая переменная. Ваша функция должна привести датафрейм к виду, в котором обычно представляют панельные данные, а именно в виде таблицы с тремя столбцами: year — год, firm — название фирмы, и value — значение переменной (в данной задаче порядок столбцов должен быть именно таким). Таблица должна быть отсортирована сначала по названию фирмы, а при совпадении названия фирмы — по году. У таблицы должен быть стандартный индекс (числа от 0 до $n-1$, где $n$ — число строк) в стандартном (возрастающем) порядке.

К примеру, из таблицы:

In [ ]:
df = pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=[2000, 2001], 
             index=['Firm_A', 'Firm_B', 'Firm_C'])
df

Ваша программа должна сделать (и вернуть) таблицу:

In [ ]:
out = pd.DataFrame([[2000, 'Firm_A', 1],
                    [2001, 'Firm_A', 2],
                    [2000, 'Firm_B', 3],
                    [2001, 'Firm_B', 4],
                    [2000, 'Firm_C', 5],
                    [2001, 'Firm_C', 6]], columns=['year', 'firm', 'value'])
out

Подсказка. Вам пригодятся методы .unstack() и .to_frame() или .melt(), а также .reset_index() и .rename(). Полезно также знать, что методу .sort_values() в качестве того, по чему происходит сортировка, можно передавать не только название одного столбца, но и список столбцов — тогда сортировка будет происходить по всем этим столбцам (по первому, при совпадении первого — по второму и т.д.)

In [ ]:
# YOUR CODE HERE
In [ ]:
def test():
    import random
    import string
    def test_panel(num_of_firms, num_of_years, seed=10):
        d = {}
        random.seed(seed)
        n = []
        f = []
        for i in range(num_of_firms):
            x = ''.join(random.choices(string.ascii_uppercase, k=3))
            f.extend([x] * num_of_years)
            y = random.sample(range(max(num_of_firms, num_of_years) * 2), num_of_years)
            n.extend([y])
            d[x] = y
        d = pd.DataFrame(d).T
        d.columns = [x for x in range(1990, 1990 + num_of_years)]
        outp = pd.DataFrame({'year': [x for x in range(1990, 1990 + num_of_years)] * num_of_firms, 'firm': f, 'value': sum(n, [])}).sort_values(['firm', 'year']).reset_index(drop=True)
        assert_frame_equal(outp[['year', 'firm', 'value']], make_panel(d), check_dtype=False)
    test_panel(1, 1)
    test_panel(10, 20)
    test_panel(15, 150)
    test_panel(150, 15)
    test_panel(1, 10)
test()

Задача 2 (2 балла)

В списке gradebooks содержатся кортежи, первый элемент каждого из которых — номер группы студентов а второй — датафрейм, содержащий оценки студентов из данного класса по разным дисципланам. В колонках записаны названия дисциплин, а в индексах строк — имена студентов. Ваша задача — написать функцию merge_gradebooks(gradebooks), которая объединяет все учебные ведомости в один датафрейм. В объединенной ведомости должен появиться столбец group, в котором записан номер группы. Индеком должны остаться имена студентов.

В этой задаче разрешено использовать ровно один цикл for, в котором выполняется не больше двух строк, либо один цикл for, в котором выполняется одна строка, и дополнительно один list comprehension. (Впрочем, можно обойтись и одним list comprehension, с помощью метода .assign.) Дополнительные функции создавать нельзя.

Подсказка. Вам пригодится pd.concat.

In [ ]:
# YOUR CODE HERE
In [ ]:
def test():
    gradebooks = [(169, pd.DataFrame({'Algebra': [2, 2, 5], 'History': [4, 4, 4], 'English': [5, 3, 1]}, index=['Alice A', 'Bob A', 'Claudia A'])),
                  (170, pd.DataFrame({'History': [5, 5, 5, 2, 2, 2], 'Algebra': [5, 5, 5, 2, 2, 2], 'English': [2, 2, 2, 5, 5, 5]}, index=['Alice B', 'Bob B', 'Claudia B', 'Daniel B', 'Eddie B', 'Frank B'])),
                  (188, pd.DataFrame({'English': [2, 3], 'Algebra': [3, 4], 'History': [4, 5]}, index=['Alice C', 'Bob C'])),
                  (209, pd.DataFrame({'English': [5, 5, 5], 'Algebra': [4, 5, 5], 'History': [5, 4, 5]}, index=['Alice D', 'Bob D', 'Charlie D']))]
    obtained = merge_gradebooks(gradebooks).sort_values(['Algebra', 'English', 'History', 'group'])
    expected = pd.DataFrame({'Algebra': {'Alice A': 2, 'Bob A': 2, 'Claudia A': 5, 'Alice B': 5, 'Bob B': 5, 'Claudia B': 5, 'Daniel B': 2, 'Eddie B': 2, 'Frank B': 2, 'Alice C': 3, 'Bob C': 4, 'Alice D': 4, 'Bob D': 5, 'Charlie D': 5}, 'English': {'Alice A': 5, 'Bob A': 3, 'Claudia A': 1, 'Alice B': 2, 'Bob B': 2, 'Claudia B': 2, 'Daniel B': 5, 'Eddie B': 5, 'Frank B': 5, 'Alice C': 2, 'Bob C': 3, 'Alice D': 5, 'Bob D': 5, 'Charlie D': 5}, 'History': {'Alice A': 4, 'Bob A': 4, 'Claudia A': 4, 'Alice B': 5, 'Bob B': 5, 'Claudia B': 5, 'Daniel B': 2, 'Eddie B': 2, 'Frank B': 2, 'Alice C': 4, 'Bob C': 5, 'Alice D': 5, 'Bob D': 4, 'Charlie D': 5}, 'group': {'Alice A': 169, 'Bob A': 169, 'Claudia A': 169, 'Alice B': 170, 'Bob B': 170, 'Claudia B': 170, 'Daniel B': 170, 'Eddie B': 170, 'Frank B': 170, 'Alice C': 188, 'Bob C': 188, 'Alice D': 209, 'Bob D': 209, 'Charlie D': 209}}).sort_values(['Algebra', 'English', 'History', 'group'])
    assert_frame_equal(obtained[sorted(obtained.columns)], expected[sorted(expected.columns)])
test()

Задача 3 (3 балла)

Напишите функцию merge_sectors(df_emp, df_names), которая принимает на вход два датафрейма. В первом датафрейме содержатся панельные данные о занятости в разничных отраслях экономики за разные годы. В колонке year записан год наблюдения, в code3 — идентификатор отрасли, состоящий из трех цифр, и в emp — число занятых людей в миллионах. Во втором датафрейме всего две колонки: code2 содержит идентификатор группы отраслей, состоящий из двух цифр, а name — название этой группы. Ваша задача — добавить в датафрейм df_emp колонку group_name, которая содержит название соответствующей группы отраслей из df_names, и вернуть его функцией return. Соответствие между отраслью и группой определяется следующим образом: две первые цифры в трехзначном идентификаторе обозначают соответствующий двузначный идентификатор. Например, у отраслей с кодами 114, 115 и 119 один общий двузначный идентификатор 11. Порядок следования строк должен быть таким же, как в df_emp.

Например, для:

In [ ]:
df_emp_example = pd.DataFrame({'year': [2000, 2001] * 3, 'code3': [111, 111, 113, 113, 122, 122], 'emp': [10, 13, 12, 11, 10, 7]})
df_names_example = pd.DataFrame({'code2': [11, 12], 'name': ['Farming', 'Manufacturing']})
df_emp_example
In [ ]:
df_names_example

Функция merge_sectors(df_emp_example, df_names_example) должна вернуть датафрейм:

In [ ]:
pd.DataFrame({'year': [2000, 2001] * 3, 'code3': [111, 111, 113, 113, 122, 122], 'emp': [10, 13, 12, 11, 10, 7], 'group_name': ['Farming', 'Farming', 'Farming', 'Farming', 'Manufacturing', 'Manufacturing']})

Подсказки:

  1. Вам понадобится .merge или .join. Помните о типах merge/join (left join, right join, inner join, outer join) — какой из них вам нужен? (Нужно сохранить порядок строк как в df_emp.)
  2. Преобразовать все элементы столбца к другому типу можно с помощью .astype.
  3. Если у вас есть столбец в датафрейме, состоящий из строк, вы можете применять строковые операции ко всем его элементам, обращаясь к нему через .str. Например:
In [ ]:
df = pd.DataFrame({'name': ['Alice', 'Bob', 'Claudia'],
                   'grade': [1, 5, 4]})
df['name_up'] = df['name'].str.upper() 
# вместо .upper может быть любая операция, применимая к строкам
df
In [ ]:
# YOUR CODE HERE
In [ ]:
df_emp_example = pd.DataFrame({'year': [2000, 2001] * 3, 'code3': [111, 111, 113, 113, 122, 122], 'emp': [10, 13, 12, 11, 10, 7]})
df_names_example = pd.DataFrame({'code2': [11, 12], 'name': ['Farming', 'Manufacturing']})
outp = merge_sectors(df_emp_example, df_names_example)
et = pd.DataFrame({'year': [2000, 2001] * 3, 'code3': [111, 111, 113, 113, 122, 122], 'emp': [10, 13, 12, 11, 10, 7], 'group_name': ['Farming', 'Farming', 'Farming', 'Farming', 'Manufacturing', 'Manufacturing']})
assert_frame_equal(outp[sorted(outp.columns)], et[sorted(et.columns)], check_dtype=False)

df_emp_t = pd.DataFrame({'code3':{0:311,1:311,2:311,3:312,4:312,5:312,6:319,7:319,8:319,9:322,10:322,11:322,12:325,13:325,14:325,15:329,16:329,17:329,18:339,19:339,20:339,21:324,22:324,23:324,24:329,25:329,26:329,27:301,28:301,29:301,30:114,31:114,32:231,33:231,34:231,35:231,36:441,37:441,38:442,39:442,40:442},'emp':{0:82.0,1:33.0,2:69.0,3:53.0,4:75.0,5:2.0,6:32.0,7:86.0,8:72.0,9:23.0,10:45.0,11:24.0,12:66.0,13:64.0,14:26.0,15:62.0,16:66.0,17:13.0,18:99.0,19:28.000000000000004,20:34.0,21:73.0,22:41.0,23:17.0,24:50.0,25:72.0,26:72.0,27:34.0,28:32.0,29:50.0,30:53.0,31:79.0,32:97.0,33:34.0,34:13.0,35:72.0,36:82.0,37:84.0,38:32.0,39:69.0,40:66.0},'year':{0:2009,1:2010,2:2011,3:2009,4:2010,5:2011,6:2009,7:2010,8:2011,9:2009,10:2010,11:2011,12:2009,13:2010,14:2011,15:2009,16:2010,17:2011,18:2009,19:2010,20:2011,21:2009,22:2010,23:2011,24:2009,25:2010,26:2011,27:2009,28:2010,29:2011,30:2009,31:2010,32:2009,33:2010,34:2011,35:2012,36:2008,37:2009,38:2009,39:2010,40:2011}})
df_names_t = pd.DataFrame({'code2': {0: 31, 1: 32, 2: 33, 3: 30, 4: 11, 5: 23, 6: 44},
 'name': {0: 'Agroculture',
  1: 'Manufacturing',
  2: 'Transportation',
  3: 'Construction',
  4: 'Oil',
  5: 'Banking',
  6: 'Military'}})
outp = merge_sectors(df_emp_t, df_names_t)
et = pd.DataFrame({'code3': {0: 311, 1: 311, 2: 311, 3: 312, 4: 312, 5: 312, 6: 319, 7: 319, 8: 319, 9: 322, 10: 322, 11: 322, 12: 325, 13: 325, 14: 325, 15: 329, 16: 329, 17: 329, 18: 339, 19: 339, 20: 339, 21: 324, 22: 324, 23: 324, 24: 329, 25: 329, 26: 329, 27: 301, 28: 301, 29: 301, 30: 114, 31: 114, 32: 231, 33: 231, 34: 231, 35: 231, 36: 441, 37: 441, 38: 442, 39: 442, 40: 442}, 'emp': {0: 82.0, 1: 33.0, 2: 69.0, 3: 53.0, 4: 75.0, 5: 2.0, 6: 32.0, 7: 86.0, 8: 72.0, 9: 23.0, 10: 45.0, 11: 24.0, 12: 66.0, 13: 64.0, 14: 26.0, 15: 62.0, 16: 66.0, 17: 13.0, 18: 99.0, 19: 28.000000000000004, 20: 34.0, 21: 73.0, 22: 41.0, 23: 17.0, 24: 50.0, 25: 72.0, 26: 72.0, 27: 34.0, 28: 32.0, 29: 50.0, 30: 53.0, 31: 79.0, 32: 97.0, 33: 34.0, 34: 13.0, 35: 72.0, 36: 82.0, 37: 84.0, 38: 32.0, 39: 69.0, 40: 66.0}, 'year': {0: 2009, 1: 2010, 2: 2011, 3: 2009, 4: 2010, 5: 2011, 6: 2009, 7: 2010, 8: 2011, 9: 2009, 10: 2010, 11: 2011, 12: 2009, 13: 2010, 14: 2011, 15: 2009, 16: 2010, 17: 2011, 18: 2009, 19: 2010, 20: 2011, 21: 2009, 22: 2010, 23: 2011, 24: 2009, 25: 2010, 26: 2011, 27: 2009, 28: 2010, 29: 2011, 30: 2009, 31: 2010, 32: 2009, 33: 2010, 34: 2011, 35: 2012, 36: 2008, 37: 2009, 38: 2009, 39: 2010, 40: 2011}, 'group_name': {0: 'Agroculture', 1: 'Agroculture', 2: 'Agroculture', 3: 'Agroculture', 4: 'Agroculture', 5: 'Agroculture', 6: 'Agroculture', 7: 'Agroculture', 8: 'Agroculture', 9: 'Manufacturing', 10: 'Manufacturing', 11: 'Manufacturing', 12: 'Manufacturing', 13: 'Manufacturing', 14: 'Manufacturing', 15: 'Manufacturing', 16: 'Manufacturing', 17: 'Manufacturing', 18: 'Transportation', 19: 'Transportation', 20: 'Transportation', 21: 'Manufacturing', 22: 'Manufacturing', 23: 'Manufacturing', 24: 'Manufacturing', 25: 'Manufacturing', 26: 'Manufacturing', 27: 'Construction', 28: 'Construction', 29: 'Construction', 30: 'Oil', 31: 'Oil', 32: 'Banking', 33: 'Banking', 34: 'Banking', 35: 'Banking', 36: 'Military', 37: 'Military', 38: 'Military', 39: 'Military', 40: 'Military'}})
assert_frame_equal(outp[sorted(outp.columns)], et[sorted(et.columns)], 
                   check_dtype=False)

Задача 4 (3 балла)

На вход которой подаются два датафрейма: один с телефонными номерами жителей различных штатов, а другой с их адресами.

Первый датафрейм содержит следующие колонки:

  • First Name — Имя
  • Last Name — Фамилия
  • Phone — Телефонный номер
  • State — Штат проживания

Второй содержит колонки:

  • Full Name — Имя и фамилия, разделенные пробелом
  • State — Штат проживания
  • Address — Адрес проживания
  • ZIP — Почтовый индекс

Ваша функция make_books(df_phone, df_address) должна создать датафреймы, в которых будут объединены телефоны и адреса жителей. Функция должна вернуть список, элементами которого будут такие датафреймы для каждого штата. Иными словами, если во входных датафреймах представлены 3 штата, то функция должна вернуть список длиной 3. В каждом датафрейме должны быть только такие записи, для которых есть и телефонный номер, и адрес (например, в тестирующих данных для имени Miah Mccoy есть адрес, но нет телефона. Следовательно, такого человека в финальной таблице быть не должно). Общая таблица должна содержать эти (и только эти) колонки:

  • First Name — Имя
  • Last Name — Фамилия
  • Phone — Телефонный номер
  • State — Штат
  • Address — Адрес
  • ZIP — Почтовый индекс

Обратите внимание, что имена могут повторяться (как только имя или только фамилия, так и имя с фамилией вместе), но гарантируется, что внутри одного штата тезок нет.

В этой задаче можно использовать ровно один цикл for, в теле которого использовать ровно одну строку.

Подсказка. Вам может пригодиться метод pd.Series .unique().

In [ ]:
# YOUR CODE HERE
In [ ]:
def test():
    def sort_tester(lst):
        lst = sorted(lst, key=lambda x: x['State'].iloc[0])
        lst2 = []
        for book in lst:
            lst2.append(book.sort_values(['Phone', 'ZIP']).reindex(sorted(book.columns), axis=1))
        return lst2

    books_test = sort_tester([pd.DataFrame({'State': {80: 'IL', 82: 'IL', 81: 'IL', 79: 'IL'}, 'Address': {80: '22 Summit St.', 82: '7800A Sunset Ave.', 81: '709 Clinton St.', 79: '7147 Greenrose Ave.'}, 'ZIP': {80: 54942, 82: 90943, 81: 94710, 79: 84939}, 'First Name': {80: 'Devon', 82: 'Iyana', 81: 'Leticia', 79: 'Matthew'}, 'Last Name': {80: 'Schultz', 82: 'Arnold', 81: 'Blake', 79: 'Hancock'}, 'Phone': {80: 586838, 82: 593006, 81: 591314, 79: 580199}}),
    pd.DataFrame({'State': {37: 'NY', 38: 'NY', 40: 'NY', 34: 'NY', 30: 'NY', 42: 'NY', 29: 'NY', 39: 'NY', 33: 'NY', 28: 'NY', 36: 'NY', 32: 'NY', 31: 'NY', 41: 'NY', 35: 'NY'}, 'Address': {37: '779 SE. Cooper Ave.', 38: '10 NE. Clay St.', 40: '7714 Gates Dr.', 34: '7595 Gregory St.', 30: '8230 N. Anderson St.', 42: '17 Main St.', 29: '481 North Fairview Road', 39: '23 Lincoln Dr.', 33: '8164 South Rockcrest Drive', 28: '993 Belmont Court', 36: '15 Catherine St.', 32: '1 Willow St.', 31: '383 Trout Lane', 41: '11 Lilac St.', 35: '7471 Rockaway St.'}, 'ZIP': {37: 87632, 38: 80601, 40: 77291, 34: 89309, 30: 71355, 42: 68331, 29: 70112, 39: 55888, 33: 89500, 28: 58515, 36: 55292, 32: 52792, 31: 99289, 41: 41719, 35: 65388}, 'First Name': {37: 'Abigail', 38: 'Ali', 40: 'Beckett', 34: 'Caiden', 30: 'Cristina', 42: 'Edgar', 29: 'Junior', 39: 'Kaitlin', 33: 'Liana', 28: 'Meadow', 36: 'Miah', 32: 'Mireya', 31: 'Quinton', 41: 'Reuben', 35: 'Rory'}, 'Last Name': {37: 'Solis', 38: 'Owen', 40: 'Howard', 34: 'Christensen', 30: 'Bryan', 42: 'Walker', 29: 'Haley', 39: 'Bates', 33: 'Callahan', 28: 'Olson', 36: 'Mccoy', 32: 'Pruitt', 31: 'Freeman', 41: 'Roberson', 35: 'Wheeler'}, 'Phone': {37: 367691, 38: 377633, 40: 390475, 34: 337400, 30: 313932, 42: 398691, 29: 306057, 39: 382981, 33: 336884, 28: 301406, 36: 360406, 32: 329789, 31: 315293, 41: 391911, 35: 342394}}),
    pd.DataFrame({'State': {22: 'PA', 17: 'PA', 21: 'PA', 20: 'PA', 19: 'PA', 18: 'PA', 23: 'PA'}, 'Address': {22: '71 Locust St.', 17: '7528 Bayport Avenue', 21: '92 Roehampton Lane', 20: '334 Homewood Ave.', 19: '6 Glen Eagles St.', 18: '403 Saxon Street', 23: '70 Crescent St.'}, 'ZIP': {22: 99244, 17: 89201, 21: 89697, 20: 41615, 19: 58653, 18: 45719, 23: 72582}, 'First Name': {22: 'Adyson', 17: 'Anabel', 21: 'Barbara', 20: 'Jesse', 19: 'Kolby', 18: 'Marcus', 23: 'Rodney'}, 'Last Name': {22: 'Walls', 17: 'Cochran', 21: 'Stuart', 20: 'Blackwell', 19: 'Baker', 18: 'Chapman', 23: 'Fry'}, 'Phone': {22: 257734, 17: 236247, 21: 257427, 20: 256591, 19: 253514, 18: 243919, 23: 258540}}),
    pd.DataFrame({'State': {16: 'MN', 12: 'MN', 13: 'MN', 14: 'MN', 15: 'MN'}, 'Address': {16: '8630 Roberts Drive', 12: '7329 East Gates Ave.', 13: '990 Orchard Road', 14: '9204 Acacia Street', 15: '419 W. Campfire Road'}, 'ZIP': {16: 66129, 12: 63186, 13: 57087, 14: 65185, 15: 87181}, 'First Name': {16: 'Angela', 12: 'Isabelle', 13: 'Melanie', 14: 'Nyasia', 15: 'Reginald'}, 'Last Name': {16: 'Burgess', 12: 'Barr', 13: 'Chandler', 14: 'Simon', 15: 'Flores'}, 'Phone': {16: 210491, 12: 203718, 13: 204696, 14: 204883, 15: 206684}}),
    pd.DataFrame({'State': {10: 'AZ', 9: 'AZ', 11: 'AZ', 7: 'AZ', 8: 'AZ'}, 'Address': {10: '8712 10th Lane', 9: '7643 W. Nut Swamp Court', 11: '8 Adams Street', 7: '8912 Prospect St.', 8: '7893 SW. St Paul Street'}, 'ZIP': {10: 80854, 9: 68964, 11: 93603, 7: 61053, 8: 83892}, 'First Name': {10: 'Jovan', 9: 'Lance', 11: 'Quinton', 7: 'Regina', 8: 'Shaniya'}, 'Last Name': {10: 'Shah', 9: 'Barrera', 11: 'Freeman', 7: 'Haas', 8: 'Logan'}, 'Phone': {10: 168634, 9: 164789, 11: 189370, 7: 157183, 8: 158344}}),
    pd.DataFrame({'State': {25: 'TX', 27: 'TX', 24: 'TX', 26: 'TX'}, 'Address': {25: '7954 Cardinal Street', 27: '275 Proctor Dr.', 24: '52 Walnut Lane', 26: '8107 University Drive'}, 'ZIP': {25: 83350, 27: 47270, 24: 97084, 26: 75708}, 'First Name': {25: 'Gauge', 27: 'Kolby', 24: 'Valeria', 26: 'Walter'}, 'Last Name': {25: 'Booker', 27: 'Jacobs', 24: 'Benjamin', 26: 'Kelly'}, 'Phone': {25: 266701, 27: 277954, 24: 261736, 26: 275102}}),
    pd.DataFrame({'State': {45: 'MI', 55: 'MI', 51: 'MI', 47: 'MI', 52: 'MI', 58: 'MI', 46: 'MI', 43: 'MI', 57: 'MI', 49: 'MI', 56: 'MI', 53: 'MI', 54: 'MI', 44: 'MI', 48: 'MI', 50: 'MI'}, 'Address': {45: '753 Bank St.', 55: '731 Southampton Drive', 51: '53 Kent Dr.', 47: '56 Durham Court', 52: '871 Sycamore Ave.', 58: '40 53rd Ave.', 46: '52 Taylor Street', 43: '69 Longbranch St.', 57: '21 Arnold St.', 49: '7702 Carson Lane', 56: '48 Applegate Street', 53: '354 Green St.', 54: '410 Beaver Ridge St.', 44: '458 Gonzales Dr.', 48: '6 W. Newbridge Ave.', 50: '9629 Prairie Drive'}, 'ZIP': {45: 83183, 55: 80610, 51: 50769, 47: 79955, 52: 90827, 58: 61349, 46: 42689, 43: 66261, 57: 63918, 49: 63124, 56: 87754, 53: 55402, 54: 90135, 44: 96451, 48: 46854, 50: 55832}, 'First Name': {45: 'Andrew', 55: 'Casey', 51: 'Christian', 47: 'Elisa', 52: 'Gage', 58: 'Harrison', 46: 'Heidi', 43: 'Jase', 57: 'Jayson', 49: 'Lailah', 56: 'Laura', 53: 'Leon', 54: 'Makai', 44: 'Reginald', 48: 'Rosemary', 50: 'Ryder'}, 'Last Name': {45: 'Griffith', 55: 'Ritter', 51: 'Dodson', 47: 'Mccormick', 52: 'Dawson', 58: 'Shepard', 46: 'Mills', 43: 'Cummings', 57: 'Wiggins', 49: 'Peters', 56: 'Watson', 53: 'Valenzuela', 54: 'Reilly', 44: 'Rivers', 48: 'Mcdowell', 50: 'Hickman'}, 'Phone': {45: 419227, 55: 461176, 51: 442874, 47: 433310, 52: 451763, 58: 489794, 46: 422371, 43: 401298, 57: 475408, 49: 437214, 56: 462717, 53: 454443, 54: 461089, 44: 418498, 48: 436932, 50: 437558}}),
    pd.DataFrame({'State': {87: 'CT', 88: 'CT', 92: 'CT', 89: 'CT', 91: 'CT', 90: 'CT'}, 'Address': {87: '94 North Catherine Lane', 88: '29 S. St Margarets St.', 92: '7 SW. Argyle St.', 89: '9093 Leatherwood Drive', 91: '553 Hickory St.', 90: '43 Sussex Road'}, 'ZIP': {87: 78225, 88: 72722, 92: 77590, 89: 61685, 91: 57374, 90: 80942}, 'First Name': {87: 'Alexander', 88: 'Avery', 92: 'Hugo', 89: 'Korbin', 91: 'Sierra', 90: 'Sylvia'}, 'Last Name': {87: 'Pham', 88: 'Haney', 92: 'Shannon', 89: 'Carpenter', 91: 'Trevino', 90: 'Lam'}, 'Phone': {87: 652049, 88: 660664, 92: 694634, 89: 673128, 91: 687143, 90: 685939}}),
    pd.DataFrame({'State': {70: 'FL', 73: 'FL', 76: 'FL', 72: 'FL', 77: 'FL', 78: 'FL', 75: 'FL', 71: 'FL', 74: 'FL'}, 'Address': {70: '9114 Van Dyke Dr.', 73: '342 NW. 1st St.', 76: '72 Birch Hill St.', 72: '779 Hilldale Street', 77: '969 James Lane', 78: '80 Old Indian Spring Circle', 75: '9575 Cherry Hill Court', 71: '8836 Rockaway St.', 74: '64 Rockcrest Ave.'}, 'ZIP': {70: 64107, 73: 79731, 76: 44749, 72: 68518, 77: 69286, 78: 46562, 75: 82248, 71: 92105, 74: 50740}, 'First Name': {70: 'Arianna', 73: 'Bethany', 76: 'Braylen', 72: 'Danielle', 77: 'June', 78: 'Karlee', 75: 'Linda', 71: 'Miah', 74: 'Phoebe'}, 'Last Name': {70: 'Howell', 73: 'Riley', 76: 'Carey', 72: 'Calderon', 77: 'Mcbride', 78: 'Arellano', 75: 'Barry', 71: 'Mccoy', 74: 'Henry'}, 'Phone': {70: 551104, 73: 562632, 76: 568821, 72: 558342, 77: 575632, 78: 578343, 75: 565193, 71: 553671, 74: 564211}}),
    pd.DataFrame({'State': {68: 'WA', 67: 'WA', 66: 'WA', 69: 'WA'}, 'Address': {68: '817 Sycamore St.', 67: '667 Paris Hill Dr.', 66: '8109 Spring St.', 69: '85 College Street'}, 'ZIP': {68: 89618, 67: 63786, 66: 43762, 69: 91106}, 'First Name': {68: 'Alan', 67: 'Allyson', 66: 'Hugo', 69: 'Janessa'}, 'Last Name': {68: 'Cannon', 67: 'Roberson', 66: 'Shannon', 69: 'Bender'}, 'Phone': {68: 543367, 67: 539776, 66: 531138, 69: 548772}}),
    pd.DataFrame({'State': {85: 'GA', 86: 'GA', 84: 'GA', 83: 'GA'}, 'Address': {85: '40 North Wilson Lane', 86: '642 Beacon Dr.', 84: '12 Holly St.', 83: '7972 Windfall Ave.'}, 'ZIP': {85: 69420, 86: 67592, 84: 75982, 83: 56686}, 'First Name': {85: 'Alivia', 86: 'Esperanza', 84: 'Ibrahim', 83: 'Marilyn'}, 'Last Name': {85: 'Ibarra', 86: 'Johns', 84: 'Castillo', 83: 'Banks'}, 'Phone': {85: 631672, 86: 639781, 84: 619493, 83: 604399}}),
    pd.DataFrame({'State': {4: 'CA', 3: 'CA', 2: 'CA', 0: 'CA', 5: 'CA', 6: 'CA', 1: 'CA'}, 'Address': {4: '71 West Front Drive', 3: '712 W. Marvon Dr.', 2: '60 Tarkiln Hill Court', 0: '547 Marconi Street', 5: '460 Talbot Ave.', 6: '7219 South Arrowhead Ave.', 1: '699 Anderson Dr.'}, 'ZIP': {4: 56102, 3: 41016, 2: 73892, 0: 63101, 5: 50095, 6: 41333, 1: 91918}, 'First Name': {4: 'Caden', 3: 'Colt', 2: 'Ibrahim', 0: 'Kinley', 5: 'Marshall', 6: 'Mylee', 1: 'Pedro'}, 'Last Name': {4: 'Lucas', 3: 'Hays', 2: 'Briggs', 0: 'Schmidt', 5: 'Burgess', 6: 'Valdez', 1: 'Morrison'}, 'Phone': {4: 143233, 3: 141489, 2: 139069, 0: 120461, 5: 147878, 6: 148726, 1: 133357}}),
    pd.DataFrame({'State': {63: 'OR', 61: 'OR', 60: 'OR', 65: 'OR', 64: 'OR', 62: 'OR', 59: 'OR'}, 'Address': {63: '207 Bellevue Dr.', 61: '59 Acacia Drive', 60: '7344 Hawthorne Ave.', 65: '8543 North Pulaski Street', 64: '65 Main Lane', 62: '3 Wood Street', 59: '56 Nichols Drive'}, 'ZIP': {63: 96663, 61: 54765, 60: 79049, 65: 60673, 64: 93497, 62: 90972, 59: 65734}, 'First Name': {63: 'Brynn', 61: 'Eva', 60: 'Hugo', 65: 'Kolby', 64: 'Maeve', 62: 'Orlando', 59: 'Reginald'}, 'Last Name': {63: 'Stout', 61: 'Hanson', 60: 'Shannon', 65: 'Osborn', 64: 'Mckay', 62: 'Buchanan', 59: 'Holt'}, 'Phone': {63: 517267, 61: 516061, 60: 512679, 65: 527414, 64: 521869, 62: 517208, 59: 500767}})])

    phones_test = pd.DataFrame({'First Name': {0: 'Kinley', 1: 'Pedro', 2: 'Ibrahim', 3: 'Colt', 4: 'Caden', 5: 'Marshall', 6: 'Mylee', 7: 'Regina', 8: 'Shaniya', 9: 'Lance', 10: 'Jovan', 11: 'Quinton', 12: 'Isabelle', 13: 'Melanie', 14: 'Nyasia', 15: 'Reginald', 16: 'Angela', 17: 'Anabel', 18: 'Marcus', 19: 'Kolby', 20: 'Jesse', 21: 'Barbara', 22: 'Adyson', 23: 'Rodney', 24: 'Valeria', 25: 'Gauge', 26: 'Walter', 27: 'Kolby', 28: 'Meadow', 29: 'Junior', 30: 'Cristina', 31: 'Quinton', 32: 'Mireya', 33: 'Liana', 34: 'Caiden', 35: 'Rory', 36: 'Miah', 37: 'Abigail', 38: 'Ali', 39: 'Kaitlin', 40: 'Beckett', 41: 'Reuben', 42: 'Edgar', 43: 'Jase', 44: 'Reginald', 45: 'Andrew', 46: 'Heidi', 47: 'Elisa', 48: 'Rosemary', 49: 'Lailah', 50: 'Ryder', 51: 'Christian', 52: 'Gage', 53: 'Leon', 54: 'Makai', 55: 'Casey', 56: 'Laura', 57: 'Jayson', 58: 'Harrison', 59: 'Reginald', 60: 'Hugo', 61: 'Eva', 62: 'Orlando', 63: 'Brynn', 64: 'Maeve', 65: 'Kolby', 66: 'Hugo', 67: 'Allyson', 68: 'Alan', 69: 'Janessa', 70: 'Arianna', 71: 'Miah', 72: 'Danielle', 73: 'Bethany', 74: 'Phoebe', 75: 'Linda', 76: 'Braylen', 77: 'June', 78: 'Karlee', 79: 'Matthew', 80: 'Devon', 81: 'Leticia', 82: 'Iyana', 83: 'Marilyn', 84: 'Ibrahim', 85: 'Alivia', 86: 'Esperanza', 87: 'Alexander', 88: 'Avery', 89: 'Korbin', 90: 'Sylvia', 91: 'Sierra', 92: 'Hugo'}, 'Last Name': {0: 'Schmidt', 1: 'Morrison', 2: 'Briggs', 3: 'Hays', 4: 'Lucas', 5: 'Burgess', 6: 'Valdez', 7: 'Haas', 8: 'Logan', 9: 'Barrera', 10: 'Shah', 11: 'Freeman', 12: 'Barr', 13: 'Chandler', 14: 'Simon', 15: 'Flores', 16: 'Burgess', 17: 'Cochran', 18: 'Chapman', 19: 'Baker', 20: 'Blackwell', 21: 'Stuart', 22: 'Walls', 23: 'Fry', 24: 'Benjamin', 25: 'Booker', 26: 'Kelly', 27: 'Jacobs', 28: 'Olson', 29: 'Haley', 30: 'Bryan', 31: 'Freeman', 32: 'Pruitt', 33: 'Callahan', 34: 'Christensen', 35: 'Wheeler', 36: 'Mccoy', 37: 'Solis', 38: 'Owen', 39: 'Bates', 40: 'Howard', 41: 'Roberson', 42: 'Walker', 43: 'Cummings', 44: 'Rivers', 45: 'Griffith', 46: 'Mills', 47: 'Mccormick', 48: 'Mcdowell', 49: 'Peters', 50: 'Hickman', 51: 'Dodson', 52: 'Dawson', 53: 'Valenzuela', 54: 'Reilly', 55: 'Ritter', 56: 'Watson', 57: 'Wiggins', 58: 'Shepard', 59: 'Holt', 60: 'Shannon', 61: 'Hanson', 62: 'Buchanan', 63: 'Stout', 64: 'Mckay', 65: 'Osborn', 66: 'Shannon', 67: 'Roberson', 68: 'Cannon', 69: 'Bender', 70: 'Howell', 71: 'Mccoy', 72: 'Calderon', 73: 'Riley', 74: 'Henry', 75: 'Barry', 76: 'Carey', 77: 'Mcbride', 78: 'Arellano', 79: 'Hancock', 80: 'Schultz', 81: 'Blake', 82: 'Arnold', 83: 'Banks', 84: 'Castillo', 85: 'Ibarra', 86: 'Johns', 87: 'Pham', 88: 'Haney', 89: 'Carpenter', 90: 'Lam', 91: 'Trevino', 92: 'Shannon'}, 'Phone': {0: 120461, 1: 133357, 2: 139069, 3: 141489, 4: 143233, 5: 147878, 6: 148726, 7: 157183, 8: 158344, 9: 164789, 10: 168634, 11: 189370, 12: 203718, 13: 204696, 14: 204883, 15: 206684, 16: 210491, 17: 236247, 18: 243919, 19: 253514, 20: 256591, 21: 257427, 22: 257734, 23: 258540, 24: 261736, 25: 266701, 26: 275102, 27: 277954, 28: 301406, 29: 306057, 30: 313932, 31: 315293, 32: 329789, 33: 336884, 34: 337400, 35: 342394, 36: 360406, 37: 367691, 38: 377633, 39: 382981, 40: 390475, 41: 391911, 42: 398691, 43: 401298, 44: 418498, 45: 419227, 46: 422371, 47: 433310, 48: 436932, 49: 437214, 50: 437558, 51: 442874, 52: 451763, 53: 454443, 54: 461089, 55: 461176, 56: 462717, 57: 475408, 58: 489794, 59: 500767, 60: 512679, 61: 516061, 62: 517208, 63: 517267, 64: 521869, 65: 527414, 66: 531138, 67: 539776, 68: 543367, 69: 548772, 70: 551104, 71: 553671, 72: 558342, 73: 562632, 74: 564211, 75: 565193, 76: 568821, 77: 575632, 78: 578343, 79: 580199, 80: 586838, 81: 591314, 82: 593006, 83: 604399, 84: 619493, 85: 631672, 86: 639781, 87: 652049, 88: 660664, 89: 673128, 90: 685939, 91: 687143, 92: 694634}, 'State': {0: 'CA', 1: 'CA', 2: 'CA', 3: 'CA', 4: 'CA', 5: 'CA', 6: 'CA', 7: 'AZ', 8: 'AZ', 9: 'AZ', 10: 'AZ', 11: 'AZ', 12: 'MN', 13: 'MN', 14: 'MN', 15: 'MN', 16: 'MN', 17: 'PA', 18: 'PA', 19: 'PA', 20: 'PA', 21: 'PA', 22: 'PA', 23: 'PA', 24: 'TX', 25: 'TX', 26: 'TX', 27: 'TX', 28: 'NY', 29: 'NY', 30: 'NY', 31: 'NY', 32: 'NY', 33: 'NY', 34: 'NY', 35: 'NY', 36: 'NY', 37: 'NY', 38: 'NY', 39: 'NY', 40: 'NY', 41: 'NY', 42: 'NY', 43: 'MI', 44: 'MI', 45: 'MI', 46: 'MI', 47: 'MI', 48: 'MI', 49: 'MI', 50: 'MI', 51: 'MI', 52: 'MI', 53: 'MI', 54: 'MI', 55: 'MI', 56: 'MI', 57: 'MI', 58: 'MI', 59: 'OR', 60: 'OR', 61: 'OR', 62: 'OR', 63: 'OR', 64: 'OR', 65: 'OR', 66: 'WA', 67: 'WA', 68: 'WA', 69: 'WA', 70: 'FL', 71: 'FL', 72: 'FL', 73: 'FL', 74: 'FL', 75: 'FL', 76: 'FL', 77: 'FL', 78: 'FL', 79: 'IL', 80: 'IL', 81: 'IL', 82: 'IL', 83: 'GA', 84: 'GA', 85: 'GA', 86: 'GA', 87: 'CT', 88: 'CT', 89: 'CT', 90: 'CT', 91: 'CT', 92: 'CT'}, 'Full Name': {0: 'Kinley Schmidt', 1: 'Pedro Morrison', 2: 'Ibrahim Briggs', 3: 'Colt Hays', 4: 'Caden Lucas', 5: 'Marshall Burgess', 6: 'Mylee Valdez', 7: 'Regina Haas', 8: 'Shaniya Logan', 9: 'Lance Barrera', 10: 'Jovan Shah', 11: 'Quinton Freeman', 12: 'Isabelle Barr', 13: 'Melanie Chandler', 14: 'Nyasia Simon', 15: 'Reginald Flores', 16: 'Angela Burgess', 17: 'Anabel Cochran', 18: 'Marcus Chapman', 19: 'Kolby Baker', 20: 'Jesse Blackwell', 21: 'Barbara Stuart', 22: 'Adyson Walls', 23: 'Rodney Fry', 24: 'Valeria Benjamin', 25: 'Gauge Booker', 26: 'Walter Kelly', 27: 'Kolby Jacobs', 28: 'Meadow Olson', 29: 'Junior Haley', 30: 'Cristina Bryan', 31: 'Quinton Freeman', 32: 'Mireya Pruitt', 33: 'Liana Callahan', 34: 'Caiden Christensen', 35: 'Rory Wheeler', 36: 'Miah Mccoy', 37: 'Abigail Solis', 38: 'Ali Owen', 39: 'Kaitlin Bates', 40: 'Beckett Howard', 41: 'Reuben Roberson', 42: 'Edgar Walker', 43: 'Jase Cummings', 44: 'Reginald Rivers', 45: 'Andrew Griffith', 46: 'Heidi Mills', 47: 'Elisa Mccormick', 48: 'Rosemary Mcdowell', 49: 'Lailah Peters', 50: 'Ryder Hickman', 51: 'Christian Dodson', 52: 'Gage Dawson', 53: 'Leon Valenzuela', 54: 'Makai Reilly', 55: 'Casey Ritter', 56: 'Laura Watson', 57: 'Jayson Wiggins', 58: 'Harrison Shepard', 59: 'Reginald Holt', 60: 'Hugo Shannon', 61: 'Eva Hanson', 62: 'Orlando Buchanan', 63: 'Brynn Stout', 64: 'Maeve Mckay', 65: 'Kolby Osborn', 66: 'Hugo Shannon', 67: 'Allyson Roberson', 68: 'Alan Cannon', 69: 'Janessa Bender', 70: 'Arianna Howell', 71: 'Miah Mccoy', 72: 'Danielle Calderon', 73: 'Bethany Riley', 74: 'Phoebe Henry', 75: 'Linda Barry', 76: 'Braylen Carey', 77: 'June Mcbride', 78: 'Karlee Arellano', 79: 'Matthew Hancock', 80: 'Devon Schultz', 81: 'Leticia Blake', 82: 'Iyana Arnold', 83: 'Marilyn Banks', 84: 'Ibrahim Castillo', 85: 'Alivia Ibarra', 86: 'Esperanza Johns', 87: 'Alexander Pham', 88: 'Avery Haney', 89: 'Korbin Carpenter', 90: 'Sylvia Lam', 91: 'Sierra Trevino', 92: 'Hugo Shannon'}})
    addresses_test = pd.DataFrame({'Full Name': {0: 'Kinley Schmidt', 1: 'Miah Mccoy', 2: 'Pedro Morrison', 3: 'Ibrahim Briggs', 4: 'Colt Hays', 5: 'Caden Lucas', 6: 'Marshall Burgess', 7: 'Mylee Valdez', 8: 'Regina Haas', 9: 'Shaniya Logan', 10: 'Lance Barrera', 11: 'Jovan Shah', 12: 'Quinton Freeman', 13: 'Isabelle Barr', 14: 'Melanie Chandler', 15: 'Nyasia Simon', 16: 'Reginald Flores', 17: 'Kolten Roberson', 18: 'Angela Burgess', 19: 'Anabel Cochran', 20: 'Marcus Chapman', 21: 'Kolby Baker', 22: 'Jesse Blackwell', 23: 'Barbara Stuart', 24: 'Adyson Walls', 25: 'Rodney Fry', 26: 'Valeria Benjamin', 27: 'Gauge Booker', 28: 'Walter Kelly', 29: 'Kolby Jacobs', 30: 'Meadow Olson', 31: 'Junior Haley', 32: 'Cristina Bryan', 33: 'Quinton Freeman', 34: 'Linda Cruz', 35: 'Giovanny Davidson', 36: 'Mireya Pruitt', 37: 'Liana Callahan', 38: 'Caiden Christensen', 39: 'Rory Wheeler', 40: 'Miah Mccoy', 41: 'Abigail Solis', 42: 'Ali Owen', 43: 'Kaitlin Bates', 44: 'Beckett Howard', 45: 'Reuben Roberson', 46: 'Edgar Walker', 47: 'Jase Cummings', 48: 'Reginald Rivers', 49: 'Andrew Griffith', 50: 'Heidi Mills', 51: 'Elisa Mccormick', 52: 'Rosemary Mcdowell', 53: 'Lailah Peters', 54: 'Ryder Hickman', 55: 'Christian Dodson', 56: 'Gage Dawson', 57: 'Leon Valenzuela', 58: 'Makai Reilly', 59: 'Casey Ritter', 60: 'Laura Watson', 61: 'Jayson Wiggins', 62: 'Harrison Shepard', 63: 'Reginald Holt', 64: 'Hugo Shannon', 65: 'Eva Hanson', 66: 'Orlando Buchanan', 67: 'Brynn Stout', 68: 'Maeve Mckay', 69: 'Kolby Osborn', 70: 'Hugo Shannon', 71: 'Allyson Roberson', 72: 'Alan Cannon', 73: 'Janessa Bender', 74: 'Taylor Richard', 75: 'Arianna Howell', 76: 'Miah Mccoy', 77: 'Danielle Calderon', 78: 'Bethany Riley', 79: 'Phoebe Henry', 80: 'Linda Barry', 81: 'Braylen Carey', 82: 'June Mcbride', 83: 'Karlee Arellano', 84: 'Matthew Hancock', 85: 'Devon Schultz', 86: 'Leticia Blake', 87: 'Iyana Arnold', 88: 'Marilyn Banks', 89: 'Ibrahim Castillo', 90: 'Alivia Ibarra', 91: 'Christian Roberson', 92: 'Julia Brady', 93: 'Esperanza Johns', 94: 'Alexander Pham', 95: 'Avery Haney', 96: 'Korbin Carpenter', 97: 'Sylvia Lam', 98: 'Sierra Trevino', 99: 'Hugo Shannon'}, 'State': {0: 'CA', 1: 'CA', 2: 'CA', 3: 'CA', 4: 'CA', 5: 'CA', 6: 'CA', 7: 'CA', 8: 'AZ', 9: 'AZ', 10: 'AZ', 11: 'AZ', 12: 'AZ', 13: 'MN', 14: 'MN', 15: 'MN', 16: 'MN', 17: 'MN', 18: 'MN', 19: 'PA', 20: 'PA', 21: 'PA', 22: 'PA', 23: 'PA', 24: 'PA', 25: 'PA', 26: 'TX', 27: 'TX', 28: 'TX', 29: 'TX', 30: 'NY', 31: 'NY', 32: 'NY', 33: 'NY', 34: 'NY', 35: 'NY', 36: 'NY', 37: 'NY', 38: 'NY', 39: 'NY', 40: 'NY', 41: 'NY', 42: 'NY', 43: 'NY', 44: 'NY', 45: 'NY', 46: 'NY', 47: 'MI', 48: 'MI', 49: 'MI', 50: 'MI', 51: 'MI', 52: 'MI', 53: 'MI', 54: 'MI', 55: 'MI', 56: 'MI', 57: 'MI', 58: 'MI', 59: 'MI', 60: 'MI', 61: 'MI', 62: 'MI', 63: 'OR', 64: 'OR', 65: 'OR', 66: 'OR', 67: 'OR', 68: 'OR', 69: 'OR', 70: 'WA', 71: 'WA', 72: 'WA', 73: 'WA', 74: 'WA', 75: 'FL', 76: 'FL', 77: 'FL', 78: 'FL', 79: 'FL', 80: 'FL', 81: 'FL', 82: 'FL', 83: 'FL', 84: 'IL', 85: 'IL', 86: 'IL', 87: 'IL', 88: 'GA', 89: 'GA', 90: 'GA', 91: 'GA', 92: 'GA', 93: 'GA', 94: 'CT', 95: 'CT', 96: 'CT', 97: 'CT', 98: 'CT', 99: 'CT'}, 'Address': {0: '547 Marconi Street', 1: '17 North Lake View St.', 2: '699 Anderson Dr.', 3: '60 Tarkiln Hill Court', 4: '712 W. Marvon Dr.', 5: '71 West Front Drive', 6: '460 Talbot Ave.', 7: '7219 South Arrowhead Ave.', 8: '8912 Prospect St.', 9: '7893 SW. St Paul Street', 10: '7643 W. Nut Swamp Court', 11: '8712 10th Lane', 12: '8 Adams Street', 13: '7329 East Gates Ave.', 14: '990 Orchard Road', 15: '9204 Acacia Street', 16: '419 W. Campfire Road', 17: '62 Brandywine Dr.', 18: '8630 Roberts Drive', 19: '7528 Bayport Avenue', 20: '403 Saxon Street', 21: '6 Glen Eagles St.', 22: '334 Homewood Ave.', 23: '92 Roehampton Lane', 24: '71 Locust St.', 25: '70 Crescent St.', 26: '52 Walnut Lane', 27: '7954 Cardinal Street', 28: '8107 University Drive', 29: '275 Proctor Dr.', 30: '993 Belmont Court', 31: '481 North Fairview Road', 32: '8230 N. Anderson St.', 33: '383 Trout Lane', 34: '8138 Ivy Avenue', 35: '32 Garden Street', 36: '1 Willow St.', 37: '8164 South Rockcrest Drive', 38: '7595 Gregory St.', 39: '7471 Rockaway St.', 40: '15 Catherine St.', 41: '779 SE. Cooper Ave.', 42: '10 NE. Clay St.', 43: '23 Lincoln Dr.', 44: '7714 Gates Dr.', 45: '11 Lilac St.', 46: '17 Main St.', 47: '69 Longbranch St.', 48: '458 Gonzales Dr.', 49: '753 Bank St.', 50: '52 Taylor Street', 51: '56 Durham Court', 52: '6 W. Newbridge Ave.', 53: '7702 Carson Lane', 54: '9629 Prairie Drive', 55: '53 Kent Dr.', 56: '871 Sycamore Ave.', 57: '354 Green St.', 58: '410 Beaver Ridge St.', 59: '731 Southampton Drive', 60: '48 Applegate Street', 61: '21 Arnold St.', 62: '40 53rd Ave.', 63: '56 Nichols Drive', 64: '7344 Hawthorne Ave.', 65: '59 Acacia Drive', 66: '3 Wood Street', 67: '207 Bellevue Dr.', 68: '65 Main Lane', 69: '8543 North Pulaski Street', 70: '8109 Spring St.', 71: '667 Paris Hill Dr.', 72: '817 Sycamore St.', 73: '85 College Street', 74: '9 Bradford Drive', 75: '9114 Van Dyke Dr.', 76: '8836 Rockaway St.', 77: '779 Hilldale Street', 78: '342 NW. 1st St.', 79: '64 Rockcrest Ave.', 80: '9575 Cherry Hill Court', 81: '72 Birch Hill St.', 82: '969 James Lane', 83: '80 Old Indian Spring Circle', 84: '7147 Greenrose Ave.', 85: '22 Summit St.', 86: '709 Clinton St.', 87: '7800A Sunset Ave.', 88: '7972 Windfall Ave.', 89: '12 Holly St.', 90: '40 North Wilson Lane', 91: '8404 Halifax Dr.', 92: '16 West Court', 93: '642 Beacon Dr.', 94: '94 North Catherine Lane', 95: '29 S. St Margarets St.', 96: '9093 Leatherwood Drive', 97: '43 Sussex Road', 98: '553 Hickory St.', 99: '7 SW. Argyle St.'}, 'ZIP': {0: 63101, 1: 98301, 2: 91918, 3: 73892, 4: 41016, 5: 56102, 6: 50095, 7: 41333, 8: 61053, 9: 83892, 10: 68964, 11: 80854, 12: 93603, 13: 63186, 14: 57087, 15: 65185, 16: 87181, 17: 80405, 18: 66129, 19: 89201, 20: 45719, 21: 58653, 22: 41615, 23: 89697, 24: 99244, 25: 72582, 26: 97084, 27: 83350, 28: 75708, 29: 47270, 30: 58515, 31: 70112, 32: 71355, 33: 99289, 34: 48737, 35: 45290, 36: 52792, 37: 89500, 38: 89309, 39: 65388, 40: 55292, 41: 87632, 42: 80601, 43: 55888, 44: 77291, 45: 41719, 46: 68331, 47: 66261, 48: 96451, 49: 83183, 50: 42689, 51: 79955, 52: 46854, 53: 63124, 54: 55832, 55: 50769, 56: 90827, 57: 55402, 58: 90135, 59: 80610, 60: 87754, 61: 63918, 62: 61349, 63: 65734, 64: 79049, 65: 54765, 66: 90972, 67: 96663, 68: 93497, 69: 60673, 70: 43762, 71: 63786, 72: 89618, 73: 91106, 74: 64600, 75: 64107, 76: 92105, 77: 68518, 78: 79731, 79: 50740, 80: 82248, 81: 44749, 82: 69286, 83: 46562, 84: 84939, 85: 54942, 86: 94710, 87: 90943, 88: 56686, 89: 75982, 90: 69420, 91: 85668, 92: 40073, 93: 67592, 94: 78225, 95: 72722, 96: 61685, 97: 80942, 98: 57374, 99: 77590}})
    ans = sort_tester(make_books(phones_test, addresses_test))
    for got, should_got in zip(ans, books_test):
        assert_frame_equal(got[sorted(got.columns)].reset_index(drop=True), 
                           should_got[sorted(should_got.columns)].reset_index(drop=True), check_dtype=False)
test()

Задача 5 (4 балла)

В датафрейме purchases находится информация о покупках: кто, какого товара и сколько единиц купил. В датафрейме goods указана цена каждого товара. В датафрейме discounts указана скидка (в процентах) для некоторых покупателей. Пример:

In [ ]:
purchases = pd.DataFrame([['Alice', 'sweeties', 4],
                          ['Bob', 'chocolate', 5],
                          ['Alice', 'chocolate', 3],
                          ['Claudia', 'juice', 2]],
                        columns=['client', 'item', 'quantity'])
goods = pd.DataFrame([['sweeties', 15],
                      ['chocolate', 7],
                      ['juice', 8],
                      ['lemons', 3]], columns=['good', 'price'])
discounts = pd.DataFrame([['Alice', 10],
                         ['Bob', 5],
                         ['Patritia', 15]], 
                         columns=['client', 'discount'])
In [ ]:
purchases
In [ ]:
goods
In [ ]:
discounts

Вам необходимо написать функцию totals(purchases, goods, discounts), которая возвращает датафрейм, в котором по строчкам записаны все клиенты, которые есть в purchases, по столбцам — все товары, которые есть в goods, на пересечении — сколько всего денег выручил магазин с данного клиента за данный товар. (Эту таблицу потом будет удобно использовать, чтобы быстро определить, сколько денег мы получили с каждого клиента и сколько денег получили с продажи каждого товара.)

Например, для приведенных выше данных функция должна вернуть pd.DataFrame со следующим содержимым:

good     sweeties  chocolate  juice  lemons
client                                     
Alice        54.0      18.90    0.0     0.0
Bob           0.0      33.25    0.0     0.0
Claudia       0.0       0.00   16.0     0.0

Циклы и if использовать запрещено, нужно пользоваться методами pandas.

Подсказка. Вам скорее всего понадобятся методы merge (объединение двух таблиц), fillna (заполнение пропусков) и pivot_table (создание сводной таблицы). Один из методов решения этой задачи такой. Сначала объедините таблицу purchases с двумя другими таблицами таким образом, чтобы про каждую покупку знать, какова стоимость купленного товара и какова скидка покупателя для данной покупки; там, где скидка не определена, нужно добавить нули (это как раз можно сделать с помощью fillna — кстати, он может заполнять какие-то отдельные столбцы, для этого ему нужно передать словарь), затем нужно вычислить цену с учётом скидки и сумму, уплаченную за конкретный товар, а потом применить к результату pivot_table. Наконец, вам нужно добавить колонки для тех товаров, которые присутствуют в goods, но не упоминаются в покупках — это можно сделать с помощью reindex.

Это непростая задача, но мы рекомендуем её сделать. Уверены, вам понравится результат!

In [ ]:
# YOUR CODE HERE
In [ ]:
purchases = pd.DataFrame([['Alice', 'sweeties', 4],
                          ['Bob', 'chocolate', 5],
                          ['Alice', 'chocolate', 3],
                          ['Claudia', 'juice', 2]],
                        columns=['client', 'item', 'quantity'])
goods = pd.DataFrame([['sweeties', 15],
                      ['chocolate', 7],
                      ['juice', 8],
                      ['lemons', 3]], columns=['good', 'price'])
discounts = pd.DataFrame([['Alice', 10],
                         ['Bob', 5],
                         ['Patritia', 15]], 
                         columns=['client', 'discount'])
aaa = pd.DataFrame(totals(purchases, goods, discounts).to_dict())
bbb = pd.DataFrame({'chocolate': {'Alice': 18.899999999999999, 'Bob': 33.25, 'Claudia': 0.0},
                     'juice': {'Alice': 0.0, 'Bob': 0.0, 'Claudia': 16.0},
                     'lemons': {'Alice': 0.0, 'Bob': 0.0, 'Claudia': 0.0},
                     'sweeties': {'Alice': 54.0, 'Bob': 0.0, 'Claudia': 0.0}})
bbb = bbb.sort_values(['chocolate']).reindex(sorted(bbb.columns), axis=1)
aaa = aaa.sort_values(['chocolate']).reindex(sorted(aaa.columns), axis=1)
assert_frame_equal(aaa, bbb, check_dtype=False)

Задача 6 (5 баллов)

В датафрейме grades находятся оценки студентов, полученные ими за самостоятельные работы в классе. Если студент не сдал работу, в соответствующей ячейке стоит NaN. В датафрейме excuses находится список различных причин, по которым данный студент мог не посетить занятие и не сдать соответствующую работу. Профессор МакГонагалл уважительной причиной считает только пропуск по болезни. Некоторые студенты настолько её боятся, что ходят на занятия и сдают работы, даже имея уважительную причину не ходить.

В конце года профессор МакГонагалл выставляет итоговые оценки путём вычисления среднего от всех полученных оценок. Если студент пропустил работу по неуважительной причине, то ему или ей за неё выставляется 0, если же пропуск был по уважительной причине, то эта работа просто не учитывается при вычислении среднего (как будто такого занятия для данного студента просто не было).

Например, рассмотрим такие данные.

In [ ]:
import datetime
grades = pd.DataFrame([[5, np.nan, 7, np.nan], 
                       [2, np.nan,      np.nan, 4]], index=['Hermione', 'Ron'],
                     columns=pd.DatetimeIndex(start="2017-02-01", freq="W", 
                                              periods=4))
excuses = pd.DataFrame([['Hermione', datetime.datetime(2017, 2, 5), 
                         'was ill'],
                        ['Hermione', datetime.datetime(2017, 2, 12), 
                         'illness'],
                        ['Ron', datetime.datetime(2017, 2, 19), 'family'],
                        ['Harry',datetime.datetime(2017, 2, 19), 
                         'quidditch']],
                      columns=['student', 'date', 'reason'])
In [ ]:
grades
In [ ]:
excuses

Здесь уважительной причиной МакГонагалл посчитает только те, в описании которых есть слово ill. Гермиона не сдала работу 2017-02-12 по болезни и эта работа будет исключена при подсчёте. А на 2017-02-26 у неё нет никакой уважительной причины и за неё она получит 0. Итоговая оценка Гермионы будет (5 + 7 + 0) / 3 = 4.

У Рона нет уважительных причин пропуска, поэтому он получит (2 + 0 + 0 + 4) / 4 = 1.5.

Написать функцию, final_grades(grades, excuses), которая бы принимала на вход два указанных датафрейма и возвращала pd.Series, в котором индексами были имена студентов (в том же порядке, как в grades), а значениями — итоговые оценки. Для примера выше функция должна вернуть pd.Series, имеющий вид:

Hermione    4.0
Ron         1.5
dtype: float64

Замечание. Задачу можно и нужно решить без циклов и ifов, но, возможно, по первому времени придётся поломать голову. Если ничего не будет получаться — обращайтесь за подсказками. Пока что скажем, что задачу можно решать с помощью pivot_table и fillna — последний может принимать на вход датафрейм и заполнять незаполненные ячейки исходного датафрейма ячейками переданного. Вам также может пригодиться reindex. Чтобы проверить наличие подстроки в строках, лежащих в столбце pandas, нужно использовать .str.contains(). Впрочем, вероятно, есть и другие способы решать эта задачу, и тогда вам пригодится что-нибудь другое.

In [ ]:
# YOUR CODE HERE
In [ ]:
import pandas as pd
import numpy as np
import datetime


grades = pd.DataFrame([[5, np.nan, 7, np.nan], 
                       [2, np.nan,      np.nan, 4]], index=['Hermione', 'Ron'],
                     columns=pd.DatetimeIndex(start="2017-02-01", freq="W", 
                                              periods=4))
excuses = pd.DataFrame([['Hermione', datetime.datetime(2017, 2, 5), 
                         'was ill'],
                        ['Hermione', datetime.datetime(2017, 2, 12), 
                         'illness'],
                        ['Ron', datetime.datetime(2017, 2, 19), 'family'],
                        ['Harry',datetime.datetime(2017, 2, 19), 
                         'quidditch']],
                      columns=['student', 'date', 'reason'])


assert final_grades(grades, excuses=excuses).to_dict() == {'Hermione': 4.0, 
                                                           'Ron': 1.5}

grades = pd.DataFrame([[5, np.nan, 7,      np.nan],
                       [2, np.nan, np.nan, 4]], index=['Hermione', 'Ron'],
                       columns=pd.DatetimeIndex(start="2017-02-01", freq="W",
                                              periods=4))
excuses = pd.DataFrame([['Hermione', datetime.datetime(2017, 2, 5),
                         'was ill'],
                        ['Hermione', datetime.datetime(2017, 2, 12),
                         'illness'],
                        ['Ron', datetime.datetime(2017, 2, 19), 'family'],
                        ['Harry',datetime.datetime(2017, 2, 19),
                         'quidditch']],
                      columns=['student', 'date', 'reason'])

grades1 = pd.DataFrame([[np.nan, 3, np.nan, np.nan],
                        [2, np.nan, np.nan, np.nan]], index=['Hermione', 'Ron'],
                        columns=pd.DatetimeIndex(start="2017-02-01", freq="W",
                                              periods=4))
excuses1 = pd.DataFrame([['Hermione', datetime.datetime(2017, 2, 5),
                         'was ill'],
                        ['Hermione', datetime.datetime(2017, 2, 12),
                         'illness'],
                        ['Ron', datetime.datetime(2017, 2, 5), 'ill or not to ill'],
                        ['Harry',datetime.datetime(2017, 2, 19),
                         'quidditch']],
                      columns=['student', 'date', 'reason'])


grades2 = pd.DataFrame([[3, 3, 3, 3, 3, 3, 3],
                        [2, 2, 2, 3, 3, 3, 2.5]], index=['Hermione', 'Ron'],
                        columns=pd.DatetimeIndex(start="2017-02-01", freq="W",
                                              periods=7))
excuses2 = pd.DataFrame([['nobody', datetime.datetime(1900, 1, 1), 'no reason']],
                      columns=['student', 'date', 'reason'])

import zlib

names = ['Sonja Mahon', 'Cassidy Carnegie', 'Lashay Percy', 'Jonathan Ong',
         'Millie Gurrola', 'Shavon Voisin', 'Jackeline Virgil', 'Christena Thurman',
         'Corinne Herbert', 'Hannah Crystal', 'Laura Clay', 'Sharie Brazell',
         'Marcelina Botello', 'Zita Tinnin', 'Diedre Shawn', 'Darell Tippett',
         'Danae Hanscom', 'Oda Norling', 'Minnie Elsey', 'NObody']

compressed = b'x\x01\xadZ[\x8f\x137\x14~\xef\xaf\x98\xb7\x05)\xaaH\x80\x16^Y\xaa"\xc4B%*\x1e\xba\xe2\xc1\xec\x8e\xd8\xb4\xd9\t\x9a\x84V\xdb_\xdf\xcfs|\x9c\xcf\xd4\xce\x9c\x93E\x9a\x9d\x19\xdb\xe7~\xf3\x19g\x1f\\^>_t\xc3\x97\x1f\x870,\xba\xe5\xa2\xfb\x19\xf7G\x8b\xee\xf1a\x12\xa3\x9f\x16\x1d\xeeO\x16\x1d\xc1bB\xd1\xf4\x89\xa9\x88\x0bh\x9d\x01\x19\x10\xc4]^\xb0\xf8\xec\xb0\x18a\x97\x1f\x17?\\b\x0e\x97\x90\xc7\x0b1\x01\x1a\xa8=\x15\xba\x90nB\x99\xa01\x07t\\\x80\xc6\x15\x17V\x85\xd8 \x04l!7)\x159\x11i\xac\x08\x080AL\x86\x00\x14]3\x1eV#m\x8cA^\xd5\x82\xa8\xfa\xdaz\x02\x07\xec!2\xd0\x9eD\xd6\n\x881\x88M\x8a\x00\x06\x0c\xa3p\x07z`\x8f\x8b\x18\x88$\x00\xc5E"L*eR\x18\x01NYD\xe2b\x9fU\xe4\x8c\x111\x00ia $\xc51\xb0\x0b\x86"\xb0\x18\x14\x14\x95\x1c0h\x84e\x92\x0e(\x98\x00\xb3\x88\x0b\x12\x82\x93\x9c\x1a\'\x80\x89%%\xf5\xedS\x8c\x01zB\x15t )\xe0\x81\x85\x19\xbc\x83\x02\xee\x00\xc3\x92"\xcb\x8a\xd8E\xe7\xb0\xfc<r\x050I\xa7\xab\x91J\x16\xee@\x08\xc0\n\x01\x9e\xb0\x8a\xb0"+\x8b\xa5@\x91\xd8GJ\x15b\x90\nR\x8b\x06B5\xca#\xb2\x82z\xf29H\xe1\rwL\x81[\xba@\x0ec\x12\x1c"\x00S`\xf0\x82\x8b\x04\xd5W\x08\xa7\xaf\xe4\xfc\x03o]\xd4\'d\x83\x84`\xf9\x8d2`\x86\x8b\xb8\x83\x9d\xccA.\x11C\xc4\xc6\x90\xa0\xc46\xa0\x88\x8bXL\xc1.\x88:\xabO\xd1Dx\t\x0f\x08C\xd8\xd0"\x99\t\x9ac\x1ew\x88\n\xc8oL\x1e\x87\xa0"&\x03\xc6\x14\xe5\x00S>\xfa\xc4\x12\x84\x04$\xee\xb4\x1c\xd1\xe3\x9f\xac\xc5\x81\x8c\x15\rC\x82\x06\x17\x01\x84@\x98\xc6P\xe10\xc2\x8a\x8c\xa2\xafa\x1b\x8cA\x0f|\x89\x80\x82\x033K\x0c[email protected]$=\xa0c\xa6\xa3< \x83\xe2\xe9\x13\x84\x81\x86\x05\xc0\xe3\x05L\x9eE\xae\x98V\x08H(2D\xc2dWP\x07\xb4B\xc5E\x0c\xb1\x8e;\xe0\xf1\x12\xa7 \x1b\x01\x89nQ\xc6i%\x020\xfe\xf2QR\x18\xcbX\x8ar\xc9\x03\xc4\xe4\xc2\x90\xc8\x91~\x82\x815\xbcD$\xf0\x10\xcb@-L1\x17\x11Qg\xa6\xc0\x02\'\x11\r\x14\xb1,\xc8[email protected],\xe1\x8a2M\x00X\xc5\x05`\x89i\xb1vbN\x0c\x85\xa0\xd2\x98\xf4\xcb\x83\x92>d\x07\xb5\xc8\x11\xe8 \xaa`B\x01\xf7\xecf\x99\x01G\x08\xa0P\xc4R\xa7\xf4)\x80\xc0\x06\xd1B\x808\x9e\x94"n\x91?)\x8dW^\x9cd\xc3\\\x84J\xde\x810i\x04\x0e\xca\x12\x82\t\xd7D\x0e\xbaMQ\x15\xfdC\xd4I~\xe2\x02\x00\x10UR\xdf>a#\x90\xd6Y\xc0\x82\xa6\x8e"m\xa0\xe2\x0e\xe1t\xb2\x84\xc6\x1a&\xa2\xec1\xd4`5H\r\n\x98\xc3\x1d\xf2`&R\x91\x01\x91\x86|\x02\x03\x00\xbc\x90\xe4\xca\'\xa2\ti\xdc\x056/M\x13O\x13G0\x87\x12\xa4"\x10qa\x0e+b9,\x8a\x9e\x90\x00K2\x0f\xce\xa2\\&\x8b%LbUgH`\x9dBNE\xbe\x97g\xefo\xc2\xdf\xdb\xa1\xfb\xb0]\xef\xd6\xc3\xd9\xa2\xbb\x0e\xfb~\xbf\xbe\xed\x7f\xd4\x97\x07\xabGKp\x85A\xe2#\n\xf5p\xd1\x9d\xed\xb6\xb7}\xb7\xdd\xdf\xf4c7\xf6a\xb7\x1d\xce"\xb9\xb3?\xd6\xfb\xd0\xfd\xbe\x1e\x86YZ\xb3\xa4."\x95\xbe\xfbe\xb3\xeb\xeff\xe4\x82\x17T\xae\xf5F\x04y\x19\xc6~\xb3\x81(_\xbe\xf4\xfb\xfdq\xfc\x15t;\xe0\'\x02\xaf\xc20\x84\x9b\xee|\xbc\xdb\xed\xc3\xe68\x01q\xc7d\x97\xf5&\xe1\xbf\x0eW\x7f\xf5\x9b\xf5\xd0w\x1f\xd6\xe3g\x88u\xd4\xb4\xab\xc8\x1fW4m&\xe1\xd2\xa1&\xc3\xf9\xcd\xb8\xde\xed\xfb\x01.\xb9\xf9:\xde\x869\xff"d\xfe/\xc4\xba\xbf\x1e\xfb\x0ea\xf2\xcf\x0c\xfa\xaa\xe2\x06\xa0\x8dp\xe2\x8b1\xfc\x0bw\x1c\xb7\xc1\x92Th\x84\x97\xd3\xa8\x92\x17\xc9\xa8\'\xc4Ea\x0f\xc1\x7f\x13v7\xe1\xae\xfb\xad\x1f\xaff\xa2rE\xd9\xa2Qy\xbe\x1d\x11\xd5}\xf7\xaa\x1f?\xf5\xe3LX.+\xec}\x06@\x158\xf8S\xe4\x7fw\x1d\xba\xb7\xdb\x11\x81\xf9\xf9\xb87V\xa8U\x8a\xdc\xf0\xc6E\x18\xafb\x88\x87\xee\xc5v\x0f\xf7n\x8fS4\x94\x0fO\xc4S\xd2~\x1f\xf9\x1a98\x04\xf8+\x0c\xbb\xab\xed\xedq\xfdV\x14n-\x89\xec5\r\x15_\xcd\x9f\x0b\x82\xb9\xba\x16\x99\x98\n\x92\'\x15W\x94\x8a\x99\xfbKs%(\xe2\xee\xa4z\x18w\xcd\x83\xfa\x12\xba\xef\xb7\xc3\x9f\xa1\xbb\x087\xd8l\x8e\x16SK!\xd9\x0ea\x7f\x13\x86\xee\xddl\x1e\x14i|\xc2\xe6\xc0a\x9fm\xf9&|\x1dCw\xbe\tsEd>\x0b\xed\x19]\xb8El\xea\xd8d\x8b\x90\x10l\xcf\x1e\xc9fhd\x879\xbc\xd9\xc1\xd9\xa4/\x839Sk\x95\xf5\xed\xbbO\xdb\xeb9o\xdc\xcf\x82\x14\xd3\r\x0b8R\x8c\x88\xe9\xeer\x01[`\xb3\xfd\xf5\xeb8n7a&G(\xac\xef/L\xadX\xb9\xa4)\x82\xebP\xb0\xac\x9di\x8d\xbf\xbd\\\xd0>\xdb0\x85=M\x8a\xca\x9b\xea\x96\xa7\x07"Yrd{\xf7\xd9"4\xd4\x98\xd6\xe2\xb9"[6\xaca\xef\x81\x1e\x93(\xf7&V\xdd\x94\xec^\xa6~![\xd6\xd7\x8fQ-\xd6\x94s\x11\xe0\x06A\t\xb8ve*?\rkz\x1a(\xee\xf0\xb2E\xac\x1b\xd3\xb2fN{\xf9bS4Tym\xde\xa2\x97\xd4x\x9dbW\xf2kC\x16\xf3\xce4O\xca\x1c\xb0\xa4TC*W\xf0-\x8b\xca\x94\xaa\x82\xabFSaP+\xbb\x1a\x80\xa2\xb4Ii\xf4i0\xbfey\xe4YU\xe41\xfb\xb9<28E\x19\xe2\xdep\xaf5\x159\x8f\x1b\xa4<e\x81\xac\xacn\xf6\xa0sahHc\xb6r\xad\xc6\xd87\x9f\xf9\\\xf4}M\x13\xbd\\.\x9d\',\xb5\x983\x17\x84\xf9\x88q\xa5SM\x1d\x8f\xa3\ra\xe7(\xe15Wco\xb46\x80|\xd8\xd2\x88:\xb3\x9d\x0bQ$\xb5\xcd\x11\xcbg\x08\r9\xce\xc3n\xb7\xbe\xbe\xeb\xce\xc38\xf4\x9f\xd7\xfdL\x9fN\x9dq\x83\xa0/\x8a\xf9\xdb\xa9A\xd0\xacm\xb9\xa7\xa4*\xe8Ro5\xdf\xd2\xd8\x83\xa8\xcc\xae\xb4\xc5\xd9?\x8fi\x83\xbc\xafa8\x1cs\xad\xb0+R\xec\xb4I\x0fWnS\xefpJ\r\xe7\xee,\x8b\xef\x8b\xdb\x92\x84\x84\x86/Pk\xc7\xc9\xf6\xe2_~,\x08\x7f\xe7w\x14\xd5\xdbl\x04{\x87\xcb\'X\x8dp\xf2\x9cX\x90G\x1b\xd4N\xd7\xaeA\xd0t"2_\x9e<{J\xcd\xe4\xae\x1e\xb5\xa8\xdd)sLzP\xca5\xcc\xe1\x8b^\xf2\xd7!x\xec\'T$NF7\x97\xe5\xfb\xd5AS\xe0:~\\\xa3\n\xaf\xb5\xc8\\\x93\r\xe7\x07>\xb7\xd4\xca\x92%>\x96\xa4E#>\xee\x1b\xe7\xe6\xfe\xc4\xd0xyN\x15\x0c6v\xed=\xf3\xdb\xa8\xd9\xfd\x86\xee\xe0\xbb\x16d\xfb\x0eMeO\x83\xdaQ\xcd\x8b\xecNm\x93\xe7\x97\xc1\xea\xd7\xe7}~\xebM\xfb#J\x8d\xf5\xfc\xb8\xf6\xf5\xe0\n\x13\xfa\xccl$\x94\xb9\xde\x19ZZK\x8e\xf3\xc9g.\xba\xbe\xae\x87%\xd1\xb8\xb0\xb0\xe6\x1fG\x14\xcf\xb5\xef\x15\x1b\xe7\t\x11E\x11\xd9\xf0\x86\xa7\xbc\x95\xf1\x99\xb6a\xcf\xd9\xcc\xf7\x15\x87BM\x8d\xeb\xd1\xa6\xfc\xcaq4\x15|.\xd9\xb0\xaa\xf3\xfc\xc0\xb0\ry>\x9a\xe9t>\x07\xbc\x07\xbf\x88\xbad\x18\xc7O%\xd4\xac5\xcc\xe3\xa8\xeeEE\xce\xb2\xd8\x0b\x1am[\x87 \xb1\xff\xa2h\xe9\x0f\xcc\xd4\xca\xfc\xf1\x17g\xde7\xb3c}\x8d\x12G\x9a\xda\xc3T\xc9\x8a\xc6WDw%\x1b\x19\xf2 \xba\xf9\xa7\x80\xda\xb6\xe4)\xa45\xcb\xbb\xb6\xb5Z\x18:\xbeZ\x0b\xed\xc5z\xf6n\xa9\xa8s)\x05\x9c\xfb\x17\x15^\xf5\xba\xa7 \x14I\x94$8\xb1\xa9\xca)l\xfd/\xc0\x9a\xf6\x8e\x9elYQ\xdd\xb3e\xd5\xfe\xa7\xc0z\x82O^o\x14B\xfb\xc7\x01\x99\xa1A\xcb\xa3Uy\x90%\x01i)\x03E\x1a\x08\x9a#\x0bh_i\xe8\xe0\xdc7\x0b\xdf\xa6\xc8\xb2:\xa7\x86k1A\xf9\x15\xae\x95\xd0\xbc\x07\xf0\xef(9\x15\xad?\xe2\xcf\x1b\xd0\xaa}\xd9\xfc\x88\x16\x9e\x82J\x92\xe4r\xee*\xa8\xd5\xae\xdc#A\xed\x1c\xd1\xd1\xa4\xf0\xff\xa1\xa5\xc8q}]W\xf0O\xcb\x04\r\x02{=\xe5\x93\x1c\xc5\xb6\xfa\xbdH\xe0\xa4\xb7\xcbo\xfcI\xd3Jb\xd7O\x05e$\xaa+\xac\x19A\xbdf\x8eCO\x14\xf1)\x89\xda\xd2y\xe2ZU\xc0Y\xc8(\x9d\x1aF\xb5:\x98\xcaZ\x83\x92y\xcfaWg\xe3Z\xe5 \x8d\xd4\xae\xae\xfc*\xbea\xfc\xf5\x89\x7f\xb0S\xfe\xbe8\xa7\xc8j\x18\xd2\x91\xef\xb5\x06\xcaQ\xac\xc8\x18\rY<\x1dQ\xa5\x95\xb7\xcbR\xeb\x87\xcc\xadl\x99m)\xd7\xbd\xe9F\xc6\xc8\x9e\xf5\x1d\x92U\xbf\xa3<\xedS\xed<\xc8Su(\x1c\x1a\xfe\xb4t"\xd5\xfc\xb4o#E\x18\xe8V\xe03\xe4|Sj>\xec\xa3\xba\x95k\x8d9\xae\xa8\xd1\x8e!\xf1\xf1\xe1\x7f\xf1j\xaf\x8b'
data = eval(zlib.decompress(compressed).decode())

grades3 = pd.DataFrame(data[0], index=names,
                        columns=pd.DatetimeIndex(start="2017-02-01", freq="D",
                                              periods=30))

excuses3 = pd.DataFrame(data[1],
                      columns=['student', 'date', 'reason']).groupby(
    ['student', 'date']).first().reset_index()

assert np.isclose(pd.Series(final_grades(grades3, excuses3).to_dict()
                           ).sort_index(),
                  pd.Series({'Shavon Voisin': 4.333333333333333, 'Sonja Mahon': 3.566666666666667,
'Darell Tippett': 3.966666666666667, 'Zita Tinnin': 4.266666666666667, 'Minnie Elsey': 4.933333333333334,
'Millie Gurrola': 3.9, 'Jackeline Virgil': 2.7, 'Sharie Brazell': 4.133333333333334, 'Cassidy Carnegie': 5.766666666666667,
'Oda Norling': 3.0, 'Christena Thurman': 4.0, 'Marcelina Botello': 5.7, 'Jonathan Ong': 4.9, 'Diedre Shawn': 5.310344827586207,
'Hannah Crystal': 4.8, 'Danae Hanscom': 4.0, 'Corinne Herbert': 3.6666666666666665, 'NObody': 4.066666666666666,
'Laura Clay': 4.466666666666667, 'Lashay Percy': 4.0}).sort_index()).all()
assert np.isclose(pd.Series(final_grades(grades, excuses).to_dict()
                           ).sort_index(),
                  pd.Series({'Hermione': 4.0, 'Ron': 1.5}).sort_index()).all()
assert np.isclose(pd.Series(final_grades(grades1, excuses1).to_dict()
                           ).sort_index(),
                  pd.Series({'Hermione': 1.0, 'Ron': 0.5}).sort_index()).all()
assert np.isclose(pd.Series(final_grades(grades2, excuses2)).sort_index(),
                  pd.Series({'Hermione': 3.0, 'Ron': 2.5}).sort_index()).all()