Система рекомендаций фильмов с GUI на Python

Автор оригинала: JC Smidt
  • Перевод
  • Tutorial

«Без опыта я никому не нужен! Где взять опыт?» — часто думают люди, осваивающие новую для себя сферу или изучающие новый язык программирования. Решение есть — делать пет-проекты. Представленный под катом проект системы рекомендации фильмов не претендует на сложность и точность аналогичных систем от энтерпрайз-контор, но может стать практическим стартом для новичка, которому интересны системы рекомендации в целом. Этот пост также подойдет для демонстрации как использовать Python-библиотеку EasyGUI на практике.

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



Начало работы


В этой статье я расскажу, как создать базовую систему рекомендаций фильмов со встроенным графическим пользовательским интерфейсом. Прежде всего нам нужны данные. Чтобы получить хорошее представление о том, насколько хорошо система рекомендаций работает на самом деле, нам понадобится довольно большой набор данных. Используем MovieLens на 25M, который вы можете скачать здесь. Набор данных состоит из шести файлов .csv и файла readme, объясняющих набор данных. Не стесняйтесь взглянуть на него, если хотите. Мы будем использовать только эти три файла:

movies.csv; ratings.csv; tags.csv

Также потребуется несколько библиотек Python:

  1. NumPy
  2. Pandas
  3. Progress (pip install progress)
  4. Fuzzywuzzy (pip install fuzzywuzzy & pip install python-Levenshtein)
  5. EasyGUI

Вероятно, все они могут быть установлены через pip. Точные команды будут зависеть от ОС. Кроме того, должна работать любая IDE Python (см. материал по ним вот тут). Я пользуюсь Geany, легкой IDE для Raspbian. Посмотрим на набор данных:


movies.csv

Выше показан файл movies.csv с тремя столбцами данных, а именно: movieId, title и genres — идентификатор фильма, название и жанр. Все очень удобно и просто. Будем работать со всеми тремя.

Ниже мы видим tags.csv. Здесь используются только столбцы movieId и tag, связывающие тег со столбцом movieId, которые также есть в файлах movies.csv и rating.csv.


tags.csv

И последний, но не менее важный файл: rating.csv. От этого парня мы возьмем столбцы movieId и rating.


ratings.csv

Отлично, теперь давайте запустим IDE и начнем. Импортируем библиотеки, как показано ниже. Pandas и NumPy хорошо известны в области Data Science. Fuzzywuzzy, EasyGUI и библиотека Progress менее известны^ судя по тому, что мне удалось собрать, однако вы, возможно, знакомы с ними. Я добавлю в код много комментариев, чтобы все было понятно. Посмотрите:



Программа в основном состоит из набора функций, кроме начальной загрузки набора данных и настройки дисплея для gui, в который будет подаваться массив NumPy. Без настройки отображения в строке 12, когда список рекомендуемых фильмов из системы слишком длинный, мы получим урезанное и бесполезное изображение.

Есть разные способы сортировки значений на дисплее, например, цикл по массиву и добавление каждой строки к переменной, инициализированной в пустой список, однако этот метод мы будем использовать именно в строке 12.

Строки 16, 17, 33 и 35 — это, в основном, индикатор прогресса из библиотеки Progress. Цикл выполняется только один раз. Загрузка набора данных в мою систему занимает около 30 секунд, поэтому мы используем индикатор прогресса, чтобы показать, что набор данных загружается после запуска программы, как показано ниже. После этого нам не придется загружать его снова во время навигации по графическому интерфейсу.



Загрузка набора данных и главное меню графического интерфейса пользователя.

Внутри цикла, набор данных загружается во фреймы Pandas и данные немного изменяются для удобства. Начнем со слияния фильма и рейтингового фрейма в один фрейм. В столбце рейтинга есть несколько значений NaN, и мы будем иметь дело с этим, заполняя его средним рейтингом, вычисленным во всей колонке рейтингов. Поскольку некоторые фильмы имеют оценки с сотен площадок, мы хотим получить средний рейтинг каждого фильма и сгруппировать фильмы по movieId, который связан с названием фильма. Теперь, когда с данными проще работать, пришло время создавать функции.

Первая функция называется which_way(). Эта функция вызывает главное меню графического интерфейса пользователя и предлагает сделать выбор. Ищите фильмы по жанру или по тегу. Пользователь, конечно, тоже может нажать кнопку отмены и вообще не искать фильмы, это полностью его дело.


which_way()

Внутри функции мы определяем параметры для EasyGUI chiocebox, который отображается пользователю. Строка параметра, которую вводит пользователь, возвращается и сохраняется в переменной fieldValues. После условный оператор направляет пользователя к следующей функции или окну на основе выбора.

