Python для сбора и анализа данных

Курс повышения квалификации

НИУ ВШЭ, Санкт-Петербург

Страница курса

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

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

Оценивание

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

Мы рекомендуем сделать по крайней мере все простые задачи (по одному-двум баллам) из каждого раздела. Пропускать простые задачи можно только в том случае, когда вы точно уверены, что легко их решите.

Проверка

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

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

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

Часть 1: работа с файлами

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

Написать функцию sum_ints_in_file(filename), принимающую на вход имя файла, содержащего целые числа (каждое число на новой строке). Функция должна вернуть сумму этих чисел. Файл может содержать пустые строки, их следует игнорировать.

Не забудьте закрыть файл!

In [ ]:
# YOUR CODE HERE
In [ ]:
try:
    del open
except:
    pass
from tempfile import NamedTemporaryFile
import os

testsuite = [([1, 2, 3]),
             ([5]),
             ([111, 23, 123]),
             ([0]),
             ([-1, -10, -123]),
             (0, -1, 1, 100, 500, 9999)]

for testlist in testsuite:
    try:
        f = NamedTemporaryFile(dir='.', delete=False, mode='w')
        name = f.name
        f.file.write("\n".join(map(str, testlist)))
        f.file.close()
        assert sum_ints_in_file(name) == sum(testlist)
    finally:
        os.remove(name)

import io

test_txt = io.StringIO("1\n2\n")

def get_test_txt():
    return test_txt

def open(file, mode = 'r', *args, **kwargs):
    return get_test_txt()

try:
    s = sum_ints_in_file("test.txt")
    assert test_txt.closed, "Вы забыли закрыть файл"
    assert s == 3
finally:
    del open
In [ ]:
try:
    f = NamedTemporaryFile(dir='.', delete=False, mode='w')
    name = f.name
    f.file.write("1\n2\n\n3\n\n")
    f.file.close()
    assert sum_ints_in_file(name) == 6
except:
    print("Ошибка! Обратите внимание: файл может содержать пустые строки, их следует игнорировать")
    raise
finally:
    os.remove(name)

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

Написать функцию seq(a, b, filename), создающую файл filename и записывающую в неё целые числа от a до b включительно, каждое число на своей строке. Не забудьте закрыть файл!

In [ ]:
# YOUR CODE HERE
In [ ]:
from tempfile import NamedTemporaryFile
import os

testsuite = [(10, 11), (100, 1000), (0, 10), (1, 2), (1, 1), (5, 5)]

for a, b in testsuite:
    f = NamedTemporaryFile(dir='.', delete=False)
    name = f.name
    f.close()
    try:
        seq(a, b, name)
        with open(name) as f:
            collected_output = list(map(int, f))
            assert collected_output == list(range(a, b + 1)), \
                "Incorrect output for a = {}, b = {}\n".format(a, b) + \
                "Your output:\n" + "\n".join(map(str,collected_output))
    finally:
        os.remove(name)

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

Написать функцию censore_haha(filename), считывающую файл с именем, лежащем в переменной filename и записывающим его в новый файл, имя которого получается добавлением к концу имени исходного файла .censored.txt. При записи в новый файл все вхождения слова haha должны быть заменены на [censored].

Например, если функция была вызвана как censore_haha('test.txt'), она должна создать файл test.txt.censored.txt и записать в него отцензурированную версию исходного файла.

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

In [ ]:
# YOUR CODE HERE
In [ ]:
from tempfile import NamedTemporaryFile
import os

def test_censore(inp, outp):
    try:
        f = NamedTemporaryFile(dir='.', delete=False, mode='w')
        name = f.name
        f.file.write(inp)
        f.file.close()

        censore_haha(f.name)
        with open(f.name + ".censored.txt") as f:
            content = f.read()
        assert content == outp, ("input file: {inp}, "
                                 "expected output: {outp} "
                                 "obtained output: {content}".format(
                                 inp=inp, outp=outp, content=content
                                 ))
    finally:
        os.remove(name)

test_censore(
    "haha test\nanother haha haha test\nhahahaha hahahaha\n"
    "this is a test\nwell",
    ("[censored] test\nanother [censored] [censored] test\n"
    "[censored][censored] [censored][censored]\nthis is a test\nwell")
)

test_censore(
    (
        "this is a haha haha haha\n"
        "haha ha ha hahahahahaha ha haha\n"
        "\n"
        "ha\n"
        "ha\n"
        "\n"
        "thisisahahahathis\n"
        "well...\n"
        "\n"
        "Hello, world!\n"
    ),
    (
        "this is a [censored] [censored] [censored]\n"
        "[censored] ha ha [censored][censored][censored] ha [censored]\n"
        "\n"
        "ha\n"
        "ha\n"
        "\n"
        "thisisa[censored]hathis\n"
        "well...\n"
        "\n"
        "Hello, world!\n"
    )
)

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

Написать функцию three_best_applicants(portfolio), принимающую на вход имя файла с портфолио в формате, аналогичном этому файлу, и возвращающую список фамилий и имён трёх лучших абитуриентов, упорядоченных по числу набранных ими баллов (по убыванию). Каждый элемент возвращаемого списка должен был кортежем, в котором на первом месте стоит фамилия студента, а на втором — его имя. В файле идет сначала имя, потом фамилия, а потом число баллов, причём между именем и фамилией стоит пробел, а между фамилием и числом баллов — символ табуляции (\t).

In [ ]:
# YOUR CODE HERE
In [ ]:
from tempfile import NamedTemporaryFile
import os

def test_portfolio(inp, outp):
    try:
        f = NamedTemporaryFile(dir='.', delete=False, mode='w')
        name = f.name
        f.file.write(inp)
        f.file.close()

        obtained = three_best_applicants(f.name)
        assert obtained == outp, ("input file: {inp}, "
                                 "expected output: {outp} "
                                 "obtained output: {obtained}").format(
            inp=inp, outp=outp, obtained=obtained)
    finally:
        os.remove(name)

test_portfolio(
"""Ann Brown\t25
Emily Calvert\t89
Alice Charr\t78
Bill Taylor\t94
Polly Smith\t32
Jill Acker\t68
Tom Bass\t15
Victoria Greg\t48
Philipp Pruitt\t65
Cristine Evans\t82
""",[('Taylor', 'Bill'), ('Calvert', 'Emily'), ('Evans', 'Cristine')])

test_portfolio(
"""Ann Brown\t125
Emily Calvert\t89
Alice Charr\t78
Bill Taylor\t94
Polly Smith\t932
Victoria Greg\t648
Philipp Pruitt\t65
Cristine Evans\t82
""",[('Smith', 'Polly'), ('Greg', 'Victoria'), ('Brown', 'Ann')])

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

Необязательная задача, можно смело пропустить.

Функция save_bill(clientname, cart, filename) принимает на вход имя клиента, список покупок cart, состоящий из трехэлементных кортежей вида (название товара, количество, цена за единицу), и имя файла filename. Функция должна создать файл, имя которого указано в переменной filename с чеком по заданному образцу и ничего не возвращать. Все числа должны выводиться в файл с двумя значащами цифрами после десятичной точки. Например, save_bill('Alice', [('Oil', 2, 100.11), ('Bread', 0.345, 90), ('Milk', 1, 50.32)], "somefile.txt") должна создать файл somefile.txt со следующим содержимым:

Client name: Alice

Oil x 2.00: 200.22
Bread x 0.34: 31.05
Milk x 1.00: 50.32

Total: 281.59

Подсказка. Записать число с двумя знаками после десятичной точки можно так:

"{:.2f}".format(12.345)

Или с помощью f-strings: если в переменной cost лежит вещественное число, его можно включить в строку с помощью такого синтаксиса:

f"This is a cost: {cost:.2f}"
In [ ]:
# YOUR CODE HERE
In [ ]:
from tempfile import NamedTemporaryFile
import os

testsuite = [("Alice", [('Oil', 2, 100.11), ('Bread', 0.345, 90),
                        ('Milk', 1, 50.32)],
                        ('Client name: Alice\n\nOil x 2.00: 200.22\nBread'
                        ' x 0.34: 31.05\nMilk x 1.00: 50.32\n\nTotal: 281'
                        '.59')),
             ("Bill Clinton", [('Thing', 1, 10),
                               ('Other thing', 1.234, 32.32)],
              ('Client name: Bill Clinton\n\nThing x 1.00: 10.0'
               '0\nOther thing x 1.23: 39.88\n\nTotal: 49.88')),
             ("Claudia", [('This', 1.3, 2.12),
                          ('This', 1.6, 2.12)],
             ('Client name: Claudia\n\nThis x 1.30: 2.76\nThis'
              ' x 1.60: 3.39\n\nTotal: 6.15'))
            ]

