Программирование

Авторы задания: И. В. Щуров, А. Щенников.

Данный notebook является набором задач по курсу «Программирование» (Магистерская программа «Журналистика данных», НИУ ВШЭ, 2018-19).

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

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

Максимум за ДЗ можно набрать 10 баллов.

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

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

Напишите функцию orders(path). На вход функция получает путь к директории, содержащей какое-то количество документов в формате txt в кодировке utf-8. Она должна найти среди них все документы, текст которых начинается со стороки ПРИКАЗ, и вернуть в виде списка только имена этих документов (без пути к ним) в формате имя.txt.

Подсказка. Если при открытии файла с помощью open(filename) вы передаёте ей просто имя файла (например, open("order.txt")), то она пытается открыть файл с таким именем в текущей папке. Чтобы открыть файл, лежащий в другой папке, нужно в filename указать его путь. Например, путь к файлу order.txt, лежащему в папке my_dir, выглядит так: my_dir\order.txt (под Windows) или my_dir/order.txt — под Mac или Linux. При этом в Python в строках обратный слэш \ имеет специалный смысл и должен вводиться как \\, то "my_dir\\order.txt". Если path — путь к папке, а filename — имя файла, который лежит в этой папке, то вы можете создать полный путь к этому файлу с помощью функции os.path.join(path, filename) (но сначала надо импортировать os с помощью import os).

Ещё подсказка. Функция os.listdir(path) возвращает список всех файлов и папок, лежащих в папке с данным путём. Например, os.listdir(".") вернёт список всех файлов и папок, лежащих в текущей папке.

In [ ]:
# YOUR CODE HERE
In [ ]:
try:
    import os
    from tempfile import NamedTemporaryFile, mkdtemp
    import random

    temp_dir = mkdtemp()
    random.seed(123)
    for _ in range(5):
        answer = set()
        for i in range(5):
            order = random.choice([True, False])
            rand_title = {True: "ПРИКАЗ\n\ntext", 
                          False: random.choice(["ЗАКОНОПРОЕКТ\n\ntext",
                                                "ДОКУМЕНТ\n\nПРИКАЗ", 
                                                "ПРОЕКТ ПРИКАЗА\n\ntext"])}[order]
            f = NamedTemporaryFile(dir=temp_dir, 
                                   delete=False,
                                   mode="w",
                                   suffix=".txt",
                                   encoding="utf-8")
            f.write(rand_title)
            if order:
                answer.add(os.path.basename(f.name))
            f.close()

        try:
            assert type(orders(temp_dir)) == list
            assert answer == set(orders(temp_dir))
        finally:
            for file in os.listdir(temp_dir): 
                os.remove(os.path.join(temp_dir, file))

    print("Отлично!")
finally:
    os.rmdir(temp_dir)

    del random, os, NamedTemporaryFile, mkdtemp

Загрузка данных для задачи 2

Запустите эту ячейку, чтобы загрузить данные для задачи 2. Достаточно запустить её один раз.

In [ ]:
import requests
r = requests.get(
  "http://math-info.hse.ru/f/2018-19/dj-prog/students_data.csv")
with open("students_data.csv", "wb") as f:
    f.write(r.content)

Задача 2

У вас есть csv-таблица students_data.csv (разделитель — табуляция, "\t", кодировка — utf-8) в которой содержаться данные о трёх тысячах студентов. Таблица выглядит следущим образом:

full_name profession university year_of_study gpa
Иван Иванов Дата-журналист ВШЭ 2 9.9

Задача состоит в том чтобы открыть файл, найти в нём всех студентов ВШЭ, которые учатся на старших курсах (3, 4) журналистики данных и имеют GPA не ниже 7,5. Полученный список необходимо отсортировать в порядке убывания года обучения и GPA. Ответ нужно записать в csv-файл. Файл с ответом должен содержать столбцы с именем, годом обучения и GPA:

full_name year_of_study gpa
Пётр Петров 4 9.7
Ирина Семёнова 4 8.9
Татьяна Смирнова 3 9.9
Глеб Феклин 3 7.5

Задача разбита на три этапа.

Этап 1: считываем файл (1 балл)

Напишите функцию open_csv(path), которая получает на вход путь к csv-таблице, считывает все её строки кроме названия колонок в список списков и возвращает его. Учитывайте, что разделителем является табуляция, а не запятая.

