DMI Subjects Recommender System (DSRS)

Introduzione

DMI Subjects Recommender System (DSRS) è un bot Telegram che permette di raccomandare agli studenti del corso di laurea in Informatica, presso l'università degli studi di Catania, quali sono le materie del terzo anno che potrebbero essere di loro interesse sfruttando il sistema di raccomandazione Content Based.

Content-Based Recommendations

Consiglia una materia $s_j$ allo studente $x_i$ simile alle materie precedenti valutate altamente da $x_i$.

La pipeline principale è la seguente:

  • Partiamo da uno studente a cui piacciono alcune materie. Ciò è necessario per creare una matrice di utilità.
  • Ad ogni materia viene assegnato un profilo, che ne specifica le proprietà principali.
  • A questo punto sappiamo quali sono le proprietà gradite dallo studente e possiamo costruire un profilo studente.
  • Il passo successivo è quello di "abbinare" il profilo dello studente con nuove materie che non ha valutato. Se queste materie condividono caratteristiche simili con il profilo studente, allora possiamo dedurre che lo studente potrebbe essere interessato a loro e quindi vengono raccomandate.

Informazioni sulle materie

Per implementare un sistema di raccomandazione Content Based, bisogna prima creare il profilo delle materie.

Per creare il profilo delle materie sono state utilizzate delle tecniche di web scraping. Tutta questa parte è contenuta nella cartella ./scraper

Analizziamo i file che sono presenti all'interno della cartella.

subjects.py

La funzione __check_error_input controlla se ci sono errori in input da parte dell'utente, ovvero se l'utente ha dimenticato di inserire l'url su cui bisogna effettuare lo scraping.

def __check_error_input(args:str) -> str:
    if len(args) != 2:
        __print_help()
    i:int = 0
    url:str = ""
    while(i < len(args)):
        if args[i] == "-url":
            i += 1
            url = args[i]
        else:
            __print_help()
        i += 1
    return url

La funzione __print_help stampa un aiuto su come utilizzare lo scraper.

def __print_help() -> None:
    h:str = "\nUsage: python3 subjects.py -url <url>\n\n"
    h += "-url:\tIt is the site where we want to get the subjects\n"
    print(h)
    exit(-1)

È presente anche il main che utilizza le funzioni descritte sopra e quelle presenti nelle classi WebScraping e Extractor per creare un file CSV chiamato subjects.csv.

def main(args:str) -> None:
    url:str = __check_error_input(args)
    print("Extracting subjects...")
    Extractor(WebScraping(url).extract_subjects()).extract_data_frame().to_csv("./Dati/subjects.csv", index = False)
    print("DONE!\n")

Il file subjects.csv contiene un id e una descrizione associati ad una materia, dove all'inizio di ogni descrizione è presente il nome della materia.

WebScraping.py

In questo file è stata definita la classe WebScraping che permette di estrarre i nomi di tutte le materie del terzo anno e di scaricare i PDF di ogni materia.

Il costruttore della classe ha un solo parametro che è l'url sul quale sarà fatta una chiamata HTTP utilizzando urllib.request.urlopen. L'html ottenuto sarà analizzato utilizzando BeautifulSoup.

def __init__(self, url:str):
    self.url = url
    u_client = uRequest(url)
    self.page_html = u_client.read()
    self.page_soup = soup(self.page_html, features="lxml")

Il metodo __create_link crea il link per poter scaricare il PDF che riguarda una determinata materia. L'unico parametro della funzione link deve essere il link che riporta al syllabus di una determinata materia.

def __create_link(self, link) -> str:
    result:str = ""
    split:list = link.split("/")
    for k in split[0:-1]:
        result += (k + "/")
    return ("http://syllabus.unict.it/insegnamento.php?id=" + split[len(split)-1][5:] + "&pdf")

Il metodo extract_subjects ritorna una lista contenente i link che riportano ai PDF delle varie materie a scelta.

