예제 #1
0
def annotate():
	sys.path.append(os.path.dirname(__file__))
	chrome_path = 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe %s'
	insta_path = 'http://www.instagram.com/'

	### Ouvre le client SQL. ###
	sqlClient = SqlClient()
	sqlClient.openCursor()

	### Récupère les noms d'utilisateurs à annoter. ###
	users = sqlClient.getUsernameUrls(labeled=False)
	print('Fetched %s users.' % str(len(users)))
	sqlClient.closeCursor()

	for index, user in enumerate(users):

		### Ouvre un nouvel onglet, sur la page Instagram de l'utilisateur à annoter. ###
		webbrowser.get(chrome_path).open(user)
		label = -1

		username = user.split(insta_path)[1]

		### On recommence tant que l'utilisateur n'a pas été annoté par 0 ou par 1. -2 correspond à un utilisateur non trouvé. ###
		while label not in ACCEPTED_VALUES:
			label = input('%s. %s : ' % (str(index), user))

			### Si on a fait une erreur et que l'on veur revenir en arrière. ###
			if label == 'previous':
				if index > 0:
					### Ouvre de nouveau la page précédente. ###
					webbrowser.get(chrome_path).open(users[index - 1])

					labelprevious = -1
					while labelprevious not in ACCEPTED_VALUES:
						labelprevious = input('%s. %s : ' % (str(index - 1), users[index - 1]))
					### Set le label en base. ###
					sqlClient.openCursor()
					sqlClient.setLabel(users[index - 1].split(insta_path)[1], labelprevious)
					sqlClient.closeCursor()
				else:
					print('Cannot go previous first user.')

				### Réouvre la page courante. ###		
				webbrowser.get(chrome_path).open(user)

		### Set le label en base. ###
		sqlClient.openCursor()
		sqlClient.setLabel(username, label)
		sqlClient.closeCursor()

		sqlClient.openCursor()
		testRatio = sqlClient.getTestRatio()
		print(testRatio)
		if testRatio < 0.25:
			sqlClient.setTest(username, True)
		sqlClient.closeCursor()
예제 #2
0
    def mig_1_rollback(self):
        """
        Rollback de la migration n°1. Permet de séparer les utilisateurs annotés en jeu d'entraînement et de test.
        """

        ### Définition du client SQL. ###
        sqlClient = SqlClient()

        ### Récupération des utilisateurs annotés.
        sqlClient.openCursor()
        usernames = sqlClient.getUserNames(limit=0)
        sqlClient.closeCursor

        ### Reset des valeurs de la colonne 'test_set'. ###
        for user in usernames:
            sqlClient.openCursor()
            sqlClient.setTest(user['user_name'], False)
            sqlClient.closeCursor()
예제 #3
0
    def mig_1(self):
        """
        Migration n°1. Permet de séparer les utilisateurs annotés en jeu d'entraînement et de test.
        """

        ### Définition du client SQL. ###
        sqlClient = SqlClient()

        ### Récupération des utilisateurs annotés.
        sqlClient.openCursor()
        usernames = sqlClient.getUserNames(limit=0)
        sqlClient.closeCursor

        random.shuffle(usernames)

        ### Indexation au jeu d'entraînement ou de test. ###
        for index, user in enumerate(usernames):
            isTest = False
            if index % 4 == 0:
                isTest = True
            sqlClient.openCursor()
            sqlClient.setTest(user['user_name'], isTest)
            sqlClient.closeCursor()