Комментарий. Обычно для работы с csv-файлами лучше использовать специализированную библиотеку csv, а не разделять строки по разделителем вручную. Эта библиотека правильно обработает ситуации, когда, например, разделитель оказывается частью записи. Но в нашем случае все csv-файлы очень простые, поэтому мы можем обрабатывать их вручную.

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

s = [['h1', 'h2', 'h4'],
     ['ein', 'zwei', 'drei'],
     ['vier', 'fuenf', 'sechs'],
     ['seben', 'acht', 'neun']]
f = NamedTemporaryFile(dir='.', delete=False, mode='w', suffix='.csv')
name = f.name
ss = ["\t".join(i) for i in s]
for i in ss:
    print(i, file=f.file)
f.file.close()
try:
    assert open_csv(name) == s[1:]
finally:
    os.remove(f.name)
    
assert len(open_csv("students_data.csv")) == 3000
for raw in open_csv("students_data.csv"):
    assert len(raw) == 5, "Оюратите внимание на требование к разделителю"

del NamedTemporaryFile, os

Этап 2: фильтруем и сортируем (2 балла)

Напишите функцию filter_data(data). Функция получает на вход наши данные в виде списка списков и фильтрует его таким образом, чтобы в нем остались только студенты с профессией «Дата-журналист», обучающиеся в ВШЭ на старших курсах (3, 4), с GPA не ниже 7,5. Она должна вернуть отфильтрованный список списков, отсортированый в порядке убывания года обучения и GPA (сначала по году обучения, а среди тех, у кого год обучения одинаковый — по GPA). Будьте внимательны с типами объектов, содержащихся в списках.

In [ ]:
# YOUR CODE HERE
In [ ]:
import random

assert len(filter_data(open_csv("students_data.csv"))) == 12
assert [float(row[-1]) 
        for row in filter_data(open_csv("students_data.csv"))
       ] == [9.1,8.9,8.6,
             8.6,8.4,8.4,
             9.6,8.9,8.9,
             8.5,8.4,8.0]

l = [["Пётр Петров", "Дата-журналист", "ВШЭ", "4", "9.7"],
     ["Ирина Семёнова", "Дата-журналист", "ВШЭ", "4", "8.9"],
     ["Татьяна Смирнова", "Дата-журналист", "ВШЭ", "3", "9.9"],
     ["Глеб Феклин", "Дата-журналист", "ВШЭ", "3", "7.5"],
     ["Виктор Аристов", "Программист", "МГУ", "4", "8.4"],
     ["Давид Попов", "ИТ-генетик", "Синергия", "3", "7.7"],
     ["Мария Должанская", "Дата-журналист", "ВШЭ", "4", "6.9"]]

lw = l.copy()
random.seed(123)
random.shuffle(lw)

assert filter_data(lw) == l[:4]

del random

Этап 3: записываем результат

Напишите функцию write_answer(filtered_data, outfilename). Функция принимает на вход отфильтрованные и отсортированные списки списков, и записывает их в таблицу outfilename с табуляцией "\t" в качестве разделителя. В таблице с ответом должны быть только колонки full_name, year_of_study, gpa. Первая строка должна содержать в себе названия колонок.

(У этой задачи нет теста, она будет проверена тестом к следующему этапу.)

In [ ]:
# YOUR CODE HERE

Этап 4: собираем всё вместе (2 балла)

Напишите функцию solve_students_data_problem(), считывающую файл students_data.csv и записывающую ответ в файл answer_students_data.csv. Вам необходимо использовать ранее написанные функции. Эта функция должна быть очень простой, не больше 4 строк.

In [ ]:
# YOUR CODE HERE
In [ ]:
import os
solve_students_data_problem()
assert os.path.isfile("answer_students_data.csv"), "Файл не создан, имеет неверное имя или находится не в той директории"
with open("answer_students_data.csv", encoding='utf-8') as f:
    header = f.readline()
    rows = f.readlines()
assert "\t" in header, "Задан неверный разделитель ячеек"
assert "full_name\tyear_of_study\tgpa" in header, "Нарушен порядок колонок"
assert len(rows) == 12

Загрузка данных для задачи 3

Запустите следующую ячейку, чтобы загрузить данные для задачи 3. Это достаточно сделать один раз.

In [ ]:
import requests
import zipfile
from io import BytesIO