def extract_subjects(self) -> list:
    excluded:list = ['ALGORITMI RANDOMIZZATI ED APPROSSIMATI', 'FISICA', 'METODI MATEMATICI E STATISTICI']
    td = self.page_soup.findAll('td')
    i:int = 0
    while("3° anno" not in td[i].text):
        td.remove(td[i])
    subjects:list = []
    for elem in td:
        result = elem.findAll('a')
        subject:list = []
        if len(result) > 0 and result[0].text not in excluded:
            subject.append(result[0].text)
            subject.append(self.__create_link(result[0]["href"]))
            subjects.append(subject)
    return subjects

Il metodo create_pdf permette di creare in locale un file PDF a partire da una risposta ottenuta dopo una richiesta HTTP. Il parametro name serve a specificare il nome del file.

def create_pdf(self, name) -> None:
    open(name + ".pdf", 'wb').write(self.page_html)

Extractor.py

In questo file è stata definita la classe Extractor che permette di estrarre le descrizioni di ogni materia a partire da un file PDF e di creare un dataframe con la struttura del file CSV definito prima. La lettura del PDF è facilitata dall'uso di FormatPDF.

Il costruttore della classe prende in ingresso una lista che contiene i link ai PDF delle varie materie.

def __init__(self, subjects:list):
    self.subjects = subjects

Il metodo __extract_subject_description permette di estrarre la descrizione di una materia presente all'interno di un PDF. La funzione prende in ingresso una lista di frasi, che sono le righe di un PDF, e ritorna una stringa.

def __extract_subject_description(self, text: list) -> str:
    i:int = 0
    while(text[i].upper() != "CONTENUTI DEL CORSO"):
        i += 1
    i += 1
    description:str = ""
    while(text[i].upper() != "TESTI DI RIFERIMENTO"):
        description += text[i] + " "
        i += 1
    while(text[i].upper() != "PROGRAMMAZIONE DEL CORSO"):
        i += 1
    i += 3
    while(text[i].upper() != "VERIFICA DELL'APPRENDIMENTO"):
        if text[i] != None:
            description += text[i][3:].strip() + " "
        i += 1
    return description

Il metodo extract_data_frame ritorna un dataframe, che ha come colonne:

  • id: che identifica una determinata materia;
  • description: che contiene la descrizione di una specifica materia. All'inizio di ogni descrizione è presente il nome della materia.
def extract_data_frame(self) -> any:
    ids:list = []
    descriptions:list = []
    i:int = 0
    print("Please Wait... It will take some time\n")
    for subject in self.subjects:
        print(subject[0] + "\n")
        web_page:WebScraping = WebScraping(subject[1])
        web_page.create_pdf(subject[0])
        descrip.tion:str = self.__extract_subject_description(FormatPDF.format_pdf(subject[0] + ".pdf"))
        descriptions.append(
            subject[0] + 
            " - " + 
            description
        )
        ids.append(i)
        i += 1
        os.remove(subject[0] + ".pdf")
    return pd.DataFrame({'id': ids, 'description': descriptions})

FormatPDF.py

In questo file è stata definita la classe FormatPDF che ha un solo metodo statico.

Il metodo format_pdf prende in ingresso il path di un file PDF e ritorna una lista che contiene le frasi presenti all'interno del PDF eliminando gli spazi che ci sono all'inizio e alla fine di una riga.

@staticmethod
def format_pdf(pathname: str) -> None:
    with open(pathname, "rb") as f:
        pdf = pdftotext.PDF(f)
        f.close()
    formatted_lines = []
    text = ""
    for line in pdf:
        text += line
    split_text = text.split("\n")
    for line in split_text:
        formatted_lines.append(line.strip())
    return formatted_lines

Utilizziamo il seguente comando:

python3 scraper/subjects.py -url http://web.dmi.unict.it/corsi/l-31/programmi?aa=121

Quello che si ottiene è il seguente output:

Extracting subjects...

Please Wait... It will take some time

CALCOLO NUMERICO

COMPUTER GRAFICA

DIGITAL FORENSICS

INFORMATICA MUSICALE

INTERNET SECURITY

INTRODUZIONE AL DATA MINING

IT LAW

LABORATORIO DI SISTEMI A MICROCONTROLLORE

PROGRAMMAZIONE MOBILE

PROGRAMMAZIONE PARALLELA SU ARCHITETTURE GPU

SISTEMI CENTRALI

SOCIAL MEDIA MANAGEMENT