for clientname, cart, output in testsuite:
    f = NamedTemporaryFile(dir='.', delete=False)
    name = f.name
    f.close()
    try:
        save_bill(clientname, cart, name)
        with open(name) as f:
            collected_output = f.read().strip()
            assert collected_output == output.strip(), (collected_output, output.strip())
    finally:
        os.remove(name)

Часть 2: numpy

Во всех задачах словом «массив» обозначается объект типа np.array.

Во всех задачах этого раздела запрещено использовать цикли и list comprehensions.

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

Написать функцию double_this(arr), принимающую на вход массив arr, состоящий из чисел, и возвращающую массив, полученный удвоением каждого элемента arr.

Подсказка: Операции с массивами действуют поэлементно.

Ваша функция не должна превращать arr в np.array, она уже получает на вход np.array. Для проверки можно запустить что-то вроде

double_this(np.array([1, 2, 3]))
In [ ]:
# YOUR CODE HERE
In [ ]:
from timeit import timeit
import numpy as np

def testme(f, inp, outp):
    q = f(np.array(inp))
    assert isinstance(q, np.ndarray), "Функция должна возвращать массив numpy (np.array)"
    assert np.array_equal(q, np.array(outp)), "Ошибка для входного списка "+str(np.array(inp))
def test(inp, outp):
    testme(double_this, inp, outp)

test([1, 2, 3], [2, 4, 6])
test([1.1, 2.2, 3.3], [2.2, 4.4, 6.6])
test([1]*10, [2]*10)
test([1]*10+[2]*15, [2]*10+[4]*15)

N = 1000000

benchmark = timeit("[x*x for x in np.array([1]*N)]", "from __main__ import N, np", number=1)
otherbenchmark = timeit("double_this(np.array([1]*N))", 
                        "from __main__ import N, np, double_this", number=1)
assert benchmark > otherbenchmark*2, "Код работает слишком медленно — вы точно не пользовались циклами?"

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

Написать функцию select_even(arr), принимающую на вход массив целых чисел arr и возвращающую новый массив, который состоит из всех чётных элементов arr.

Подсказка: напомним, что все арифметические операции, а также операции сравнения, действуют на массивы поэлементно.

In [ ]:
# YOUR CODE HERE
In [ ]:
from timeit import timeit
import numpy as np

def testme(f, inp, outp):
    q = f(np.array(inp))
    assert isinstance(q, np.ndarray), "Функция должна возвращать массив numpy (np.array)"
    assert np.array_equal(q, np.array(outp)), "Ошибка для входного списка "+str(np.array(inp))
def test(inp, outp):
    testme(select_even, inp, outp)
    
test([1, 2, 3, 4, 5], [2, 4])
test([], [])
test([1, 3, 5], [])
test([5, 4, 3, 2, 0], [4, 2, 0])
test([100, 200, 300, 199, 299, 150], [100, 200, 300, 150])

N = 100000
benchmark = timeit("[x for x in np.array([1]*N) if x*2]", "from __main__ import N, np", number=1)
otherbenchmark = timeit("select_even(np.array([1]*N))", 
                        "from __main__ import N, select_even, np", number=1)
assert benchmark > otherbenchmark*2, "Код работает слишком медленно — вы точно не пользовались циклами?"
# should be at least two times faster then list comprehensions

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

Написать функцию wipe_even(arr, target_value, in_place), принимающую на вход массив целых чисел arr, и возвращающую массив, полученный из arr путём замены всех чётных элементов на target_value. Если target_value не указано, то оно должно считаться равным числу 0. Если указан параметр in_place и он равен True, то функция должна менять исходный массив, а если не указан или указан в False, то нужно создать копию исходного массива и менять эту копию, так, чтобы сам исходный массив остался неизменным.

Подсказка. Чтобы получить копию исходного массива можно использовать метод .copy(). Вам не нужно превращать arr в np.array, он уже является таковым. Для проверки функции можно запустить что-то вроде

wipe_even(np.array([1, 2, 3, 4, 6]), 100)
In [ ]:
# YOUR CODE HERE
In [ ]:
from timeit import timeit
import numpy as np

def test(inp, outp, target=0, in_place=False):
    inp = np.array(inp)
    inp_backup = np.array(inp)
    
    q = wipe_even(inp, target, in_place)
    assert isinstance(q, np.ndarray), "Функция должна возвращать массив numpy (np.array)"
    assert np.array_equal(q, np.array(outp)), "Ошибка для входного списка "+str(np.array(inp))
    if in_place:
        assert np.array_equal(inp, np.array(outp)), "Функция должна менять исходный массив"
    else:
        assert np.array_equal(inp, inp_backup), "Исходный массив должен остаться неизменным"
    
test([1, 2, 3, 4, 5], [1, 0, 3, 0, 5], in_place=True)
test([], [], in_place=True)
test([1, 3, 5], [1, 3, 5], in_place=True)
test([5, 4, 3, 2, 0], [5, 0, 3, 0, 0], in_place=True)
test([100, 200, 300, 199, 299, 150], [0, 0, 0,  199, 299, 0], in_place=True)

test([1, 2, 3, 4, 5], [1, 99, 3, 99, 5], target = 99, in_place=True)

N = 100000
benchmark = timeit("[0 if x*2 else x for x in np.array([1]*N)]", 
                   "from __main__ import np, N", number=1)
otherbenchmark = timeit("wipe_even(np.array([1]*N), in_place=True)", 
                        "from __main__ import np, N, wipe_even", number=1)
assert benchmark > otherbenchmark*1.5, "Код работает слишком медленно — вы точно не пользовались циклами?"
In [ ]:
# test not in_place behaviour now

test([1, 2, 3, 4, 5], [1, 0, 3, 0, 5])
test([], [])
test([1, 3, 5], [1, 3, 5])
test([5, 4, 3, 2, 0], [5, 0, 3, 0, 0])
test([100, 200, 300, 199, 299, 150], [0, 0, 0,  199, 299, 0])

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

Написать функцию weighted_sum(weights, grades, normalize), возвращающую взвешенную сумму оценок, записанных в массив grades, в соответствии с весами, записанными в массив weights. Например, для weights = np.array([0.3, 0.3, 0.4]) и grades = np.array([7, 9, 8]) функция должна вернуть число $0.3\times 7+0.3\times 9+0.4\times 8=8.0$.

Если параметр normalize установлен в True, а сумма всех весов отличается от 1, то следует умножить все веса на одно и то же число таким образом, чтобы их сумма была равна 1, в противном случае следует использовать веса «как есть», даже если их сумма отличается от 1. Если функция запущена без указания параметра normalize, следует считать, что normalize=False.

Подсказка: Вам помогут функции np.dot() и np.sum(). Встроенная функция sum() также работает с массивами numpy, но гораздо медленнее (проверьте с помощью %timeit!)

In [ ]:
# YOUR CODE HERE
In [ ]:
from timeit import timeit
import numpy as np

def test(w, g, out, normalize = False):
    q = weighted_sum(np.array(w), np.array(g), normalize)
    assert np.isclose(q, out)

test([0.3, 0.3, 0.4], [7, 9, 8], 8)
test([0.1, 0.2, 0.3, 0.4], [1, 5, 3, 2], 2.8)
test([1, 2, 3, 4], [1, 5, 3, 2], 28)
test([1, 2, 3, 4], [1, 5, 3, 2], 2.8, normalize=True)
In [ ]:
N = 1000000

test([1, 2, 3, 4], [1, 5, 3, 2], 28)

benchmark = timeit("sum([x/x for x in np.array([1]*N)])", "from __main__ import N, np", number=1)
otherbenchmark = timeit("weighted_sum(np.array([1.1]*N), np.array([1]*N), True)", 
                        "from __main__ import N, weighted_sum, np", number=1)
assert benchmark > otherbenchmark*1.7, "Код работает слишком медленно — вы точно использовали методы numpy?"

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

Написать функцию mean_by_gender(grades, genders), принимающую на вход два массива одинаковой длины: в массиве grades записаны оценки некоторых студентов, а в массиве genders — их пол в виде строк male или female. Требуется вернуть словарь, ключами которого будут строки male и female, а записями — среднее арифметическое оценок студентов соответствующего пола.

Например, если grades = np.array([5, 4, 3, 5, 2]) и genders = np.array(["female", "male", "male", "female", "male"]), функция должна вернуть словарь {'male': 3.0, 'female': 5.0}.

Подсказка. Для быстрого вычисления среднего есть функция np.mean() или соответствующий метод у объектов типа numpy.array.

In [ ]:
# YOUR CODE HERE
In [ ]:
from timeit import timeit
import numpy as np

def test(grades, genders, outp):
    ret = mean_by_gender(np.array(grades), np.array(genders))
    assert np.isclose(ret['female'], outp['female'])
    assert np.isclose(ret['male'], outp['male'])

