Analyse des avis récupérés sur monavislerendsgratuit.com pour le produit San Pellegrino Limone + Tea ou Pesca + Tea

Le jeu de données a été extrait du site monavislerendsgratuit à l'aide du framework scrapy. Le code permettant l'extraction peut être trouvé ici : https://github.com/Mg30/ScrapyCrawlerConsumerReviews

Aperçu du jeu de données

In [3]:
df.head()
Out[3]:
comment consummer_rate date product_avg_rate product_name user_info username
0 On ne sent pas tellement le citron 3 3 Octobre 2019 4.16275 San Pellegrino Limone + Tea ou Pesca + Tea [\n\t\t\t\t\t\tSaint-andré-lez-lille, 43 ans\t... Virgil C
1 Super bon surtout quand il est au frais 5 4 Octobre 2019 4.16275 San Pellegrino Limone + Tea ou Pesca + Tea [\n\t\t\t\t\t\tNanterre, 41 ans\t\t\t\t\t, Ave... Saleha M
2 Très bien 5 17 Octobre 2019 4.16275 San Pellegrino Limone + Tea ou Pesca + Tea [\n\t\t\t\t\t\tLe plessis trevise, 37 ans\t\t\... Mendes K
3 Très bonne boisson the citron 5 4 Octobre 2019 4.16275 San Pellegrino Limone + Tea ou Pesca + Tea [\n\t\t\t\t\t\tGermigny, 42 ans\t\t\t\t\t] Valerie C
4 Trop bon et trop petit on y retourne 5 3 Octobre 2019 4.16275 San Pellegrino Limone + Tea ou Pesca + Tea [\n\t\t\t\t\t\tMonaco, 52 ans\t\t\t\t\t, Avec ... Elena G
In [4]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 620 entries, 0 to 619
Data columns (total 7 columns):
comment             620 non-null object
consummer_rate      620 non-null object
date                620 non-null object
product_avg_rate    620 non-null object
product_name        620 non-null object
user_info           620 non-null object
username            620 non-null object
dtypes: object(7)
memory usage: 34.0+ KB

Le jeu de données a un volume de 620 observations et 7 variables, on peut voir également que toutes les variables sont de types object, ce qui suggère qu'un travail de nettoyage va devoir être réalisé

Nettoyage du fichier

L'objectif de cette étape est de rendre le jeu de données plus propre en vue de son analyse

Traitement de la variable user_info

In [5]:
df["user_info"].head()
Out[5]:
0    [\n\t\t\t\t\t\tSaint-andré-lez-lille, 43 ans\t...
1    [\n\t\t\t\t\t\tNanterre, 41 ans\t\t\t\t\t, Ave...
2    [\n\t\t\t\t\t\tLe plessis trevise, 37 ans\t\t\...
3           [\n\t\t\t\t\t\tGermigny, 42 ans\t\t\t\t\t]
4    [\n\t\t\t\t\t\tMonaco, 52 ans\t\t\t\t\t, Avec ...
Name: user_info, dtype: object

On peut voir que la variable user_info regroupe plusieurs informations dans une structure de données de type list, l'objectif est de séparer les différentes informations dans des variables séparées

Récupération des informations relatives à la présence d'enfant ou non

Les personnes s'enregistrant sur le site peuvent renseigner un champ "avec enfant(s)", on considérera que les personnes n'ayant pas renseigné ce champ n'ont pas d'enfants. Cette aproche peut être biaisée car certaines personnes ayant des enfants peuvent ne pas avoir renseigné le champ.

In [6]:
def parse_enfant_from(user_info):
    if len(user_info) == 2:
        return 1
    else:
        return 0
In [7]:
df["enfant"] = df["user_info"].apply(parse_enfant_from)

Récupération des informations relatives à l'age

In [8]:
def parse_age_from(user_info):
    if user_info:
        age = user_info[0].split(',')[-1]
        age = int(age[:3])
        return age
In [9]:
df["age"] = df["user_info"].apply(parse_age_from)

Récupération du lieu d'habitation

In [10]:
def parse_loc_from(user_info):
    if user_info:
        loc = user_info[0].split(',')[0].replace('\t', '').replace('\n', '')
        return loc
In [11]:
df["localisation"] = df["user_info"].apply(parse_loc_from)

Traitement de la variable consummer_rate

La varibale consumer_rate représente la note attribuée au produit par le consommateur, dans le dataframe elle est de type d'object alors qu'elle devrait être de type float. Il faut donc la convertir

In [12]:
df["consummer_rate"] = df["consummer_rate"].astype(float)

Traitement de la varibale date

In [13]:
def parse_date(date):
    import locale
    locale.setlocale(locale.LC_ALL, 'fr_FR')
    date_splited = date.split(' ')
    day = date_splited[0]
    year = date_splited[-1]
    month = date_splited[-2]
    if len(day) == 1:
        day = f'0{day}'
    return  f"{day} {month} {year}"
In [14]:
df["date"] = df["date"].apply(parse_date)
df["date"] = pd.to_datetime(df["date"], format="%d %B %Y")
In [15]:
df["day_week_number"] = df["date"].apply(lambda date: pendulum.instance(date).day_of_week)
In [16]:
df["date"] = df["date"].dt.date

Traitement du username

In [17]:
def reformat(username):
    if username:
        return username[:-2]
In [18]:
df["username"] = df["username"].apply(reformat)

Suppression des variables inutiles

Désormais, il est possible de supprimer les variables suivantes :

  • product_name, car la variable n'apporte pas de plus value
  • user_info car la variable a été traitée et les informations qu'elle contient on été divisées
In [19]:
df = df.drop(columns=["user_info", "product_name"])

Résultat de la transformation

In [20]:
df.head()
Out[20]:
comment consummer_rate date product_avg_rate username enfant age localisation day_week_number
0 On ne sent pas tellement le citron 3.0 2019-10-03 4.16275 Virgil 1 43 Saint-andré-lez-lille 4
1 Super bon surtout quand il est au frais 5.0 2019-10-04 4.16275 Saleha 1 41 Nanterre 5
2 Très bien 5.0 2019-10-17 4.16275 Mendes 1 37 Le plessis trevise 4
3 Très bonne boisson the citron 5.0 2019-10-04 4.16275 Valerie 0 42 Germigny 5
4 Trop bon et trop petit on y retourne 5.0 2019-10-03 4.16275 Elena 1 52 Monaco 4

Enrichissement du jeu de données

L'objectif de cette étape est de voir à partir des variables présentent dans le jeu de données quelles autres informations il est possible d'extraire à partir du jeu de données ou en faisant appel à des services externes

Ajout d'une variable indiquant la taille de chaque commentaire

Il est intéréssant d'avoir la taille des commentaires, puisqu'il est possible que la taille d'un commentaire soit corrélée à la note attribuée. La taille d'un commentaire sera ici considérée comme le nombre de mots qui le compose.

In [21]:
df["len_comment"] = df["comment"].apply(lambda x: len(x.split(" ")))

Ajout region à partir de la ville

Pour obtenir la région à partir de la ville présente dans la variable localisation, il a été utilisé l'api https://api.gouv.fr/api/api-geo.

In [23]:
df_region.head()
Out[23]:
commune region
0 Saint-André-lez-Lille Hauts-de-France
1 Nanterre Île-de-France
2 Le Plessis-Trévise Île-de-France
3 Germigny Grand Est
4 Cagny Normandie
In [25]:
df = df.merge(df_region,left_on='localisation',right_on='commune').drop(columns=['localisation'])

Ajout genre à partir des noms

Pour déterminer le genre à partir du nom, il a été utilisé le site https://gender-api.com

In [28]:
df = df.merge(df_genre,left_on="username", right_on="Nom")
In [30]:
df.head()
Out[30]:
comment consummer_rate date product_avg_rate username enfant age day_week_number len_comment commune region ga_gender
0 Super bon surtout quand il est au frais 5.0 2019-10-04 4.16275 Saleha 1 41 5 8 Nanterre Île-de-France female
1 Boisson fraiche aromatisée, idéale pour l'été ... 4.0 2019-09-19 4.16275 Aurelie 1 36 4 32 Nanterre Île-de-France female
2 bon produit 4.0 2019-09-23 4.16275 Aurelie 0 25 1 2 Meloisey Bourgogne-Franche-Comté female
3 Je n ai pas aimer ce produit 2.0 2019-09-24 4.16275 Aurelie 1 41 2 7 Mardeuil Grand Est female
4 Très bonne boisson the citron 5.0 2019-10-04 4.16275 Valerie 0 42 5 5 Germigny Grand Est female

Ajout d'une variable indiquant si le commentaire est positif

On considérera que tous les commentaires qui sont en dessous de 3 sont négatifs et les autres possitifs

In [63]:
def is_positiv(consummer_rate):
    if consummer_rate < 3:
        return 0
    else:
        return 1
In [32]:
df["is_positiv"] = df["consummer_rate"].apply(is_positiv)

Analyses univariées

Analyse de la variable enfant

In [33]:
sns.countplot(x="enfant",data=df)
Out[33]:
<matplotlib.axes._subplots.AxesSubplot at 0x20575440b70>

Comme on peut le voir la majorité des personnes ayant commentées ont déclaré avoir un ou plusieurs enfants

Analyse de l'age

Analyse de la distribution

In [35]:
sns.boxplot(df["age"])
Out[35]:
<matplotlib.axes._subplots.AxesSubplot at 0x20508ca6518>

La majorité des personnes se situent entre 35 et 52 ans, avec une moyenne de 43 ans. On note que l'ecart en min et max est élévé avec des valeurs allant de 19 ans à 88 ans. Toutefois comme le suggère le diagramme de boite à moustache, les valeurs au dessus de 75 ans parraissent anormales.

Analyse des outliers

In [36]:
df[df["age"] > 72]
Out[36]:
comment consummer_rate date product_avg_rate username enfant age day_week_number len_comment commune region ga_gender is_positiv
83 Ces produits de San Pellegrino sont juste parf... 5.0 2019-09-09 4.16275 Siham 0 74 1 15 Toulouse Occitanie female 1
182 excellent produit, rafraichissant, douceur 5.0 2019-09-17 4.16275 Monique 0 73 2 4 Meylan Auvergne-Rhône-Alpes female 1
208 rafraîchissant 3.0 2019-09-17 4.16275 Nicole 0 73 2 1 Pessac Nouvelle-Aquitaine female 0

Il aurait été possible de suspecter pour ces outliers, des personnes renseignant une date de naissance fictive. Toutefois le détails des outliers montrent qu'il ne s'agit pas de données aberrantes.

Analyse de la variable date

Analyse de la distribution

In [37]:
groupby_date = df.groupby(by='date').describe()['age'].reset_index()
chart = sns.catplot(x='date',y='count',data= groupby_date, aspect=1.5, kind="bar", color="b")
chart.set_xticklabels(rotation=90)
Out[37]:
<seaborn.axisgrid.FacetGrid at 0x2050adbc320>

On peut observer une certaine saisonnalité dans la distribution des données, on peut voir que il y a des jours qui sont priviligés pour commenter qui peut correspondre au jour où les personnes ont fait leur courses. Analysons maintenant les données par position du jour dans la semaine.

In [38]:
groupby_date = df.groupby(by='day_week_number').describe()['age'].reset_index()
chart = sns.catplot(x='day_week_number',y='count',data= groupby_date, aspect=1.5, kind="bar", color="b")
chart.set_xticklabels(rotation=90)
Out[38]:
<seaborn.axisgrid.FacetGrid at 0x2050beb6128>

On peut voir que les personnes ont plus commenté le Lundi et que le nombre de commentaires decroient tout au long de la semaine jusqu'au vendredi au le nombres de commentaires augmentent. On peut en déduire que les personnes ont plus tendance à faire leur course en début de semaine.

Analyse de la variable consummer_rate

In [39]:
sns.boxplot(df["consummer_rate"])
Out[39]:
<matplotlib.axes._subplots.AxesSubplot at 0x2050bfbe668>

On peut voir qu'il y a des outliers, il convient d'analyser les données en dessous de 3.0

Analyse des outliers

In [40]:
outliers = df["comment"][df["consummer_rate"] < 3]
outliers
Out[40]:
3                           Je n ai pas aimer ce produit
7                        Surprenante association de goût
9                          Frais agréable mais sans plus
10     Pas très agreable en goût. Ce produit est trop...
42     Surprise par le goût, je m'attendais plus à un...
46                                      Goût desagreable
113     Décevant car absence total de gaz et peu de goût
144    vraiment très mauvais, le gout n'est vraiment ...
253    Gout de thé très présent. Pas assez pétillant ...
268                           Je n ai pas tellement aimé
Name: comment, dtype: object

On peut voir que les outliers ne représentent pas des données abérantes, elles seront donc gardées dans l'analyse postérieure

Analyse de la longueur des commentaires

In [41]:
df["len_comment"].describe()
Out[41]:
count    285.000000
mean       9.045614
std        7.716456
min        1.000000
25%        4.000000
50%        7.000000
75%       12.000000
max       46.000000
Name: len_comment, dtype: float64
In [42]:
sns.boxplot(df["len_comment"])
Out[42]:
<matplotlib.axes._subplots.AxesSubplot at 0x2050c055668>
In [43]:
sns.barplot(x='consummer_rate', y='len_comment',data=df)
Out[43]:
<matplotlib.axes._subplots.AxesSubplot at 0x2050c0d6860>
In [44]:
sns.scatterplot(x='len_comment', y='age', hue='ga_gender',size='is_positiv',data=df)
Out[44]:
<matplotlib.axes._subplots.AxesSubplot at 0x2050c13bef0>

On peut constater que la majorité des commentaires sont assez courts et ce peu importe la note attribué par l'utilisateur ou son age.

Analyse de la localisation

In [45]:
chart = sns.catplot(x='region',data= df, kind="count", color="b")
chart.set_xticklabels(rotation=90)
Out[45]:
<seaborn.axisgrid.FacetGrid at 0x2050c13b160>

On peut voir que la majorité des commentaires viennent de personnes habitant dans les régions Hauts-de-France suivi par la région Auvergne Rhones Alpes puis l'île de france.

Analyse des genres

In [46]:
sns.countplot(df["ga_gender"])
Out[46]:
<matplotlib.axes._subplots.AxesSubplot at 0x2050c00e048>
In [47]:
g = sns.catplot(x="ga_gender", hue="is_positiv",
                data=df, kind="count",
                height=4, aspect=.7);

On constate que la majorité des personnes qui ont commenté sont de sexe feminin. Toutefois on peut voir que le sexe de la personne ne semble pas influencer la note du produit.

Analyse des commentaires négatif

In [64]:
text = " ".join(
    review for review in df.comment[df["is_positiv"]==0])

Génération d'un nuage de mots

In [66]:
wordcloud = WordCloud(
    stopwords=stopwords, background_color="white", max_words=30).generate(text)
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.show()

Le nuage de mots ne renseigne pas sur les éventuels problèmes que peut avoir le produit. Bien que ce dernier ait une note de 4.1 en moyenne ce qui est élévée. Pour en savoir d'avantage, un nuage de mots bigrammes pourrait permettre d'en apprendre d'avantage sur les possibles améliorations à apporter au produit.

Génération d'un nuage de mots bigramms

In [72]:
wordcloud_bigrams = WordCloud( stopwords=stopwords,background_color="white", max_words=30)
wordcloud_bigrams.generate_from_frequencies(frequencies=freq)
plt.imshow(wordcloud_bigrams, interpolation="bilinear")
plt.axis("off")
plt.show()

On peut voir que le nuage fait ressortir des choses plutôt possitive. Toutefois on peut noter que la boisson est considérée comme sucrée. Enfin on note également que le goût citron peut être trop prononcé.

Analyse bivariées

Identification des variables qui sont corrélées

In [57]:
sns.heatmap(df.corr())
Out[57]:
<matplotlib.axes._subplots.AxesSubplot at 0x2050d7fb710>

On peut voir que la seule correlation, entre consummer_rate et is_possitiv n'est pas significative car il s'agit d'une corrélation qui a été crée dans une des étapes précédentes.

Conclusion

L'objectif de cette analyse était d'explorer les commentaires d'un produit récupérés via le web scrapping afin de déterminer quel type de personnes commentent (même si l'échantillon n'est pas représentatif de l'ensemble du site).

L'autre objectif était d'éventuellement identifié des améliorations possibles en explorant les commentaires négatifs.

En ce qui concerne le type de personnes qui commentent, sur ce jeu de données il s'agit de femmes avec enfants ayant en moyenne 43 ans vivant dans les hauts de seine.

En revanche, l'analyse des commentaires plutôt négatifs n'a pas permis de dégager des pistes d'amélioration.