r = requests.get("http://math-info.hse.ru/f/2018-19/dj-prog/hot-5_1980-2010.zip")
zipdata = BytesIO(r.content)
myzipfile = zipfile.ZipFile(zipdata)
myzipfile.extractall()

Задача 3

У вас есть директория hot-5_1980-2009 в которой содержаться csv- и txt-файлы. В таблицах записаны топ-5 песен года по версии Billboard Hot с 1980 по 2009 год. Данные отсортированы по исполнителям в алфавитном порядке и разбиты на пять таблиц (до буквы e, до буквы j и т.д.). В txt-файлах содержаться тексты всех этих песен (тексты заранее предобработанны — из них удалена вся пунктуация и переносы строк, всё приведено к нижнему регистру). Имена файлов с текстами песен имеют вид «исполнитель-песня.txt» (например, vanessa williams-save the best for last.txt), все буквы маленькие. Задача состоит в том чтобы, используя данные из этих файлов, узнать 50 самых частых слов, которые встречаются в песнях за каждое десятилетие.

Полученный ответ запишите в csv-таблицу в формате

80s 90s 00s
слово 1 слово 1 слово 1
... ... ...
слово 50 слово 50 слово 50

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

Для выполнения задачи её можно разбить на следующие этапы.

Этап 1 (1 балл)

Напишите функцию csv_files(path), которая получает путь к папке с файлами, находит в ней имена всех файлов с расширением csv и возвращает их список.

Подсказка. У строк есть метод .endswith(), проверяющий, что строка заканчивается на данную подстроку. Например:

s = "Hello!"
print(s.endswith("!")) # True
print(s.endswith(".")) # False
print(s.endswith("lo!")) # True
print(s.endswith("!!!")) # False

Подсказка. А ещё можно использовать glob.glob.

In [ ]:
# YOUR CODE HERE
In [ ]:
import random
from tempfile import NamedTemporaryFile, mkdtemp

temp_dir = mkdtemp()
answer = set()

for i in range(50):
    rand_suffix = random.choice([".csv", ".txt"])
    f = NamedTemporaryFile(dir=temp_dir, 
                           delete=False, 
                           suffix=rand_suffix)
    if rand_suffix == ".csv":
        answer.add(os.path.basename(f.name))
    f.close()
    
try:
    assert set(csv_files(temp_dir)) == answer

finally:
    for file in os.listdir(temp_dir): 
        os.remove(os.path.join(temp_dir, file))
    os.rmdir(temp_dir)
    
assert set(csv_files("hot-5_1980-2010")) == {'a-e.csv', 'k-o.csv',
                                             'f-j.csv', 'p-t.csv', 
                                             'u-z0-9.csv'}

del random, NamedTemporaryFile, mkdtemp

Этап 2 (2 балла)

Напишите функцию songs_per_decade(path, csvfiles), которая получает путь к папке с файлами path и список имён csv-файлов с таблицами csvfiles, читает все таблицы и возвращает словарь, в котором ключами являются декады (строки '90s', '80s', '00s'), а значениями списки названий песен в формате "исполнитель-песня". К декаде 90s относятся годы с 1990 по 1999 включительно, а год 2000 уже относится к декаде 00s.

Возможный вид результата:

{'90s': ['janet jackson-thats the way love goes', 'jewel-foolish games  you were meant for me'],
 '80s': ['foreigner-i want to know what love is', 'george harrison-got my mind set on you',
         'george michael-faith'],
 '00s': ['faith hill-breathe', 'fergie-big girls dont cry', 'flo rida featuring tpain-low']}

Подсказка. Для начала заведите словарь с ключами "90s", "80s", "00s" и значениями — пустыми списками. Затем вам нужно перебирать файлы из списка csvfiles. Каждый файл нужно считывать по строкам, каждую строку делить по запятым, чтобы получить отдельно имя исполнителя, название песни, год и место в рейтинге. Дальше нужно проверить, какой декаде соответствует год, и добавить в соответствующий список строку вида "имя исполнителя—название песни".

In [ ]:
# YOUR CODE HERE
In [ ]:
import re
import random


def valid_format(r_n):
    if re.search(r'\w+-\w+',r_n):
        return True

    