Если пользователь нажимает отмену, программа завершается. Но если пользователь нажимает поиск фильмов по жанру или поиск фильмов по тегу, то будут вызваны функции genre_entry() или tag_entry() и появится что-то, известное как EasyGUI multenterbox.



Из названия понятно, что это поле ввода может принимать несколько входных значений, когда необходимо. Обе функции genre_entry() и tag_entry() очень похожи, поэтому я объясню только одну из них, но в исходный код включу обе. Давай посмотрим на tag_entry().



После передачи параметров отображения multenterbox вызывается другая функция, которая проверяет ввод данных пользователем. Пользовательский ввод возвращается функцией field_check. Давайте взглянем на нее:



Иногда пользователи оставляют значение в поле пустым, возможно, по ошибке. Чтобы контролировать такое поведение, а также сделать графический интерфейс более надежным, нам нужны такие функции. Я нашел эту конкретную функцию на странице документации EasyGUI. По сути, пока вы вводите пустое поле, вам сообщат, что это поле является обязательным, и вы застрянете в цикле.



Как только пользователь что-то вводит, текст (строка) сохраняется в переменной fieldValues, а код работает со строки 114 в функции tag_entry. Пользовательский ввод из поля EasyGUI multenter возвращается в виде списка. При нажатии кнопки отмены возвращается «Нет». Чтобы использовать этот ввод и в других функциях, нам нужно объявить переменную как глобальную.

Теперь мы нарезаем список пользовательского ввода из multenterbox, и сохраняем как переменную user_input_2. Нас интересует только возвращаемый текст, если пользователь не нажимает кнопку отмены, отсюда и условный оператор:

if fieldValues != None:

Если пользователь был достаточно любезен, чтобы ввести какой-то текст, мы переходим к функции Similarity_test2(), которая в основном содержит базовые аспекты этой системы рекомендаций, в качестве альтернативы пользователь возвращается в главное меню. Similarity_test1() и Similarity_test2() очень похожи, так же как genre_entry и tag_entry. Я буду рассматривать здесь только Similarity_test2(). Давайте посмотрим на это:



Как мы видим, она принимает один параметр, переменнаую user_input_2 из функции tag_entry(). Помните тот фрейм данных, который мы создали в строке 19 из файла tags.csv? Сначала мы хотим собрать все уникальные теги из столбца тегов и сохранить их в переменной.

Существует множество способов применения библиотеки Fuzzywuzzy в зависимости от ваших задач. Мы будем работать так: output = process.extract(query, choices)

Можно передать дополнительный параметр — тип счетчика. Мы просто будем использовать синтаксис по умолчанию. По сути Fuzzywuzzy работает с расстоянием Левенштейна для вычисления различий между последовательностями и возвращает оценку в пределах 100%.

Функция process.extract(query, choices) возвращает список оценок, где каждая строка и ее оценка заключена в скобки, например (строка 95) в качестве элементов списка.

После перебора всего списка тегов и поиска совпадений с переменной user_input_2, мы перебираем список оценок и вырезаем только совпадения выше 90% и сохраняем их в переменной final_2. Мы объявляем его глобальным, чтобы использовать в следующей функции. Если fuzzywuzzy не нашел для нас соответствия, вернется []. Если совпадение по условию не найдено, просим пользователя повторить попытку, вернувшись к функции tag_entry(). В качестве альтернативы, когда у нас есть совпадения более чем на 90%, мы можем использовать их в функции tag(), как показано ниже:



tag()

Теперь, когда у нас есть совпадения более 90%, перебираем их в цикле и просматриваем каждую строку столбца tag фрейма данных df_tags, чтобы увидеть, какие теги соответствуют строкам из Fuzzywuzzy. Теперь сохраняем все совпадения тегов вместе с идентификатором movieId в переменной final_1. Чтобы очистить добавленные данные, мы отрезаем первый элемент и сбрасываем индекс фрейма данных. Теперь можно удалить столбец с именем index и все дубликаты из столбца movieId. Чтобы фильмы с наивысшим рейтингом отображались первыми в порядке убывания, отсортируем фрейм данных и удалим фильмы с рейтингом меньше 2,5/5,0.

Теперь мы можем вставить новую строку во фрейм данных прямо вверху и дублировать имена столбцов над ними. Это делается только для отображения EasyGUI. Элементу codebox не очень нравится фрейм данных pandas, поэтому нужно изменить формат фрейма.

Преобразуем каждый столбец фрейма в список, а затем повторно соберем его с помощью numpy. Да, мы просто убираем скобки и переходим к окну codebox для отображения списка. Это всё! Давайте найдем фильм по тегу: «hacker» и посмотрим, что покажут рекомендации.



