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
df.head()
| 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 |
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é
L'objectif de cette étape est de rendre le jeu de données plus propre en vue de son analyse
df["user_info"].head()
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
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.
def parse_enfant_from(user_info):
if len(user_info) == 2:
return 1
else:
return 0
df["enfant"] = df["user_info"].apply(parse_enfant_from)
def parse_age_from(user_info):
if user_info:
age = user_info[0].split(',')[-1]
age = int(age[:3])
return age
df["age"] = df["user_info"].apply(parse_age_from)
def parse_loc_from(user_info):
if user_info:
loc = user_info[0].split(',')[0].replace('\t', '').replace('\n', '')
return loc
df["localisation"] = df["user_info"].apply(parse_loc_from)
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
df["consummer_rate"] = df["consummer_rate"].astype(float)
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}"
df["date"] = df["date"].apply(parse_date)
df["date"] = pd.to_datetime(df["date"], format="%d %B %Y")
df["day_week_number"] = df["date"].apply(lambda date: pendulum.instance(date).day_of_week)
df["date"] = df["date"].dt.date
def reformat(username):
if username:
return username[:-2]
df["username"] = df["username"].apply(reformat)
Désormais, il est possible de supprimer les variables suivantes :
df = df.drop(columns=["user_info", "product_name"])
df.head()
| 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 |
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
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.
df["len_comment"] = df["comment"].apply(lambda x: len(x.split(" ")))
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.
df_region.head()
| 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 |
df = df.merge(df_region,left_on='localisation',right_on='commune').drop(columns=['localisation'])
Pour déterminer le genre à partir du nom, il a été utilisé le site https://gender-api.com
df = df.merge(df_genre,left_on="username", right_on="Nom")
df.head()
| 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 |
On considérera que tous les commentaires qui sont en dessous de 3 sont négatifs et les autres possitifs
def is_positiv(consummer_rate):
if consummer_rate < 3:
return 0
else:
return 1
df["is_positiv"] = df["consummer_rate"].apply(is_positiv)
sns.countplot(x="enfant",data=df)
<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
sns.boxplot(df["age"])
<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.
df[df["age"] > 72]
| 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.
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)
<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.
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)
<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.
sns.boxplot(df["consummer_rate"])
<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
outliers = df["comment"][df["consummer_rate"] < 3]
outliers
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
df["len_comment"].describe()
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
sns.boxplot(df["len_comment"])
<matplotlib.axes._subplots.AxesSubplot at 0x2050c055668>
sns.barplot(x='consummer_rate', y='len_comment',data=df)
<matplotlib.axes._subplots.AxesSubplot at 0x2050c0d6860>
sns.scatterplot(x='len_comment', y='age', hue='ga_gender',size='is_positiv',data=df)
<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.
chart = sns.catplot(x='region',data= df, kind="count", color="b")
chart.set_xticklabels(rotation=90)
<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.
sns.countplot(df["ga_gender"])
<matplotlib.axes._subplots.AxesSubplot at 0x2050c00e048>
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.
text = " ".join(
review for review in df.comment[df["is_positiv"]==0])
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.
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é.
sns.heatmap(df.corr())
<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.
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.