assert set(songs_per_decade("hot-5_1980-2010", ["f-j.csv"]).keys()) == {'80s', '90s', '00s'}
assert len(songs_per_decade("hot-5_1980-2010", ["a-e.csv"])['90s']) == 16
assert len(songs_per_decade("hot-5_1980-2010", ["a-e.csv", 'p-t.csv'])['80s']) == 23

test_name = songs_per_decade("hot-5_1980-2010", ['u-z0-9.csv'])
rand_key = random.choice(list(test_name.keys()))
rand_name = random.choice(test_name[rand_key])

assert valid_format(rand_name) == True, "Формат названия должен быть 'имя исполнителя—название песни', дефис без пробелов"

assert (songs_per_decade("hot-5_1980-2010", 
                         ['k-o.csv'])["80s"][8]).split('-') == (['michael jackson',
                                                                 'billie jean']), "Формат названия должен быть 'имя исполнителя—название песни'"

del re, random, valid_format

Этап 3 (2 балла)

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

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

In [ ]:
# YOUR CODE HERE
In [ ]:
assert len((texts_per_decade("hot-5_1980-2010/", songs_per_decade("hot-5_1980-2010/", ["f-j.csv"]))['90s']).split()) == 755
assert len((texts_per_decade("hot-5_1980-2010/", songs_per_decade("hot-5_1980-2010/", ["a-e.csv"]))['00s']).split()) == 3318
assert set(texts_per_decade("hot-5_1980-2010", songs_per_decade("hot-5_1980-2010/", ['u-z0-9.csv'])).keys()) == {'80s', '90s', '00s'}

Этап 4 (3 балла)

Напишите функцию words_counter(decade_to_text), которая получает на вход словарь, возвращаемый функцией texts_per_decade, подсчитывает 50 самых частых слов в текстах и возвращает словарь, у которого ключами являются декады, а значениями — списки из 50 самых частых слов (в порядке убывания частоты).

Подсказка. Чтобы найти частоты слов можно воспользоваться следующим алгоритмом. Завести пустой словарь freqs. Затем для каждого слова проверить, есть ли в freqs запись с ключом, равным этому слову. Если нет — завести такую запись со значением 1. Если есть — увеличить значение в этой записи на 1. Другой подход состоит в том, чтобы использовать collections.Counter — но если вы захотите использовать его, то вам придётся самостоятельно по документации разобраться в том, как им пользоваться. После получения словаря с частотами, отсортируйте его ключи по значениям. Проще всего это сделать, превратив словарь в список кортежей ключ-значение с помощью list(freqs.items()).

In [ ]:
# YOUR CODE HERE
In [ ]:
assert len(words_counter(texts_per_decade("hot-5_1980-2010/", songs_per_decade("hot-5_1980-2010/", ["a-e.csv"])))['00s']) == 50
assert words_counter(texts_per_decade("hot-5_1980-2010/", songs_per_decade("hot-5_1980-2010/", ["f-j.csv"])))['90s'][-1] == 'youre'
assert words_counter(texts_per_decade("hot-5_1980-2010/", songs_per_decade("hot-5_1980-2010/", ['u-z0-9.csv'])))['80s'][-1] == 'jitterbug'

Этап 5 (2 балла)

Напишите функцию save_answer_decades(decade_to_50words, outfilename), принимающую на вход словарь decade_to_50words, который вернула предыдущая функция, и записывающая его в файл outfilename (в текущей папке) в виде csv-файла. В первой строке файла должны быть написаны декады (80s,90s,00s), во второй — самые популярные слова за каждую декаду, в третьей — вторые по популярности и т.д.

In [ ]:
# YOUR CODE HERE

Итог

Если все функции написаны правильно, следующий код должен записать правильный ответ в файл answer2.csv.

In [ ]:
path = "hot-5_1980-2010"
tables = csv_files(path)
decade_to_songs = songs_per_decade(path, tables)
decade_to_texts = texts_per_decade(path, decade_to_songs)
decade_to_50words = words_counter(decade_to_texts)
save_answer_decades(decade_to_50words, "answer2.csv")
In [ ]:
assert os.path.isfile("answer2.csv") == True, "Файл не создан, имеет неверное имя или находится не в той директории"

with open("answer2.csv") as f:
    header = f.readline()
    raws = f.readlines()
    last_raw = raws[-1]
assert '80s,90s,00s' in header, "Нарушен порядок колонок"
assert len(raws) == 50
assert 'want,now,youre' in last_raw