Программа работает. Не стесняйтесь экспериментировать с ней и, пожалуйста, дайте мне знать, если я где-то ошибся!

Исходный код проекта (осторожно, под спойлером целая простыня)

# импорт библиотек
from fuzzywuzzy import fuzz 
from fuzzywuzzy import process
from progress.bar import IncrementalBar
from easygui import *
import easygui as gui
import pandas as pd
import numpy as np
import sys
# максимальное увеличение размера массива отображения numpy для отображения easygui 
np.set_printoptions(threshold=sys.maxsize)
# фрейм данных относительно большой, начальная загрузка займет около 30 секунд
# в зависимости от вашего компьютера, поэтому здесь уместна индикация загрузки
progress_bar = IncrementalBar('Loading Movie Database...', max=1)
for i in range(1):
    # чтение файлов csv
    df_tags = pd.read_csv("tags.csv", usecols = [1,2])
    df_movies = pd.read_csv("movies.csv")
    df_ratings = pd.read_csv("ratings.csv", usecols = [1,2])
    
    # объединение столбцов из отдельных фреймов данных в новый фрейм
    df_1 = pd.merge(df_movies ,df_ratings, on='movieId', how='outer')
    # заполнение значений NaN средним рейтингом
    df_1['rating'] = df_1['rating'].fillna(df_1['rating'].mean()) 
    # группирование строк df по среднему рейтингу фильма
    df_1 = pd.DataFrame(df_1.groupby('movieId')['rating'].mean().reset_index().round(1))
    # добавление столбцов title и genres в df
    df_1['title'] = df_movies['title']
    df_1['genres'] = df_movies['genres']
    
    progress_bar.next()
    # заполнение индикатора загрузки при успешной загрузке 
progress_bar.finish()
def which_way():
    '''
    Эта функция, которая выполняется при запуске программы. 
    Работает как перекресток, вы выбираете поиск фильмов по
    тегу или по жанру. По выбору пользователь переходит к следующему окну.
    '''
    # определение параметров easygui choicebox
    msg = "Choose an option:"
    title = "Main Menu"
    choices = ["Search recommended movies by genre:","Search recommended movies by tag:"]
    fieldValues = choicebox(msg,title, choices)
    
    # переменная fieldValues - это пользовательский ввод, который возвращается из графического интерфейса
    # условный оператор, направляющий пользователя к следующему интерфейсу на основе ввода
    if fieldValues == "Search recommended movies by genre:":
        genre_entry()
    
    elif fieldValues == "Search recommended movies by tag:":
        tag_entry()
def field_check(msg, title, fieldNames):
    '''
    Эта функция проверяет отсутствие вводимых пользователем значений в multenterbox
    и возвращает пользовательский ввод как переменную fieldValues.
    
    Параметры:
    
    msg, title и fieldnames графического интерфейса multienterbox
    
    '''
    
    fieldValues = multenterbox(msg, title, fieldNames)
    
    # Цикл с условием, чтобы проверить,
    # что поля ввода не пусты
    while 1:
        if fieldValues is None: break
        errmsg = ""
        for i in range(len(fieldNames)):
            if fieldValues[i].strip() == "":
                errmsg += ('"%s" is a required field.\n\n' % fieldNames[i])
        if errmsg == "":
            break # если пустых полей не найдено, перейти к следующему блоку кода
        # cохранить пользовательский ввода в виде списка в переменной fieldValues
        fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues)
    
    return fieldValues
def tag_entry():
    ''' 
    Эта функция определяет параметры easygui multenterbox и вызывает
    field_check, если пользователь вводил значнеие,
    вызывает тест на подобие; если совпадение не найдено, пользователь возвращается
    в окно ввода
    '''
    
    # определение параметров easygui multenterbox
    msg = "Enter movie tag for example: world war 2 | brad pitt | documentary \nIf tag not found you will be returned to this window"
    title = 'Search by tag'                        
    fieldNames = ["Tag"]
    
    # вызов field_check() для проверки отсутствия пользовательского ввода и
    # сохранения вода как переменной fieldValues
    fieldValues = field_check(msg, title, fieldNames)
    
    # Если пользователь ввел значение, сохраняем его в fieldValues[0]
    if fieldValues != None:
        global user_input_2
        user_input_2 = fieldValues[0]
        
        # здесь мы вызываем функцию, которая в основном проверяет строку
        # на схожесть с другими строками. Когда пользователь нажимает кнопку отмены, он возвращается в главное меню 
        similarity_test2(user_input_2)
    else:
        which_way()