예제 #4
0
class Trainer(object):
    """
	Classe d'entraînement du modèle de détection des influenceurs.
	"""
    def __init__(self):
        """
		__init__ function. On définit aussi les features que l'on va utiliser pour l'étude.
		"""

        super().__init__()
        self.key_features = [
            #'biographyscore', # Construit sur le même jeu d'entrainement que la classification actuelle, à éviter donc jusqu'à trouver une nouvelle solution.
            'avglikes',
            'avgcomments',
            #'category',
            'color_distorsion',
            'colorfulness_std',
            'contrast_std',
            'frequency',
            'engagement',
            'followings',
            'followers',
            'nmedias',
            'usermentions',
            'commentscore',
            'is_verified'
        ]

        self.features_array_train = list()
        self.features_array_test = list()
        self.labels_train = list()
        self.labels_test = list()
        self.users_array = list()

    def buildUsersModel(self):
        """
		Construit la liste des utilisateurs utile pour l'entrainement, avec les features correspondantes.

				Args:
					(none)
				Returns:
					(none)
				
		"""

        self.sqlClient = SqlClient()

        ### On récupère toutes les features nécessaires pour entraîner le modèle. ###
        self.user_model = User()

        ### `users` est une liste de dictionnaires de type [{user_name: "toto"}, {user_name: "valberthe"}, ...]. ###
        ### `users_array` est une liste de noms d'utilisateurs : ["toto", "valberthe", ...].                     ###
        users = self.user_model.getUserNames()

        users_array = [user['user_name'] for user in users]

        ### Si le modèle d'utilisateurs existe déjà, on l'ouvre. ###
        if os.path.isfile(users_model_path):
            with open(users_model_path, 'rb') as f:
                self.users_array = pickle.load(f)

        ### On parcourt le tableau des utilisateurs pour leur assigner les features. ###
        for user in tqdm(users):

            ### Si l'utilisateur se trouve déjà dans le teableau, on n'a pas à réeffectuer le traitement. ###
            if user['user_name'] in [
                    _user['username'] for _user in self.users_array
            ]:
                continue

            self.user_model.username = user['user_name']

            ### Récupère les features via la classe User. ###
            self.user_model.getUserInfoSQL()
            item = {
                'avglikes': self.user_model.avglikes,
                'avgcomments': self.user_model.avgcomments,
                'category': self.user_model.category,
                'color_distorsion': self.user_model.color_distorsion,
                'colorfulness_std': self.user_model.colorfulness_std,
                'contrast_std': self.user_model.contrast_std,
                'lastpost': self.user_model.lastpost,
                'username': self.user_model.username,
                'frequency': self.user_model.frequency,
                'engagement': self.user_model.engagement,
                'followings': self.user_model.followings,
                'followers': self.user_model.followers,
                'nmedias': self.user_model.nmedias,
                'usermentions': self.user_model.usermentions,
                'brandpresence': self.user_model.brandpresence,
                'brandtypes': self.user_model.brandtypes,
                'commentscore': self.user_model.commentscore,
                'biographyscore': self.user_model.biographyscore,
                'is_verified': self.user_model.is_verified,
                'label': self.user_model.label,
                'testset': self.user_model.testset,
            }
            self.users_array.append(item)

            ### Sauvegarde du modèle d'utilisateurs. ###
            with open(users_model_path, 'wb') as f:
                pickle.dump(self.users_array, f)

        self.users_array = [
            user for user in self.users_array
            if user['username'] in users_array
        ]

        features_dict = [{key: user[key]
                          for key in self.key_features}
                         for user in self.users_array]

        ### Assignation de la liste des features en tant que liste, et les labels correspondants. ###
        self.dictvec = DictVectorizer()
        self.dictvec.fit(features_dict)

        ### Sauvegarde le modèle du vectorisateur de dico. ###
        with open(dictvec_model_path, 'wb') as f:
            pickle.dump(self.dictvec, f)

        self.correlationAnalysis()

        for user in self.users_array:

            ### On n'a pas besoin de filtrer de nouveau les champs ici: le DictVec s'en charge! ###
            ### S'il ne connait pas un champ, il l'ignore. 									    ###
            features_list = self.dictvec.transform(user).toarray().flatten()

            if user['testset']:
                self.features_array_test.append(features_list)
                self.labels_test.append(user['label'])

            else:
                self.features_array_train.append(features_list)
                self.labels_train.append(user['label'])

    def alterUsersModel(self):
        """
		Au lieu de reconstruire le modèle d'utilisateurs à chaque fois, on change juste un champ pour des modifications occasionnelles.

				Args:
					(none)
				Returns:
					(none)
		"""

        self.sqlClient = SqlClient()
        self.user_model = User()

        ### Charge le tableau des utilisateurs dont les features sont déjà extraites. ###
        if os.path.isfile(users_model_path):
            with open(users_model_path, 'rb') as f:
                users_array = pickle.load(f)

        adjusted_users = list()

        for user in tqdm(users_array):

            self.sqlClient.openCursor()
            userserver = self.sqlClient.getUser(user['username'])
            self.sqlClient.closeCursor()

            user['is_verified'] = userserver['is_verified']
            adjusted_users.append(user)

        with open(users_model_path, 'wb') as f:
            pickle.dump(adjusted_users, f)

    def train(self):
        """
		Entraînement du modèle de classification.
		
			Args:
				(none)
			
			Returns:
				(none)
		"""

        ### Définition du classifieur de type Random Forest à 500 estimateurs. ###
        self.clf = RandomForestClassifier(n_estimators=500)

        ### On entraîne le classifieur avec le set d'entraînement (jusqu'à l'index n_split). ###
        self.clf.fit(self.features_array_train, self.labels_train)

        ### On peut avoir l'importance des features dans la décision de la classification. ###
        importance = self.clf.feature_importances_

        ### Score de la classification. ###
        scores = cross_val_score(self.clf,
                                 self.features_array_train +
                                 self.features_array_test,
                                 self.labels_train + self.labels_test,
                                 cv=5)
        print("\nAccuracy: %0.2f (+/- %0.2f)\n" %
              (scores.mean(), scores.std() * 2))

        ### On calcule une prédiction pour la matrice de confusion et le rapport de classification. ###
        pred = self.clf.predict(self.features_array_test)
        print(confusion_matrix(self.labels_test, pred))
        print('\n')

        ### On affiche l'importance des critères de classification. ###
        categories_total = 0
        for couple in zip(self.dictvec.get_feature_names(), importance):
            ### Malheureusement lorsqu'on boucle là dessus on a l'importance de chaque type de catégories... ###
            ### Pour n'afficher que les catégories au global, on fait un test sur les features names.        ###
            if 'category=' in couple[0]:
                categories_total += couple[1]
                continue
            print('    %s: %s' %
                  (str(couple[0]), '%.2f%%' % float(100 * couple[1])))
        print('    %s: %s' %
              ('category', '%.2f%%' % float(100 * categories_total)))
        print('\n')

        print(classification_report(self.labels_test, pred))

        ### Affichage des faux positifs et faux négatifs pour observer quels influenceurs sont mal détectés. ###
        #self.displayFPFN(pred)

        ### Génération des probabilités (et non du vote à majorité) du Random Forest
        y_score = self.clf.predict_proba(self.features_array_test)
        pred2 = [predclass[1] for predclass in y_score]

        ### Construction de la courbe ROC. ###
        fpr, tpr, _ = roc_curve(self.labels_test, pred2)
        ### Aire sous la courbe. ###
        roc_auc = auc(fpr, tpr)

        ### Affichage de la courbe ROC. ###
        plt.figure()
        lw = 2
        plt.plot(fpr,
                 tpr,
                 color='darkorange',
                 lw=lw,
                 label='ROC curve (area = %0.2f)' % roc_auc)
        plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
        plt.xlim([0.0, 1.0])
        plt.ylim([0.0, 1.05])
        plt.xlabel('False Positive Rate')
        plt.ylabel('True Positive Rate')
        plt.title('Receiver operating characteristic example')
        plt.legend(loc="lower right")
        plt.show()

        ### Sauvegarde le classifieur en tant que modèle. ###
        with open(model_path, 'wb') as __f:
            pickle.dump(self.clf, __f)

    def displayFPFN(self, preds):
        """
		Affiche les faux positifs et les faux négatifs pour une étude plus poussée des erreurs.

				Args:
					preds (int[]): : une liste des valeurs prédites.

				Returns:
					(none)
		"""

        ### On instancie les listes des faux positifs et des faux négatifs. ###
        fp = list()
        fn = list()

        ### On parcourt le tableau des prédictions pour obtenir les faux positifs et les faux négatifs. ###
        for index, pred in enumerate(preds):

            ### Faux positifs. ###
            if pred == 1 and self.labels[self.n_split:][index] == 0:
                fp.append(self.users_array[self.n_split:][index]['username'])
            if pred == 0 and self.labels[self.n_split:][index] == 1:
                fn.append(self.users_array[self.n_split:][index]['username'])

        print('\nFaux positifs:\n\n')
        pp.pprint(fp)
        print('\n\nFaux négatifs:\n\n')
        pp.pprint(fn)

    def classify_user(self):
        """
		Classe un utilisateur Instagram selon le modèle déjà entraîné.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        ### Ouvre le modèle de classification. ###
        with open(model_path, 'rb') as f:
            self.clf = pickle.load(f)

        ### Ouvre le modèle DictVec. ###
        with open(dictvec_model_path, 'rb') as f:
            self.dictvec = pickle.load(f)

            ### L'utilisateur entre un nom de profil Instagram afin d'utiliser le modèle de classification, et estimer si cette personne est un influenceur ou non. ###
            while True:
                username = input('Username: '******'Result:\n\n%s\nScore: %.2f%%\n' %
                        ('Influencer !' if pred == 1 else 'Not an influencer.',
                         float(y_score[0][1] * 100)))

                except Exception as e:
                    print(e)
                    print(
                        'The user doesn\'t exist or has a private account. Please try again.'
                    )
                    pass

    def pearsonr(self, x, y):
        """
		Calcule le coefficient de corrélation de Pearson.

			Args:
				x (float[]) : La liste des échantillons de la variable aléatoire X.
				y (float[]) : La liste des échantillons de la variable aléatoire Y.
			
			Returns:
				(float) Le coefficient de corrélation de Pearson compris entre 1 (corrélation linéaire parfaite) et -1 (corrélation linéaire négative parfaite).
		"""

        multlist = lambda a, b: map(operator.mul, a, b)
        xy = multlist(x, y)
        xx = multlist(x, x)
        yy = multlist(y, y)
        return (mean(xy) - mean(x) * mean(y)) / math.sqrt(
            (mean(xx) - mean(x)**2) * (mean(yy) - mean(y)**2))

    def correlationAnalysis(self):
        mat = []
        for index, xkey in enumerate(tqdm(self.key_features)):
            row = []
            for ykey in self.key_features[index:]:
                row.append(
                    self.pearsonr([user[xkey] for user in self.users_array],
                                  [user[ykey] for user in self.users_array]))
            row = [0] * index + row
            mat.append(row)
        pp.pprint(mat)
예제 #5
0
class User(object):
    """
	Classe utilisateur.
	"""
    def __init__(self):
        """
		__init__ function.
		"""

        ### L'utilisateur hérite de la classe `object`. ###
        super().__init__()

        ### On charge les variables depuis le fichier de config. ###
        self.config = configparser.ConfigParser()
        self.config.read(config_path)

        self.username = ''

        ### Instanciation du client SQL. ###

        self.n_clusters = N_CLUSTERS

        ### Initialisation des features pour l'apprentissage. ###
        self.lastpost = 0
        self.frequency = 0
        self.engagement = 0
        self.followings = 0
        self.followers = 0
        self.usermentions = 0
        self.brandpresence = 0
        self.brandtypes = 0
        self.commentscore = 0

        ### Paramètres de fonctions maths, pour ajustement de scores. ###
        self.K = 0.17
        self.K_ = 7
        self.B = 0.5

    def getUserNames(self, limit=0):
        """
		Récupère les usernames des utilisateurs annotés, sous la forme:

		[
			{"username": "******"},
			{"username": "******"},
			...
		]

		Args:
				limit (int) : La limite du nombre d'utilisateurs à retourner.

		Returns:
				(list) : la liste des noms d'utilisateur issus de la BDD.
		"""
        self.sqlClient = SqlClient()

        ### Récupération des noms d'utilisateurs annotés. ###
        self.sqlClient.openCursor()
        allUsers = self.sqlClient.getUserNames(limit, labeled=True)
        self.sqlClient.closeCursor()
        return allUsers

    def getUserInfoIG(self):
        """
		Récupération des critères de l'utilisateur via l'API d'Instagram.
		Utilisée lorsqu'on veut tester notre modèle en live, sur un utilisateur qui n'est pas forcément en base.

		Args:
				(none)

		Returns:
				(none)
		"""

        igusername = self.config['Instagram']['user']
        igpassword = self.config['Instagram']['password']

        ### Connexion à l'API. ###
        self.InstagramAPI = InstagramAPI(igusername, igpassword)
        self.InstagramAPI.login()
        ### On essaye d'extraire les features du profil Instagram. 								  					 ###
        ### Si il y a une erreur, on pass (on ne veut pas break e script en cas de re-promptage). 					 ###
        ### Les `time.sleep` préviennent des erreurs 503, dues à une sollicitation trop soudaine de l'API Instagram. ###

        self.loadModels()

        username = self.username

        ### On questionne l'API à propos du nom d'utilisateur, cela nous retourne l'utilisateur en entier. ###
        self.InstagramAPI.searchUsername(username)
        user_server = self.InstagramAPI.LastJson['user']

        ########################
        ### AUDIENCE, MEDIAS ###
        ########################

        self.followings = int(user_server['following_count'])
        self.followers = int(user_server['follower_count'])
        self.usermentions = int(user_server['usertags_count'])
        self.nmedias = int(user_server['media_count'])
        self.biography = str(user_server['biography'])
        if 'category' in user_server:
            self.category = str(user_server['category'])
        else:
            self.category = ''
        self.is_verified = str(user_server['is_verified'])

        ### On récupère le feed entier de l'utilisateur, afin d'analyser certaines métriques. ###
        self.InstagramAPI.getUserFeed(user_server['pk'])
        self.feed = self.InstagramAPI.LastJson['items']

        if len(self.feed) == 0:
            print('This user has no posts !')
            return

        ### On affiche la longueur du feed retourné par l'API. ###
        print('Feed is %s post-long' % str(len(self.feed)))

        ### On initialise les listes utiles pour l'étude. ###
        self.initLists()

        ### On boucle sur le feed afin d'en extraire les données pertinentes pour le calcul de nos features. 					       ###
        ### On prend l'intégralité de la première réponse de l'API (~ 12 - 18 posts) pour ne pas avoir un temps d'éxécution trop long. ###
        for post in tqdm(self.feed):  ### ICI CA PEUT PETER

            ###################################
            ### LIKES, COMMENTS, ENGAGEMENT ###
            ###################################

            ### Ajout du nombre de likes et de commentaires pour moyenner sur le feed. ###
            self.likeslist.append(post['like_count'])
            self.commentslist.append(post['comment_count'])

            ### On ajoute le timestamp du post pour les analyses de fréquence. ###
            self.timestamps.append(int(post['taken_at']))

            self.addEngagementRate(n_likes=post['like_count'],
                                   n_comments=post['comment_count'])

            ##############
            ### IMAGES ###
            ##############

            ### Depuis le JSON de réponse de l'API, on récupère l'adresse URL de la plus petite image. ###
            url = get_post_image_url(post)

            ### On fait une requête HTTP.GET sur l'adresse récupérée, puis on entrait les octets de l'image en réponse. ###
            response = requests.get(url)
            self.imageAnalysis(response.content)

            ##############
            ### BRANDS ###
            ##############

            ### Fetch les marques détectées dans les posts. ###
            brpsc = self.getBrandPresence(post)
            if brpsc:
                self.brpscs.extend(brpsc)

            ################
            ### COMMENTS ###
            ################

            ### On récupère le score de commentaires sur tout le feed de l'utilisateur. ###
            self.InstagramAPI.getMediaComments(str(post['id']))
            comments_server = self.InstagramAPI.LastJson

            if 'comments' in comments_server:
                comments = [
                    comment['text'] for comment in comments_server['comments']
                ]
            else:
                comments = list()

            self.addCommentScore(comments)

        ################
        ### FEATURES ###
        ################

        ### Assignation des critères et affichage des résultats. ###
        self.extractFeatures()

        self.printFeatures()

    def getUserInfoSQL(self):
        """
		On récupère les posts de l'utilisateur à partir de la BDD, et on en extrait les features nécessaires pour l'apprentissage.
		L'intérêt de cette méthode est qu'on peut solliciter la BDD très vite par rapport à l'API Instagram, ce qui nous permet de faire un
		apprentissage 'rapide'!
		La méthode est cependant très similaire à `self.getUserInfoIG()`.

				Args:
						(none)

				Returns:
						(none)
		"""
        self.sqlClient = SqlClient()

        self.sqlClient.openCursor()
        posts = self.sqlClient.getUserPosts(self.username)

        ###	Initialisation des listes de stockage pour les métriques. ###
        self.initLists()
        self.loadModels()

        ########################
        ### AUDIENCE, MEDIAS ###
        ########################

        self.followings = int(posts[0]['n_following'])
        self.followers = int(posts[0]['n_follower'])
        self.usermentions = int(posts[0]['n_usertags'])
        self.nmedias = int(posts[0]['n_media'])
        self.biography = str(posts[0]['biography'])
        self.category = str(posts[0]['category'])
        self.is_verified = posts[0]['is_verified']

        self.feed = posts

        for post in posts:

            self.likeslist.append(post['n_likes'])
            self.commentslist.append(post['n_comments'])

            #######################################
            ### TIMESTAMPS ET TAUX D'ENGAGEMENT ###
            #######################################

            self.timestamps.append(int(post['timestamp']))
            self.addEngagementRate(n_likes=post['n_likes'],
                                   n_comments=post['n_comments'])

            ##############
            ### IMAGES ###
            ##############

            img = post['image']

            self.imageAnalysis(img)

            ##############
            ### BRANDS ###
            ##############

            ### Pour l'instant on ne s'en sert pas, à ré-utiliser quand on s'intéressera à la détection des placements de produits. ###
            """
			brpsc = self.getBrandPresence(post)
			if brpsc:
				brpscs.extend(brpsc)
			"""

            ################
            ### COMMENTS ###
            ################

            # On récupère les commentaires depuis la BDD, grâce à l'ID du post.
            self.sqlClient.openCursor()
            comments = self.sqlClient.getComments(str(post['id_post']))
            self.sqlClient.closeCursor()

            comments_only = [comment['comment'] for comment in comments]

            ### On parcourt les commentaires du post pour en extraire le "score de commentaires". ###
            self.addCommentScore(comments_only)

        ### Dernière phase: on affecte les variables d'instance (= features) une fois que tous les critères ont été traités. ###

        ################
        ### FEATURES ###
        ################

        self.extractFeatures()

        ### Ici, on cherche à avoir la distorsion des k-means des couleurs du feed. 						###
        ### Parfois, on peut avoir que une ou deux couleurs outputées du k-mean.                            ###
        ### Si on a une erreur, on baisse le nombre de clusters jusqu'à ce que le k-mean puisse être opéré. ###

        self.label = int(post['label'])
        self.testset = post['test_set']

    def loadModels(self):
        """
		Charge les modèles pré-enregistrés pour l'analyse de l'utilisateur.

				Args:
					None
				
				Returns:
					None
		"""

        if not os.path.isfile(os.path.join(comments_model_path)):
            print('Creating comments model...')

            ### Crée le modèle de commentaires s'il n'existe pas. ###
            self.createCommentsModel()

        if not os.path.isfile(os.path.join(biographies_model_path)):
            print('Creating biographies model...')

            ### Crée le modèle de biographies s'il n'existe pas. ###
            self.createBiographiesModel()

        ### Charge le modèle de commentaires. ###
        self.comments_model = pickle.load(open(comments_model_path, 'rb'))

        ### Charge le modèle de biographies. ###
        self.count_vect, self.tfidf_transformer, self.clf = pickle.load(
            open(biographies_model_path, 'rb'))

    def initLists(self):
        """
		Initialise les listes de stockage utilisées pour l'étude du profil.

				Args:
					None
				
				Returns:
					None
		"""
        self.rates = list()
        self.timestamps = list()
        self.comment_scores = list()
        self.brpscs = list()
        self.colorfulness_list = list()
        self.dominant_colors_list = list()
        self.contrast_list = list()
        self.likeslist = list()
        self.commentslist = list()

    def addEngagementRate(self, n_likes, n_comments):
        """
		Ajoute le taux d'engagement du post à la liste des taux d'engagements.

				Args:
					- n_likes (int) : le nombre de likes du post.
					- n_comments (int) : le nombre de commentaires du post.

				Returns:
					None 
		"""

        ### Si l'utilisateur n'a pas de followers, on considère que le taux d'engagement est 0 (au lieu d'infini). ###
        if self.followers == 0:
            engagement_rate = 0

        else:
            ### Pourcentage du taux d'engagement. ###
            k = 100 / self.followers

            ### On distingue plusieurs cas: celui où il y a commentaires et likes, celui où il en manque un des deux, et celui où il n'y a rien. ###
            if n_likes and n_likes > 0:
                if n_comments and n_comments > 0:
                    engagement_rate = (int(n_likes) + int(n_comments)) * k
                else:
                    engagement_rate = int(n_likes) * k

            else:
                if n_comments and n_comments > 0:
                    engagement_rate = int(n_comments) * k
                else:
                    engagement_rate = 0

        ### On ajoute le taux d'engagement du post à la liste de taux d'engagement. ###
        self.rates.append(engagement_rate)

    def imageAnalysis(self, imageIO):
        """
		Effectue une analyse des images du feed de l'utilisateur à partir de l'URL donnée.
		On récupère le code binaire des images, et on y opère les traitements :
		- STD du contraste
		- STD de l'intensité colorimétrique
		- Distorsion des clusters de couleur

				Args:
					- url (str) : l'URL de l'image, à aller récupérer.
				
				Returns:
					None
		"""

        try:
            img = Image.open(BytesIO(imageIO))

            ### On convertit l'image en N&B pour l'étude du contraste. ###
            grayscale_img = img.convert('LA')

            ### On ajoute la couleur dominante du post pour une analyse colorimétrique. ###
            most_dominant_colour = self.getMostDominantColour(img)
            self.dominant_colors_list.append(most_dominant_colour)

            ### On récupère le taux de colorité de l'image, qu'on ajoute à la liste globale si cette première n'est pas nan. ###
            colorfulness = self.getImageColorfulness(img)
            self.colorfulness_list.append(colorfulness)

            ### On récupère le taux de contraste de l'image, qu'on ajoute à la liste globale si cette première n'est pas nan. ###
            contrast = self.getContrast(grayscale_img)
            if not math.isnan(contrast):
                self.contrast_list.append(contrast)
        except Exception as e:
            print(e)

    def addCommentScore(self, comments):
        """
		Génère le score de commentaires pour le post et l'ajoute à la liste de scores de commentaires.

				Args:
					comments (str[]) : la liste des commentaires du post.
				
				Returns:
					None
		"""
        ### On cherche tous les commentaires retournés dans la variable `comments`. ###
        for comment in comments[:10]:
            ### On ne prend que les 10 premier commentaires pour chaque post. ###

            score = self.getCommentScore(comment)
            self.comment_scores.append(score)

    def extractFeatures(self):
        """
		Extrait les features relatives à l'étude.

			Args:
				None

			Returns:
				None
		"""

        self.lastpost = time.time() - max(self.timestamps)
        self.frequency = self.calculateFrequency(len(self.feed),
                                                 min(self.timestamps))
        self.engagement = mean(self.rates)
        self.avglikes = mean(self.likeslist) if len(self.likeslist) > 1 else 0
        self.avgcomments = mean(
            self.commentslist) if len(self.commentslist) > 1 else 0
        self.brandpresence = self.brpscs
        self.brandtypes = self.getBrandTypes(self.brpscs)
        self.commentscore = mean(self.comment_scores) * (1 + stdev(
            self.comment_scores)) if len(self.comment_scores) > 1 else 0
        self.biographyscore = self.getBiographyScore(self.biography)
        self.colorfulness_std = stdev(
            self.colorfulness_list) if len(self.colorfulness_list) > 1 else 0
        self.contrast_std = stdev(
            self.contrast_list) if len(self.contrast_list) > 1 else 0
        self.colors = [[color.lab_l, color.lab_a, color.lab_b]
                       for color in self.dominant_colors_list]
        self.colors_dispersion = self.calcCentroid3d(self.colors)

        while True:
            try:
                if (self.n_clusters == 0):
                    break
                self.codes, self.color_distorsion = scipy.cluster.vq.kmeans(
                    np.array(self.colors), self.n_clusters)
            except Exception as e:
                self.n_clusters = self.n_clusters - 1
                continue
            break

    def printFeatures(self):
        """
		Affiche les différents critères de l'étude.

			Args:
				None
			
			Returns:
				None
		"""

        print('Username : %s' % self.username)
        print('Is verified: %s' % str(self.is_verified))
        print('Category: %s' % str(self.category))
        print('N media: %s' % str(self.nmedias))
        print('Last post: %s' % self.uiGetIlya(max(self.timestamps)))
        print('Frequency: %.2f' % float(self.frequency))
        print('Engagement: %.2f%%' % float(self.engagement))
        print('Average like count: %.2f' % float(self.avglikes))
        print('Average comment count: %.2f' % float(self.avgcomments))
        print('N followings: %s' % self.uiFormatInt(self.followings))
        print('N followers: %s' % self.uiFormatInt(self.followers))
        print('User mentions: %s' % self.uiFormatInt(self.usermentions))
        print('Brand presence: %s' % str(self.brandpresence))
        print('Brand types: %s' % str(self.brandtypes))
        print('Biography score: %.2f' % float(self.biographyscore))
        print('Comments score: %.2f' % float(self.commentscore))
        print('Colorfulness standard deviation: %.2f' %
              float(self.colorfulness_std))
        print('Contrast standard deviation: %.2f' % float(self.contrast_std))
        print('Overall color distorsion : %.2f' % float(self.color_distorsion))

    def uiFormatInt(self, n):
        """
		Conversion du nombre de followers/abonnements en K (mille) et M (million).

		Args:
				n (int) : nombre à convertir en format souhaité.

		Returns:
				(str) Un string formatté.
		"""

        if n > 1000000:
            return '{:.1f}M'.format(n / 1000000)
        elif n > 1000:
            return '{:.1f}K'.format(n / 1000)
        return str(n)

    def uiGetIlya(self, _time):
        """
		Génération du string (exemple) :
		Il y a 15 jours, 0 heure, 36 minutes et 58 secondes.

		Args:
				_time (int) le temps en format POSIX.

		Returns:
				(str) Un string formatté.
		"""

        ### Différence de time POSIX (donc des secondes) entre le temps considéré, et la date actuelle. ###
        ilya = math.floor(time.time() - _time)

        ### Définition des mutliplicateurs jours, heures, secondes. ###
        days_mult = 60 * 60 * 24
        hours_mult = 60 * 60
        minutes_mult = 60

        ### Conversion et troncage des jours, heures, secondes pour le formatttage en chaine de caractère. ###
        days = ilya // days_mult
        hours = (ilya - days * days_mult) // hours_mult
        minutes = (ilya - days * days_mult -
                   hours * hours_mult) // minutes_mult
        seconds = ilya % minutes_mult

        ### On retourne les valeurs en prenant en compte les singuliers et pluriels. ###
        return '%s day%s, %s hour%s, %s minut%s, %s second%s' % (
            days, '' if days in [0, 1] else 's', hours, '' if hours in [0, 1]
            else 's', minutes, '' if minutes in [0, 1] else 's', seconds,
            '' if seconds in [0, 1] else 's')

    def calculateFrequency(self, n, min_time):
        """
		Calcul de la fréquence de post.

		Args:
				n (int) : le nombre de posts à considérer dans l'intervalle de temps donné.
				min_time (int) : le plus vieux post, à comparer avec la date actuelle.

		Returns:
				(int) La fréquence de post.
		"""

        ### Différence de time POSIX (donc des secondes) entre le post le plus ancien, et la date actuelle. ###
        ilya = math.floor(time.time() - min_time)

        ### Calcul de la fréquence de post. ###
        days = ilya // (60 * 60 * 24)
        days = days if days != 0 else 1
        return float(n / days)

    def calcCentroid3d(self, _list):
        """
		Calcul des distances de tous les points au barycentre des points de couleur dans le repère lab*.

		Args:
				_list ((int * 3)[]) : liste de points 3D dont on veut calculer le barycentre.

		Returns:
				(int) La distance moyenne de tous les points du nuage au barycentre de ce dernier.
		"""

        arr = np.array(_list)
        length = arr.shape[0]
        sum_x = np.sum(arr[:, 0])
        sum_y = np.sum(arr[:, 1])
        sum_z = np.sum(arr[:, 2])

        ### Calcul des coordonnées du barycentre. ###
        centroid = np.array([sum_x / length, sum_y / length, sum_z / length])

        ### Calcul des distances de tous les points au barycentre. ###
        distances = [np.linalg.norm(data - centroid) for data in _list]
        return float(mean(distances))

    def getMostDominantColour(self, image):
        """
		Retourne la couleur dominante de l'image.

				Args:
						image (Image PIL) : l'image qu'on considère pour l'étude.

				Returns:
						(tuple) La couleur dominante de l'image dans le repère lab*.
		"""

        ### Définition du nombre de clusters pour les pixels. ###
        NUM_CLUSTERS = 5

        ### On resize l'image pour que les temps de traitement soient réduits. ###

        ar = np.array(image)
        shape = ar.shape
        ar = ar.reshape(scipy.product(shape[:2]), shape[2]).astype(float)

        ### On opère un K-mean sur les pixels de l'image. ###
        codes, dist = scipy.cluster.vq.kmeans(ar, NUM_CLUSTERS)
        vecs, dist = scipy.cluster.vq.vq(ar, codes)
        counts, bins = scipy.histogram(vecs, len(codes))
        index_max = scipy.argmax(counts)

        ### La couleur la plus importante de l'image, déduite du décompte de l'histogramme des couleurs. ###
        peak = codes[index_max]

        ### Conversion de la couleur RGB dans l'espace lab*. ###
        rgb = sRGBColor(*peak)
        return convert_color(rgb, LabColor)

    def getImageColorfulness(self, image):
        """
		Retourne l'intensité colorimétrique de l'image.

				Args:
						image (Image PIL) : l'image qu'on considère pour l'étude.

				Returns:
						(int) L'intensité colorimétrique de l'image.
		"""

        ar = np.array(image)

        ### Transforme l'image pour OpenCV. ###
        open_cv_image = ar[:, :, ::-1].copy()

        ### Performe une analyse des composantes RGB de l'image. ###
        B, G, R, *A = cv2.split(open_cv_image.astype("float"))
        rg = np.absolute(R - G)
        yb = np.absolute(0.5 * (R + G) - B)
        (rbMean, rbStd) = (np.mean(rg), np.std(rg))
        (ybMean, ybStd) = (np.mean(yb), np.std(yb))
        stdRoot = np.sqrt((rbStd**2) + (ybStd**2))
        meanRoot = np.sqrt((rbMean**2) + (ybMean**2))
        return float(stdRoot + (0.3 * meanRoot))

    def getContrast(self, img):
        """
		Retourne le contraste global de l'image en passant par un calcul d'entropie.

				Args:
					img (Image PIL) : l'image N&B qu'on considère pour l'étude (matrice d'entiers allant de 0 à 255).
				
				Returns:
					(int) Le contraste de l'image.
		"""

        ### Transforme l'image pour OpenCV. ###
        ar = np.array(img)

        ### Histogramme de l'image (niveaux de gris). ###
        hist = np.histogram(ar)
        data = hist[0]

        ### Normalisation de l'histogramme. ###
        data = data / data.sum()

        ### On retourne le calcul de l'entropie. ###
        return float(-(data * np.log(np.abs(data))).sum())

    def getBrandPresence(self, post):
        """
		Retourne les mentions utilisateur qui matchent avec les utilisateurs mentionnés dans la description du post.
		
				Args:
					post (dict) : un post Instagram sous forme de dictionnaire.

				Returns:
					(str[]) Le tableau contenant les marques détectées.
		"""

        ### Définition de la liste de stockage des marques. ###
        brands = list()

        ### On essaye de voir s'il y a des marques dans le champ du post. 									  ###
        ### Pour cela, on compare les mentions utilisateurs dans la description du post :                     ###
        ### Les mentions dans la descriptions sont activées par un '@utilisateur'.                            ###
        ### On compare ces dernières avec les utilisateurs tagués sur la photo.                               ###
        ### Si les deux existent, on considère que l'utilisateur a cherché à mettre le profil tagué en avant. ###
        ### S'il n'y a pas de marques, on a une erreur; on pass. 		                                      ###
        try:
            text = post['caption']['text']

            ### On ajoute un '@' pour matcher avec les mentions trouvées dans la description du post. ###
            if hasattr(post, 'usertags'):
                usertags = [
                    '@%s' % user['user']['username']
                    for user in post['usertags']['in']
                ]

                ### On match les mentions utilisateur dans la description du post. ###
                matches = regex.findall(r'@[\w\.]+', text)

                ### Si il y a un utilisateur qui se retrouve à la fois tagué sur la photo et en description du post, alors on l'extrait. 			###
                ### Ce modèle est perfectible, on peut aussi décider de s'occuper uniquement des personnes taguées et/ou des personnes mentionnées. ###
                for match in matches:
                    if match in usertags:
                        brands.append(match.split('@')[1])

            return brands

        except:
            pass

    def getBrandTypes(self, brands):
        """
		Retourne les types de business que sont les 'marques' mentionnées à la fois dans la description du post et en mention utilisateur.

				Args:
					brands (str[]) : le tableau des utilisateurs détectés.
				
				Returns:
					(Counter) Le compteur de types de profils utilisateur (Blog, Photographe, Acteur, etc.), si le type de compte est 'Business' seulement.
		"""

        ### Définition du compteur de marques. ###
        brand_counter = Counter()

        ### On itère sur les noms de marques. ###
        for brand in brands:

            ### On récupère le type de compte (si le compte est de type 'Business' seulement). ###
            self.InstagramAPI.searchUsername(brand)
            brand_full = self.InstagramAPI.LastJson['user']

            ### Si l'utilisateur est 'Business'. ###
            if 'category' in brand_full:
                brand_counter[brand_full['category']] += 1
        return brand_counter

    def getBiographyScore(self, bio):
        """
		Retourne le score de biographie basé sur le modèle de biographies.
		
				Args:
					comment (str): la biographie sous forme de texte.
				
				Returns:
					(int) Le score de qualité de biographie.
		"""

        if not os.path.isfile(os.path.join(biographies_model_path)):
            print('Creating biographies model...')

            ### Crée le modèle de biographies s'il n'existe pas. ###
            self.createBiographiesModel()

        self.count_vect, self.tfidf_transformer, self.clf = pickle.load(
            open(biographies_model_path, 'rb'))

        X_test_counts = self.count_vect.transform([bio])
        self.X_test_tfidf = self.tfidf_transformer.transform(X_test_counts)

        pred = self.clf.predict_proba(self.X_test_tfidf)[0][1]
        return float(pred)

    def getCommentScore(self, comment):
        """
		Retourne le score de commentaire basé sur le modèle de commentaires.
		Les commentaire les plus pertinents pour une photo (= dont les mots importants sont peu utilisés dans le modèle de commentaires) sont privilégiés.
		Les commentaires les plus longs sont privilégiés.
		
				Args:
					comment (str): le commentaire texte.
				
				Returns:
					(int) Le score de qualité de commentaire, situé entre 0 et 1.
		"""

        ### Pickle le modèle (dictionnaire de mots contenus dans les commentaires + leurs occurences) pour ne pas avoir à le générer à chaque run. ###

        ### Liste des scores de commentaires. ###
        word_scores = list()

        ### Regex pour attraper les mots dans différents alphabets, dont les emojis. ###
        for word in regex.compile(r'[@#\p{L}_\u263a-\U0001f645]+').findall(
                comment):

            ### Pré-process le mot. ###
            _word = self.processWordComment(word)

            if _word:

                ### Attribue un score de commentaire inversement proportionnel à ses occurences dans tous les commentaires de la BDD. ###
                if self.comments_model[_word] > 0:
                    word_score = 1 / self.comments_model[_word]

                else:
                    word_score = 1
                word_scores.append(word_score)

        ### S'il n'y a pas de mots, on attribue 0 pour le commentaire. ###
        ### Sinon, on prend la moyenne. 							   ###
        if len(word_scores) > 0:
            comment_score = mean(word_scores)
        else:
            comment_score = 0

        ### On mutliplie le score de commentaires par des composantes paramétriques: on privilégie d'abord les commentaires les plus longs. ###
        k = 1 - math.exp(-self.K * len(word_scores))

        ### Ensuite, on privilégie les commentaires qui on des mots dont l'écart-type des scores est important. 					    ###
        ### La raison derrière cela, est que les stop-words sont des mots qui vont forcément avoir un score faible.                     ###
        ### De ce fait, si les mots-clés du commentaire sont 'importants' = si leur score est élevé, alors l'écart-type sera important. ###
        j = 1 / (1 + math.exp(-self.K_ * (stdev(word_scores) - self.B))
                 ) if len(word_scores) > 1 else 0

        return k * j * comment_score * len(word_scores)

    def createCommentsModel(self):
        """
		Crée le modèle de commentaires.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        self.sqlClient = SqlClient()

        ### Récupère tous les commentaires en base. ###
        self.sqlClient.openCursor()
        comments = self.sqlClient.getAllComments()
        self.sqlClient.closeCursor()

        ### Déclaration des variables locales pour les compteurs. ###
        comment_count = Counter()
        i = 0
        j = 0

        ### On boucle sur les commentaires et on affiche la progression avec TQDM. ###
        for comment in tqdm(comments):
            comment = str(comment)

            ### Trouve tous les mots, dont les emojis et les caractères de différents alphabets. ###
            wordArray = regex.compile(r'[@#\p{L}_\u263a-\U0001f645]+').findall(
                comment)
            length = len(wordArray)

            ### Pour chaque mot trouvé, on le préprocess. ###
            for word in wordArray:
                _word = self.processWordComment(word)

                ### Ajoute au compteur non pas l'occurence simple du mot, mais dans quel contexte celui-là apparaît (se trouve-t-il dans une phrase longue ou courte ?). ###
                if _word:
                    i += 1
                    comment_count[_word] += 1 / length
                else:
                    j += 1
        print('Éléments considérés : %s' % str(i))
        print('Éléments non considérés : %s' % str(j))

        ### Sauvegarde le modèle dans le dossier models. ###
        with open(os.path.join(comments_model_path), 'wb') as outfile:
            pickle.dump(comment_count, outfile)

    def createBiographiesModel(self):
        """
		Crée le modèle de commentaires.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        self.sqlClient = SqlClient()

        ### Récupère toutes les biographies des utilisateurs annotés. ###
        self.sqlClient.openCursor()
        response = self.sqlClient.getAllBiographies()
        self.sqlClient.closeCursor()

        ### Récupération des champs biographies, et labels. ###
        bios = [row['biography'] for row in response]
        labels = [row['label'] for row in response]

        ### Construction du modèle TF-IDF et apprentissage d'un classifieur calibré pour sortir des probabilités. ###
        ### Vecteurs de comptage. ###
        self.count_vect = CountVectorizer()
        X_train_counts = self.count_vect.fit_transform(bios)

        ### Transformateur TF-IDF. ###
        self.tfidf_transformer = TfidfTransformer()
        self.X_train_tfidf = self.tfidf_transformer.fit_transform(
            X_train_counts)

        ### Entraînement du classificateur. ###
        oneVsRest = CalibratedClassifierCV()
        self.clf = oneVsRest.fit(self.X_train_tfidf, labels)

        ### Sauvegarde du modèle. ###
        with open(os.path.join(biographies_model_path), 'wb') as outfile:
            pickle.dump((self.count_vect, self.tfidf_transformer, self.clf),
                        outfile)

    def processWordComment(self, word):
        """
		Pré-processe le commentaire.

				Args:
					word (str) : le mot.

				Return:
					(str) Le mot pré-processé.
		"""

        ### On vérifie si le mot ne commence pas par un @ (mention) ou un # (hashtag). ###
        ### On vérifie si le "mot" n'est pas une ponctuation. 						   ###
        if word[0] in ['#', '@'] or word in [
                '.', '!', '?', ',', ':', ';', '-', '+', '=', '/', '&', '@',
                '$', '_'
        ]:
            word = None

        else:
            try:
                ### Ici ça pète quand il y a du russe ou des emojis. ###
                word = str(word).lower()
            except:
                word = word
        return word

    def cleanNonExistingUsers(self):
        """
		Supprime les utilisateurs en base qui ont changé de nom/n'existent plus.

				Args:
					None
				
				Returns:
					None
		"""
        self.sqlClient = SqlClient()

        self.sqlClient.openCursor()
        usernames = self.sqlClient.getUserNames(0)
        self.sqlClient.closeCursor()

        self.InstagramAPI = InstagramAPI(self.config['Instagram']['user'],
                                         self.config['Instagram']['password'])
        self.InstagramAPI.login()

        for username in tqdm(usernames):

            self.InstagramAPI.searchUsername(username['user_name'])
            user_server = self.InstagramAPI.LastJson

            if user_server['status'] == 'fail':
                self.sqlClient.openCursor()
                self.sqlClient.setLabel(username['user_name'], '-2')
                self.sqlClient.closeCursor()

            time.sleep(1)

    def testCommentScore(self):
        """
		Tooling permettant de tester les scores des commentaires utilisateur.

				Args:
					(none)
				
				Returns:
					(none)
		"""

        while True:

            ### L'utilisateur rentre un nom de compte Instagram pour tester le modèle de commentaires. ###
            text = input(
                'Comment and I will tell your score ! (type \'exit\' to go next) : '
            )

            ### L'utilisateur entre "exit" pour sortir de la boucle. ###
            if text == 'exit':
                break

            ### Sort le score de commentaires pour l'utilisateur en question. ###
            else:
                comment_score = self.getCommentScore(text)
                print(text)
                print(comment_score)
예제 #6
0
sys.path.append(os.path.dirname(__file__))

### Custom libs. ###
from sql_client import SqlClient

pp = pprint.PrettyPrinter(indent=2)

if __name__ == "__main__":

    ### Définition du graphe. ###
    G = nx.DiGraph()

    sqlClient = SqlClient()

    ### Récupère tous les likes pour effectuer un graphe de liker/liké. ###
    sqlClient.openCursor()
    likes = sqlClient.getAllLikes(500)
    sqlClient.closeCursor()

    ### Ajout des nodes et des edges via. la requête à la BDD. ###
    G.add_edges_from(likes)
    '''d_in=G.in_degree(G)
    d_out=G.out_degree(G)
    g2 = G.copy()
    for n in g2.nodes():
        if d_in[n]==0 and d_out[n] == 1: 
            G.remove_node(n)'''

    ### Show Graph. ###

    options = {