test([5, 4, 3, 5, 2], ["female", "male", "male", "female", "male"], {'male': 3.0, 'female': 5.0})
test([1, 0]*10, ['female', 'male']*10, {'female': 1, 'male': 0})
test(range(100), ['female', 'male']* 50, {'female': 49.0, 'male': 50.0})
test(list(range(100))+[100], ['male']*100+['female'], {'male':49.5, 'female': 100.0})
# mean_by_gender(np.array(range(100)), ['female', 'male']* 50)

def benchmark_test(a, b):
    xx = 0
    yy = 0
    im = 0
    fi = 0
    for x, y in zip(a, b):
        if x != y:
            xx += x
            yy += x
            im += 1
            fi += 1
    
    return xx+yy

N = int(1E5)
grades = np.array([1.1]*N + [2.2]*N)
genders = np.array(['male']*N + ['female']*N)

benchmark = timeit("assert np.isclose(mean_by_gender(grades, genders)['male'], 1.1)",
                   "from __main__ import np, mean_by_gender, grades, genders",
                   number=1)
reference_benchmark = timeit("benchmark_test(grades, genders)",
                             "from __main__ import benchmark_test, grades, genders",
                             number=1)

assert reference_benchmark > benchmark * 10, "Код работает слишком медленно — вы точно использовали методы numpy?"

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

Необязательная задача, можно пропустить.

В некотором царстве, в некотором государстве, налог на доходы физических лиц вычисляется следующим образом. Базовая ставка налога составляет 13%. Если в каком-то месяце ваш заработок за год составит больше тысячи тугриков, то на оставшуюся часть года (не включая этот месяц) устанавливается ставка в 20%. Например, если вы зарабатываете каждый месяц 150 тугриков, то к июлю заработаете $150\times 7 = 1050$ тугриков и начиная с августа подоходный налог будет начисляться по ставке 20%. Написать функцию calculate_tax(income), принимающую на вход массив, содержащий доход за каждый месяц года, начиная с первого и возвращающую общую сумму налога, который предстоит заплатить за год. Год в некотором царстве может длиться более 12 месяцев, если по этому поводу будет принят соответствующий высочайший декрет.

Подсказка. Вам поможет функция np.cumsum(). Чтобы создать новый массив, длина которого равна длине какого-то другого, можно использовать функции np.zeros_like() или np.ones_like().

In [ ]:
# YOUR CODE HERE
In [ ]:
from timeit import timeit
import numpy as np

assert np.isclose(calculate_tax(np.array([150]*12)), 286.5)
assert np.isclose(calculate_tax(np.array([100]*12)), 163)
assert np.isclose(calculate_tax(np.array([50]*12)), 78)
assert np.isclose(calculate_tax(np.array([1000]*12)), 2260)

assert np.isclose(calculate_tax(np.array(range(12))*100), 1215)
assert np.isclose(calculate_tax(np.array(range(11,-1,-1))*100), 1243)
In [ ]:
def dummy(x):
    z = 0
    for y in x:
        z += y
        z += y*0.12
        if z:
            z += y
    return z

assert np.isclose(calculate_tax(np.array(range(12))*100), 1215)

N = int(1E6)
arr = np.array([1]*N)
benchmark = timeit("calculate_tax(arr)", "from __main__ import calculate_tax, arr", number=1)
reference_benchmark = timeit("dummy(arr)", "from __main__ import dummy, arr", number=1)

assert reference_benchmark > benchmark*5, "Код работает слишком медленно — вы точно использовали методы numpy?"

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

В задачах машинного обучения часто требуется нормализовать данные перед тем, как их использовать. Пусть в переменной X находится двумерный np.array, по строкам которого записаны разные объекты, а по столбцам — признаки. Вам необходимо написать функцию normalize(X), принимающую на вход массив X и нормализующий все переменные таким образом, чтобы их среднее равнялось 0, а стандартное отклонение 1. Иными словами, для каждого столбца необходимо из всех элементов вычесть среднее по этому столбцу и результат разделить на стандартное отклонение по этому столбцу. Более формально: если $X=(x_{ij})$ — наша матрица, $x_{ij}$ — элемент, который стоит в её $i$-й строке и $j$-м столбце, и $x_{\cdot j}$ — $j$-й столбец, то в новой матрице на $i$-й строке в $j$-м столбце будет стоять элемент

$$\widehat{x}_{ij}=\frac{x_{ij}-\overline{x_{\cdot j}}}{\sigma_{x_{\cdot j}}},$$

где $\overline{x_{\cdot j}}$ — выборочное среднее (среднее арифметическое) всех элементов $j$-го столбца, $\sigma_{x_{\cdot j}}$ — стандартное отклонение всех элементов $j$-го столбца.

Подсказка. Вычислить среднее можно с помощью метода .mean(), стандартное отклонение — с помощью .std(). Обе функции принимают на вход параметр axis, с помощью которого можно применять их к строкам или столбцам двумерного массива. Использовать циклы, как обычно, запрещено. Задачу можно решить в одну строчку.

In [ ]:
# YOUR CODE HERE
In [ ]:
assert np.isclose(normalize(np.array([[ 1.00766597, -1.1201796 ,  2.47274732, -0.33619288,  1.50555214],
       [ 1.48986823,  0.80894409,  0.55980545,  0.67813423, -0.3187493 ]])), np.array([[-1., -1.,  1., -1.,  1.],
       [ 1.,  1., -1.,  1., -1.]])).all()
assert np.isclose(normalize(np.array([[-0.98607026],
       [ 1.93312384],
       [-0.99905497],
       [-0.95934573],
       [ 0.05295053]])), np.array([[-0.69959273],
       [ 1.87124093],
       [-0.71102792],
       [-0.67605736],
       [ 0.21543708]])).all()
assert np.isclose(normalize(np.array([[-1.63419424],
       [ 0.39451389],
       [-0.11346483],
       [ 0.56117231],
       [ 0.35460207],
       [ 1.50836012],
       [ 0.5176692 ],
       [-1.20605276],
       [ 0.7904588 ],
       [ 1.28349441]])), np.array([[-1.9874883 ],
       [ 0.15738144],
       [-0.37968359],
       [ 0.33358254],
       [ 0.11518431],
       [ 1.33500529],
       [ 0.28758849],
       [-1.53483191],
       [ 0.57599773],
       [ 1.09726401]])).all()
assert np.isclose(normalize(np.array([[-1.31158329,  2.5954087 , -1.01662736, -0.27565263,  0.52639556,
         0.58218805, -0.35961103,  0.31096071,  0.52193677, -0.41754881],
       [-0.19218836, -0.03416295,  0.80408723, -1.18733572,  0.14422448,
         0.6091103 ,  0.67617586,  0.17732224,  0.99660189, -0.07798097]])), np.array([[-1.,  1., -1.,  1.,  1., -1., -1.,  1., -1., -1.],
       [ 1., -1.,  1., -1., -1.,  1.,  1., -1.,  1.,  1.]])).all()
assert np.isclose(normalize(np.array([[-0.28368534, -0.90928588, -1.35180963],
       [ 1.30199557,  1.32081835,  1.11951334]])), np.array([[-1., -1., -1.],
       [ 1.,  1.,  1.]])).all()
assert np.isclose(normalize(np.array([[-0.34089722,  0.93727935],
       [ 0.14410815, -0.96321317],
       [-1.98355493, -0.0310602 ]])), np.array([[ 0.42383229,  1.23244371],
       [ 0.95653353, -1.21689804],
       [-1.38036582, -0.01554567]])).all()
assert np.isclose(normalize(np.array([[ 1.53033913,  0.05456373,  0.22504087, -1.16687133, -0.23619502],
       [-0.81477156,  1.96405223, -1.5506048 , -2.08082958, -0.23459537],
       [-0.80961303, -0.55950949, -1.07953561,  0.571387  , -1.03341414],
       [ 0.10526012, -2.06172783, -1.1661957 , -1.00297227, -1.02432731],
       [ 0.04661   , -0.21104596, -0.84339233,  0.22806353, -0.34655384]])), np.array([[ 1.77181211,  0.16828692,  1.84979571, -0.49193269,  0.90886342],
       [-0.96400966,  1.64710001, -1.11468608, -1.43524085,  0.91315434],
       [-0.95799169, -0.30728521, -0.32822509,  1.30214625, -1.22961355],
       [ 0.10930543, -1.47068586, -0.47290614, -0.32277035, -1.20523884],
       [ 0.04088381, -0.03741586,  0.0660216 ,  0.94779764,  0.61283463]])).all()

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