def tag():
    '''
    Эта функция добавляет все совпадающие по тегам фильмы во фрейм данных pandas,
    изменяет фрейм данных для правильного отображения easygui, отбросив некоторые
    столбцы, сбрасывая индекс df, объединяя фреймы и сортируя элементы так,
    чтобы показывались фильмы с рейтингом >= 2.5. Она также преобразует столбцы df в списки
    и приводит их в порядок в массиве numpy для отображения easygui.  
    '''
    
    # добавление тегов найденных фильмов как объекта фрейма
    final_1 = []
    for i in final_2:
        final_1.append(df_tags.loc[df_tags['tag'].isin(i)])
    
    # сброс индекса df, удаление столбца индекса, а также повторяющихся записей
    lst = final_1[0]
    lst = lst.reset_index()
    lst.drop('index', axis=1, inplace=True)
    lst = lst.drop_duplicates(subset='movieId')
# слияние movieId с названиями и жанрами + удаление тега и идентификатора фильма
    df = pd.merge(lst, df_1, on='movieId', how='left')
    df.drop('tag', axis=1, inplace=True)
    df.drop('movieId', axis=1, inplace=True)
# сортировка фильмов по рейтингам, отображение только фильмов с рейтингом выше или равным 2,5
    data = df.sort_values(by='rating', ascending=False)
    data = data[data['rating'] >= 2.5]
    heading = [] # добавление названий столбцов как первой строки фрейма данных для отображения easygui
    heading.insert(0, {'rating': 'Rating', 'title': '----------Title',
     'genres': '----------Genre'})
    data = pd.concat([pd.DataFrame(heading), data], ignore_index=True, sort=True)
    
    # преобразование столбцов фрейма данных в списки
    rating = data['rating'].tolist()
    title = data['title'].tolist()
    genres = data['genres'].tolist()
    
    # составление массива numpy из списков столбцов dataframe для отображения easygui
    data = np.concatenate([np.array(i)[:,None] for i in [rating,title,genres]], axis=1)
    data = str(data).replace('[','').replace(']','')
    
    # отображение фильмов пользователю
    gui.codebox(msg='Movies filtered by tag returned from database:',
    text=(data),title='Movies')
    
    which_way()
def genre_entry():
    ''' 
    Эта функция определяет параметры easygui multenterbox
    и вызывает field_check, если пользователь что-то вводил,
    вызывается тест на подобие. Если совпадение не найдено, пользователь возвращается
    в то же окно  
    '''
    # определение параметров easygui multenterbox
    msg = "Enter movie genre for example: mystery | action comedy | war \nIf genre not found you will be returned to this window"
    title = "Search by genre"
    fieldNames = ["Genre"]
    
    # вызов field_check() для проверки отсутствия пользовательского ввода и
    # сохранения ввода в fieldValues.
    fieldValues = field_check(msg, title, fieldNames)
    
    # Если пользовательский ввод не пуст, сохраняет его в переменной user_input
    if fieldValues != None:
        global user_input
        user_input = fieldValues[0]
        
    # здесь мы вызываем функцию, которая в основном проверяет строку
    # на подобие с другими строками. Если пользователь нажмет кнопку отмена, то он вернется в главное меню 
        similarity_test1(user_input)
    else:
        which_way()
def genre():
    '''
    Эта функция добавляет все соответствующие жанру фильмы во фрейм pandas,
    изменяет фрейм для правильного отображения easygui, отбросив некоторые
    столбцы, сбрасывает индекс df, объединеняет фреймы и сортирует фильмы для отображения
    только фильмов с рейтингом >= 2.5. Она также преобразует столбцы конечного df в списки
    и приводит их в порядок в массиве numpy для отображения easygui.
    '''
    
    # добавление соответствующих жанру фильмов во фрейм.
    final_1 = []
    for i in final:
        final_1.append(df_movies.loc[df_movies['genres'].isin(i)])
    
    # сброс индекса df, удаление индекса столбцов и дубликатов записей
    lst = final_1[0]
    lst = lst.reset_index()
    lst.drop('index', axis=1, inplace=True)
    lst.drop('title', axis=1, inplace=True)
    lst.drop('genres', axis=1, inplace=True)
    lst = lst.drop_duplicates(subset='movieId')
    
    # объединение идентификатора фильма с названием, рейтингом и жанром + удаление индекса, названия и жанра
    df = pd.merge(lst, df_1, on='movieId', how='left')
    
    # сортировка по рейтингу, отображение только фильмов с рейтингом выше или равным 2,5
    data = df.sort_values(by='rating', ascending=False)
    data.drop('movieId', axis=1, inplace=True)
    data = data[data['rating'] >= 2.5]
    heading = [] # add column names as first dataframe row for easygui display
    heading.insert(0, {'rating': 'Rating', 'title': '----------Title',
     'genres': '----------Genre'})
    data = pd.concat([pd.DataFrame(heading), data], ignore_index=True, sort=True)
    
    # преобразование столбцов фрейма данных в списки
    rating = data['rating'].tolist()
    title = data['title'].tolist()
    genres = data['genres'].tolist()
    
    # составление массива numpy из списков столбцов фрейма для отображения easygui
    data = np.concatenate([np.array(i)[:,None] for i in [rating,title,genres]], axis=1)
    data = str(data).replace('[','').replace(']','')
    
    # отображение фильмов пользователю
    gui.codebox(msg='Movies filtered by genre returned from database:',
    text=(data),title='Movies')
    
    which_way()