STARTUP DI IMPRESA E MODELLI DI BUSINESS

SVILUPPO DI GIOCHI DIGITALI

TECHNOLOGIES FOR ADVANCED PROGRAMMING

TECNOLOGIE PER I SISTEMI DISTRIBUITI E IL WEB CON LABORATORIO

WEB PROGRAMMING, DESIGN & USABILITY

DONE!

Il file ./Dati/subjects.csv sarà utilizzato per estrarre le caratteristiche di ogni materia, utilizzando TF-IDF, dalle quali è possibili costruire i feature vectors, ovvero i profili delle materie.

Telegram Bot

Dopo aver estratto le informazioni riguardanti le varie materie, è stato realizzato un bot Telegram che, in base a delle valutazioni date da uno studente, riesce a raccomandare delle materie.

Il "core" di tutto il bot è presente all'interno della cartella ./modules

Analizziamo il contenuto della cartella.

./modules/data

Questa cartella contiene il file data_reader.py

data_reader.py

Permette di ottenere il token presente all'interno del file ./config/settings.yaml

./modules/Logger

Questa cartella contiene il file logger.py

logger.py

In questo file è stata definita la classe Logger che è un Singleton e serve per ottenere i log.

./modules/handlers

Questa cartella contiene i seguenti file:

  • command_handler.py
  • callback_handlers.py

Tutte le funzioni tornano 0 o 1 che indicano lo stato della conversazione e che permettono di capire al bot quale funzione deve richiamare in base al bottone cliccato dall'utente.

command_handler.py

In questo file sono presenti le funzioni che vengono richiamate in base ad uno specifico comando che viene inviato al bot. Nel file è presente solo la funzione start dato che l'unico comando del bot è /start

La funzione start inizializza alcuni parametri importanti, che sono:

  • context.user_data["index_list_subject_length"]: serve per stabilire il numero di bottoni da visualizzare.
  • context.user_data["ratings"]: contiene le valutazioni che sono state date da uno studente.
  • context.user_data["subject_names"]: contiene i nomi di tutte le materie.

Queste variabili sono univoche per ogni studente.

