Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Continuando o livro Data Science do Zero

Agora implementamos a busca por pessoas que possuam conexões com nossas conexões

Capa do livro Data Science do Zero

Prolegômenos

Este é o segundo texto sobre esse livro. Se você não viu o primeiro texto, por favor, aqui está ele.

Uma ano se passou, com esse livro me olhando torto toda vez que passava por ele. Um ano se passou, com a alteração que fiz no código me olhando torto toda vez que eu não conseguia fazer os teste voltarem a funcionar.

Chegou a hora de implementar uma busca mais elaborada, aquela coisa nas redes sociais de 'pessoas que você talvez conheça'. A abordagem do livro é, na seção 'Cientistas de Dados Que Você Talvez Conheça' (pp. 6-7).

A ideia aqui é verificar em todos os usuários que não são conexões sua, se eles tem conexão com alguma conexão sua. Se alguém não conectado com dada conta possui conexão com alguma conexão sua, possivelmente se conheçam, ou muito provavelmente seja interessante conhecer-se.

Relembrando, minha ideia é escrever testes (que validem os exemplos e dados usados no livro) e classes que implementem as ideias apresentadas no texto, ao invés de apenas copiar o código lá exibido.

Primeiras alterações

Testes

Facilitar a execução dos testes foi algo bem fácil. Bastou incluir um __init__.py_ no diretório de testes, para que a execução dos mesmos fosse disparada com o simples comando:


➤ python3 -munittest
.......
----------------------------------------------------------------------
Ran 7 tests in 0.002s

OK

Também um alteração em como as fixtures são inseridas no objeto User:


--- a/tests/test_friendship.py
+++ b/tests/test_friendship.py
@@ -12,6 +12,8 @@ class TestFriendship(unittest.TestCase):
         """Get friendships array."""
         fixtures = helper_test.Fixtures()
         self.users = user.User()
+        [self.users.append(x) for x in fixtures.users]
+

Lista de amigos

Outra alteração, bem simples, foi em como conseguir uma lista de conexões de um dado usuário:


     def friends(self, user_id):
         """Returns a friends list for a given user."""
-        return [x[1] for x in self.friendships if x[0] == user_id]
+        return [x['friend'] for x in self.friendships if x['user'] == user_id]

Testes para o código novo

Um teste que foi implementado, após esse um ano de abandono, era na funcção how_many_friends, que conta quantas conexões um dados usuário possui:


def test_how_many_friends(self):
    """Test nymber of friends by user. Order is important."""
    expected = [
        (1, 3), (2, 3), (3, 3), (5, 3), (8, 3),
        (0, 2), (4, 2), (6, 2), (7, 2), (9, 1),
    ]
    self.assertEqual(expected, self.users.how_many_friends())

Lembrando que os dados do teste são os exemplos usados no livro. A ordem dos dados, pela quantidade de conexões é importante, pois um dos critérios para essa tarefa é justamente saber que 'é mais conectado'.

Outro teste e para saber se o cálculo de sugestões para novas conexões bate com a proposta apresentada:


def test_friends_of_friend_ids(self):
    """Test list of common friends with a friend."""
    expected = {0: 2, 5: 1}
    self.assertEqual(expected, self.users.friends_of_friend_ids(3))

As funções adicionadas

Aqui calculamos quantas conexões um dado usuário tem. Levanta-se quais são as conexões, faz-se a contagem, ordena-se, e pronto.


def how_many_friends(self):
    """Returns how many friends have each user."""
    friends_by_id = [
        (user['id'], self.friends(user['id']))
        for user in self.data
    ]
    num_friends_by_id = [(x[0], len(x[1])) for x in friends_by_id]
    return sorted(num_friends_by_id, key=lambda x: x[1], reverse=True)

Necessária para sugerir conexões novas, essa funcção verifica se quais são as conexões em comum de dois usuários usando a operação de interseção entre conjuntos. Por isso a conversão para o tipo set(), e o uso de &, o operador Python para operação de interseção entre conjuntos.


def common_friends(self, user1, user2):
    friends1 = set(self.friends(user1))
    friends2 = set(self.friends(user2))
    return friends1 & friends2

E aqui a pérola de hoje, que retorna uma lista de sugestões de conexão. Para cada usuário existente no sistema, que não é conectado ao usuário informado, verifica-se se tem conexões em comum. Exclui-se o próprio usuário pesquisado, e os já conectados.


def friends_of_friend_ids(self, user_id):
    """Returns counters for commons friends of each friend."""
    my_friends = self.friends(user_id)
    exclude = my_friends + [user_id, ]
    commons = dict()
    for person in [x['user'] for x in self.friendships]:
        if person in exclude:
            continue
        common = self.common_friends(user_id, person)
        if len(common) > 0:
            commons[person] = len(common)
    return commons

Essas mudanças estão na tag 002friendsofafriend no GitHub.