def similarity_test1(user_input):
    '''
    Эта функция проверяет схожесть строк путем сопоставления пользовательского ввода
    для жанров фильмов, совпадения > 90% сохраняется в переменной, которая
    затем передается функции жанра для сопоставления с базой данных и
    возврата в окно ввода, если совпадение не найдено
    '''
    # сохранение жанров фильмов в качестве тестовой базы и пользовательского ввода для тестирования 
    genre_list = df_movies['genres'].unique()
    query = user_input
    choices = genre_list 
    # here fuzzywuzzy does its magic to test for similarity
    output = process.extract(query, choices)
    
    # сохранение совпадений в переменной и их передача следующей функции
    global final
    final = [i for i in output if i[1] > 90]
    
    # если совпадений > 90%  не найдено, вернуть пользователя в окно жанра
    if final == []:
        genre_entry()
    else:
        genre()
def similarity_test2(user_input_2):
    '''
    Эта функция проверяет схожесть строк путем сопоставления пользовательского ввода
    в теги фильмов, совпадение > 90% сохраняется в переменной, которая
    затем передается в функцию тега для сопоставления базы данных и
    возврата в окно ввода, если совпадение не найдено
    '''
    # сохранение тега фильма в качестве тестовой базы и пользовательского ввода для тестирования
    tag_list = df_tags['tag'].unique()
    query = user_input_2
    choices = tag_list 
    # here fuzzywuzzy does its magic to test for similarity
    output = process.extract(query, choices)
    
    # сохранение возвращенных совпадений в переменной и их передача следующей функции
    global final_2
    final_2 = [i for i in output if i[1] > 90]
    
    #если совпадение> 90% не найдено, возврат в окно ввода
    if final_2 == []:
        tag_entry()
    else:
        tag()
if __name__ == '__main__':
    which_way()



image

Получить востребованную профессию с нуля или Level Up по навыкам и зарплате, можно, пройдя наши онлайн-курсы.



Читать еще


SkillFactory
Школа Computer Science. Скидка 10% по коду HABR

Комментарии 10

    +2
    Думаю что изучение Python следует начинать с PEP8 и базовых вещей типа is not None, хотя != None работает так же, так как None — синглтон, но это не по питонячему.
    Ну и всякие штуки типа if some_list == [] тоже не лучшая идея делать…
    Импорт * тоже не приветствуется, это не только Python касается
    + всякие выражения, генераторы, enumerate, zip и тд. и тп.
    Ну и избежать глобальные переменные
      –1
      Лучше с этого думаете?
        0
        Лучше читаю код, да, и лучше думаю при поддержке
    • НЛО прилетело и опубликовало эту надпись здесь
      0

      можете пожалуйста в двух словах принцип рекомендаций объяснить, почему при запросе хакер выдало вот тот вот список фильмов? пытался осилить по тексту но не смог, спасибо

        +1

        Читайте место про Fuzzywuzzy в тексте, а лучше посмотрите описание на гитхабе — там примеры понятные. Если кратко, то идет определение похожести текста в процентном соотношении. Замашка на систему рекомендаций тут условная.


        Сам код конечно довольно низкого уровня (не путать с простотой). Особенно нейминг функций: то камел-стиль, то эти 1,2,3 в конце названий и прочее.


        Мне кажется, что учебный материал должен быть простым и понятным, но одновременно формировать правильные навыки. Если вы такой материал даёте на курсах — это плохо.

          0

          похожести какого текста в процентном соотношении? там же названия вообще не похожи..

            0
            А, вы про это. Поиск идёт по облаку тегов, обратите внимание.
        +1

        Никогда особо не интересовался gui в python, но тут зацепило, надо бы побаловаться на досуге. Спасибо за статью)

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

        Самое читаемое