def start(update: Update, context: CallbackContext) -> int:
    user = update.message.from_user
    context.user_data["username"] = user.username
    Logger.getInstance().info("L'utente " + user.username + " ha iniziato la conversazione.")
    keyboard = [
        [
            InlineKeyboardButton("LINK", url="http://web.dmi.unict.it/corsi/l-31/programmi"),
            InlineKeyboardButton("Iniziamo!", callback_data=str(0))
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    context.user_data["index_list_subject_length"] = 4
    context.user_data["ratings"] = []
    context.user_data["subject_names"] = []
    Subjects.getInstance().init_array(context.user_data["ratings"], context.user_data["subject_names"])
    update.message.reply_text(
        text="Prima di iniziare è necessario che tu legga i programmi delle varie materie del 3° anno.\n" +
        "Questo è un passo fondamentale perché dopo ti verrà chiesto di valutarne alcuni!\n" +
        "Puoi cliccare nel bottone sottostante per raggiungere la pagina dei programmi.\n" +
        "Dopo aver letto tutto attentamente clicca su \"Iniziamo!\"",
        reply_markup=reply_markup
    )
    return 0

callback_handlers.py

In questo file sono definite le funzioni che vengono richiamate quando si clicca su un bottone.

La funzione shift_menu_left permette di scorrere la lista delle materie verso sinistra.

def shift_menu_left(update: Update, context: CallbackContext) -> int:
    query = update.callback_query
    query.answer()
    reply_markup = InlineKeyboardMarkup(create_keyboard(context, -1, Subjects.getInstance().get_subjects()))
    query.edit_message_text(text=query.message.text, reply_markup=reply_markup)
    return 0

La funzione shift_menu_right permette di scorrere la lista delle materie verso destra.

def shift_menu_right(update: Update, context: CallbackContext) -> int:
    query = update.callback_query
    query.answer()
    reply_markup = InlineKeyboardMarkup(create_keyboard(context, 1, Subjects.getInstance().get_subjects()))
    query.edit_message_text(text=query.message.text, reply_markup=reply_markup)
    return 0

La funzione rate_best_subject serve per ottenere la materia preferita di uno studente e stabilire quanti altri voti dovrà dare. Lo studente darà da un minimo di 2 a un massimo di 4 voti.

def rate_best_subject(update: Update, context: CallbackContext) -> int:
        context.user_data["rate_number"] = random.randint(0, 2)
        context.user_data["rate"] = 5
        query = update.callback_query
        query.answer()
        context.user_data["index_list_subject"] = -context.user_data["index_list_subject_length"]
        reply_markup = InlineKeyboardMarkup(create_keyboard(context, 1, Subjects.getInstance().get_subjects()))
        query.edit_message_text(
            text="In base ai programmi letti, qual è la materia che ha stimolato maggiormente la tua curiosità?", 
            reply_markup=reply_markup
        )
        return 0

La funzione rate_subject serve per dare un voto compreso tra 1 e 5 ad una specifica materia indicata dallo studente e controlla se è stato raggiunto il massimo numero di voti che lo studente deve fornire.

def rate_subject(query, context: CallbackContext) -> int:
    context.user_data["rate"] = random.choices(range(5), weights=[0.24, 0.34, 0.22, 0.10, 0.10], k = 1)[0] + 1
    context.user_data["index_list_subject"] = -context.user_data["index_list_subject_length"]
    reply_markup = InlineKeyboardMarkup(create_keyboard(context, 1, Subjects.getInstance().get_subjects()))
    query.edit_message_text(
        text="A quale materia daresti un voto pari a " + str(context.user_data["rate"]) + " in base al programma?", 
        reply_markup=reply_markup
    )
    if(np.count_nonzero(np.array(context.user_data["ratings"])) > context.user_data["rate_number"]):
        return 1
    return 0

La funzione update_rating aggiorna la valutazione di uno studente rispetto ad una specifica materia, quindi modifica context.user_data["ratings"], nei log sarà riportata questa informazione.

def update_rating(index: str, context: CallbackContext) -> None:
    context.user_data["ratings"][int(index)] = context.user_data["rate"]
    Logger.getInstance().info("L'utente " + context.user_data["username"] + " ha dato un voto pari a " + str(context.user_data["rate"]) + 
    " a " + Subjects.getInstance().get_subjects()[index] + ".")

La funzione update_info richiama la funzione update_rating ed elemina dalla lista context.user_data["subject_names"] quella materia che è già stata valutata dallo studente, infine richiama la funzione rate_subject.

def update_info(update: Update, context: CallbackContext) -> int:
    query = update.callback_query
    query.answer()
    index = update.callback_query.data.split(" - ")[0]
    update_rating(index, context)
    Subjects.getInstance().delete_subject_name(index, context)
    return rate_subject(query, context)

La funzione end conclude la conversazione con lo studente inviando un messaggio con le 5 materie consigliate in base ai voti da lui dati.

def end(update: Update, context: CallbackContext) -> int:
    query = update.callback_query
    query.answer()
    index = update.callback_query.data.split(" - ")[0]
    update_rating(index, context)
    Subjects.getInstance().delete_subject_name(index, context)
    query.edit_message_text(text="Utilizzo i dati da te forniti per capire quali materie potrebbero essere interessanti per te!")
    recommender_subjects = recommender_system(context)
    message:str = "Le 5 materie che ti consiglio sono:\n\n"
    for index in recommender_subjects:
        message += "· " + Subjects.getInstance().get_subjects()[str(index)] + "\n\n"
    query.edit_message_text(text=
        message +
        "Ricorda che io sono solo un bot che cerca di migliorare le proprie capacità in base all'esperienza!\n" +
        "Per ricominciare usa il comando /start"
    )
    Logger.getInstance().info("Conversazione conclusa con l'utente " + context.user_data["username"])
    return ConversationHandler.END

La funzione start_over ricomincia la conversazione con uno studente, azzerando la discussione precedente.

def start_over(update: Update, context: CallbackContext) -> int:
    query = update.callback_query
    query.answer()
    Logger.getInstance().info("L'utente " + context.user_data["username"] + " ha ricominciato la conversazione.")
    keyboard = [
        [
            InlineKeyboardButton("LINK", url="http://web.dmi.unict.it/corsi/l-31/programmi"),
            InlineKeyboardButton("Iniziamo!", callback_data=str(0))
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    context.user_data["index_list_subject_length"] = 4
    context.user_data["ratings"] = []
    context.user_data["subject_names"] = []
    Subjects.getInstance().init_array(context.user_data["ratings"], context.user_data["subject_names"])
    query.edit_message_text(
        text="Prima di iniziare è necessario che tu legga i programmi delle varie materie del 3° anno.\n" +
        "Questo è un passo fondamentale perché dopo ti verrà chiesto di valutarne alcuni!\n" +
        "Puoi cliccare nel bottone sottostante per raggiungere la pagina dei programmi.\n" +
        "Dopo aver letto tutto attentamente clicca su \"Iniziamo!\"",
        reply_markup=reply_markup
    )
    return 0

./modules/utils

Questa cartella contiene i seguenti file:

  • keyboard.py
  • subject_ratings.py

keyboard.py

Questo file contiene un'unica funzione, chiamata create_keyboard che serve per creare l'insieme dei bottoni che l'utente deve cliccare in un determinato stato.

def create_keyboard(context: CallbackContext, direction: int, subjects: dict) -> list:
    keyboard_subject: list = []
    list_length = context.user_data["index_list_subject_length"]
    context.user_data["index_list_subject"] += (list_length * direction)
    if(context.user_data["index_list_subject"] >= len(context.user_data["subject_names"]) or context.user_data["index_list_subject"] <= -4):
        context.user_data["index_list_subject"] -= (list_length * direction)
    start_index = context.user_data["index_list_subject"]
    end_index = start_index + list_length

    for key in context.user_data["subject_names"][start_index:end_index]:
        keyboard_subject.append([InlineKeyboardButton(subjects[key], callback_data=key + " - delete")])
    
    button: list = []
    if start_index > 0:
        button.append(InlineKeyboardButton("⏪", callback_data="LEFT"))
    if end_index < len(context.user_data["subject_names"]):
        button.append(InlineKeyboardButton("⏩", callback_data="RIGHT"))
    keyboard_subject.append(button)
    keyboard_subject.append([InlineKeyboardButton("🔁 RESTART 🔁", callback_data="restart")])
    return keyboard_subject

subject_ratings.py

In questo file è definita la classe Subjects che è un Singleton, questo perché la classe contiene:

  • un dizionario, dove sono presenti tutte le materie indicizzate attraverso il loro id, e di questo ne deve esistere solo uno.
  • un dataframe, dove sono presenti i voti che degli studenti hanno dato alle materie attraverso un google form, e di questo ne deve esistere solo uno.

La funzione get_subjects ritorna il dizionario.

def get_subjects(self) -> dict:
    return self.__subjects

La funzione get_data ritorna il dataframe.

def get_data(self):
    return self.__data

La funzione load_subjects utilizza i dati nel file ./Dati/Dati.csv, che sono quelli raccolti attraverso il google form, per creare il dataframe e utilizza il file ./Dati/subjects.csv per creare il dizionario.

def load_subjects(self) -> None:
    self.__data = pd.read_csv("./Dati/Dati.csv")
    self.__data = self.__data.pivot_table(index='user_id', columns='subject_id', values='rating')
    self.__subjects = {}
    with open("./Dati/subjects.csv", 'r') as subject:
        for s in subject:
            split:str = s.split(',')
            if(split[0] != "id"):
                name:str = split[1].split(" - ")[0]
                if name[0] == "\"":
                    name = name[1:]
                self.__subjects[split[0]] = name

La funzione init_array inizializza:

  • context.user_data["ratings"];
  • context.user_data["subject_names"].

Che vengono passati come parametri alla funzione.

def init_array(self, ratings: list, subject_names: list) -> None:
    for key in self.__subjects:
        subject_names.append(key)
        ratings.append(0)

La funzione delete_subjects_name elimina da context.user_data["subject_names"] il nome della materia per cui lo studente ha già dato un voto.

def delete_subject_name(self, index: str, context: CallbackContext) -> None:
    context.user_data["subject_names"].remove(index)

./modules/recommender_system

Questa cartella contiene i seguenti file:

  • ContentBased.py
  • performance_assessment.py
  • recommender_system.py

ContentBased.py

In questo file è definita la classe ContentBased che contiene i metodi che permettono di predire quali sono i possibili voti che uno studente potrebbe dare alle materie per cui non ha espresso un voto.

Il costruttore della classe ha i seguenti parametri:

  • subjects_filename: che deve essere un file CSV con due colonne id e description, come quello presente in ./Dati/subjects.csv.
  • data_filename: che deve essere un file CSV con quattro colonne: user_id, subject_id, subject_name, rating. Un file di esempio è quello presente in ./Dati/Dati.csv.
  • data: che deve essere un dataframe dove sono presenti i voti che degli studenti hanno dato alle materie.
  • threshold: un valore di soglia che serve per capire se una caratteristica è presente oppure no in una materia. Il valore di default è 0,22.

data_filename e data sono opzionali, ma almeno uno di loro deve essere presente.

def __init__(self, subjects_filename, data_filename = None, data = None, threshold = 0.22) -> None:
    self.threshold = threshold
    self.__build_tfidf_matrix(subjects_filename)
    if(data_filename is not None):
        self.__build_utility_matrix(data_filename)
    elif(data is not None):
        self.data = data
        self.utility_matrix = data
    else:
        print("Error in data initialization!")
        exit(-1)
    self.__build_normalized_utility_matrix()
    self.__build_user_profiles()

Il metodo __build_tfidf_matrix prende in ingresso il nome di un file, come ./Dati/subjects.csv, e calcola TF-IDF sulla descrizione di ogni materia e utilizzando il threshold si stabilisce se una caratteristica è presente (1) oppure non è presente (0). Quindi si ottiene una matrice binaria, dove ogni riga è il profilo di una materia.

def __build_tfidf_matrix(self, subjects_filename) -> None:
    ds = pd.read_csv(subjects_filename)
    tf = TfidfVectorizer(stop_words = get_stop_words("italian"))
    self.tfidf_matrix = tf.fit_transform([(k.split(" - ")[1]) for k in ds['description']])
    self.tfidf_matrix = np.where(np.array(self.tfidf_matrix.todense()) > self.threshold, 1, 0)

Il metodo __build_utility_matrix prende in ingresso il nome di un file, come ./Dati/Dati.csv, e costruisce la matrice di utilità.

def __build_utility_matrix(self, data_filename) -> None:
    self.data = pd.read_csv(data_filename)
    self.utility_matrix = self.data.pivot_table(index='user_id', columns='subject_id', values='rating')

Il metodo __build_normalized_utility_matrix normalizza la matrice di utilità.

def __build_normalized_utility_matrix(self) -> None:
    self.n_utility_matrix = self.utility_matrix.sub(self.utility_matrix.mean(1), axis = 'index').fillna(0).to_numpy()

Il metodo __build_user_profiles costruisce i profili degli studenti.

def __build_user_profiles(self) -> None:
    self.profiles = []
    for user in self.n_utility_matrix:
        profile = []
        for item in self.tfidf_matrix.transpose():
            profile.append((np.sum(user * item) + 1)/(np.count_nonzero(user) + 1))
        self.profiles.append(profile)
    return np.array(self.profiles)

Il metodo __scale permette di mappare dei valori in un determinato range, la funzione prende in ingresso X (scalare, vettore o matrice), il minimo del nuovo range e il massimo del nuovo range.

def __scale(self, X, x_min, x_max):
    nom = (X + 1) * (x_max - x_min)
    denom = 2
    return x_min + nom / denom

Il metodo __cosine_distance permette di calcolare la distanza del coseno tra due vettori.

def __cosine_distance(self, x, y) -> float:
    return (np.dot(x, y) + 1) / ((np.sqrt(np.dot(x, x)) + 1) * np.sqrt(np.dot(y, y) + 1))

Il metodo print_utility_matrix stampa a video la matrice di utilità.

def print_utility_matrix(self):
    print(self.utility_matrix)

Il metodo predict predice il voto che uno studente $x_i$ darebbe ad una materia $s_j$ e ritorna questo valore.

def predict(self, xi, sj) -> float:
    return self.__scale(self.__cosine_distance(self.profiles[xi], self.tfidf_matrix[sj]), 0, 5)

performance_assessment.py

Questo file contiene delle funzioni che hanno permesso di trovare il valore ottimale del threshold.

recommender_system.py

Questo file contiene le funzioni che permettono di predire il voto che uno studente $x_i$ darebbe ad una materia $s_j$ utilizzando il bot Telegram.

La funzione predict calcola il voto che uno studente darebbe a tutte quelle materie per cui non ha espresso un voto.

def predict(cb: ContentBased, context: CallbackContext) -> None:
    i:int = 0
    while(i < len(context.user_data["ratings"])):
        if(context.user_data["ratings"][i] == 0):
            context.user_data["ratings"][i] = cb.predict(14, i)
        i += 1

La funzione create_new_row crea una nuova riga da inserire nel dataframe che contiene i voti di uno determinato studente.

def create_new_row(context: CallbackContext) -> list:
    row = np.array(context.user_data["ratings"])
    row = row.astype('float')
    row[row == 0] = np.NaN
    return row.tolist()

La funzione save_user_ratings salva i voti che sono stati dati da uno studente nel file ./users/<username>.csv

def save_user_ratings(context: CallbackContext) -> None:
    s: str = ""
    for key in Subjects.getInstance().get_subjects():
        s += (
            context.user_data["username"] + "," +
            key + "," +
            Subjects.getInstance().get_subjects()[key] + "," +
            str(context.user_data["ratings"][int(key)]) + "\n"
        )
    with open("./Dati/users/" + context.user_data["username"] + ".csv", "w+") as file:
        file.write(s)

La funzione recommender_system sfrutta le funzioni descritte precedentemente e ritorna una lista contenente le 5 materie consigliate per uno specifico studente.

def recommender_system(context: CallbackContext) -> list:
    Subjects.getInstance().get_data().loc[context.user_data["username"]] = create_new_row(context)
    cb = ContentBased("./Dati/subjects.csv", data=Subjects.getInstance().get_data())
    predict(cb, context)
    save_user_ratings(context)
    return (np.argsort(context.user_data["ratings"]).tolist()[::-1])[0:5]

DMI_Subjects_Recommender_System.py

Questo file contiene il main e sfrutta tutte le funzioni e le classi viste precedentemente per far funzionare il bot.

Il bot presenta due stati:

  • FIRST: lo stato in cui, subito dopo il comando /start, lo studente interagisce attivamente con il bot;
  • SECOND: lo stato in cui il bot analizza i voti dati dallo studente e restituisce le 5 materie.

Come si usa?

Per prima cosa bisogna avere Python 3.8 installato nella propria macchina.

Successivamente, bisogna inserire il token del proprio bot all'interno del file ./config/settings.yaml.dist e rinominare il file in settings.yaml.

A questo punto bisogna installare tutte le dipendenze e per farlo basta eseguire il seguente comando:

pip3 install -r ./requirements.txt

Per utilizzare lo scraper bisogna eseguire il seguente comando:

python3 scraper/subjects.py -url <url>
  • -url : indica l'url su cui si vuole fare scraping.

Per utilizzare il bot e salvare i log in un file bisogna eseguire il seguente comando:

python3 DMI_Subjects_Recommender_System.py > log.txt

Docker

Nel caso in cui si hanno problemi nell'installare Python 3.8 o le varie dipendenze, è possibile utilizzare Docker.

Per prima cosa bisogna inserire il token del proprio bot all'interno del file ./config/settings.yaml.dist e rinominare il file in settings.yaml.

A questo punto bisogna eseguire il seguente comando:

docker build --tag dmi_recommender_system .

Per utilizzare lo scraper bisogna eseguire il seguente comando:

docker run --rm -it dmi_recommender_system scraper/subjects.py -url <url>
  • -url : indica l'url su cui si vuole fare scraping.

Per utilizzare il bot e salvare i log in un file bisogna eseguire il seguente comando:

docker run --rm -it dmi_recommender_system DMI_Subjects_Recommender_System.py > log.txt

Esempio