(blog ‘lucindo)

um dia eu aprendo a programar

Netflix Prize

Para quem ainda não sabe, o Netflix Prize é uma competição que começou no final do 2006 cujo objetivo é melhorar em 10% o algoritmo de sugestão de filmes da Netflix. Quem conseguir fazer isso recebe o prêmio de U$ 1.000.000.

Bom, fiz um pequeno código esses dias que joga os dados do Netflix Prize no Postgres para depois poder brincar um pouco com isso e quem sabe ganhar uma graninha :)

Usei basicamente o Postmodern e a nova versão do Postmodern-utils. Ainda preciso fazer uma página com a documentação do pacote. Nessa versão entraram algumas funções novas: set-connection-spec, create-tables, map-dao e for-each-dao.

Seguindo a sugestão do Leonardo Varuzza, agora o Postmodern-utils poder ser instalado assim:

* (asdf-install:install :postmodern-utils)

Vamos ao código. Primeiro a definição do pacote:

(eval-when (:load-toplevel :compile-toplevel :execute)
  (require :postmodern)
  (require :postmodern-utils)
  (require :cl-fad)
  (require :split-sequence))

(defpackage :netflix
  (:use :common-lisp :postmodern :postmodern-utils)
  (:export #:setup-database
           #:load-data
           #:predict-ratings
           #:movies #:id-of #:year-of #:title-of #:get-movie
           #:ratings #:user-of #:movie-of #:rating-of #:date-of
           #:get-ratings-of-user #:get-ratings-of-movie))

(in-package :netflix)

Agora alguns dados globais (mude o parametro *netflix-data-dir*):

(defparameter *netflix-data-dir* “/path/to/netflix/download”)
(defparameter *movies-file*
  (concatenate ’string *netflix-data-dir* “/movie_titles.txt”))
(defparameter *ratings-dir*
  (concatenate ’string *netflix-data-dir* “/training_set/”))

(eval-when (:load-toplevel :compile-toplevel :execute)
  (set-connection-spec :username “netflix”
                       :password “”
                       :database “netflix”
                       :hostname “localhost”))

Agora a definição da tabela movies e funções para criar e acessar daos desse tipo:

(deftable movies ()
  ((movie-id :type integer
             :accessor id-of
             :initarg :movie-id)
   (year :type string ;; may be “NULL”
         :accessor year-of
         :initarg :year)
   (title :type string
          :accessor title-of
          :initarg :title))
  (:indices movie-id)
  (:class-name movies))

(defun make-movie (movie-id year title)
  (with-pooled-connection
      (save-dao (make-instance ‘movies
                               :movie-id movie-id
                               :year year
                               :title title))))

(defun get-movie (movie-id)
  (with-pooled-connection
      (get-dao ‘movies movie-id)))

Para ler e parsear os arquivos vamos usar a seguinte função auxiliar:

(defun process-file-by-line (file-name fn)
  (with-open-file (stream file-name :external-format :iso-8859-1)
    (do ((line (read-line stream nil)
               (read-line stream nil)))
        ((null line))
      (funcall fn line))))

Com ela podemos parsear o arquivo movie_titles.txt assim:

(defun load-movies (movies-file)
  (process-file-by-line
   movies-file
   #’(lambda (line)
       (let* ((splitted-line (split-sequence:split-sequence #\, line))
              (movie-id (parse-integer (first splitted-line)))
              (year (second splitted-line))
              (title (third splitted-line)))
         (make-movie movie-id year title)))))

Depois definimos a tabela ratings e uma função para criar os daos desse tipo:

(deftable ratings ()
  ((user-id :type integer
            :accessor user-of
            :initarg :user-id)
   (movie-id :type integer
             :accessor movie-of
             :initarg :movie-id)
   (rating :type integer
           :accessor rating-of
           :initarg :rating)
   (date :type string
         :accessor date-of
         :initarg :date))
  (:indices (user-id movie-id) rating)
  (:class-name ratings))

(defun make-rating (user-id movie-id rating date)
  (with-pooled-connection
      (save-dao (make-instance ‘ratings
                               :user-id user-id
                               :movie-id movie-id
                               :rating rating
                               :date date))))

Podemos fazer algumas funções para selecionar listas de ratings, como por exemplo:

(defun get-ratings-of-user (user-id)
  (with-pooled-connection
    (select-daos ‘ratings :test `(:= user-id ,user-id))))

(defun get-ratings-of-movie (movie-id)
  (with-pooled-connection
    (select-daos ‘ratings :test `(:= movie-id ,movie-id))))

O parse dos arquivos com ratings é feito pelas seguintes funções:

(defun load-ratings-file (file)
  (let ((movie-id 0))
    (process-file-by-line
     file
     #’(lambda (line)
         (if (search “:” line)
             (setq movie-id (parse-integer line :junk-allowed t))
             (let* ((splitted-line (split-sequence:split-sequence #\, line))
                    (user-id (parse-integer (first splitted-line)))
                    (rating (parse-integer (second splitted-line)))
                    (date (third splitted-line)))
               (make-rating user-id movie-id rating date)))))
    (format t “processed: ~a~%” file)))

(defun load-ratings (ratings-dir)
  (cl-fad:walk-directory
   ratings-dir
   #’(lambda (file)
       (load-ratings-file file))))

Agora as funções para carregar todos os dados no Postgres:

(defun load-data ()
  (load-movies *movies-file*)
  (load-ratings *ratings-dir*))

(defun setup-database (&optional (first-drop nil))
  (with-pooled-connection
    (create-tables ‘(movies ratings) :first-drop first-drop)))

Por fim a função para processar os arquivos de entradas e gerar o arquivo com as previsões dada uma função de previsão que recebe três parêmetros: id do usuário, id do filme e a data da votação.

(defun predict-ratings (query-file predict-file predict-fn)
  (with-open-file (stream predict-file :direction :output)
    (let ((movie-id 0))
      (process-file-by-line
       query-file
       #’(lambda (line)
           (if (search “:” line)
               (progn
                 (setq movie-id (parse-integer line :junk-allowed t))
                 (format stream “~a:~%” movie-id))
               (let* ((splitted-line (split-sequence:split-sequence #\, line))
                      (user-id (parse-integer (first splitted-line)))
                      (date (second splitted-line)))
                 (format stream “~1$~%”
                         (funcall predict-fn user-id movie-id date)))))))))

Agora tenho todos os dados e funções para fazer a manipulação. Só falta a função de 1 milhão de dólares e rodar:

(predict-ratings “qualifying.txt” “winner.txt” #’million-dollar-function)



Download: netflix.lisp

Observação 1: carregar todos os dados no Postgres dessa maneira demora (poucos dias), pois faço um insert para cada rating, e são 100 milhões deles.

Observação 2: vale a pena ver também o excelente post do Juho Snellman sobre como processar os dados do Netflix Prize usando Common Lisp.



 | Enviar por e-mail  | Hits para esta publicação: 786

Deixe uma resposta.