В двумерном массиве scores записаны баллы нескольких студентов, строка — студент, столбец — домашнее задание. Имеется также массив max_scores, который содержит столько же элементов, сколько столбцов в scores: в нём написано максимальное число баллов, которые можно было получить за соответствующее домашнее задание. Теоретически, студент мог нарешать задач на большее количество баллов, но те баллы, которые набраны сверх максимального, в зачёт не идут. Оценка за домашнюю работу является вещественным числом от 0 до 10 и определяется как набранные баллы / максимальное число баллов × 10. Например, если максимальное число баллов за какую-то домашнюю работу равно 8, а студент набрал за неё 4 балла, то есть оценка равна 4/8×10=5. А если бы он набрал 12 баллов, то в зачёт бы пошло 8 баллов и оценкой было бы число 10. Оценка по курсу вычисляется как среднее арифметическое от всех оценок за домашние работы, округлённое до целого числа с помощью функциюю np.round. Написать функцию get_grades(scores, max_scores), возвращающую массив итоговых оценок. Запрещено использовать циклы и if'ы.

Подсказка. Вам пригодится функция np.minimum.

In [ ]:
# YOUR CODE HERE
In [ ]:
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([1, 1])), np.array([ 10.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([1, 2])), np.array([ 10.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([1, 3])), np.array([  8.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([1, 6])), np.array([  7.,   8.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([ 1, 10])), np.array([ 6.,  7.,  8.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([2, 1])), np.array([  8.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([2, 2])), np.array([  8.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([2, 3])), np.array([  6.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([2, 6])), np.array([  4.,   8.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([ 2, 10])), np.array([ 4.,  7.,  8.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([3, 1])), np.array([  7.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([3, 2])), np.array([  7.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([3, 3])), np.array([  5.,  10.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([3, 6])), np.array([  3.,   8.,  10.])).all()
assert np.isclose(get_grades(np.array([[1, 2],
       [3, 4],
       [5, 6]]), np.array([ 3, 10])), np.array([ 3.,  7.,  8.])).all()
assert np.isclose(get_grades(np.array([[  9.,   9.,  10.],
       [  1.,   9.,   0.],
       [  1.,   3.,  10.],
       [  5.,   5.,   2.],
       [  3.,   9.,   3.]]), np.array([ 9.,  9.,  2.])), np.array([ 10.,   4.,   5.,   7.,   8.])).all()
assert np.isclose(get_grades(np.array([[  8.,   3.,   5.,  10.,   4.],
       [  9.,   0.,   5.,  10.,   6.],
       [  0.,   1.,   7.,   2.,   9.]]), np.array([ 9.,  3.,  3.,  2.,  6.])), np.array([ 9.,  8.,  7.])).all()
assert np.isclose(get_grades(np.array([[ 6.,  4.,  2.,  7.,  0.],
       [ 8.,  1.,  4.,  4.,  8.],
       [ 1.,  3.,  5.,  5.,  3.],
       [ 2.,  5.,  3.,  4.,  8.],
       [ 7.,  0.,  7.,  1.,  8.]]), np.array([ 5.,  5.,  8.,  3.,  8.])), np.array([ 6.,  7.,  6.,  8.,  6.])).all()

Часть 3: pandas

Во всех задачах этой части запрещено использовать циклы.

In [ ]:
import pandas as pd

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

В датафрейме df находится информация об успеваемости студентов: в столбцах First Name и Last Name — имя и фамилия, а в следующих столбцах — оценки за разные курсы по пятибальной системе (целые числа от 0 до 5). Напишите функцию get_grade(df, lastname, firstname, course), возвращающую оценку данного студента по данному курсу. Предполагается, что не бывает студентов, у которых совпадали бы одновременно фамилия и имя.

Пример:

Входная таблица

  Last Name First Name  Algebra  Calculus  Music  Law
0       Doe       John        4         5      3    5
1     Smith      Alice        5         4      2    4

записывается в виде

df = pd.DataFrame(
    [
        ['Doe', 'John', 4, 5, 3, 5], 
        ['Smith', 'Alice', 5, 4, 2, 4]
    ], 
    columns=['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law']
)

Для неё функция get_grade(df, 'Doe', 'John', 'Algebra') должна вернуть число 4.

Подсказка. Не забудьте превратить результат в целое число (int)!

In [ ]:
# YOUR CODE HERE
In [ ]:
def test(table, columns):
    df = pd.DataFrame(table, columns=columns)
    for row in table:
        firstname = row[columns.index('First Name')]
        lastname = row[columns.index('Last Name')]
        for j, course in enumerate(columns[2:], 2):
            assert get_grade(df, lastname, firstname, course) == row[j]

test(
    [
        ['Doe', 'John', 1, 2, 3, 4], 
        ['Smith', 'Alice', 5, 4, 2, 4]
    ], 
    columns=['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law']
)

test(
    [
        ['John', 'Doe', 1, 2, 3, 4], 
        ['Max', 'Katz', 5, 4, 2, 4]
    ], 
    columns=['First Name', 'Last Name', 'Algebra', 'Calculus', 'Music', 'Law']
)

test(
    [
        ['John', 'Doe', 1, 2, 3, 4, 3, 2], 
        ['Jennifer', 'Lopez', 5, 4, 2, 4, 1, 1],
        ['John', 'Smith', 2, 1, 4, 3, 3, 2]
    ],
    columns=['First Name', 'Last Name', 'Algebra', 'Calculus', 'Music', 'Law', 'CS', 'Physics']
)

test(
    [
        ['John', 'Doe', 1, 2, 3, 4, 3, 2], 
        ['Jack', 'Doe', 5, 4, 2, 4, 1, 1],
        ['John', 'Smith', 2, 1, 4, 3, 3, 2]
    ],
    columns=['First Name', 'Last Name', 'Algebra', 'Calculus', 'Music', 'Law', 'CS', 'Physics']
)

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

В датафрейме df задана некоторая таблица. Написать функцию get_rows_after_5(df, n), возвращающую датафрейм, в котором записано n строк, начиная с пятой сверху (включая 5-ю). Например, get_row_after_5(df, 1) должна вернуть только пятую строку, а get_row_after_5(df, 2) — 5-ю и 6-ю.

Внимание! Индексами (именами строк) могут быть не числа, а что угодно.

In [ ]:
# YOUR CODE HERE
In [ ]:
df = pd.DataFrame([[1, 2, 3], 
                   [4, 5, 6], 
                   [7, 8, 9], 
                   [10, 11, 12], 
                   [13, 14, 15], 
                   [16, 17, 18], 
                   [19, 20, 21]])

assert get_rows_after_5(df, 1).equals(pd.DataFrame([[13, 14, 15]], index=[4]))
assert get_rows_after_5(df, 2).equals(pd.DataFrame([[13, 14, 15], [16, 17, 18]], index=[4, 5]))
assert get_rows_after_5(df, 3).equals(pd.DataFrame([[13, 14, 15], [16, 17, 18], [19, 20, 21]], index=[4, 5, 6]))


df.index = list(range(6, -1, -1))
assert get_rows_after_5(df, 1).equals(pd.DataFrame([[13, 14, 15]], index=[2]))

df.index = list('abcdefg')
df.sort_values(0, ascending=False, inplace=True)

assert get_rows_after_5(df, 1).equals(pd.DataFrame([[7, 8, 9]], index=['c']))
assert get_rows_after_5(df, 2).equals(pd.DataFrame([[7, 8, 9], [4, 5, 6]], index=['c', 'b']))

df['hello'] = list('qwertyu')

get_rows_after_5(df, 2)

assert get_rows_after_5(df, 2).equals(pd.DataFrame([[7, 8, 9, 't'], [4, 5, 6, 'y']], columns = [0, 1, 2, 'hello'], index=['c', 'b']))

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

В датафрейме df задана некоторая таблица, её индексами являются целые числа, не обязательно идущие по порядку. Написать функцию between(df, n, m), возвращающую все строки этой таблицы, расположенные между строками с индексами n и m, включая строки с индексами n и m. Гарантируется, что строка с индексом n встречается раньше строки с индексом m.

In [ ]:
# YOUR CODE HERE
In [ ]:
df = pd.DataFrame([[1, 2, 3], 
                   [4, 5, 6], 
                   [7, 8, 9], 
                   [10, 11, 12], 
                   [13, 14, 15], 
                   [16, 17, 18], 
                   [19, 20, 21]])
assert between(df, 2, 3).equals(pd.DataFrame([[7, 8, 9], [10, 11, 12]], index=[2, 3]))

df.index=[1, 2, 10, 9, 8, 7, 3]

assert between(df, 2, 3).equals(pd.DataFrame([[4, 5, 6], [7, 8, 9], [10, 11, 12], [13, 14, 15], [16, 17, 18], [19, 20, 21]], columns=[0, 1, 2], index=[2, 10, 9, 8, 7, 3]))

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

В датафрейме grades находится таблица с оценками разных студентов по разным формам контроля (студент — строка, форма контроля — столбец, имена студентов записаны в index по строкам и не являются частью таблицы). В series weights находятся веса, с которыми каждая форма контроля входит в итоговую оценку. (Сумма весов равна 1.) Напишите функцию weight_grades(grades, weights), которая возвращает series, в котором указано, какой студент какую итоговую оценку получил.

Подсказка. Вам пригодится метод .dot для датафрейма, который делает матричное умножение (аналог функции SUMPRODUCT в табличных процессорах) — умножает каждый элемент строки на соответствующий вес, а потом всё складывает.

Примеры входных и выходных данных см. в тесте.

In [ ]:
# YOUR CODE HERE
In [ ]:
grades = pd.DataFrame(dict(hw1=[3, 2, 2], hw2=[2, 3, 4]), index=['Alice', 
                                                             'Claudia', 
                                                             'Bob'])
weights = pd.Series([0.75, 0.25], index=['hw2', 'hw1'])
assert (weight_grades(grades, weights) == pd.Series([2.25, 2.75, 3.5],
                                                   index = ['Alice', 
                                                            'Claudia', 
                                                            'Bob'])).all()

grades = pd.DataFrame(dict(hw1=[3, 2], hw2=[2, 3], hw3=[0, 1]),
                      index=['Alice', 'Claudia'])
weights = pd.Series(dict(hw1=0.25, hw2=0.5, hw3=0.25))
assert weight_grades(grades, weights).to_dict() == {'Alice': 1.75, 
                                                    'Claudia': 2.25}

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

Написать функцию mean_by_gender(grades, genders), принимающую на вход два series одинаковой длины: в grades записаны оценки некоторых студентов, а в genders — их пол в виде строк male или female. Требуется вернуть словарь, ключами которого будут строки "male" и "female", а записями — среднее арифметическое оценок студентов соответствующего пола.

Например, если grades = pd.Series([5, 4, 3, 5, 2]) и genders = pd.Series(["female", "male", "male", "female", "male"]), функция должна вернуть словарь {'male': 3.0, 'female': 5.0}.

Подсказка. Вы уже решали похожую задачу в прошлом задании с помощью numpy. Подумайте, как это более изящно сделать с помощью pandas. Можно, конечно, создать два отдельных series, в один поместить оценки всех юношей, а в другой — всех девушек, но есть решение и поизящнее: создать датафрейм, столбцами которого являются наши series, а затем применить метод groupby. Кстати, у series есть метод to_dict().

Чтобы создать датафрейм с заданными столбцами можно передать pd.DataFrame на вход словарь:

pd.DataFrame({'column1': [1, 2, 3], 'column2': [4, 5, 6]})
In [ ]:
# YOUR CODE HERE
In [ ]:
def test(a, b, c):
    assert mean_by_gender(pd.Series(a), pd.Series(b)) == c

assert (mean_by_gender(pd.Series([5, 4, 3, 5, 2], index=['Alice', 'Bob',
                                                        'Dan', 'Claudia',
                                                        'John']), 
                       pd.Series(["male", "female",  "male", 
                                  "female", "male"], index=['Bob','Alice', 
                                                            'Dan',
                                                            'Claudia', 
                                                            'John'])
                      ) == {'female': 5, 
                                                          'male': 3})
test([1, 0]*10, ['female', 'male']*10, {'female': 1, 'male': 0})
test(range(100), ['female', 'male']* 50, {'female': 49.0, 'male': 50.0})
test(list(range(100))+[100], ['male']*100+['female'], {'male':49.5, 
                                                       'female': 100.0})

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

В датафрейме df находится информация об успеваемости студентов. Написать функцию gpa_top(df), принимающую на вход датафрейм df и модифицирующую его следующим образом:

  • Добавить в df столбец с именем 'GPA', содержащим среднюю оценка студента. Разные студенты могут брать разный набор курсов, поэтому среди оценок студентов может встречаться NaN (это означает, что студент не брал соответствующий курс). Среднее считается среди тех курсов, которые студент брал.

  • Отсортировать датафрейм по убыванию GPA.

  • Вернуть только те строчки датафрейма, в которых GPA не меньше 4 баллов.

Подсказки:

  1. Для сортировки датафрейма нужно использовать метод sort_values().
  2. Метод mean() игнорирует строки и NaN'ы.

В следующих ячейках приведено два примера.

In [ ]:
# входной датафрейм
pd.DataFrame([['Doe', 'John', 4, 5, 3.0, 5], 
              ['Smith', 'Alice', 5, 4, float("nan"), 4]], 
             columns=['Last Name', 'First Name', 
                      'Algebra', 'Calculus', 'Music', 'Law'], index=[0, 1])
In [ ]:
# выходной датафрейм
pd.DataFrame([['Smith', 'Alice', 5, 4, float("nan"), 4, 4.333333333333333], 
              ['Doe', 'John', 4, 5, 3.0, 5, 4.25]], 
             columns=['Last Name', 'First Name', 'Algebra', 
                      'Calculus', 'Music', 'Law', 'GPA'], index=[1, 0])
In [ ]:
# входной датафрейм
pd.DataFrame([['Doe', 'John', 1, 5, 3.0, 5], 
              ['Smith', 'Alice', 5, 4, float("nan"), 4]], 
             columns=['Last Name', 'First Name', 'Algebra', 'Calculus', 
                      'Music', 'Law'], index=[0, 1])
In [ ]:
# выходной датафрейм
pd.DataFrame([['Smith', 'Alice', 5, 4, float("nan"), 4, 4.333333333333333]], 
             columns=['Last Name', 'First Name', 'Algebra', 'Calculus', 
                      'Music', 'Law', 'GPA'], index=[1])
In [ ]:
# YOUR CODE HERE
In [ ]:
import pandas as pd
def pd_repr(df):
    content = repr(df.values.tolist()).replace('nan', 'float("nan")')
    columns = repr(df.columns.tolist())
    index = repr(df.index.tolist())
    return "pd.DataFrame(%s, columns=%s, index=%s)" % (content, columns, index)

def test(table, columns, newtable, newindex):
    inp = pd.DataFrame(table, columns=columns)
    expected = pd.DataFrame(newtable, columns = columns + ['GPA'], index=newindex)
    out = gpa_top(inp)
    if len(out) == 0 and len(expected) == 0:
        return
    assert out.equals(expected), "Что-то пошло не так для входного датафрейма %s" % pd_repr(inp)

test([['Doe', 'John', 4, 5, 3.0, 5]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law'], [['Doe', 'John', 4, 5, 3.0, 5, 4.25]], [0])
test([['Doe', 'John', 4, 5, 3.0, 5], ['Smith', 'Alice', 5, 4, float('nan'), 4]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law'], [['Smith', 'Alice', 5, 4, float('nan'), 4, 4.333333333333333], ['Doe', 'John', 4, 5, 3.0, 5, 4.25]], [1, 0])
test([['Doe', 'John', 1, 5, 3.0, 5], ['Smith', 'Alice', 5, 4, float('nan'), 4]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law'], [['Smith', 'Alice', 5, 4, float('nan'), 4, 4.333333333333333]], [1])
test([['Doe', 'John', 4, float('nan'), 3.0, float('nan')], ['Smith', 'Alice', 2, 4, float('nan'), 4]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law'], [], [])
test([['Doe', 'John', 4, float('nan'), 5.0, float('nan')], ['Smith', 'Alice', 5, 5, float('nan'), 4]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law'], [['Smith', 'Alice', 5, 5.0, float('nan'), 4.0, 4.666666666666667], ['Doe', 'John', 4, float('nan'), 5.0, float('nan'), 4.5]], [1, 0])
test([['Doe', 'John', 4, float('nan'), 5.0, float('nan'), 4, 5], ['Smith', 'Alice', 5, 5, float('nan'), 4, 4, float('nan')]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law', 'Science', 'English'], [['Doe', 'John', 4, float('nan'), 5.0, float('nan'), 4, 5.0, 4.5], ['Smith', 'Alice', 5, 5.0, float('nan'), 4.0, 4, float('nan'), 4.5]], [0, 1])
test([['Doe', 'John', 4, float('nan'), 5.0, float('nan'), 4, 5], ['Smith', 'Alice', 5, 5, float('nan'), 4, 5, float('nan')], ['Doe', 'Alice', 4, float('nan'), 5.0, float('nan'), 4, 5], ['Smith', 'John', 5, 5, float('nan'), 3, 4, float('nan')], ['Doe', 'John', 4, float('nan'), 5.0, 2, 4, 5], ['Smith', 'Alice', 2, 2, float('nan'), 4, 4, float('nan')]], ['Last Name', 'First Name', 'Algebra', 'Calculus', 'Music', 'Law', 'Science', 'English'], [['Smith', 'Alice', 5, 5.0, float('nan'), 4.0, 5, float('nan'), 4.75], ['Doe', 'John', 4, float('nan'), 5.0, float('nan'), 4, 5.0, 4.5], ['Doe', 'Alice', 4, float('nan'), 5.0, float('nan'), 4, 5.0, 4.5], ['Smith', 'John', 5, 5.0, float('nan'), 3.0, 4, float('nan'), 4.25], ['Doe', 'John', 4, float('nan'), 5.0, 2.0, 4, 5.0, 4.0]], [1, 0, 2, 3, 4])

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

В таблице df записана информация о покупках товаров в некотором магазине. Пример:

        Покупатель     Товар  Количество  Цена
0    Иван Петрович  Макароны           4   120
1  Лариса Ивановна    Плюшки          10   100
2    Иван Петрович    Плюшки           1   100
3             Петя   Леденцы           5    20

Один и тот же товар может продаваться по разным ценам.

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

({'Иван Петрович': 580, 'Лариса Ивановна': 1000, 'Петя': 100},
 {'Леденцы': 100, 'Макароны': 480, 'Плюшки': 1100})

Подсказки:

  1. Сначала создайте столбец, в котором будет указано, во сколько обошлась каждая покупка (с учётом количества товара и его цены). Одномерные элементы pandas (например, столбцы и строки датафреймов — их тип называется pd.DataSeries) ведут себя как np.array(), то есть операции с ними производятся поэлементно.
  2. Вам поможет метод groupby().
  3. У элементов типа pd.DataSeries есть метод to_dict(), превращающий их в словари.
In [ ]:
# YOUR CODE HERE
In [ ]:
# базовые тесты
assert check_table(pd.DataFrame([['Иван Иванович', 'Молоко', 1, 10]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0])) == ({'Иван Иванович': 10}, {'Молоко': 10})
assert check_table(pd.DataFrame([['Иван Иванович', 'Молоко', 1, 10], ['Иван Иванович', 'Кефир', 2, 20]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1])) == ({'Иван Иванович': 50}, {'Молоко': 10, 'Кефир': 40})
assert check_table(pd.DataFrame([['Иван Иванович', 'Молоко', 1, 10], ['Иван Никифорович', 'Кефир', 2, 20]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1])) == ({'Иван Никифорович': 40, 'Иван Иванович': 10}, {'Молоко': 10, 'Кефир': 40})
assert check_table(pd.DataFrame([['Иван Иванович', 'Молоко', 1, 10], ['Иван Никифорович', 'Молоко', 2, 20]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1])) == ({'Иван Никифорович': 40, 'Иван Иванович': 10}, {'Молоко': 50})

# стресс-тестирование
assert check_table(pd.DataFrame([['Петя', 'Кефир', 5, 983], ['Петя', 'Колбаса', 7, 301], ['Элизабет', 'Молоко', 1, 332], ['Иван Никифорович', 'Простокваша', 1, 318], ['Элизабет', 'Кефир', 7, 334], ['Петя', 'Колбаса', 3, 376], ['Петя', 'Чай', 1, 952], ['Ваня', 'Чай', 2, 930], ['Петя', 'Молоко', 8, 759], ['Иван Иванович', 'Кефир', 7, 720], ['Элизабет', 'Простокваша', 2, 958], ['Петя', 'Простокваша', 8, 904], ['Элизабет', 'Кефир', 6, 213], ['Иван Иванович', 'Молоко', 2, 878], ['Ваня', 'Колбаса', 7, 819], ['Петя', 'Журнал "Мурзилка"', 6, 924], ['Элизабет', 'Кефир', 6, 862], ['Ваня', 'Журнал "Мурзилка"', 7, 324], ['Петя', 'Простокваша', 3, 200], ['Иван Иванович', 'Молоко', 8, 881]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) == ({'Ваня': 9861, 'Петя': 28550, 'Иван Никифорович': 318, 'Иван Иванович': 13844, 'Элизабет': 11036}, {'Чай': 2812, 'Колбаса': 8968, 'Журнал "Мурзилка"': 7812, 'Простокваша': 10066, 'Молоко': 15208, 'Кефир': 18743})
assert check_table(pd.DataFrame([['Ваня', 'Простокваша', 8, 189], ['Иван Никифорович', 'Молоко', 2, 723], ['Иван Иванович', 'Молоко', 1, 558], ['Иван Никифорович', 'Молоко', 5, 209], ['Петя', 'Простокваша', 8, 522], ['Иван Иванович', 'Чай', 3, 193], ['Иван Иванович', 'Чай', 4, 312], ['Петя', 'Молоко', 7, 662], ['Элизабет', 'Простокваша', 7, 56], ['Петя', 'Колбаса', 9, 415], ['Иван Никифорович', 'Колбаса', 7, 772], ['Петя', 'Хлеб', 6, 825], ['Элизабет', 'Колбаса', 4, 24], ['Иван Никифорович', 'Хлеб', 9, 68], ['Иван Никифорович', 'Чай', 6, 143], ['Иван Иванович', 'Колбаса', 8, 794], ['Элизабет', 'Простокваша', 2, 333], ['Иван Иванович', 'Хлеб', 2, 272], ['Иван Иванович', 'Колбаса', 2, 250], ['Элизабет', 'Простокваша', 1, 642]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) == ({'Ваня': 1512, 'Петя': 17495, 'Иван Никифорович': 9365, 'Иван Иванович': 9781, 'Элизабет': 1796}, {'Колбаса': 16087, 'Чай': 2685, 'Простокваша': 7388, 'Молоко': 7683, 'Хлеб': 6106})
assert check_table(pd.DataFrame([['Элизабет', 'Чай', 9, 354], ['Иван Никифорович', 'Колбаса', 3, 523], ['Элизабет', 'Молоко', 4, 476], ['Элизабет', 'Молоко', 5, 253], ['Иван Иванович', 'Кефир', 5, 829], ['Иван Никифорович', 'Молоко', 7, 874], ['Элизабет', 'Чай', 3, 901], ['Петя', 'Хлеб', 4, 644], ['Иван Иванович', 'Хлеб', 8, 943], ['Элизабет', 'Колбаса', 5, 80], ['Иван Никифорович', 'Чай', 3, 537], ['Иван Иванович', 'Журнал "Мурзилка"', 7, 204], ['Петя', 'Журнал "Мурзилка"', 2, 251], ['Петя', 'Колбаса', 1, 344], ['Иван Никифорович', 'Простокваша', 5, 694], ['Иван Иванович', 'Кефир', 3, 661], ['Иван Никифорович', 'Чай', 7, 365], ['Элизабет', 'Хлеб', 6, 36], ['Ваня', 'Колбаса', 9, 40], ['Элизабет', 'Чай', 5, 846]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) == ({'Ваня': 360, 'Петя': 3422, 'Иван Никифорович': 15323, 'Иван Иванович': 15100, 'Элизабет': 13904}, {'Чай': 14285, 'Колбаса': 2673, 'Журнал "Мурзилка"': 1930, 'Хлеб': 10336, 'Простокваша': 3470, 'Молоко': 9287, 'Кефир': 6128})
assert check_table(pd.DataFrame([['Элизабет', 'Чай', 5, 547], ['Иван Иванович', 'Хлеб', 2, 883], ['Иван Иванович', 'Журнал "Мурзилка"', 6, 616], ['Петя', 'Хлеб', 9, 313], ['Петя', 'Чай', 1, 73], ['Иван Никифорович', 'Хлеб', 8, 665], ['Иван Иванович', 'Колбаса', 5, 219], ['Элизабет', 'Журнал "Мурзилка"', 8, 207], ['Петя', 'Кефир', 7, 512], ['Иван Никифорович', 'Молоко', 6, 302], ['Иван Иванович', 'Колбаса', 9, 467], ['Иван Никифорович', 'Хлеб', 8, 548], ['Ваня', 'Простокваша', 9, 331], ['Иван Никифорович', 'Чай', 5, 414], ['Иван Иванович', 'Журнал "Мурзилка"', 6, 606], ['Элизабет', 'Чай', 8, 17], ['Иван Иванович', 'Простокваша', 9, 139], ['Иван Иванович', 'Кефир', 3, 730], ['Элизабет', 'Кефир', 2, 727], ['Элизабет', 'Журнал "Мурзилка"', 1, 618]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) == ({'Ваня': 2979, 'Петя': 6474, 'Иван Никифорович': 13586, 'Иван Иванович': 17837, 'Элизабет': 6599}, {'Чай': 5014, 'Колбаса': 5298, 'Журнал "Мурзилка"': 9606, 'Хлеб': 14287, 'Простокваша': 4230, 'Молоко': 1812, 'Кефир': 7228})
assert check_table(pd.DataFrame([['Петя', 'Простокваша', 8, 516], ['Петя', 'Молоко', 1, 779], ['Петя', 'Журнал "Мурзилка"', 2, 12], ['Иван Никифорович', 'Колбаса', 3, 776], ['Элизабет', 'Простокваша', 7, 810], ['Иван Никифорович', 'Журнал "Мурзилка"', 8, 368], ['Петя', 'Журнал "Мурзилка"', 6, 129], ['Иван Никифорович', 'Колбаса', 6, 246], ['Иван Иванович', 'Чай', 9, 297], ['Петя', 'Журнал "Мурзилка"', 7, 558], ['Петя', 'Колбаса', 3, 210], ['Ваня', 'Хлеб', 2, 916], ['Элизабет', 'Чай', 2, 395], ['Петя', 'Простокваша', 5, 317], ['Иван Иванович', 'Чай', 5, 892], ['Петя', 'Хлеб', 2, 389], ['Ваня', 'Кефир', 6, 771], ['Иван Никифорович', 'Простокваша', 5, 33], ['Элизабет', 'Чай', 3, 103], ['Элизабет', 'Кефир', 2, 91]], columns=['Покупатель', 'Товар', 'Количество', 'Цена'], index=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) == ({'Ваня': 6458, 'Петя': 12604, 'Иван Никифорович': 6913, 'Иван Иванович': 7133, 'Элизабет': 6951}, {'Чай': 8232, 'Колбаса': 4434, 'Журнал "Мурзилка"': 7648, 'Хлеб': 2610, 'Простокваша': 11548, 'Молоко': 779, 'Кефир': 4808})

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

В некотором царстве, некотором государстве, раз в четыре года на земском соборе проходят выборы царя. Царство имеет федеративную структуру и состоит из земель. Каждая земля имеет право направить на съезд определённое число делегатов, чьи голоса и определят президента… ой, то есть царя. У разных земель разное число делегатов. По традиции, каждая земля на своей территории проводит выборы самостоятельно, после чего подводится их итог и определяется победитель в данной земле. Делегаты, которых отправляют на собор, обязаны проголосовать за того кандидата, который набрал в их земле большинство голосов («победитель получает всё»). Царём становится тот кандидат, который набрал большинство голосов делегатов. Разные земли имеют разное число делегатов.

Требуется написать функцию winner_votes(results), возвращающую двухэлементный кортеж: первый элемент — победитель, второй — число голосов делегатов, которые он набрал. Функция принимает на вход таблицу (pd.DataFrame), выглядящую примерно так:

          state  electors  Arya Stark  Tyrion Lannister  Deineris Targarien
0    Winterfell         3         0.6               0.3                 0.1
1      Riverrun         5         0.3               0.2                 0.5
2  Vaes Dothrak         2         0.2               0.3                 0.5

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

Например, для приведенной выше таблицы победителем в Winterfell будет Arya Stark, в остальных двух землях — Deineris Targarien. Арья наберёт 3 голоса делегатов, Дейнерис — 7 голосов. Победителем будет Дейнерис. Функция должна вернуть ("Deineris Targarien", 7).

Как обычно, запрещено использовать циклы.

В случае, если два кандидата набирают равное число голосов, то побеждает тот, кто идёт первым по алфавиту.

Подсказка. Вам пригодится метод .idxmax(). И ещё .sort_index().

In [ ]:
# YOUR CODE HERE
In [ ]:
import random
import numpy as np

df = pd.DataFrame([['Winterfell', 3, 0.6, 0.3, 0.1],
 ['Riverrun', 5, 0.3, 0.2, 0.5],
 ['Vaes Dothrak', 2, 0.2, 0.3, 0.5]], columns=['state', 'electors', 
                                               'Arya Stark', 
                                               'Tyrion Lannister', 
                                               'Deineris Targarien'])


assert winner_votes(df) == ("Deineris Targarien", 7)

def mktable(seed):
    states = ['Florida', 'Connecticut', 'Georgia', 'Texas', 'Vermont', 
              'New Mexico', 'Illinois', 'Kentucky', 'Iowa', 'Alaska', 
              'New York', 'Massachusetts', 'Arkansas', 'Missouri', 'Kansas', 
              'Idaho', 'Wisconsin', 'Mississippi', 'Washington', 'Oklahoma', 
              'California', 'South Carolina', 'Hawaii', 'Maryland', 'Arizona', 
              'Montana', 'Ohio', 'Oregon', 'Rhode Island', 'South Dakota', 
              'Alabama', 'North Dakota', 'Virginia', 'New Jersey', 'Wyoming', 
              'Maine', 'D.C.', 'Tennessee', 'Pennsylvania', 'Nebraska', 
              'Delaware', 'Michigan', 'New Hampshire', 'Indiana', 
              'North Carolina', 'Colorado', 'West Virginia', 'Utah', 
              'Minnesota', 'Louisiana', 'Nevada']
    candidates = ['Clinton', 'Trump', 'Johnson', 'Stein', 'Castle', 
                  'McMullin']
    random.seed(seed)
    np.random.seed(seed)
    states_ = random.sample(states, random.randrange(1, len(states)))
    candidates_ = random.sample(candidates, 
                                random.randrange(1, len(candidates)))
    results = np.random.uniform(size=(len(candidates_), len(states_)))
    results = (results / results.sum(axis=0)).T
    electors = np.random.randint(1, 20, size=len(states_))
    return pd.concat([pd.Series(states_, name='state'),
                      pd.Series(electors, name='electors'), 
                      pd.DataFrame(results,
                                   columns=candidates_)], axis=1)

for i, result in enumerate([('Stein', 107), ('Clinton', 48), ('Castle', 18), 
                            ('Trump', 63), ('Johnson', 88), ('Johnson', 196), 
                            ('Johnson', 88), ('McMullin', 62), ('Trump', 51), 
                            ('Johnson', 295), ('Johnson', 79), ('Stein', 84), 
                            ('Clinton', 285), ('Trump', 84), ('Stein', 55), 
                            ('McMullin', 59), ('Clinton', 110), 
                            ('McMullin', 162), ('Johnson', 45), 
                            ('Clinton', 165), ('Castle', 156), 
                            ('Johnson', 47), ('Trump', 83), ('Trump', 287), 
                            ('Stein', 268), ('Castle', 275), ('Clinton', 235), 
                            ('Trump', 434), ('Stein', 24), ('Castle', 135), 
                            ('Trump', 99), ('Stein', 17), ('Clinton', 23), 
                            ('Clinton', 133), ('Trump', 159), ('Trump', 88), 
                            ('McMullin', 77), ('Johnson', 436), 
                            ('Stein', 211), ('Johnson', 158), ('Trump', 114), 
                            ('Castle', 259), ('Johnson', 431), 
                            ('Johnson', 19), ('Castle', 304), ('Trump', 118), 
                            ('Castle', 18), ('McMullin', 141), 
                            ('Clinton', 197), ('McMullin', 14), 
                            ('Trump', 259), ('Castle', 87), ('Trump', 171), 
                            ('Castle', 120), ('Johnson', 48), ('Stein', 54), 
                            ('Trump', 382), ('Trump', 30), ('Trump', 134), 
                            ('McMullin', 77), ('Trump', 72), ('Stein', 114), 
                            ('Clinton', 152), ('McMullin', 105), 
                            ('Clinton', 279), ('Trump', 241), ('Castle', 23), 
                            ('McMullin', 27), ('Stein', 148), ('Trump', 420), 
                            ('Castle', 42), ('Clinton', 114), ('Stein', 23), 
                            ('Castle', 68), ('Clinton', 328), 
                            ('Johnson', 149), ('Trump', 97), ('Trump', 91), 
                            ('Trump', 51), ('McMullin', 45), ('Johnson', 56), 
                            ('McMullin', 167), ('Stein', 57), ('Castle', 111), 
                            ('Stein', 477), ('McMullin', 82), 
                            ('Clinton', 173), ('Clinton', 77), ('Trump', 273), 
                            ('Trump', 43), ('Trump', 68), ('Stein', 34), 
                            ('McMullin', 185), ('Clinton', 293), 
                            ('Johnson', 138), ('Stein', 261), 
                            ('Johnson', 131), ('Johnson', 58), 
                            ('Trump', 85), ('McMullin', 283)]):
    assert winner_votes(mktable(i)) == result, "Something wrong "\
                                               "with table\n" + \
                                            str(mktable(i)) + "\nExpected " +\
                                            str(result) + "\nObtained: " + \
                                            str(winner_votes(mktable(i)))

Advanced pandas

В задачах этого раздела нельзя использовать циклы. Задачи сложные, и в принципе их можно пропустить, но даже частичное продвижение позволит вам лучше разобраться в том, как работает pandas. Так что мы советуем по крайней мере попробовать.

Задача 22 (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

Подсказка. Вам скорее всего понадобятся методы 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'])
import numpy as np
from pandas.testing import assert_frame_equal

assert_frame_equal(totals(purchases, goods, discounts),
    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}}), check_like=True)

Задача 23 (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()

Веб-скреппинг

В задачах этого раздела можно и нужно использовать библиотеки requests и bs4. Здесь также нет ограничения на использование циклов и if'ов.

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

Написать функцию any_news_about_harry(url), принимающую на вход адрес веб-страницы url, загружающую эту веб-страницу и проверяющую, встречается ли в ней слово Harry (с большой буквы). Функция должна возвращать True, если встречается, и False в противном случае. Также функция должна возвращать False, если страницу не удалось открыть (например, была получена ошибка 404 Not Found.)

Подсказка. Чтобы загрузить страницу, нужно использовать библиотеку requests:

import requests
r = requests.get(url)

Содержимое страницы затем окажется в r.text. Проверить, что запрос увенчался успехом, можно так:

if r:
    # увенчался успехом

В этой задаче вам не нужно использовать BeautifulSoup. Чтобы проверить наличие подстроки в строке, можно использовать in. (Например, 'ell' in "Hello" вернёт True.)

In [ ]:
# YOUR CODE HERE
In [ ]:
assert any_news_about_harry("https://en.wikipedia.org/w/index.php?title=J._K._Rowling&oldid=694008857")
assert any_news_about_harry("https://en.wikipedia.org/w/index.php?title=Star_Wars&oldid=694701430")
assert not any_news_about_harry("https://en.wikipedia.org/w/index.php?title=Darth_Vader&oldid=694617684")
In [ ]:
assert not any_news_about_harry("http://math-info.hse.ru/there_is_no_Harry_here")
# assert not any_news_about_harry("http://nonexistent.domain.ji8bohVe/")

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

Написать функцию get_strong(html), принимающую на вход html-страницу в виде длинной строки, записанной в переменную html, и возвращающую строчку, содержащуюся в первом теге strong.

Примеры см. в тестах.

Подсказка. Вы можете создать объект BeautifulSoup, передав ему строку с html в качестве параметра. Например:

from bs4 import BeautifulSoup
page = BeautifulSoup("<html><body><p>Hello</p></body></html>", "html.parser")
print(page.p)
In [ ]:
# YOUR CODE HERE
In [ ]:
assert get_strong("<html><body><p>Hello, <strong>World</strong>!") == "World"
html = """<html>
    <body>
        <p>
            Hello,
            <strong>
                World
            </strong>
        </p>
    </body>
</html>"""
assert get_strong(html).strip() == "World"
assert get_strong("<html><body><p>tag &lt;strong&gt; is used in HTML\n to make letters <strong>stronger</strong>") == "stronger"
assert get_strong("<html><body><strong>One\nTwo</strong></body></html>") == "One\nTwo"

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

Для вставки картинок в HTML используется тег <img>, содержащий параметр src — адрес файла с картинкой. Например, <img src="https://upload.wikimedia.org/wikipedia/commons/b/bd/Struthio_camelus_portrait_Whipsnade_Zoo.jpg"/>. Написать функцию all_images_src(html), принимающую на вход длинную строчку с HTML-документом, а возвращающую список адресов всех картинок, встречающихся в этом документе (в том порядке, в котором они встречаются в документе).

Подсказка. Для обращения к атрибутам тега нужно использовать квадратные скобки, как если бы тег был словарём.

In [ ]:
# YOUR CODE HERE
In [ ]:
assert all_images_src('<html><body><img src="https://upload.wikimedia.org/wikipedia/commons/b/bd/Struthio_camelus_portrait_Whipsnade_Zoo.jpg"/>') == ["https://upload.wikimedia.org/wikipedia/commons/b/bd/Struthio_camelus_portrait_Whipsnade_Zoo.jpg"]
assert all_images_src( ('<html><body><IMG src="test.jpg">\n'
                        '<p>Some text\n'
                        '<img SRC=\'well.png\'>\n'
                        '</p></body></html>') ) == ["test.jpg", "well.png"]
assert all_images_src('<html><body><p><a href="link.html">'
                      '<img alt="Just a test image" src="this is a test.jpg"><ul>' + "\n"
                      .join("<li><img src='img%04i.png'></li>" % i for i in range(1000)) + 
                      "</ul></p></body></html>"
                     ) == ['this is a test.jpg'] + ['img%04i.png' % i for i in range(1000)]

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

Написать функцию get_all_headings(url), принимающую на вход адрес страницы в Википедии и возвращающую список, состоящий из названий разделов статьи (в порядке появления в статье). Если такой страницы не существует, функция должна вернуть список, состоящей из строки "Not found".

Подсказка. С помощью функции вашего браузера inspect element или аналогичной, исследуйте, в каких тегах и с какими классами находятся искомые заголовки. Не во всех страницах есть содержание! Например, ваш код должен корректно обрабатывать эту страницу.

In [ ]:
# YOUR CODE HERE
In [ ]:
from urllib.parse import urlencode
entrypoint = "https://ru.wikipedia.org/w/index.php?"
def mkurl(title, oldid):
    return entrypoint+urlencode(dict(title=title, oldid=oldid))
assert get_all_headings(mkurl("Северовирджинская кампания",75043192)) == ['Предыстория',
                                                                          'Силы сторон',
                                                                          'Сражения',
                                                                          'Последствия',
                                                                          'Примечания',
                                                                          'Литература',
                                                                          'Ссылки']

assert get_all_headings(mkurl('User:Ilya_Voyager/sandbox/h2test',"75055744")) == ['Заголовок', 'Ещё один заголовок', 'Третий заголовок']
assert get_all_headings(mkurl('User:Ilya_Voyager/This Page Will Never Exist', "")) == ["Not found"]
del urlencode, mkurl

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

Написать функцию city_tz(name), принимающую на вход название города и возвращающую строку, содержащую часовой пояс, действующий в этом городе (например, 'UTC+3'), согласно данным русской Википедии. Если такого города Википедия не знает, или если у города не указан часовой пояс None.

Предполагается, что вы будете решать эту задачу, обрабатывая HTML-код веб-страницы, а не исходный код статей, и не будете пользоваться сторонними библиотеками (кроме urllib, requests, BeautifulSoup).

Подсказка. Как сформировать адрес страницы, зная название статьи, можно подсмотреть в тесте к задаче 6. Впрочем, можно передать адрес страницы напрямую в requests.get, см. официальную документацию.

In [ ]:
# YOUR CODE HERE
In [ ]:
res = [('Абакан', 'UTC+7'), 
       ('Анадырь', 'UTC+12'), 
       ('Киров (Кировская область)', 'UTC+03:00'), 
       ('Южно-Сахалинск', 'UTC+11'), 
       ('Усть-Каменоустюгск', None)]
for city, site in res:
    assert city_tz(city) == site, (site, city_tz(city))

Бонусная часть: API

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

Написать функцию diff_lat(place1, place2), которая бы с помощью геокодера Яндекса находила координаты двух объектов, заданных строками place1 и place2, и возвращала бы число с плавающей точкой, являющееся ответом на вопрос: на сколько градусов place2 севернее, по сравнению с place1?

In [ ]:
# YOUR CODE HERE
In [ ]:
assert abs(diff_lat("Москва", "Апатиты") - 11.81) < 0.1
assert abs(diff_lat("Шаболовка, 26", "Кочновский, 3")-0.086) < 0.001
assert abs(diff_lat("Краснодар", "Петропавловск-Камчатский") - 8) < 0.1
assert abs(diff_lat("Геленджик", "Саратов") - 7) < 0.1
assert abs(diff_lat("Саратов", "Геленджик") + 7) < 0.1

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

С помощью API Google Books можно получать информацию о различных книгах. Например, вот так можно получить данные по книге по её ISBN: https://www.googleapis.com/books/v1/volumes?q=isbn:9785699648146. Напишите функцию book_table(isbns), принимающую на вход список ISBN'ов и возвращающую таблицу pandas, содержащую заглавие, авторов, язык и число страниц. Названия колонок должны соответствовать названиям полей в ответе API. Если авторов несколько, они должны быть разделены запятой и пробелом. Пример см. в тесте.

In [ ]:
# YOUR CODE HERE
In [ ]:
obtained = book_table(['9781292153964', '9780262035613', '9785457499850'])
expected = pd.DataFrame({'authors': {0: 'Stuart Russell, Peter Norvig',
  1: 'Ian Goodfellow, Yoshua Bengio, Aaron Courville',
  2: 'Рэй Брэдбери'},
 'language': {0: 'en', 1: 'en', 2: 'ru'},
 'pageCount': {0: 1152, 1: 800, 2: 499},
 'title': {0: 'Artificial Intelligence',
  1: 'Deep Learning',
  2: 'Вино из одуванчиков'}})
assert obtained.to_dict() == expected.to_dict()
In [ ]: