├── scripts ├── .gitignore ├── demo.py ├── ver_unver.json ├── timeseries_unver.json ├── timeseries.json ├── timeseries_ver.json ├── main.py ├── data_utils.py ├── requirements.txt ├── sentiment_analysis.py ├── ver_counts.json ├── data_exploration.py └── topic_analysis.py ├── report.pdf ├── webapp ├── src │ ├── stylus │ │ └── main.styl │ ├── assets │ │ └── logo.png │ ├── plugins │ │ └── vuetify.js │ ├── App.vue │ └── main.js ├── public │ ├── favicon.ico │ ├── B005NF5NTK.jpg │ ├── B0092KJ9BU.jpg │ ├── B00MXWFUQC.jpg │ ├── B00UCZGS6S.jpg │ ├── B00UH3L82Y.jpg │ ├── B00VH88CJ0.jpg │ ├── B00X5RV14Y.jpg │ ├── B018JW3EOY.jpg │ ├── PORTABLECHARGERS.jpg │ ├── index.html │ └── lda_B00VH88CJ0.html ├── vue.config.js ├── .gitignore ├── README.md └── package.json ├── dataframes ├── negative.pkl ├── positive.pkl ├── dominant_topics.pkl ├── most_repr_rews.pkl └── topic_sents_keywords.pkl ├── figures ├── 2_zipf_law.png ├── ext │ ├── webapp1.png │ ├── webapp2.png │ ├── webapp3.png │ ├── webapp4.png │ ├── 1_monthly.png │ ├── 1_peruser.png │ ├── webapp_plot1.png │ ├── webapp_plot2.png │ ├── webapp_plot3.png │ ├── 1_selfVSforced.png │ └── 1_frequentInfrequentYelp.png └── 1_opinion_distribution.svg ├── datasets └── pull_datasets.sh ├── report ├── report.tex ├── abstract.tex ├── bibliography.bib ├── 5_conclusioni.tex ├── 1_introduction.tex ├── 4_topic_analysis.tex ├── 3_sentiment_analysis.tex └── 2_esplorazione.tex ├── README.md └── .gitignore /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | lib64 3 | pyvenv.cfg 4 | share/ -------------------------------------------------------------------------------- /report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/report.pdf -------------------------------------------------------------------------------- /webapp/src/stylus/main.styl: -------------------------------------------------------------------------------- 1 | require('typeface-barlow') 2 | $body-font-family = 'Barlow' 3 | -------------------------------------------------------------------------------- /dataframes/negative.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/dataframes/negative.pkl -------------------------------------------------------------------------------- /dataframes/positive.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/dataframes/positive.pkl -------------------------------------------------------------------------------- /figures/2_zipf_law.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/2_zipf_law.png -------------------------------------------------------------------------------- /figures/ext/webapp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp1.png -------------------------------------------------------------------------------- /figures/ext/webapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp2.png -------------------------------------------------------------------------------- /figures/ext/webapp3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp3.png -------------------------------------------------------------------------------- /figures/ext/webapp4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp4.png -------------------------------------------------------------------------------- /figures/ext/1_monthly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/1_monthly.png -------------------------------------------------------------------------------- /figures/ext/1_peruser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/1_peruser.png -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/src/assets/logo.png -------------------------------------------------------------------------------- /figures/ext/webapp_plot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp_plot1.png -------------------------------------------------------------------------------- /figures/ext/webapp_plot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp_plot2.png -------------------------------------------------------------------------------- /figures/ext/webapp_plot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/webapp_plot3.png -------------------------------------------------------------------------------- /webapp/public/B005NF5NTK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B005NF5NTK.jpg -------------------------------------------------------------------------------- /webapp/public/B0092KJ9BU.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B0092KJ9BU.jpg -------------------------------------------------------------------------------- /webapp/public/B00MXWFUQC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B00MXWFUQC.jpg -------------------------------------------------------------------------------- /webapp/public/B00UCZGS6S.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B00UCZGS6S.jpg -------------------------------------------------------------------------------- /webapp/public/B00UH3L82Y.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B00UH3L82Y.jpg -------------------------------------------------------------------------------- /webapp/public/B00VH88CJ0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B00VH88CJ0.jpg -------------------------------------------------------------------------------- /webapp/public/B00X5RV14Y.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B00X5RV14Y.jpg -------------------------------------------------------------------------------- /webapp/public/B018JW3EOY.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/B018JW3EOY.jpg -------------------------------------------------------------------------------- /dataframes/dominant_topics.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/dataframes/dominant_topics.pkl -------------------------------------------------------------------------------- /dataframes/most_repr_rews.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/dataframes/most_repr_rews.pkl -------------------------------------------------------------------------------- /figures/ext/1_selfVSforced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/1_selfVSforced.png -------------------------------------------------------------------------------- /dataframes/topic_sents_keywords.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/dataframes/topic_sents_keywords.pkl -------------------------------------------------------------------------------- /webapp/public/PORTABLECHARGERS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/webapp/public/PORTABLECHARGERS.jpg -------------------------------------------------------------------------------- /figures/ext/1_frequentInfrequentYelp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avivace/reviews-sentiment/HEAD/figures/ext/1_frequentInfrequentYelp.png -------------------------------------------------------------------------------- /webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ## Run
39 |
40 | Set up the a Python virtual environment and install required packages
41 |
42 | ```bash
43 | cd scripts
44 | python3 -m venv .
45 | source bin/activate
46 | pip3 install -r requirements.txt
47 | python3 -m spacy download en
48 | ```
49 |
50 | Optionally, install a ipynb kernel to use the venv packages
51 | ```bash
52 | pip3 install --user ipykernel
53 | python -m ipykernel install --user --name=myenv
54 | # Check the installed kernels
55 | jupyter kernelspec list
56 | # Run Jupyter
57 | jupyter lab
58 | ```
59 |
60 |
61 | Now, to run the full pipeline:
62 | ```bash
63 | python3 main.py
64 | ```
65 |
66 | A Flask application exposes a simple API (on port 5000) allowing the trained models to be used on demand via simple HTTP requests (in main.py). The VueJS application needs a recent version of NodeJS and npm.
67 |
68 | ```bash
69 | cd webapp
70 | npm install
71 | # serve the web application with hot reload at localhost:8080/reviews-sentiment
72 | npm run serve
73 | # builds the web application for production
74 | npm run build
75 | # deploys the build on the master branch, making github serve it on https://avivace.github.io/reviews-sentiment
76 | npm run deploy
77 | ```
78 |
79 |
80 | #### Antuz notes
81 |
82 | Accent is `#B71C1C`, typeface is *Barlow* 500. On the plots and graphs, typeface is *Inter* 600, palette is `#4DAF4A`, `#FF7F00`, `#C73E31`.
83 |
84 | #### Final notes from our supervisor, E.Fersini
85 |
86 | Unverified/Spam "boom" happens around the first-publishing of some product, aggregating data from a category will hardly show this (there are papers on this)
87 |
--------------------------------------------------------------------------------
/report/5_conclusioni.tex:
--------------------------------------------------------------------------------
1 | \chapter{Web app}
2 | Abbiamo sviluppato una applicazione web interattiva che dimostra alcuni dei nostri risultati.
3 |
4 | È strutturata in due parti secondo i principi Restful: Il backend utilizza Flask per poter offrire una semplice API attraverso il quale è possibile sfruttare i modelli allenati ed esporne la funzione \texttt{pred\_prob}, in modo da visualizzare in tempo reale il comportamento del classificatore su di un testo personalizzato, non facente parte del dataset iniziale. Il sistema che abbiamo costruito per esporre i modelli fittati è un buon modo di rendere utilizzabile da chiunque, senza dover passare da script, i risultati del nostro progetto. È facilmente estendibile per altri classificatori e parametri.
5 | \par
6 | Il frontend è sviluppato con Vue JS, un framework Javascript per sviluppare applicazioni reattive. Offre un'interfaccia che consuma l'API appena descritta, visualizzando il risultato della computazione.
7 | \par
8 | Per LDA, un'altra pagina nella stessa applicazione raccoglie sei plot interattivi generati da pyLDAvis, permettendo di consularli e mostrando descrizione, codice e titolo di ognuno degli articoli a cui si riferiscono.
9 |
10 | \begin{figure}[H]
11 | \centering
12 | \captionsetup{margin=1cm}
13 | \includegraphics[width=1\linewidth]{figures/ext/webapp1.png}
14 | \caption{Vista Sentiment Analysis della Demo}
15 | \label{zipf_law}
16 | \end{figure}
17 |
18 | \begin{figure}[H]
19 | \centering
20 | \captionsetup{margin=1cm}
21 | \includegraphics[width=1\linewidth]{figures/ext/webapp2.png}
22 | \caption{Vista LDA della Demo (pyLDAvis)}
23 | \label{zipf_law}
24 | \end{figure}
25 |
26 | \begin{figure}[H]
27 | \centering
28 | \captionsetup{margin=1cm}
29 | \includegraphics[width=1\linewidth]{figures/ext/webapp3.png}
30 | \caption{Export pyLDAvis per un singolo prodotto}
31 | \label{zipf_law}
32 | \end{figure}
33 |
34 | \chapter{Conclusioni}
35 | L'esplorazione delle recensioni di prodotti Amazon ci ha permesso di constatare l'enorme numero di informazioni che possono essere estratte da opinioni degli acquirenti con lo scopo di stilare statistiche e valutazioni e poter quindi prendere decisioni in ambito aziendale per migliorare i servizi offerti o centrare meglio la propria clientela.
36 | \par
37 | Lo studio di sentiment analysis dimostra che si possono ottenere modelli addestrati con metriche di controllo molto soddisfacenti e pronti per essere usati nell'analisi sentimentale delle future recensioni.
38 | \par
39 | Per quanto concerne la topic analysis, gli argomenti individuati sui prodotti più recensiti attraverso il modello LDA non sempre sono facili da interpretare. Abbiamo evitato la creazione di argomenti troppo generali ma non abbiamo sempre ottenuto argomenti facilmente utilizzabili per fare ragionamenti complessi sui prodotti. Con ogni probabilità, strumenti più avanzati di topic sentiment analysis porterebbero ad una scelta degli argomenti più logica ed intuitiva.
40 | \par
41 | I modelli utilizzati sono facilmente adattabili a qualsiasi categoria di e-Commerce dotata di una forma di recensioni e, considerati i risultati raggiunti con strumenti base di Data Analytics, non c'è da stupirsi se Amazon sia riuscito a raggiungere la vetta facendo leva su queste tecnologie.
--------------------------------------------------------------------------------
/scripts/timeseries_unver.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "x": 1359590400000,
4 | "y": 694
5 | },
6 | {
7 | "x": 1362009600000,
8 | "y": 548
9 | },
10 | {
11 | "x": 1364688000000,
12 | "y": 537
13 | },
14 | {
15 | "x": 1367280000000,
16 | "y": 501
17 | },
18 | {
19 | "x": 1369958400000,
20 | "y": 607
21 | },
22 | {
23 | "x": 1372550400000,
24 | "y": 587
25 | },
26 | {
27 | "x": 1375228800000,
28 | "y": 663
29 | },
30 | {
31 | "x": 1377907200000,
32 | "y": 583
33 | },
34 | {
35 | "x": 1380499200000,
36 | "y": 532
37 | },
38 | {
39 | "x": 1383177600000,
40 | "y": 665
41 | },
42 | {
43 | "x": 1385769600000,
44 | "y": 605
45 | },
46 | {
47 | "x": 1388448000000,
48 | "y": 732
49 | },
50 | {
51 | "x": 1391126400000,
52 | "y": 795
53 | },
54 | {
55 | "x": 1393545600000,
56 | "y": 682
57 | },
58 | {
59 | "x": 1396224000000,
60 | "y": 824
61 | },
62 | {
63 | "x": 1398816000000,
64 | "y": 868
65 | },
66 | {
67 | "x": 1401494400000,
68 | "y": 985
69 | },
70 | {
71 | "x": 1404086400000,
72 | "y": 1847
73 | },
74 | {
75 | "x": 1406764800000,
76 | "y": 4862
77 | },
78 | {
79 | "x": 1409443200000,
80 | "y": 5429
81 | },
82 | {
83 | "x": 1412035200000,
84 | "y": 5189
85 | },
86 | {
87 | "x": 1414713600000,
88 | "y": 5991
89 | },
90 | {
91 | "x": 1417305600000,
92 | "y": 3784
93 | },
94 | {
95 | "x": 1419984000000,
96 | "y": 2402
97 | },
98 | {
99 | "x": 1422662400000,
100 | "y": 2805
101 | },
102 | {
103 | "x": 1425081600000,
104 | "y": 2738
105 | },
106 | {
107 | "x": 1427760000000,
108 | "y": 3529
109 | },
110 | {
111 | "x": 1430352000000,
112 | "y": 3527
113 | },
114 | {
115 | "x": 1433030400000,
116 | "y": 3331
117 | },
118 | {
119 | "x": 1435622400000,
120 | "y": 2781
121 | },
122 | {
123 | "x": 1438300800000,
124 | "y": 2593
125 | },
126 | {
127 | "x": 1440979200000,
128 | "y": 2829
129 | },
130 | {
131 | "x": 1443571200000,
132 | "y": 3360
133 | },
134 | {
135 | "x": 1446249600000,
136 | "y": 3333
137 | },
138 | {
139 | "x": 1448841600000,
140 | "y": 4304
141 | },
142 | {
143 | "x": 1451520000000,
144 | "y": 4071
145 | },
146 | {
147 | "x": 1454198400000,
148 | "y": 4149
149 | },
150 | {
151 | "x": 1456704000000,
152 | "y": 3320
153 | },
154 | {
155 | "x": 1459382400000,
156 | "y": 4051
157 | },
158 | {
159 | "x": 1461974400000,
160 | "y": 4759
161 | },
162 | {
163 | "x": 1464652800000,
164 | "y": 4484
165 | },
166 | {
167 | "x": 1467244800000,
168 | "y": 6143
169 | },
170 | {
171 | "x": 1469923200000,
172 | "y": 4830
173 | },
174 | {
175 | "x": 1472601600000,
176 | "y": 3115
177 | },
178 | {
179 | "x": 1475193600000,
180 | "y": 2665
181 | },
182 | {
183 | "x": 1477872000000,
184 | "y": 1869
185 | },
186 | {
187 | "x": 1480464000000,
188 | "y": 1260
189 | },
190 | {
191 | "x": 1483142400000,
192 | "y": 1396
193 | },
194 | {
195 | "x": 1485820800000,
196 | "y": 1132
197 | },
198 | {
199 | "x": 1488240000000,
200 | "y": 723
201 | },
202 | {
203 | "x": 1490918400000,
204 | "y": 800
205 | },
206 | {
207 | "x": 1493510400000,
208 | "y": 746
209 | },
210 | {
211 | "x": 1496188800000,
212 | "y": 666
213 | },
214 | {
215 | "x": 1498780800000,
216 | "y": 666
217 | },
218 | {
219 | "x": 1501459200000,
220 | "y": 833
221 | },
222 | {
223 | "x": 1504137600000,
224 | "y": 679
225 | },
226 | {
227 | "x": 1506729600000,
228 | "y": 500
229 | },
230 | {
231 | "x": 1509408000000,
232 | "y": 459
233 | },
234 | {
235 | "x": 1512000000000,
236 | "y": 407
237 | },
238 | {
239 | "x": 1514678400000,
240 | "y": 403
241 | }
242 | ]
--------------------------------------------------------------------------------
/scripts/timeseries.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "x": 1359590400000,
4 | "y": 7389
5 | },
6 | {
7 | "x": 1362009600000,
8 | "y": 6121
9 | },
10 | {
11 | "x": 1364688000000,
12 | "y": 6232
13 | },
14 | {
15 | "x": 1367280000000,
16 | "y": 5897
17 | },
18 | {
19 | "x": 1369958400000,
20 | "y": 6590
21 | },
22 | {
23 | "x": 1372550400000,
24 | "y": 6730
25 | },
26 | {
27 | "x": 1375228800000,
28 | "y": 7585
29 | },
30 | {
31 | "x": 1377907200000,
32 | "y": 7784
33 | },
34 | {
35 | "x": 1380499200000,
36 | "y": 6660
37 | },
38 | {
39 | "x": 1383177600000,
40 | "y": 7830
41 | },
42 | {
43 | "x": 1385769600000,
44 | "y": 7970
45 | },
46 | {
47 | "x": 1388448000000,
48 | "y": 9832
49 | },
50 | {
51 | "x": 1391126400000,
52 | "y": 10697
53 | },
54 | {
55 | "x": 1393545600000,
56 | "y": 8668
57 | },
58 | {
59 | "x": 1396224000000,
60 | "y": 9675
61 | },
62 | {
63 | "x": 1398816000000,
64 | "y": 9086
65 | },
66 | {
67 | "x": 1401494400000,
68 | "y": 9135
69 | },
70 | {
71 | "x": 1404086400000,
72 | "y": 9717
73 | },
74 | {
75 | "x": 1406764800000,
76 | "y": 17970
77 | },
78 | {
79 | "x": 1409443200000,
80 | "y": 19101
81 | },
82 | {
83 | "x": 1412035200000,
84 | "y": 18890
85 | },
86 | {
87 | "x": 1414713600000,
88 | "y": 22104
89 | },
90 | {
91 | "x": 1417305600000,
92 | "y": 22577
93 | },
94 | {
95 | "x": 1419984000000,
96 | "y": 27335
97 | },
98 | {
99 | "x": 1422662400000,
100 | "y": 28805
101 | },
102 | {
103 | "x": 1425081600000,
104 | "y": 28347
105 | },
106 | {
107 | "x": 1427760000000,
108 | "y": 28717
109 | },
110 | {
111 | "x": 1430352000000,
112 | "y": 25657
113 | },
114 | {
115 | "x": 1433030400000,
116 | "y": 24539
117 | },
118 | {
119 | "x": 1435622400000,
120 | "y": 24519
121 | },
122 | {
123 | "x": 1438300800000,
124 | "y": 25943
125 | },
126 | {
127 | "x": 1440979200000,
128 | "y": 26416
129 | },
130 | {
131 | "x": 1443571200000,
132 | "y": 26011
133 | },
134 | {
135 | "x": 1446249600000,
136 | "y": 28206
137 | },
138 | {
139 | "x": 1448841600000,
140 | "y": 27989
141 | },
142 | {
143 | "x": 1451520000000,
144 | "y": 27912
145 | },
146 | {
147 | "x": 1454198400000,
148 | "y": 30385
149 | },
150 | {
151 | "x": 1456704000000,
152 | "y": 26183
153 | },
154 | {
155 | "x": 1459382400000,
156 | "y": 29246
157 | },
158 | {
159 | "x": 1461974400000,
160 | "y": 28392
161 | },
162 | {
163 | "x": 1464652800000,
164 | "y": 28183
165 | },
166 | {
167 | "x": 1467244800000,
168 | "y": 28803
169 | },
170 | {
171 | "x": 1469923200000,
172 | "y": 28538
173 | },
174 | {
175 | "x": 1472601600000,
176 | "y": 27613
177 | },
178 | {
179 | "x": 1475193600000,
180 | "y": 24331
181 | },
182 | {
183 | "x": 1477872000000,
184 | "y": 20732
185 | },
186 | {
187 | "x": 1480464000000,
188 | "y": 18169
189 | },
190 | {
191 | "x": 1483142400000,
192 | "y": 20972
193 | },
194 | {
195 | "x": 1485820800000,
196 | "y": 20163
197 | },
198 | {
199 | "x": 1488240000000,
200 | "y": 14324
201 | },
202 | {
203 | "x": 1490918400000,
204 | "y": 15718
205 | },
206 | {
207 | "x": 1493510400000,
208 | "y": 12842
209 | },
210 | {
211 | "x": 1496188800000,
212 | "y": 11181
213 | },
214 | {
215 | "x": 1498780800000,
216 | "y": 10209
217 | },
218 | {
219 | "x": 1501459200000,
220 | "y": 10101
221 | },
222 | {
223 | "x": 1504137600000,
224 | "y": 9644
225 | },
226 | {
227 | "x": 1506729600000,
228 | "y": 8017
229 | },
230 | {
231 | "x": 1509408000000,
232 | "y": 7343
233 | },
234 | {
235 | "x": 1512000000000,
236 | "y": 7196
237 | },
238 | {
239 | "x": 1514678400000,
240 | "y": 6432
241 | }
242 | ]
--------------------------------------------------------------------------------
/scripts/timeseries_ver.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "x": 1359590400000,
4 | "y": 6695
5 | },
6 | {
7 | "x": 1362009600000,
8 | "y": 5573
9 | },
10 | {
11 | "x": 1364688000000,
12 | "y": 5695
13 | },
14 | {
15 | "x": 1367280000000,
16 | "y": 5396
17 | },
18 | {
19 | "x": 1369958400000,
20 | "y": 5983
21 | },
22 | {
23 | "x": 1372550400000,
24 | "y": 6143
25 | },
26 | {
27 | "x": 1375228800000,
28 | "y": 6922
29 | },
30 | {
31 | "x": 1377907200000,
32 | "y": 7201
33 | },
34 | {
35 | "x": 1380499200000,
36 | "y": 6128
37 | },
38 | {
39 | "x": 1383177600000,
40 | "y": 7165
41 | },
42 | {
43 | "x": 1385769600000,
44 | "y": 7365
45 | },
46 | {
47 | "x": 1388448000000,
48 | "y": 9100
49 | },
50 | {
51 | "x": 1391126400000,
52 | "y": 9902
53 | },
54 | {
55 | "x": 1393545600000,
56 | "y": 7986
57 | },
58 | {
59 | "x": 1396224000000,
60 | "y": 8851
61 | },
62 | {
63 | "x": 1398816000000,
64 | "y": 8218
65 | },
66 | {
67 | "x": 1401494400000,
68 | "y": 8150
69 | },
70 | {
71 | "x": 1404086400000,
72 | "y": 7870
73 | },
74 | {
75 | "x": 1406764800000,
76 | "y": 13108
77 | },
78 | {
79 | "x": 1409443200000,
80 | "y": 13672
81 | },
82 | {
83 | "x": 1412035200000,
84 | "y": 13701
85 | },
86 | {
87 | "x": 1414713600000,
88 | "y": 16113
89 | },
90 | {
91 | "x": 1417305600000,
92 | "y": 18793
93 | },
94 | {
95 | "x": 1419984000000,
96 | "y": 24933
97 | },
98 | {
99 | "x": 1422662400000,
100 | "y": 26000
101 | },
102 | {
103 | "x": 1425081600000,
104 | "y": 25609
105 | },
106 | {
107 | "x": 1427760000000,
108 | "y": 25188
109 | },
110 | {
111 | "x": 1430352000000,
112 | "y": 22130
113 | },
114 | {
115 | "x": 1433030400000,
116 | "y": 21208
117 | },
118 | {
119 | "x": 1435622400000,
120 | "y": 21738
121 | },
122 | {
123 | "x": 1438300800000,
124 | "y": 23350
125 | },
126 | {
127 | "x": 1440979200000,
128 | "y": 23587
129 | },
130 | {
131 | "x": 1443571200000,
132 | "y": 22651
133 | },
134 | {
135 | "x": 1446249600000,
136 | "y": 24873
137 | },
138 | {
139 | "x": 1448841600000,
140 | "y": 23685
141 | },
142 | {
143 | "x": 1451520000000,
144 | "y": 23841
145 | },
146 | {
147 | "x": 1454198400000,
148 | "y": 26236
149 | },
150 | {
151 | "x": 1456704000000,
152 | "y": 22863
153 | },
154 | {
155 | "x": 1459382400000,
156 | "y": 25195
157 | },
158 | {
159 | "x": 1461974400000,
160 | "y": 23633
161 | },
162 | {
163 | "x": 1464652800000,
164 | "y": 23699
165 | },
166 | {
167 | "x": 1467244800000,
168 | "y": 22660
169 | },
170 | {
171 | "x": 1469923200000,
172 | "y": 23708
173 | },
174 | {
175 | "x": 1472601600000,
176 | "y": 24498
177 | },
178 | {
179 | "x": 1475193600000,
180 | "y": 21666
181 | },
182 | {
183 | "x": 1477872000000,
184 | "y": 18863
185 | },
186 | {
187 | "x": 1480464000000,
188 | "y": 16909
189 | },
190 | {
191 | "x": 1483142400000,
192 | "y": 19576
193 | },
194 | {
195 | "x": 1485820800000,
196 | "y": 19031
197 | },
198 | {
199 | "x": 1488240000000,
200 | "y": 13601
201 | },
202 | {
203 | "x": 1490918400000,
204 | "y": 14918
205 | },
206 | {
207 | "x": 1493510400000,
208 | "y": 12096
209 | },
210 | {
211 | "x": 1496188800000,
212 | "y": 10515
213 | },
214 | {
215 | "x": 1498780800000,
216 | "y": 9543
217 | },
218 | {
219 | "x": 1501459200000,
220 | "y": 9268
221 | },
222 | {
223 | "x": 1504137600000,
224 | "y": 8965
225 | },
226 | {
227 | "x": 1506729600000,
228 | "y": 7517
229 | },
230 | {
231 | "x": 1509408000000,
232 | "y": 6884
233 | },
234 | {
235 | "x": 1512000000000,
236 | "y": 6789
237 | },
238 | {
239 | "x": 1514678400000,
240 | "y": 6029
241 | }
242 | ]
--------------------------------------------------------------------------------
/scripts/main.py:
--------------------------------------------------------------------------------
1 | print("# Initialisation")
2 | import data_exploration
3 | import sentiment_analysis
4 | import topic_analysis
5 | import pandas as pd
6 | from data_utils import load_dataset
7 | from data_utils import feature_manipulation
8 | from data_utils import add_features
9 | from pathlib import Path
10 | import re
11 |
12 | from flask import Flask
13 | from flask import request
14 | from flask import jsonify
15 | from flask_cors import CORS
16 |
17 | from bs4 import BeautifulSoup
18 |
19 | app = Flask(__name__)
20 | CORS(app)
21 |
22 | def load_initial_dataset():
23 | dataset_folder = Path("../datasets/")
24 | try:
25 | # Try to load a cached version of the dataframe
26 | print("Trying to load the cached dataframe...")
27 | df = pd.read_pickle(dataset_folder / 'cached_dataframe.pkl')
28 | print("Done")
29 | except:
30 | print("No cached dataframe, loading the dataset from disk")
31 | path_file = dataset_folder / 'Cell_Phones_and_Accessories.json'
32 | print(path_file)
33 | df = load_dataset(path_file)
34 | # Store the dataframe on disk
35 | print("Caching the dataframe")
36 | df.to_pickle(dataset_folder / 'cached_dataframe.pkl')
37 | return df
38 |
39 |
40 | def pp_add_features(df):
41 | preprocessed = df.copy(True)
42 | # Runs vote_to_opinion(), words_count(), transform_unix_date()
43 | add_features(preprocessed)
44 | return preprocessed
45 |
46 |
47 | def preprocessing_post_exploration_dataset(df):
48 | dataset_folder = Path("../datasets/")
49 | try:
50 | print("Trying to load the cached preprocessed dataframe...")
51 | preprocessed = pd.read_pickle(dataset_folder / 'cached_preprocessed_dataframe.pkl')
52 | print("Done")
53 | except:
54 | print("No cached dataframe, loading the dataset from disk")
55 | preprocessed = df.copy(True)
56 | feature_manipulation(preprocessed)
57 | print("Caching the preprocessed dataframe")
58 | preprocessed.to_pickle(dataset_folder / 'cached_preprocessed_dataframe.pkl')
59 | return preprocessed
60 |
61 | @app.route('/')
62 | def hello():
63 | result ={}
64 | result["positive"] = sentiment_analysis.compute_single(request.args.get('text'), best_nb, count_vector)[0][1]
65 | return jsonify(result)
66 |
67 | def striphtml(reviews):
68 | n = 0
69 | filtered_reviews = []
70 | for text in df['reviewText']:
71 | m = re.search('<\s*a[^>]*>(.*?)<\s*/\s*a>', text)
72 | if m:
73 | soup = BeautifulSoup(text,features="html.parser")
74 | stripped_text = soup.get_text()
75 | filtered_reviews.append(stripped_text)
76 | n = n + 1
77 | else:
78 | filtered_reviews.append(text)
79 |
80 | print("HTML stripped on",n,"reviews")
81 | return filtered_reviews
82 |
83 | def clean_dirt(df):
84 | reviews = df['reviewText'].tolist()
85 | htmlcleaned_reviews = striphtml(reviews)
86 | df['reviewText'] = [''.join(review) for review in htmlcleaned_reviews]
87 |
88 |
89 | def check_dirt(df):
90 | c = 0
91 | for text in df['reviewText']:
92 | m = re.search('<\s*a[^>]*>(.*?)<\s*/\s*a>', text)
93 | if m:
94 | c = c+1
95 | print(c,"dirty reviews")
96 |
97 | def find_reviews_with_custom_text(df):
98 | list_res = []
99 | i = 0
100 |
101 | while i < 200: #while len(df.index) > 0:
102 | print("Finding reviews at chunk ", i)
103 | temp_df = df.head(10000)
104 | for index, row in temp_df.iterrows():
105 | if "portable charger" in row['reviewText']:
106 | list_res.append(row)
107 | df = df.iloc[10000:]
108 | i += 1
109 |
110 | result = pd.DataFrame(list_res, columns=df.columns)
111 | print("Result df is ", len(result.index))
112 | return result
113 |
114 | def preprocess_for_custom_LDA(df):
115 | print("Preprocessing for custom LDA")
116 | preprocessed = df.copy(True)
117 | add_features(preprocessed)
118 | feature_manipulation(preprocessed)
119 | return preprocessed
120 |
121 |
122 | if __name__ == "__main__":
123 | df = load_initial_dataset()
124 |
125 | # Remember to set this back to df
126 | df_rich = pp_add_features(df)
127 |
128 | #print(df_exploration.columns)
129 | #print("MIN YEAR ", df_exploration.year.min())
130 | #print("MAX YEAR ", df_exploration.year.max())
131 | print("\n# Data Exploration")
132 | # data_exploration.top_50_products_verified_unverified_both(df_rich)
133 | #data_exploration.count_reviews(df_rich)
134 | data_exploration.export_week_day(df_rich)
135 | data_exploration.export_month(df_rich)
136 | #data_exploration.export_year(df_rich)
137 | #data_exploration.run(df_exploration)
138 | #df_analysis = preprocessing_post_exploration_dataset(df_exploration)
139 |
140 | #check_dirt(df_analysis)
141 | #clean_dirt(df_analysis)
142 | #check_dirt(df_analysis)
143 |
144 | # Web server exposing the trained models
145 | #best_nb, best_lr, count_vector = sentiment_analysis.run(df_analysis)
146 | #app.run()
147 |
148 | # Enable Topic Analysis
149 | #topic_analysis.run(df_analysis)
150 |
151 | #"Portable charger" reviews topic analysis
152 | #df = find_reviews_with_custom_text(df)
153 | #df_final = preprocess_for_custom_LDA(df)
154 | #topic_analysis.run_for_custom_analysis(df_final)
155 |
156 |
--------------------------------------------------------------------------------
/report/1_introduction.tex:
--------------------------------------------------------------------------------
1 | \chapter{Introduzione}
2 |
3 | Negli ultimi decenni, l'avvento e la popolarizzazione di servizi online ha cambiato il volto dello shopping su larga scala. Piattaforme come Amazon ed eBay fanno parte della vita di tutti i giorni ed è frequente consultare risorse online prima di acquistare. Nel 2017, il volume di vendite nel mercato statunitense che vengono effettuate online ha raggiunto il 9\% e ci si aspetta che arrivi al 12\% nel 2021 \cite{usb1}.
4 |
5 | La crescita del traffico e della portata dei portali di commercio online genera una quantità crescente di dati sulla natura delle transizioni e degli utenti di questo servizio.
6 |
7 | Una parte importante di questi dati è costituita dai contenuti generati dagli utenti che valutano i prodotti acquistati e condividono la loro esperienza. Si tratta principalmente di valutazioni numeriche, spesso corredate da un breve paragrafo testuale.
8 |
9 | Avere a disposizione un insieme di strumenti che possa processare in modo automatico questa mole di dati è fondamentale per tutti gli attori coinvolti nelle transazioni: produttori, clienti/consumatori, venditori, piattaforme di vendita e pubblicitari.
10 |
11 | Discipline come la Sentiment Analysis estraggono dei dati strutturati da questi contenuti testuali, permettendo uno sguardo statistico sulle tendenze di comunità di acquirenti sotto diversi aspetti di diversi prodotti. Avere un'idea di quali siano gli elementi più o meno apprezzati di un prodotto, secondo le diverse categorie di utenti permette di agire in modo dinamico e veloce sul loro sviluppo e sulla loro pubblicizzazione. I gestori di questi portali invece saranno interessanti a profilare gruppi di utenti, estraendone le preferenze, e gli elementi di successo dei prodotti, per proporre raccomandazioni sempre più vincenti, accurate e vicine ai desideri dell'utente.
12 |
13 | Un altro aspetto da non sottovalutare è quello del valore "genuino" che i contenuti generati da altri consumatori riescono a trasmettere. Le recensioni vengono infatti recepite come fonti affidabili e privi di natura pubblicitaria, rappresentando uno strumento molto potente.
14 |
15 | Amazon ha sviluppato un sistema per assegnare rilevanza alle recensioni e non è raro che venga usato insieme ad altre tecniche di (auto) marketing, promuovendo articoli con recensioni positive e utili, presentandole ordinate dalla più "convincente" all'utente che sta attraversando il processo di decisione.
16 |
17 | \section{Obiettivo del progetto}
18 |
19 | Questo lavoro si sviluppa in tre fasi di seguito sintetizzate.
20 |
21 | \subsection{Esplorazione}
22 |
23 | Per approfondire e comprendere la natura di questi contributi, sono state effettuate analisi preliminari sulle recensioni, concentrandoci sulla distribuzione delle opinioni, sulle caratteristiche delle recensioni sui prodotti più rilevanti e sull'evoluzione di questi ultimi fattori nel tempo.
24 | \par
25 | L'obbiettivo è sviluppare una visione su diversi aspetti soggettivi (e variabili nel tempo e per categoria) che caratterizzano le recensioni, al fine di comprendere in che modo vengano prodotte ed interpretate.
26 | \par
27 | Abbiamo approfondito caratteristiche come \texttt{verified} e in che modo influenzano il totale dei dati.
28 | \par
29 | Infine, vengono brevemente presentate alcune ricerche che investigano la questione dello sbilanciamento delle recensioni.
30 |
31 | \subsection{Sentiment Analysis}
32 |
33 | In questa fase, analizziamo sistematicamente le parti testuali delle recensioni per estrarne un'opinione.
34 | \par
35 | Una parte preliminare pre-processa e prepara il dataset. Vengono scartate recensioni prolisse e ritenute inutili e fatte ulteriori esplorazioni sul nuovo (ristretto) corpo di recensioni.
36 | \par
37 | Infine, alleniamo due classificatori, Naive Bayes e Logistic Regression, che etichettano queste istanze con la variabile target \texttt{opinion} e valutiamo le loro performance.
38 |
39 | \subsection{Topic Analysis}
40 |
41 | In questa fase, viene utilizzato un algoritmo che consente di identificare gli argomenti più discussi all'interno di un corpus di documenti.
42 | \par La fase di preparazione del dataset è la stessa della fase di sentiment analysis.
43 | \par
44 | Vengono inoltre analizzati gli svantaggi e alcune possibili soluzioni del metodo analizzato. Infine, gli argomenti risultanti dalla sua applicazione vengono visualizzati in maniera interattiva.
45 |
46 | \section{Dataset}
47 |
48 | Il dataset utilizzato \cite{amazondataset} proviene da un gruppo di ricerca dell'Università di San Diego, che ha estratto e processato le recensioni rilasciate dagli utenti sul sito Amazon.com fino al 2018 in formato JSON.
49 | \par
50 | Abbiamo scelto il dataset della categoria "Cellulari ed Accessori", in una versione densa, contenente solo i dati generati da utenti con almeno 5 recensioni (\textit{5-core}).
51 |
52 | \section{Software utilizzati}
53 |
54 | Python è stato lo strumento fondamentale in questo lavoro, scelta dovuta alla grande quantità di strumenti e librerie open source disponibili per questo linguaggio.
55 | \paragraph{}
56 | Tra le librerie utilizzate, ricordiamo:
57 | \begin{itemize}
58 | \item Pandas per il caricamento, manipolazione e querying dei dataset
59 | \item Matplotlib per il rendering di grafici e figure direttamente da dataframe pandas
60 | \item numpy per un supporto efficiente a matrici e vettori di grosse dimensioni
61 | \item sklearn per machine learning
62 | \item pyLDAvis per la visualizzazione interattiva dei topic model
63 | \item NLTK per Natural Language Processing
64 | \item VueJS per applicazioni web reattive
65 | \item Flask per realizzare un'API Restful con le funzionalità implementate
66 | \end{itemize}
67 |
68 | Il versionamento del codice e la possibilità di lavorare in gruppo sono due importanti strumenti offerti da Git, mentre la documentazione è scritta in \LaTeX{}.
69 | \par
70 | I prodotti del progetto sono: script per ogni fase della pipeline, Notebook Jupyter interattivi, figure e grafici vettoriali ed un'applicazione web composta da un backend in Python e un frontend in Vue.js che offre un un'interfaccia utente di facile utilizzo che espone alcune funzionalità del nostro lavoro.
71 | \par
72 | Inoltre, per lo sviluppo della demo sono state utilizzate tecnologie frontend basate su Javascript.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | scripts/DatasetsPath.txt
2 | # Don't try to commit gigabytes of datasets
3 | datasets/*
4 | # But keep the script to pull them there
5 | !datasets/pull_datasets.sh
6 |
7 | # Ignore cached dataframes
8 | *.pkl
9 |
10 | # Exported images and web pages
11 | *.svg
12 |
13 | ### Python ###
14 | # Byte-compiled / optimized / DLL files
15 | __pycache__/
16 | *.py[cod]
17 | *$py.class
18 |
19 | # C extensions
20 | *.so
21 |
22 | # Distribution / packaging
23 | .Python
24 | build/
25 | develop-eggs/
26 | dist/
27 | downloads/
28 | eggs/
29 | .eggs/
30 | lib/
31 | lib64/
32 | parts/
33 | sdist/
34 | var/
35 | wheels/
36 | pip-wheel-metadata/
37 | share/python-wheels/
38 | *.egg-info/
39 | .installed.cfg
40 | *.egg
41 | MANIFEST
42 |
43 | # PyInstaller
44 | # Usually these files are written by a python script from a template
45 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
46 | *.manifest
47 | *.spec
48 |
49 | # Installer logs
50 | pip-log.txt
51 | pip-delete-this-directory.txt
52 |
53 | # Unit test / coverage reports
54 | htmlcov/
55 | .tox/
56 | .nox/
57 | .coverage
58 | .coverage.*
59 | .cache
60 | nosetests.xml
61 | coverage.xml
62 | *.cover
63 | .hypothesis/
64 | .pytest_cache/
65 |
66 | # Translations
67 | *.mo
68 | *.pot
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # pyenv
80 | .python-version
81 |
82 | # pipenv
83 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
84 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
85 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
86 | # install all needed dependencies.
87 | #Pipfile.lock
88 |
89 | # celery beat schedule file
90 | celerybeat-schedule
91 |
92 | # SageMath parsed files
93 | *.sage.py
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # Mr Developer
103 | .mr.developer.cfg
104 | .project
105 | .pydevproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 | .dmypy.json
113 | dmypy.json
114 |
115 | # Pyre type checker
116 | .pyre/
117 |
118 | ### LaTeX ###
119 | ## Core latex/pdflatex auxiliary files:
120 | *.aux
121 | *.lof
122 | *.log
123 | *.lot
124 | *.fls
125 | *.out
126 | *.toc
127 | *.fmt
128 | *.fot
129 | *.cb
130 | *.cb2
131 | .*.lb
132 |
133 | ## Intermediate documents:
134 | *.dvi
135 | *.xdv
136 | *-converted-to.*
137 | # these rules might exclude image files for figures etc.
138 | # *.ps
139 | # *.eps
140 | # *.pdf
141 |
142 | ## Generated if empty string is given at "Please type another file name for output:"
143 | .pdf
144 |
145 | ## Bibliography auxiliary files (bibtex/biblatex/biber):
146 | *.bbl
147 | *.bcf
148 | *.blg
149 | *-blx.aux
150 | *-blx.bib
151 | *.run.xml
152 |
153 | ## Build tool auxiliary files:
154 | *.fdb_latexmk
155 | *.synctex
156 | *.synctex(busy)
157 | *.synctex.gz
158 | *.synctex.gz(busy)
159 | *.pdfsync
160 |
161 | ## Build tool directories for auxiliary files
162 | # latexrun
163 | latex.out/
164 |
165 | ## Auxiliary and intermediate files from other packages:
166 | # algorithms
167 | *.alg
168 | *.loa
169 |
170 | # achemso
171 | acs-*.bib
172 |
173 | # amsthm
174 | *.thm
175 |
176 | # beamer
177 | *.nav
178 | *.pre
179 | *.snm
180 | *.vrb
181 |
182 | # changes
183 | *.soc
184 |
185 | # comment
186 | *.cut
187 |
188 | # cprotect
189 | *.cpt
190 |
191 | # elsarticle (documentclass of Elsevier journals)
192 | *.spl
193 |
194 | # endnotes
195 | *.ent
196 |
197 | # fixme
198 | *.lox
199 |
200 | # feynmf/feynmp
201 | *.mf
202 | *.mp
203 | *.t[1-9]
204 | *.t[1-9][0-9]
205 | *.tfm
206 |
207 | #(r)(e)ledmac/(r)(e)ledpar
208 | *.end
209 | *.?end
210 | *.[1-9]
211 | *.[1-9][0-9]
212 | *.[1-9][0-9][0-9]
213 | *.[1-9]R
214 | *.[1-9][0-9]R
215 | *.[1-9][0-9][0-9]R
216 | *.eledsec[1-9]
217 | *.eledsec[1-9]R
218 | *.eledsec[1-9][0-9]
219 | *.eledsec[1-9][0-9]R
220 | *.eledsec[1-9][0-9][0-9]
221 | *.eledsec[1-9][0-9][0-9]R
222 |
223 | # glossaries
224 | *.acn
225 | *.acr
226 | *.glg
227 | *.glo
228 | *.gls
229 | *.glsdefs
230 |
231 | # uncomment this for glossaries-extra (will ignore makeindex's style files!)
232 | # *.ist
233 |
234 | # gnuplottex
235 | *-gnuplottex-*
236 |
237 | # gregoriotex
238 | *.gaux
239 | *.gtex
240 |
241 | # htlatex
242 | *.4ct
243 | *.4tc
244 | *.idv
245 | *.lg
246 | *.trc
247 | *.xref
248 |
249 | # hyperref
250 | *.brf
251 |
252 | # knitr
253 | *-concordance.tex
254 | # TODO Comment the next line if you want to keep your tikz graphics files
255 | *.tikz
256 | *-tikzDictionary
257 |
258 | # listings
259 | *.lol
260 |
261 | # luatexja-ruby
262 | *.ltjruby
263 |
264 | # makeidx
265 | *.idx
266 | *.ilg
267 | *.ind
268 |
269 | # minitoc
270 | *.maf
271 | *.mlf
272 | *.mlt
273 | *.mtc[0-9]*
274 | *.slf[0-9]*
275 | *.slt[0-9]*
276 | *.stc[0-9]*
277 |
278 | # minted
279 | _minted*
280 | *.pyg
281 |
282 | # morewrites
283 | *.mw
284 |
285 | # nomencl
286 | *.nlg
287 | *.nlo
288 | *.nls
289 |
290 | # pax
291 | *.pax
292 |
293 | # pdfpcnotes
294 | *.pdfpc
295 |
296 | # sagetex
297 | *.sagetex.sage
298 | *.sagetex.py
299 | *.sagetex.scmd
300 |
301 | # scrwfile
302 | *.wrt
303 |
304 | # sympy
305 | *.sout
306 | *.sympy
307 | sympy-plots-for-*.tex/
308 |
309 | # pdfcomment
310 | *.upa
311 | *.upb
312 |
313 | # pythontex
314 | *.pytxcode
315 | pythontex-files-*/
316 |
317 | # tcolorbox
318 | *.listing
319 |
320 | # thmtools
321 | *.loe
322 |
323 | # TikZ & PGF
324 | *.dpth
325 | *.md5
326 | *.auxlock
327 |
328 | # todonotes
329 | *.tdo
330 |
331 | # vhistory
332 | *.hst
333 | *.ver
334 |
335 | # easy-todo
336 | *.lod
337 |
338 | # xcolor
339 | *.xcp
340 |
341 | # xmpincl
342 | *.xmpi
343 |
344 | # xindy
345 | *.xdy
346 |
347 | # xypic precompiled matrices
348 | *.xyc
349 |
350 | # endfloat
351 | *.ttt
352 | *.fff
353 |
354 | # Latexian
355 | TSWLatexianTemp*
356 |
357 | ## Editors:
358 | # WinEdt
359 | *.bak
360 | *.sav
361 |
362 | # Texpad
363 | .texpadtmp
364 |
365 | # LyX
366 | *.lyx~
367 |
368 | # Kile
369 | *.backup
370 |
371 | # KBibTeX
372 | *~[0-9]*
373 |
374 | # auto folder when using emacs and auctex
375 | ./auto/*
376 | *.el
377 |
378 | # expex forward references with \gathertags
379 | *-tags.tex
380 |
381 | # standalone packages
382 | *.sta
383 |
384 | ### LaTeX Patch ###
385 | # glossaries
386 | *.glstex
387 |
388 | #
389 | */etc/*
390 | */.idea/*
391 | */.ipynb_checkpoints/*
392 |
--------------------------------------------------------------------------------
/scripts/data_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ### Import libraries ###
4 |
5 | import pandas as pd
6 | import numpy as np
7 | import re
8 | import nltk
9 | from nltk.tokenize import RegexpTokenizer
10 | nltk.download('punkt')
11 | nltk.download('stopwords')
12 | import spacy
13 |
14 | from collections import defaultdict
15 |
16 | from html.parser import HTMLParser
17 |
18 | ### Functions ###
19 |
20 | def load_dataset(pathfile):
21 | df = pd.read_json(pathfile, lines=True)
22 | df['vote'].fillna(0, inplace=True)
23 | df['vote'] = pd.to_numeric(df['vote'], errors='coerce')
24 | df = df[np.isfinite(df['vote'])]
25 | df['vote'] = df['vote'].astype(int)
26 | df.dropna(subset=['reviewText'], inplace=True)
27 | return df
28 |
29 |
30 | ### DATA MANIPULATION ###
31 | def remove_cols(df):
32 | df.drop(['image',
33 | 'reviewTime',
34 | 'reviewerName',
35 | 'style',
36 | 'unixReviewTime'], axis=1, inplace=True)
37 |
38 |
39 | def vote_to_opinion(df):
40 | df.loc[df.overall == 3, 'opinion'] = "neutral"
41 | df.loc[df.overall > 3, 'opinion'] = "positive"
42 | df.loc[df.overall < 3, 'opinion'] = "negative"
43 |
44 |
45 | def words_count(df):
46 | df['n_words'] = [len(t) for t in df['reviewText']]
47 |
48 |
49 | def transform_unix_date(df):
50 | df['date'] = pd.to_datetime(df['unixReviewTime'], unit='s')
51 | df['month_year'] = df['date'].dt.to_period('M')
52 | df['month'] = df['date'].dt.month
53 | df['year'] = df['date'].dt.year
54 | df['week_day'] = df['date'].dt.dayofweek
55 |
56 | contractions_dict = {
57 | "ain't": "am not / are not / is not / has not / have not",
58 | "aren't": "are not / am not",
59 | "can't": "cannot",
60 | "can't've": "cannot have",
61 | "'cause": "because",
62 | "could've": "could have",
63 | "couldn't": "could not",
64 | "couldn't've": "could not have",
65 | "didn't": "did not",
66 | "doesn't": "does not",
67 | "don't": "do not",
68 | "hadn't": "had not",
69 | "hadn't've": "had not have",
70 | "hasn't": "has not",
71 | "haven't": "have not",
72 | "he'd": "he had / he would",
73 | "he'd've": "he would have",
74 | "he'll": "he shall / he will",
75 | "he'll've": "he shall have / he will have",
76 | "he's": "he has / he is",
77 | "how'd": "how did",
78 | "how'd'y": "how do you",
79 | "how'll": "how will",
80 | "how's": "how has / how is / how does",
81 | "I'd": "I had / I would",
82 | "I'd've": "I would have",
83 | "I'll": "I shall / I will",
84 | "I'll've": "I shall have / I will have",
85 | "I'm": "I am",
86 | "I've": "I have",
87 | "isn't": "is not",
88 | "it'd": "it had / it would",
89 | "it'd've": "it would have",
90 | "it'll": "it shall / it will",
91 | "it'll've": "it shall have / it will have",
92 | "it's": "it has / it is",
93 | "let's": "let us",
94 | "ma'am": "madam",
95 | "mayn't": "may not",
96 | "might've": "might have",
97 | "mightn't": "might not",
98 | "mightn't've": "might not have",
99 | "must've": "must have",
100 | "mustn't": "must not",
101 | "mustn't've": "must not have",
102 | "needn't": "need not",
103 | "needn't've": "need not have",
104 | "o'clock": "of the clock",
105 | "oughtn't": "ought not",
106 | "oughtn't've": "ought not have",
107 | "shan't": "shall not",
108 | "sha'n't": "shall not",
109 | "shan't've": "shall not have",
110 | "she'd": "she had / she would",
111 | "she'd've": "she would have",
112 | "she'll": "she shall / she will",
113 | "she'll've": "she shall have / she will have",
114 | "she's": "she has / she is",
115 | "should've": "should have",
116 | "shouldn't": "should not",
117 | "shouldn't've": "should not have",
118 | "so've": "so have",
119 | "so's": "so as / so is",
120 | "that'd": "that would / that had",
121 | "that'd've": "that would have",
122 | "that's": "that has / that is",
123 | "there'd": "there had / there would",
124 | "there'd've": "there would have",
125 | "there's": "there has / there is",
126 | "they'd": "they had / they would",
127 | "they'd've": "they would have",
128 | "they'll": "they shall / they will",
129 | "they'll've": "they shall have / they will have",
130 | "they're": "they are",
131 | "they've": "they have",
132 | "to've": "to have",
133 | "wasn't": "was not",
134 | "we'd": "we had / we would",
135 | "we'd've": "we would have",
136 | "we'll": "we will",
137 | "we'll've": "we will have",
138 | "we're": "we are",
139 | "we've": "we have",
140 | "weren't": "were not",
141 | "what'll": "what shall / what will",
142 | "what'll've": "what shall have / what will have",
143 | "what're": "what are",
144 | "what's": "what has / what is",
145 | "what've": "what have",
146 | "when's": "when has / when is",
147 | "when've": "when have",
148 | "where'd": "where did",
149 | "where's": "where has / where is",
150 | "where've": "where have",
151 | "who'll": "who shall / who will",
152 | "who'll've": "who shall have / who will have",
153 | "who's": "who has / who is",
154 | "who've": "who have",
155 | "why's": "why has / why is",
156 | "why've": "why have",
157 | "will've": "will have",
158 | "won't": "will not",
159 | "won't've": "will not have",
160 | "would've": "would have",
161 | "wouldn't": "would not",
162 | "wouldn't've": "would not have",
163 | "y'all": "you all",
164 | "y'all'd": "you all would",
165 | "y'all'd've": "you all would have",
166 | "y'all're": "you all are",
167 | "y'all've": "you all have",
168 | "you'd": "you had / you would",
169 | "you'd've": "you would have",
170 | "you'll": "you shall / you will",
171 | "you'll've": "you shall have / you will have",
172 | "you're": "you are",
173 | "you've": "you have"
174 | }
175 | contractions_re = re.compile('(%s)' % '|'.join(contractions_dict.keys()))
176 | punctuation_re = re.compile('([!,.:;?])(\w)')
177 | tokenizer = RegexpTokenizer(r'\w+')
178 |
179 |
180 | def expand_contractions(string, contractions_dict=contractions_dict):
181 | def replace(match):
182 | return contractions_dict[match.group(0)]
183 |
184 | return contractions_re.sub(replace, string)
185 |
186 |
187 | def fix_punctuation(string, contractions_dict=contractions_dict):
188 | def replace(match):
189 | # print(match)
190 | # print(match.group(1) + ' ' + match.group(2))
191 | return match.group(1) + ' ' + match.group(2)
192 |
193 | return punctuation_re.sub(replace, string)
194 |
195 |
196 | def remove_less_frequent_words(reviews):
197 | frequency = defaultdict(int)
198 | for review in reviews:
199 | for token in review:
200 | frequency[token] += 1
201 |
202 | cleaned = [[token for token in review if frequency[token] > 1] for review in reviews]
203 | return cleaned
204 |
205 |
206 | nlp = spacy.load('en', disable=['parser', 'ner'])
207 |
208 |
209 | def lemmatization(text, allowed_postags=['NOUN', 'ADJ', 'VERB', 'ADV']):
210 | """https://spacy.io/api/annotation"""
211 | doc = nlp(' '.join(text))
212 | return [token.lemma_ for token in doc if token.pos_ in allowed_postags]
213 |
214 | class MLStripper(HTMLParser):
215 | def __init__(self):
216 | super().__init__()
217 | self.reset()
218 | self.fed = []
219 | def handle_data(self, d):
220 | self.fed.append(d)
221 | def get_data(self):
222 | return ''.join(self.fed)
223 |
224 | def remove_html(review):
225 | s = MLStripper()
226 | s.feed(review)
227 | return s.get_data()
228 |
229 | def text_preprocessing(reviews, remove_less_frequent=True):
230 | #print(reviews)
231 | #reviews = reviews.lower()
232 | reviews = [review.lower() for review in reviews]
233 | reviews = [remove_html(review) for review in reviews]
234 | stopwords = nltk.corpus.stopwords.words("english")
235 | filtered_reviews = []
236 | no_review = 0
237 | for review in reviews:
238 | no_review += 1
239 | if no_review % 100 == 0:
240 | print('Review n.', no_review, '/', len(reviews))
241 | try:
242 | review = fix_punctuation(review)
243 | review = expand_contractions(review)
244 | except:
245 | print(review, "something happened")
246 | filtered_review = []
247 | for word in tokenizer.tokenize(review):
248 | if word not in stopwords and len(word) > 2:
249 | filtered_review.append(word)
250 | lemmatized = lemmatization(filtered_review)
251 | filtered_reviews.append(lemmatized)
252 |
253 | if (remove_less_frequent):
254 | filtered_reviews = remove_less_frequent_words(filtered_reviews)
255 |
256 | return filtered_reviews
257 |
258 |
259 | def preprocessed_reviews(df):
260 | reviews = df['reviewText'].tolist()
261 | preprocessed_reviews = text_preprocessing(reviews)
262 | df['preprocessedReview'] = [' '.join(review) for review in preprocessed_reviews]
263 | df = df[df.preprocessedReview != '']
264 |
265 |
266 | def feature_manipulation(df):
267 | remove_cols(df)
268 | preprocessed_reviews(df)
269 | return df
270 |
271 |
272 | def add_features(df):
273 | vote_to_opinion(df)
274 | words_count(df)
275 | transform_unix_date(df)
276 |
277 |
278 | def most_reviewed_products(df, n_products):
279 | reviews_per_product = df['asin'].value_counts()
280 | most_reviews = reviews_per_product.nlargest(n_products)
281 | most_reviews = most_reviews.reset_index()
282 | most_reviews = most_reviews.drop('asin', axis=1)
283 | definitive = df.merge(most_reviews, left_on='asin', right_on='index')
284 | definitive = definitive.drop('index', axis=1)
285 | return definitive
286 |
--------------------------------------------------------------------------------
/report/4_topic_analysis.tex:
--------------------------------------------------------------------------------
1 | \chapter{Topic analysis}
2 | La topic analysis consente di identificare gli argomenti più discussi semplicemente contando le parole all’interno di un corpus di documenti e raggruppando modelli di parole simili.
3 | \par
4 | È una tecnica di machine learning che organizza e comprende grandi raccolte di dati testuali, assegnando tag o categorie in base all’argomento o al tema di ogni singolo testo.
5 | \par
6 | I risultati sono più dettagliati e interessanti rispetto alla sentiment analysis, in quanto la topic analysis esamina più da vicino le informazioni dietro un testo.
7 | \par
8 | Sono comunque due metodi che, se usati in combinazione, consentono di restringere ulteriormente queste informazioni per trovare con precisione quali temi vengono discussi, fornendo quindi informazioni fruibili riguardanti il prodotto.
9 |
10 | \section{Algoritmo utilizzato}
11 | Il metodo di riferimento di topic analysis è Latent Dirichlet Allocation (LDA) \cite{blei2003latent}: è una tecnica di machine learning non supervisionata che consente di inferire schemi e raggruppare espressioni simili senza la necessità di definire gli argomenti a priori. L'assunzione di LDA è che ogni documento può essere descritto da una distribuzione di argomenti, e ciascun argomento può altresì essere descritto da una distribuzione di parole.
12 | \par
13 | LDA è un modello ampiamente documentato in Python e per questo motivo è stata la scelta naturale per effettuare il task di topic analysis.
14 | \par
15 | Detto questo, è da far notare che la letteratura scientifica negli ultimi anni ha prodotto diversi modelli che mettono in risalto alcuni svantaggi del modello LDA.
16 | \subsection{Individuazione topic}
17 | \label{indiv_topic}
18 | Una limitazione è stata riscontrata nello sviluppo del modello MG-LDA in cui viene asserito che i modelli standard (come LDA) tendono a produrre topic che corrispondono alle proprietà globali degli oggetti in analisi piuttosto che agli aspetti di un oggetto che tendono ad essere valutati da un utente.
19 | \par
20 | La soluzione adottata in questo progetto, vista l'assenza di una implementazione per il modello MG-LDA \cite{titov2008joint}, è stata quella di applicare LDA su prodotti presi singolarmente - considerando ovviamente i prodotti con più osservazioni all'interno del dataset - nonostante la pratica più diffusa in letteratura sembra quella di applicare il modello sull'intero corpus di documenti a prescindere dall'eterogeneità dei documenti stessi. Questa scelta è stata presa di proprosito per evitare la formazione di macro-topic.
21 | \subsection{Sentiment topic}
22 | Un'altra carenza riscontrata è l'assenza di rilevazione del sentiment: questo compito è risolto da diversi modelli (per esempio JST \cite{lin2009joint}, basato su LDA), che suddividono il testo in argomenti, assegnando simultaneamente a ciascuno un livello di sentimento.
23 | \par
24 | L'idea per questo progetto, vista la mancanza di una implementazione del modello JST, è stata quella di utilizzare un approccio lexicon-based (VADER \cite{hutto2014vader}) in combinazione con l'approccio non supervisionato, in modo da poter fornire una visione generale ed approssimata del sentiment dei topic prodotti da LDA. Di seguito viene presentata la procedura implementata:
25 | \begin{itemize}
26 | \item Per ogni recensione viene calcolato il rispettivo sentiment. In particolare, l'output fornito è una variabile \texttt{compound} $\in [-1, +1]$ che rappresenta un sentiment:
27 | \begin{itemize}
28 | \item \textbf{positivo} se \texttt{compound} $\geq 0.05$
29 | \item \textbf{neutrale} se -0.05 < \texttt{compound} < 0.05
30 | \item \textbf{negativo} se \texttt{compound} $\leq -0.05$
31 | \end{itemize}
32 | \item In seguito all'applicazione del modello LDA, ogni recensione ha associata la probabilità con la quale ha contribuito alla formazione del topic.
33 | \item Per ogni recensione viene estratto il topic con probabilità maggiore (se > 0.7). In questo caso, si assume che la recensione sia inerente al suddetto topic, altrimenti viene scartata dal conto.
34 | \item Per ogni topic viene effettuata la media del sentiment delle recensioni associate al suddetto topic
35 | \end{itemize}
36 |
37 | Questa soluzione non è parsa consistente in quanto circa il 0.1\% delle recensioni appartenevano ad un topic specifico con probabilità superiore a 0.7. L'abbassamento della soglia di probabilità è anch'essa una soluzione inconsistente.
38 |
39 | \section{Procedimento}
40 |
41 | La fase di topic analysis condivide le operazioni preliminari di preparazione del dataset presentate nel Capitolo \ref{preprocessing} e nel Capitolo \ref{bow}.
42 | \par
43 | L'algoritmo LDA è stato applicato su oggetti presi singolarmente per i problemi sottolineati nel Capitolo \ref{indiv_topic}. Il criterio di scelta dei prodotti da analizzare è la loro popolarità in termini di recensioni.
44 | \par
45 | In particolare, soffermandoci sulla Figura \ref{opinion_bestseller_products} si può notare quanto sono dominanti, in percentuale, le recensioni positive. Ciò rispecchia connotati già riscontrati durante l'esplorazione del dataset; nonostante ciò alcuni prodotti tra i più recensiti mostrano una percentuale di recensioni negative e neutrali elevata rispetto alla media.
46 | \par
47 | Considerando i tempi di computazione, un totale di 6 prodotti si è rivelato essere un buon trade-off per un'analisi variegata, prendendo i 3 prodotti con la media di \texttt{overall} più alta e i 3 prodotti con la media di \texttt{overall} più bassa.
48 | \par
49 | Essendo LDA un algoritmo non supervisionato che produce argomenti astratti senza conoscerne il numero a priori, solitamente necessita di un tuning degli iperparametri per individuare il modello migliore. Gli iperparametri sono:
50 |
51 | \begin{itemize}
52 | \item K: è il numero di argomenti da estrarre dal corpus di documenti disponibile
53 | \item $\alpha$: è il parametro relativo alla distribuzione che regola l’aspetto della distribuzione degli argomenti per tutti i documenti del corpus. Tipicamente viene scelto un valore di $\alpha < 1$ per ottenere una distribuzione sparsa di argomenti per documento.
54 | \item $\beta$: è il parametro relativo alla distribuzione che regola l’aspetto della distribuzione delle parole in ciascun argomento. Per lo stesso motivo di $\alpha$, viene scelto un valore $\beta < 1$.
55 | \end{itemize}{}
56 |
57 | Nella Tabella \ref{values_hyper} vengono mostrati i possibili valori assumibili dagli iperparametri.
58 | \begin{table}[H]
59 | \small
60 | \centering
61 | \begin{tabular}{|p{0.20\textwidth}|p{0.28\textwidth}|}
62 | \hline
63 | Iperparametro & Valori possibili\\
64 | \hline
65 | K & [2, 3, 4, 5, 6, 7, 8, 9, 10]\\
66 | $\alpha$ & [0.1, 1]\\
67 | $\beta$ & [0.01, 0.1, 1]\\
68 | \hline
69 | \end{tabular}
70 | \caption{Possibili valori degli iperparametri di LDA}
71 | \label{values_hyper}
72 | \end{table}
73 |
74 | Per valutare la qualità degli argomenti appresi viene usato il punteggio di \textit{coherence}. Per ogni prodotto:
75 | \begin{itemize}
76 | \item Viene applicato l'algoritmo iterando sull'insieme di iperparametri
77 | \item Ogni modello risultante ottiene un punteggio
78 | \item Il modello con il punteggio più alto è l'ottimale
79 | \end{itemize}{}
80 |
81 | Nella Tabella \ref{hyper_opt} vengono mostrati i modelli ottimali per ciascun prodotto considerato per l'analisi.
82 |
83 | \begin{table}[H]
84 | \small
85 | \centering
86 | \begin{tabular}{|p{0.20\textwidth}|p{0.05\textwidth}|p{0.05\textwidth}|p{0.05\textwidth}||p{0.20\textwidth}|}
87 | \hline
88 | Codice prodotto & $\alpha$ & $\beta$ & K & Coherence score\\
89 | \hline
90 | B00MXWFUQC & 1 & 1 & 3 & 0.48 \\
91 | B0092KJ9BU & 0.1 & 1 & 7 & 0.50 \\
92 | B00UC7G565 & 0.1 & 0.01 & 2 & 0.55 \\
93 | B00VH88CJ0 & 0.1 & 0.01 & 2 & 0.57 \\
94 | B005NF5NTK & 1 & 0.1 & 3 & 0.60 \\
95 | B00X5RV14Y & 0.1 & 1 & 6 & 0.55 \\
96 | \hline
97 | \end{tabular}
98 | \caption{Iperparametri del modello ottimale con rispettivo punteggio di coherence}
99 | \label{hyper_opt}
100 | \end{table}
101 |
102 | In Figura \ref{coherence_plots} vengono invece mostrati i grafici del punteggio di \textit{coherence} per ciascun prodotto e con gli iperparametri $\alpha$ e $\beta$ mostrati nella Tabella \ref{hyper_opt}.
103 |
104 | \begin{figure}[H]
105 | \centering
106 | \subfigure[Coherence plot of B005NF5NTK]{\includesvg[width=0.42\textwidth]{figures/3_coherence_plot_B005NF5NTK.svg}}
107 | \subfigure[Coherence plot of B0092KJ9BU]{\includesvg[width=0.42\textwidth]{figures/3_coherence_plot_B0092KJ9BU.svg}}
108 | \subfigure[Coherence plot of B00MXWFUQC]{\includesvg[width=0.42\textwidth]{figures/3_coherence_plot_B00MXWFUQC.svg}}
109 | \subfigure[Coherence plot of B00UCZGS6S]{\includesvg[width=0.42\textwidth]{figures/3_coherence_plot_B00UCZGS6S.svg}}
110 | \subfigure[Coherence plot of B00VH88CJ0]{\includesvg[width=0.42\textwidth]{figures/3_coherence_plot_B00VH88CJ0.svg}}
111 | \subfigure[Coherence plot of B00X5RV14Y]{\includesvg[width=0.42\textwidth]{figures/3_coherence_plot_B00X5RV14Y.svg}}
112 | \caption{Coherence plots of products}
113 | \label{coherence_plots}
114 | \end{figure}
115 |
116 | \section{Visualizzazione dei risultati}
117 |
118 | Per valutare i risultati prodotti dall'algoritmo LDA abbiamo usufruito di pyLDAvis, una libreria Python basata su \cite{sievert2014ldavis} che permette di visualizzare gli argomenti in maniera interattiva.
119 | \par
120 | Fornisce una visione globale degli argomenti e di come differiscono l'uno dall'altro, consentendo allo stesso tempo un'analisi approfondita dei termini maggiormente associati a ciascun singolo argomento.
121 | \par
122 | Il pannello di sinistra visualizza gli argomenti come cerchi nel piano bidimensionale i cui centri sono determinati calcolando la divergenza di Jensen-Shannon tra gli argomenti.
123 | \par
124 | Il pannello di destra mostra un grafico a barre orizzontali, le cui barre rappresentano i (30) singoli termini che sono i più rilevanti per interpretare l'argomento attualmente selezionato a sinistra. Una coppia di barre sovrapposte rappresenta sia la frequenza di un determinato termine a livello di corpus (barre blu) sia la frequenza specifica dell'argomento del termine (barre rosse).
125 | \par
126 | Sempre nel pannello di destra, appena sopra il grafico a barre orizzontali, è possibile regolare per mezzo di uno slider il valore $\lambda$, con $0 \leq \lambda \leq 1$. Esso consente di classificare la pertinenza di un termine rispetto a un argomento.
127 | \par
128 | Valori di $\lambda$ vicino a 0 evidenziano i termini potenzialmente rari ma esclusivi per l'argomento selezionato, mentre valori di $ \lambda$ vicino a 1 evidenziano i termini frequenti ma non necessariamente esclusivi per l'argomento selezionato.
129 | \par
130 | L'impostazione consigliata in \cite{sievert2014ldavis} suggerisce un valore di $\lambda$ intorno a 0.6, che è stato dimostrato essere di aiuto per gli utenti per interpretare l'argomento, nonostante sia fatto presente che il valore ottimale può variare in base al dataset e gli argomenti stessi.
--------------------------------------------------------------------------------
/scripts/requirements.txt:
--------------------------------------------------------------------------------
1 | alabaster==0.7.12
2 | argh==0.26.2
3 | astroid==2.3.3
4 | atomicwrites==1.3.0
5 | attrs==19.3.0
6 | autopep8==1.4.4
7 | Babel==2.7.0
8 | backcall==0.1.0
9 | bleach==3.1.4
10 | blis==0.4.1
11 | boto==2.49.0
12 | boto3==1.10.34
13 | botocore==1.13.34
14 | catalogue==0.0.8
15 | certifi==2019.11.28
16 | cffi==1.13.2
17 | chardet==3.0.4
18 | cloudpickle==1.2.2
19 | cryptography==2.8
20 | cycler==0.10.0
21 | cymem==2.0.3
22 | decorator==4.4.1
23 | defusedxml==0.6.0
24 | diff-match-patch==20181111
25 | docutils==0.15.2
26 | entrypoints==0.3
27 | flake8==3.7.9
28 | funcy==1.14
29 | future==0.18.2
30 | gensim==3.8.1
31 | helpdev==0.6.10
32 | idna==2.8
33 | imagesize==1.1.0
34 | importlib-metadata==1.2.0
35 | intervaltree==3.0.2
36 | ipykernel==5.1.3
37 | ipython==7.0.0
38 | ipython-genutils==0.2.0
39 | isort==4.3.21
40 | jedi==0.14.1
41 | jeepney==0.4.1
42 | Jinja2==2.10.3
43 | jmespath==0.9.4
44 | joblib==0.14.0
45 | jsonschema==3.2.0
46 | jupyter-client==5.3.4
47 | jupyter-core==4.6.1
48 | keyring==20.0.0
49 | kiwisolver==1.1.0
50 | lazy-object-proxy==1.4.3
51 | MarkupSafe==1.1.1
52 | matplotlib==3.1.2
53 | mccabe==0.6.1
54 | mistune==0.8.4
55 | more-itertools==8.0.2
56 | murmurhash==1.0.2
57 | nbconvert==5.6.1
58 | nbformat==4.4.0
59 | nltk==3.4.5
60 | numexpr==2.7.0
61 | numpy==1.17.4
62 | numpydoc==0.9.1
63 | packaging==19.2
64 | pandas==0.25.3
65 | pandocfilters==1.4.2
66 | parso==0.5.1
67 | pathtools==0.1.2
68 | pexpect==4.7.0
69 | pickleshare==0.7.5
70 | Pillow==6.2.1
71 | plac==1.1.3
72 | pluggy==0.13.1
73 | preshed==3.0.2
74 | prompt-toolkit==2.0.10
75 | psutil==5.6.7
76 | ptyprocess==0.6.0
77 | py==1.8.0
78 | pycodestyle==2.5.0
79 | pycparser==2.19
80 | pydocstyle==4.0.1
81 | pyflakes==2.1.1
82 | Pygments==2.5.2
83 | pyLDAvis==2.1.2
84 | pylint==2.4.4
85 | Pympler==0.8
86 | pyparsing==2.4.5
87 | PyQt5==5.12.3
88 | PyQt5-sip==12.7.0
89 | PyQtWebEngine==5.12.1
90 | pyrsistent==0.15.6
91 | pytest==5.3.1
92 | python-dateutil==2.8.1
93 | python-jsonrpc-server==0.3.2
94 | python-language-server==0.31.2
95 | pytz==2019.3
96 | pyxdg==0.26
97 | PyYAML==5.2
98 | pyzmq==18.1.1
99 | QDarkStyle==2.7
100 | QtAwesome==0.6.0
101 | qtconsole==4.6.0
102 | QtPy==1.9.0
103 | requests==2.22.0
104 | rope==0.14.0
105 | s3transfer==0.2.1
106 | scikit-learn==0.22
107 | scipy==1.3.3
108 | seaborn==0.9.0
109 | SecretStorage==3.1.1
110 | simplegeneric==0.8.1
111 | six==1.13.0
112 | sklearn==0.0
113 | smart-open==1.9.0
114 | snowballstemmer==2.0.0
115 | sortedcontainers==2.1.0
116 | spacy==2.2.3
117 | Sphinx==2.2.2
118 | sphinxcontrib-applehelp==1.0.1
119 | sphinxcontrib-devhelp==1.0.1
120 | sphinxcontrib-htmlhelp==1.0.2
121 | sphinxcontrib-jsmath==1.0.1
122 | sphinxcontrib-qthelp==1.0.2
123 | sphinxcontrib-serializinghtml==1.1.3
124 | spyder==4.0.0
125 | spyder-kernels==1.8.1
126 | srsly==0.2.0
127 | testpath==0.4.4
128 | thinc==7.3.1
129 | tornado==6.0.3
130 | tqdm==4.40.1
131 | traitlets==4.3.3
132 | typed-ast==1.4.0
133 | ujson==1.35
134 | urllib3==1.25.7
135 | wasabi==0.4.2
136 | watchdog==0.9.0
137 | wcwidth==0.1.7
138 | webencodings==0.5.1
139 | wordcloud==1.6.0
140 | wrapt==1.11.2
141 | wurlitzer==2.0.0
142 | yapf==0.29.0
143 | zipp==0.6.0
144 | alabaster==0.7.12
145 | argh==0.26.2
146 | astroid==2.3.3
147 | astropy==3.2.3
148 | atomicwrites==1.3.0
149 | attrs==19.3.0
150 | autopep8==1.4.4
151 | Babel==2.7.0
152 | backcall==0.1.0
153 | bleach==3.1.4
154 | blis==0.4.1
155 | boto==2.49.0
156 | boto3==1.10.34
157 | botocore==1.13.34
158 | catalogue==0.0.8
159 | certifi==2019.11.28
160 | cffi==1.13.2
161 | chardet==3.0.4
162 | Click==7.0
163 | cloudpickle==1.2.2
164 | confuse==1.0.0
165 | cryptography==2.8
166 | cycler==0.10.0
167 | cymem==2.0.3
168 | dash==1.7.0
169 | dash-core-components==1.6.0
170 | dash-html-components==1.0.2
171 | dash-renderer==1.2.2
172 | dash-table==4.5.1
173 | decorator==4.4.1
174 | defusedxml==0.6.0
175 | diff-match-patch==20181111
176 | docutils==0.15.2
177 | entrypoints==0.3
178 | flake8==3.7.9
179 | Flask==1.1.1
180 | Flask-Compress==1.4.0
181 | funcy==1.14
182 | future==0.18.2
183 | gensim==3.8.1
184 | helpdev==0.6.10
185 | htmlmin==0.1.12
186 | idna==2.8
187 | imagesize==1.1.0
188 | importlib-metadata==1.2.0
189 | intervaltree==3.0.2
190 | ipykernel==5.1.3
191 | ipython==7.0.0
192 | ipython-genutils==0.2.0
193 | ipywidgets==7.5.1
194 | isort==4.3.21
195 | itsdangerous==1.1.0
196 | jedi==0.14.1
197 | jeepney==0.4.1
198 | Jinja2==2.10.3
199 | jmespath==0.9.4
200 | joblib==0.14.0
201 | json5==0.8.5
202 | jsonschema==3.2.0
203 | jupyter==1.0.0
204 | jupyter-client==5.3.4
205 | jupyter-console==6.0.0
206 | jupyter-core==4.6.1
207 | jupyterlab==1.2.4
208 | jupyterlab-server==1.0.6
209 | keyring==20.0.0
210 | kiwisolver==1.1.0
211 | lab==5.1
212 | lazy-object-proxy==1.4.3
213 | llvmlite==0.30.0
214 | MarkupSafe==1.1.1
215 | matplotlib==3.1.2
216 | mccabe==0.6.1
217 | missingno==0.4.2
218 | mistune==0.8.4
219 | more-itertools==8.0.2
220 | murmurhash==1.0.2
221 | nbconvert==5.6.1
222 | nbformat==4.4.0
223 | nltk==3.4.5
224 | notebook==6.0.2
225 | numba==0.46.0
226 | numexpr==2.7.0
227 | numpy==1.17.4
228 | numpydoc==0.9.1
229 | packaging==19.2
230 | pandas==0.25.3
231 | pandas-profiling==2.3.0
232 | pandocfilters==1.4.2
233 | parso==0.5.1
234 | pathtools==0.1.2
235 | pexpect==4.7.0
236 | phik==0.9.8
237 | pickleshare==0.7.5
238 | Pillow==6.2.1
239 | plac==1.1.3
240 | plotly==4.4.1
241 | pluggy==0.13.1
242 | preshed==3.0.2
243 | prometheus-client==0.7.1
244 | prompt-toolkit==2.0.10
245 | psutil==5.6.7
246 | ptyprocess==0.6.0
247 | py==1.8.0
248 | pycodestyle==2.5.0
249 | pycparser==2.19
250 | pydocstyle==4.0.1
251 | pyflakes==2.1.1
252 | Pygments==2.5.2
253 | pyLDAvis==2.1.2
254 | pylint==2.4.4
255 | Pympler==0.8
256 | pyparsing==2.4.5
257 | PyQt5==5.12.3
258 | PyQt5-sip==12.7.0
259 | PyQtWebEngine==5.12.1
260 | pyrsistent==0.15.6
261 | pytest==5.3.1
262 | pytest-pylint==0.14.1
263 | python-dateutil==2.8.1
264 | python-jsonrpc-server==0.3.2
265 | python-language-server==0.31.2
266 | pytz==2019.3
267 | pyxdg==0.26
268 | PyYAML==5.2
269 | pyzmq==18.1.1
270 | QDarkStyle==2.7
271 | QtAwesome==0.6.0
272 | qtconsole==4.6.0
273 | QtPy==1.9.0
274 | requests==2.22.0
275 | retrying==1.3.3
276 | rope==0.14.0
277 | s3transfer==0.2.1
278 | scikit-learn==0.22
279 | scipy==1.3.3
280 | seaborn==0.9.0
281 | SecretStorage==3.1.1
282 | Send2Trash==1.5.0
283 | simplegeneric==0.8.1
284 | simplejson==3.17.0
285 | six==1.13.0
286 | sklearn==0.0
287 | smart-open==1.9.0
288 | snowballstemmer==2.0.0
289 | sortedcontainers==2.1.0
290 | spacy==2.2.3
291 | Sphinx==2.2.2
292 | sphinxcontrib-applehelp==1.0.1
293 | sphinxcontrib-devhelp==1.0.1
294 | sphinxcontrib-htmlhelp==1.0.2
295 | sphinxcontrib-jsmath==1.0.1
296 | sphinxcontrib-qthelp==1.0.2
297 | sphinxcontrib-serializinghtml==1.1.3
298 | spyder==4.0.0
299 | spyder-kernels==1.8.1
300 | srsly==0.2.0
301 | terminado==0.8.3
302 | testpath==0.4.4
303 | thinc==7.3.1
304 | tornado==6.0.3
305 | tqdm==4.40.1
306 | traitlets==4.3.3
307 | typed-ast==1.4.0
308 | ujson==1.35
309 | urllib3==1.25.7
310 | wasabi==0.4.2
311 | watchdog==0.9.0
312 | wcwidth==0.1.7
313 | webencodings==0.5.1
314 | Werkzeug==0.16.0
315 | widgetsnbextension==3.5.1
316 | wordcloud==1.6.0
317 | wrapt==1.11.2
318 | wurlitzer==2.0.0
319 | yapf==0.29.0
320 | zipp==0.6.0
321 | alabaster==0.7.12
322 | argh==0.26.2
323 | astroid==2.3.3
324 | astropy==3.2.3
325 | atomicwrites==1.3.0
326 | attrs==19.3.0
327 | autopep8==1.4.4
328 | Babel==2.7.0
329 | backcall==0.1.0
330 | bleach==3.1.4
331 | blis==0.4.1
332 | boto==2.49.0
333 | boto3==1.10.34
334 | botocore==1.13.34
335 | catalogue==0.0.8
336 | certifi==2019.11.28
337 | cffi==1.13.2
338 | chardet==3.0.4
339 | Click==7.0
340 | cloudpickle==1.2.2
341 | confuse==1.0.0
342 | cryptography==2.8
343 | cycler==0.10.0
344 | cymem==2.0.3
345 | dash==1.7.0
346 | dash-core-components==1.6.0
347 | dash-html-components==1.0.2
348 | dash-renderer==1.2.2
349 | dash-table==4.5.1
350 | decorator==4.4.1
351 | defusedxml==0.6.0
352 | diff-match-patch==20181111
353 | docutils==0.15.2
354 | entrypoints==0.3
355 | flake8==3.7.9
356 | Flask==1.1.1
357 | Flask-Compress==1.4.0
358 | Flask-Cors==3.0.8
359 | funcy==1.14
360 | future==0.18.2
361 | gensim==3.8.1
362 | helpdev==0.6.10
363 | htmlmin==0.1.12
364 | idna==2.8
365 | imagesize==1.1.0
366 | importlib-metadata==1.2.0
367 | intervaltree==3.0.2
368 | ipykernel==5.1.3
369 | ipython==7.0.0
370 | ipython-genutils==0.2.0
371 | ipywidgets==7.5.1
372 | isort==4.3.21
373 | itsdangerous==1.1.0
374 | jedi==0.14.1
375 | jeepney==0.4.1
376 | Jinja2==2.10.3
377 | jmespath==0.9.4
378 | joblib==0.14.0
379 | json5==0.8.5
380 | jsonschema==3.2.0
381 | jupyter==1.0.0
382 | jupyter-client==5.3.4
383 | jupyter-console==6.0.0
384 | jupyter-core==4.6.1
385 | jupyterlab==1.2.4
386 | jupyterlab-server==1.0.6
387 | keyring==20.0.0
388 | kiwisolver==1.1.0
389 | lab==5.1
390 | lazy-object-proxy==1.4.3
391 | llvmlite==0.30.0
392 | MarkupSafe==1.1.1
393 | matplotlib==3.1.2
394 | mccabe==0.6.1
395 | missingno==0.4.2
396 | mistune==0.8.4
397 | more-itertools==8.0.2
398 | murmurhash==1.0.2
399 | nbconvert==5.6.1
400 | nbformat==4.4.0
401 | nltk==3.4.5
402 | notebook==6.0.2
403 | numba==0.46.0
404 | numexpr==2.7.0
405 | numpy==1.17.4
406 | numpydoc==0.9.1
407 | packaging==19.2
408 | pandas==0.25.3
409 | pandas-profiling==2.3.0
410 | pandocfilters==1.4.2
411 | parso==0.5.1
412 | pathtools==0.1.2
413 | pexpect==4.7.0
414 | phik==0.9.8
415 | pickleshare==0.7.5
416 | Pillow==6.2.1
417 | plac==1.1.3
418 | plotly==4.4.1
419 | pluggy==0.13.1
420 | preshed==3.0.2
421 | prometheus-client==0.7.1
422 | prompt-toolkit==2.0.10
423 | psutil==5.6.7
424 | ptyprocess==0.6.0
425 | py==1.8.0
426 | pycodestyle==2.5.0
427 | pycparser==2.19
428 | pydocstyle==4.0.1
429 | pyflakes==2.1.1
430 | Pygments==2.5.2
431 | pyLDAvis==2.1.2
432 | pylint==2.4.4
433 | Pympler==0.8
434 | pyparsing==2.4.5
435 | PyQt5==5.12.3
436 | PyQt5-sip==12.7.0
437 | PyQtWebEngine==5.12.1
438 | pyrsistent==0.15.6
439 | pytest==5.3.1
440 | pytest-pylint==0.14.1
441 | python-dateutil==2.8.1
442 | python-jsonrpc-server==0.3.2
443 | python-language-server==0.31.2
444 | pytz==2019.3
445 | pyxdg==0.26
446 | PyYAML==5.2
447 | pyzmq==18.1.1
448 | QDarkStyle==2.7
449 | QtAwesome==0.6.0
450 | qtconsole==4.6.0
451 | QtPy==1.9.0
452 | requests==2.22.0
453 | retrying==1.3.3
454 | rope==0.14.0
455 | s3transfer==0.2.1
456 | scikit-learn==0.22
457 | scipy==1.3.3
458 | seaborn==0.9.0
459 | SecretStorage==3.1.1
460 | Send2Trash==1.5.0
461 | simplegeneric==0.8.1
462 | simplejson==3.17.0
463 | six==1.13.0
464 | sklearn==0.0
465 | smart-open==1.9.0
466 | snowballstemmer==2.0.0
467 | sortedcontainers==2.1.0
468 | spacy==2.2.3
469 | Sphinx==2.2.2
470 | sphinxcontrib-applehelp==1.0.1
471 | sphinxcontrib-devhelp==1.0.1
472 | sphinxcontrib-htmlhelp==1.0.2
473 | sphinxcontrib-jsmath==1.0.1
474 | sphinxcontrib-qthelp==1.0.2
475 | sphinxcontrib-serializinghtml==1.1.3
476 | spyder==4.0.0
477 | spyder-kernels==1.8.1
478 | srsly==0.2.0
479 | terminado==0.8.3
480 | testpath==0.4.4
481 | thinc==7.3.1
482 | tornado==6.0.3
483 | tqdm==4.40.1
484 | traitlets==4.3.3
485 | typed-ast==1.4.0
486 | ujson==1.35
487 | urllib3==1.25.7
488 | wasabi==0.4.2
489 | watchdog==0.9.0
490 | wcwidth==0.1.7
491 | webencodings==0.5.1
492 | Werkzeug==0.16.0
493 | widgetsnbextension==3.5.1
494 | wordcloud==1.6.0
495 | wrapt==1.11.2
496 | wurlitzer==2.0.0
497 | yapf==0.29.0
498 | zipp==0.6.0
499 | bs4
--------------------------------------------------------------------------------
/scripts/sentiment_analysis.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ### Import libraries ###
4 |
5 | import pandas as pd
6 | import numpy as np
7 | import matplotlib.pyplot as plt
8 | from wordcloud import WordCloud
9 | from sklearn.feature_extraction.text import CountVectorizer
10 | from sklearn.model_selection import train_test_split, GridSearchCV
11 | from sklearn import metrics
12 | from sklearn.linear_model import LogisticRegression
13 | from sklearn.metrics import classification_report
14 | from sklearn.naive_bayes import MultinomialNB
15 | import itertools
16 | from pathlib import Path
17 | from data_utils import text_preprocessing
18 |
19 |
20 |
21 | figOutputPath = Path("../figures/")
22 |
23 | ### Functions ###
24 |
25 | def plot_confusion_matrix(cm, name_img, classes=['negative', 'positive']):
26 | fig, ax = plt.subplots(figsize=(10,10))
27 | img = ax.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
28 | #ax.set_title('Confusion matrix {}'.format(title))
29 | ax.axis('off')
30 | fig.colorbar(img)
31 | tick_marks = np.arange(len(classes))
32 | ax.set_xticks(tick_marks, classes)
33 | ax.set_yticks(tick_marks, classes)
34 | fmt = '.2f'
35 | thresh = cm.max() / 2.
36 | for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
37 | ax.text(j, i, format(cm[i, j], fmt),
38 | horizontalalignment="center",
39 | color="white" if cm[i, j] > thresh else "black")
40 |
41 | fig.tight_layout()
42 | ax.set_ylabel('True label')
43 | ax.set_xlabel('Predicted label')
44 | ax.figure.savefig(figOutputPath / '2_confusion_matrix_{}.svg'.format(name_img),
45 | format='svg')
46 |
47 |
48 | def plot_roc(y_true, y_pred, name_img, pos_label=1):
49 | fpr, tpr, _ = metrics.roc_curve(y_true, y_pred[:, 1], pos_label)
50 | roc_auc = metrics.auc(fpr, tpr)
51 | fig, ax = plt.subplots(figsize=(10,10))
52 | #ax.set_title('Receiver Operating Characteristic of {}'.format(title))
53 | ax.plot(fpr, tpr, 'b', label='AUC = %0.2f' % roc_auc)
54 | ax.legend(loc='lower right')
55 | ax.plot([0, 1], [0, 1], 'r--')
56 | ax.set_xlim(0, 1)
57 | ax.set_ylim(0, 1)
58 | ax.set_ylabel('True Positive Rate')
59 | ax.set_xlabel('False Positive Rate')
60 | ax.figure.savefig(figOutputPath / '2_roc_{}.svg'.format(name_img),
61 | format='svg')
62 |
63 |
64 | def wordcloud(text, sentiment, title=None):
65 | wordcloud = WordCloud(
66 | background_color='whitesmoke',
67 | max_words=200,
68 | max_font_size=120,
69 | scale=3,
70 | random_state=42,
71 | width=1000,
72 | height=1000,
73 | ).generate(str(text))
74 |
75 | fig, ax = plt.subplots(figsize=(10, 10))
76 | ax.axis('off')
77 | ax.imshow(wordcloud, interpolation='nearest')
78 | ax.figure.savefig(figOutputPath / '2_wordcloud_{}.svg'.format(sentiment),
79 | format='svg')
80 | print('Exported 2_wordcloud_{}.svg'.format(sentiment))
81 |
82 |
83 | def retrieve_opinion(df, sentiment):
84 | opinion = df[df['opinion'] == sentiment]
85 | reviews = opinion['preprocessedReview'].tolist()
86 | wordcloud(reviews, sentiment)
87 |
88 |
89 | def get_term_frequency(df, cvector):
90 | cvector.fit(df.preprocessedReview)
91 |
92 | negative_matrix = cvector.transform(df[df['opinion'] == 'negative']['preprocessedReview'])
93 | negative_words = negative_matrix.sum(axis=0)
94 | negative_frequency = [(word, negative_words[0, idx]) for word, idx in cvector.vocabulary_.items()]
95 | negative_tf = pd.DataFrame(list(sorted(negative_frequency, key = lambda x: x[1], reverse=True)),
96 | columns=['Terms','negative'])
97 | negative_tf = negative_tf.set_index('Terms')
98 |
99 | positive_matrix = cvector.transform(df[df['opinion'] == 'positive']['preprocessedReview'])
100 | positive_words = positive_matrix.sum(axis=0)
101 | positive_frequency = [(word, positive_words[0, idx]) for word, idx in cvector.vocabulary_.items()]
102 | positive_tf = pd.DataFrame(list(sorted(positive_frequency, key = lambda x: x[1], reverse=True)),
103 | columns=['Terms','positive'])
104 | positive_tf = positive_tf.set_index('Terms')
105 |
106 | term_frequency_df = pd.concat([negative_tf, positive_tf], axis=1)
107 | term_frequency_df['total'] = term_frequency_df['negative'] + term_frequency_df['positive']
108 | return term_frequency_df
109 |
110 |
111 | def plot_frequency(df):
112 | #Frequency plot
113 | y_pos = np.arange(500)
114 | fig, ax = plt.subplots(figsize=(10,8))
115 | s = 1
116 | expected_zipf = [df.sort_values(by='total', ascending=False)['total'][0]/(i+1)**s for i in y_pos]
117 | ax.bar(y_pos, df.sort_values(by='total', ascending=False)['total'][:500], align='center', alpha=0.5)
118 | ax.plot(y_pos, expected_zipf, color='r', linestyle='--', linewidth=2, alpha=0.5)
119 | ax.set_ylabel('Frequency')
120 | ax.set_xlabel('Token')
121 | ax.set_yticks([0, 200000, 400000])
122 | ax.set_yticklabels(["0", "200K", "400K"])
123 | #ax.set_title('Top 500 tokens in reviews')
124 | ax.figure.savefig(figOutputPath / '2_plot_frequency.svg', format='svg')
125 | print('Exported 2_plot_frequency.svg')
126 |
127 |
128 | def token_frequency(df, sentiment):
129 | y_pos = np.arange(50)
130 | plt.figure(figsize=(12,10))
131 | plt.bar(y_pos, df.sort_values(by=sentiment, ascending=False)[sentiment][:50], align='center', alpha=0.5)
132 | plt.xticks(y_pos, df.sort_values(by=sentiment, ascending=False)[sentiment][:50].index, rotation='vertical')
133 | plt.ylabel('Frequency')
134 | plt.xlabel('Token')
135 | #plt.title('Top 50 tokens in {} reviews'.format(sentiment))
136 | plt.savefig(figOutputPath / '2_token_frequency_{}.svg'.format(sentiment), format='svg')
137 | print('Exported 2_token_frequency_{}.svg'.format(sentiment))
138 |
139 |
140 | def zipf_law(df):
141 | # Plot of absolute frequency
142 | from pylab import arange, argsort, loglog, logspace, log10, text
143 | counts = df.total
144 | tokens = df.index
145 | ranks = arange(1, len(counts)+1)
146 | indices = argsort(-counts)
147 | frequencies = counts[indices]
148 | fig, ax = plt.subplots(figsize=(8,6))
149 | ax.set_ylim(1,10**6)
150 | ax.set_xlim(1,10**6)
151 | loglog(ranks, frequencies, marker=".")
152 | ax.plot([1,frequencies[0]],[frequencies[0],1],color='r')
153 | #ax.set_title("Zipf plot for phrases tokens")
154 | ax.set_xlabel("Frequency rank of token")
155 | ax.set_ylabel("Absolute frequency of token")
156 | ax.grid(True)
157 | for n in list(logspace(-0.5, log10(len(counts)-2), 15).astype(int)):
158 | dummy = text(ranks[n], frequencies[n], " " + tokens[indices[n]],
159 | verticalalignment="bottom",
160 | horizontalalignment="left")
161 | ax.figure.savefig(figOutputPath / '2_zipf_law.png', format='png')
162 | print('Exported 2_zipf_law.png')
163 |
164 |
165 | def undersampling(df):
166 | positive, negative = df.opinion.value_counts()
167 | df_positive = df[df.opinion == 'positive']
168 | df_positive = df_positive.sample(negative, random_state=42)
169 | df_negative = df[df.opinion == 'negative']
170 | df = pd.concat([df_positive, df_negative])
171 | df = df.sample(frac=1)
172 | return df
173 |
174 |
175 |
176 | def run(df):
177 | df.drop(df[df.opinion == 'neutral'].index, inplace=True)
178 | count_vector_exploration = CountVectorizer(max_features=10000, ngram_range=(1, 2))
179 | df['words'] = [len(t) for t in df['preprocessedReview']]
180 | df = df[df['words'] <= 300]
181 | df = df[df['words'] > 5]
182 | retrieve_opinion(df, 'positive')
183 | retrieve_opinion(df, 'negative')
184 | term_frequency = get_term_frequency(df, count_vector_exploration)
185 | zipf_law(term_frequency)
186 | plot_frequency(term_frequency)
187 | token_frequency(term_frequency, 'positive')
188 | token_frequency(term_frequency, 'negative')
189 | ### Machine learning ###
190 | df = undersampling(df)
191 | count_vector_sentiment = CountVectorizer(max_features=10000, ngram_range=(1, 2))
192 | reviews = np.array(df['preprocessedReview'])
193 | sentiments = np.array(df['opinion'])
194 | sentiments[sentiments == 'positive'] = 1
195 | sentiments[sentiments == 'negative'] = 0
196 | sentiments = sentiments.astype('int')
197 |
198 | # Logistic Regression CV with grid search su BOW
199 | reviews_train, reviews_validation, sentiment_train, sentiment_validation = train_test_split(reviews, sentiments, test_size=0.2, random_state=42)
200 | count_vector_features = count_vector_sentiment.fit_transform(reviews_train)
201 | count_vector_validation_features = count_vector_sentiment.transform(reviews_validation)
202 |
203 | param_grid = [
204 | {
205 | 'C': np.logspace(0, 4, 4)
206 | }
207 | ]
208 |
209 | # Create grid search object
210 | lr = LogisticRegression(max_iter=10000, random_state=42)
211 | lr_grid = GridSearchCV(lr, param_grid=param_grid, cv=5, verbose=True, n_jobs=-1)
212 | # Fit on data
213 | best_lr = lr_grid.fit(count_vector_features, sentiment_train)
214 | print("Best params")
215 | for i in best_lr.best_params_:
216 | print(i, best_lr.best_params_[i])
217 |
218 | y_true, y_pred, y_pred_roc = sentiment_validation, best_lr.predict(count_vector_validation_features), best_lr.predict_proba(count_vector_validation_features)
219 | print("Report on validation set")
220 | print(classification_report(y_true, y_pred))
221 | cm = metrics.confusion_matrix(y_true=y_true, y_pred=y_pred, labels=[0, 1])
222 | plot_confusion_matrix(cm, 'lr')
223 | plot_roc(y_true, y_pred_roc, 'lr')
224 |
225 | # Multinomial Bayes CV with grid search su BOW
226 | reviews_train, reviews_validation, sentiment_train, sentiment_validation = train_test_split(reviews, sentiments, test_size=0.2, random_state=42)
227 | count_vector_features = count_vector_sentiment.fit_transform(reviews_train)
228 | count_vector_validation_features = count_vector_sentiment.transform(reviews_validation)
229 |
230 | param_grid = [
231 | {
232 | 'alpha': (1, 0.1, 0.01, 0.001, 0.0001, 0.00001)
233 | }
234 | ]
235 |
236 | # Create grid search object
237 | nb = MultinomialNB()
238 | nb_grid = GridSearchCV(nb, param_grid=param_grid, cv=5, verbose=True, n_jobs=-1)
239 | # Fit on data
240 | best_nb = nb_grid.fit(count_vector_features, sentiment_train)
241 |
242 | print("Best params")
243 | for i in best_nb.best_params_:
244 | print(i, best_nb.best_params_[i])
245 |
246 | y_true, y_pred, y_pred_roc = sentiment_validation, best_nb.predict(count_vector_validation_features), best_nb.predict_proba(count_vector_validation_features)
247 | print("Report on validation set")
248 | print(classification_report(y_true, y_pred))
249 | cm = metrics.confusion_matrix(y_true=y_true, y_pred=y_pred, labels=[0, 1])
250 | plot_confusion_matrix(cm, 'nb')
251 | plot_roc(y_true, y_pred_roc, 'nb')
252 |
253 | return best_nb, best_lr, count_vector_sentiment
254 |
255 |
256 |
257 |
258 | def compute_single(string, model, count_vector_sentiment):
259 | a = text_preprocessing([string], remove_less_frequent=False)
260 | a = np.asarray([ " ".join(a[0]) ])
261 | count_vector_validation_features2 = count_vector_sentiment.transform(a)
262 | return model.predict_proba(count_vector_validation_features2)
--------------------------------------------------------------------------------
/report/3_sentiment_analysis.tex:
--------------------------------------------------------------------------------
1 | \chapter{Sentiment analysis}
2 | \label{sentiment_analysis}
3 | Oggigiorno siamo affetti da e produciamo un tale sovraccarico di dati che le aziende si stanno ridefinendo per raccogliere queste informazioni, come per esempio i feedback dei clienti, e strutturare il processo decisionale. L'ottenimento di questi dati è impensabile se fatto manualmente.
4 | \par
5 | In particolare, per le opinioni su prodotti e servizi viene in aiuto la sentiment analysis, una discplina che può fornire risposte riguardo le questioni più importanti dal punto di vista dei clienti.
6 | \par
7 | Il processo di sentiment analysis permette, attraverso l'elaborazione del linguaggio naturale, di estrarre e analizzare in modo automatizzato opinioni soggettive espresse dall'utente, determinarne la polarità (positiva, neutrale, negativa) e, successivamente, riassumerle in maniera da poter essere di valore per l'azienda.
8 | \par
9 | In questo modo, le decisioni possono essere prese sulla base di una quantità di dati significativa, piuttosto che da una semplice intuizione che non sempre si rivela corretta. Il rischio infatti a cui si va incontro maggiormente è quello di interpretare i messaggi avendo già un pregiudizio sull’argomento in questione, influenzando il modo in cui il testo da analizzare può essere interpretato.
10 | \par
11 | La sentiment analysis è importante perché le aziende vogliono che il loro marchio sia recepito positivamente (con un occhio alle aziende concorrenti). A tal proposito, ci si può concentrare su commenti positivi o negativi oltre che sul feedback del cliente, per valutare sia i punti di forza che i punti su cui migliorare.
12 |
13 |
14 | \section{Preprocessing}
15 | \label{preprocessing}
16 | Prima di partire con lo svolgimento del task di sentiment analysis, è necessaria una fase di preprocessing.
17 | \par
18 | Innanzitutto, sono stati rimossi dal dataset i campi ritenuti superflui per l'analisi.
19 | \par
20 | Successivamente, è stato manipolato il campo \texttt{reviewText}. La manipolazione è avvenuta sequenzialmente e con step standard per analisi di questo tipo:
21 |
22 | \begin{itemize}
23 | \item \textbf{Normalization} - conversione delle recensioni in caratteri minuscoli. Se presenti, modificate alcune espressioni contratte tipiche della lingua inglese (per esempio: \textit{hadn't} trasformata in \textit{had not}).
24 | \item \textbf{Tokenization} - suddivisione in token per ogni recensione
25 | \item \textbf{Removal} - rimozione di token altamente ricorrenti nella lingua considerata (stopwords). Inoltre, sono stati eliminati token composti da 1 o 2 caratteri o token estremamente rari (frequenza nel dataset = 1)
26 | \item \textbf{Lemmatization} - conversione del token nel proprio lemma linguistico
27 |
28 | \end{itemize}{}
29 |
30 | Questa manipolazione ha portato alla creazione del campo \texttt{preprocessedReview}.
31 | Di seguito vengono mostrate la recensione originale e la recensione dopo l'intera fase di preprocessing.
32 | \par
33 | \begin{itemize}
34 | \item[\textbf{Prima}] \texttt{Overall a great product with a fair price. I have had absolutely no problems with the product except for the volume level, which is *NOT* below standard, it is just simply what is to be expected from a headset. Very comfortable, and I personally prefer the boom mic to be longer (unlike the newer models of this headset which have shortened mics). Recommended.}
35 | \item[\textbf{Dopo}] \texttt{overall great product fair price absolutely problem product volume level standard simply expect headset comfortable personally prefer boom mic longer new model shorten mic recommend}
36 | \end{itemize}
37 |
38 | \section{Creazione di Bag of Words}
39 | \label{bow}
40 | Il campo \texttt{preprocessedReview} non è direttamente trattabile dagli algoritmi di machine learning e quindi è necessario ottenere una rappresentazione comprensibile.
41 | Innanzitutto, abbiamo rimosso dall'analisi del campo \texttt{preprocessedReview} le recensioni:
42 | \begin{itemize}
43 | \item prolisse - rientrano in questa categoria le osservazioni con più di 300 parole
44 | \item irrilevanti - rientrano in questa categoria le osservazioni con meno di 5 parole
45 | \end{itemize}{}
46 |
47 | Dopodiché, le recensioni restanti sono state utilizzate per costruire una Bag of Words composta da 10000 feature. Oltre ai token vengono considerati anche i bigrammi, poiché un loro utilizzo può aumentare l'accuratezza del modello rispetto al solo utilizzo di token singoli.
48 | \section{Esplorazione}
49 | A partire dalla nuova rappresentazione matriciale è stato creato un DataFrame fittizio composto dalle 10000 feature individuate nel Capitolo \ref{bow} e, per ognuna di esse, viene segnata la frequenza con cui appaiono rispettivamente nelle recensioni positive e nelle recensioni negative, oltre che la frequenza totale data dalla loro somma.
50 | \par
51 | Le recensioni neutrali non sono state considerate in quanto aggiungerebbero un livello ulteriore di complessità nell'apprendimento dei modelli.
52 |
53 | \subsection{Wordcloud}
54 | Una Wordcloud è una rappresentazione grafica delle parole usate di frequente in un corpus di documenti e fornisce un'idea generale di che tipologia di parole possiamo trovarvi. La grandezza di ogni parola nell'immagine è un'indicazione della frequenza di occorrenza della parola nell'intero corpus. Per questo motivo, sono molto utili quando si vuole eseguire un'analisi del testo.
55 |
56 | \begin{figure}[H]
57 | \centering
58 | \includesvg[width=0.9\linewidth]{figures/2_wordcloud_positive}
59 | \caption{Wordcloud of positive reviews}
60 | \label{pos_wordcloud}
61 | \end{figure}
62 |
63 | \begin{figure}[H]
64 | \centering
65 | \includesvg[width=0.9\linewidth]{figures/2_wordcloud_negative}
66 | \caption{Wordcloud of negative reviews}
67 | \label{neg_wordcloud}
68 | \end{figure}
69 |
70 | \subsection{Frequenza dei token}
71 |
72 | Un'ulteriore analisi che si può fare riguarda la distribuzione dei token all'interno del dataset. Nella Figura \ref{50_pos} e nella Figura \ref{50_neg} vengono mostrati gli istogrammi (ordinati dal token più frequente al token meno frequente) rispettivamente per le recensioni positive e le recensioni negative.
73 | \begin{figure}[H]
74 | \centering
75 | \includesvg[width=0.8\linewidth]{figures/2_token_frequency_positive}
76 | \caption{Top 50 tokens in positive reviews}
77 | \label{50_pos}
78 | \end{figure}
79 |
80 | \begin{figure}[H]
81 | \centering
82 | \includesvg[width=0.8\linewidth]{figures/2_token_frequency_negative}
83 | \caption{Top 50 tokens in negative reviews}
84 | \label{50_neg}
85 | \end{figure}
86 |
87 | Un modello comunemente utilizzato è la legge di Zipf, ovvero una legge empirica formulata nel 1959 in cui vi si afferma che, dato un corpus di documenti, la frequenza di ogni parola è inversamente proporzionale al suo rango nella tabella delle frequenze. Pertanto, la parola più frequente ricorre approssimativamente il doppio rispetto alla seconda parola più frequente, il triplo rispetto alla terza parola più frequente e così via.
88 |
89 | \begin{figure}[H]
90 | \centering
91 | \includesvg[width=0.8\linewidth]{figures/2_plot_frequency}
92 | \caption{Distribution of words in review for each opinion}
93 | \label{distribution_words_opinion}
94 | \end{figure}
95 |
96 | La legge di Zipf viene osservata più facilmente tracciando i dati su scala logaritmica in entrambi gli assi come mostrato in Figura \ref{zipf_law}.
97 |
98 | \begin{figure}[H]
99 | \centering
100 | \captionsetup{margin=1cm}
101 | \includegraphics[width=0.8\linewidth]{figures/2_zipf_law.png}
102 | \caption{Distribution of words in verified reviews}
103 | \label{zipf_law}
104 | \end{figure}
105 |
106 |
107 | \section{Machine learning}
108 | La tecnica con cui abbiamo affrontato la fase di sentiment analysis è il machine learning. Grazie ad essa il modello viene addestrato per riconoscere il sentimento in base alle parole usando un training set etichettato. Questo approccio dipende in gran parte dal tipo di algoritmo e dalla qualità dei dati utilizzati per l'addestramento.
109 |
110 | \par
111 | Grazie all'attenta fase di preprocessing, è stato possibile addestrare due modelli diversi con gli stessi dati e valutarne i risultati, confrontandoli.
112 | \par
113 | Per prima cosa abbiamo dovuto risolvere il problema dello sbilanciamento tra classi: le recensioni positive sono in numero molto maggiore rispetto a quelle negative e qualsiasi modello addestrato rischierebbe di ottimizzarsi più sulla classe maggiore.
114 | La soluzione più adeguata in questo caso è l'utilizzo di una tecnica di undersampling in modo da ridurre gli elementi della classe maggioritaria senza introdurre bias e rendendo equiparabili le cardinalità delle classi.
115 | \par
116 | Il problema di machine learning è di natura binaria in quanto la variabile target ha solo due possibili valori: \textit{positive} e \textit{negative}.
117 | Data questa semplicità ci sono numerosi modelli che possono essere addestrati. Sono stati scelti Multinomial Naive Bayes e Logistic Regression per il loro ottimo compromesso tra performance e tempo di addestramento. Abbiamo provato ad implementare anche una Support Vector Machine ma le risorse hardware richieste per l'addestramento non erano disponibili.
118 | \par
119 | Per entrambi i modelli è stata eseguita la tecnica di Cross Validation (CV) su 5 folds, dividendo quindi il training set in 5 sottoinsiemi e usandone uno come test set, per 5 volte.
120 |
121 | to do:split da aggiungere
122 | Per entrambi i modelli è stato tenuto da parte un validation set per la verifica finale e l'analisi delle varie metriche.
123 |
124 | \subsection{Analisi dei risultati}
125 |
126 | Al di sopra della CV è stata eseguita anche una Grid Search nel tentativo di trovare i migliori iperparametri per i due modelli.
127 | In Naive Bayes è stato trovato il migliore valore per alpha (0.1), mentre nella Logistic Regression il migliore valore dell'iperparametro C è stato 1.
128 | \par
129 | Analizzando le matrici di confusione dei due modelli in Figura \ref{cm_nb} e in Figura \ref{cm_lr} notiamo che il modello di Logistic Regression individua in totale meno falsi positivi e falsi negativi rispetto a Naive Bayes; ciò è confermato dal valore di \textit{Accuracy} leggermente più alto.
130 |
131 | \begin{figure}[H]
132 | \centering
133 | \includesvg[width=0.9\linewidth]{figures/2_confusion_matrix_nb.svg}
134 | \caption{Confusion Matrix per Naive Bayes}
135 | \label{cm_nb}
136 | \end{figure}
137 |
138 | \begin{figure}[H]
139 | \centering
140 | \includesvg[width=0.9\linewidth]{figures/2_confusion_matrix_lr.svg}
141 | \caption{Confusion Matrix per Logistic Regression}
142 | \label{cm_lr}
143 | \end{figure}
144 |
145 | \par
146 | Sia i valori di \textit{Precision} che quelli di \textit{Recall} di Logistic Regression sono più alti indicando che per entrambe le classi tale modello trova un miglior numero di True e un minor numero di False.
147 |
148 | \begin{table}[H]
149 | \centering
150 | \begin{tabular}{l l}
151 | Accuracy complessiva & 0.84\\
152 | Precision per la classe \textit{positive} & 0.84\\
153 | Precision per la classe \textit{negative} & 0.85\\
154 | Recall per la classe \textit{positive} & 0.85\\
155 | Recall per la classe \textit{negative} & 0.84\\
156 | \end{tabular}
157 | \caption{Metriche risultate dell'esecuzione della cross validation su Naive Bayes}
158 | \end{table}
159 |
160 | \begin{table}[H]
161 | \centering
162 | \begin{tabular}{l l}
163 | Accuracy complessiva & 0.87\\
164 | Precision per la classe \textit{positive} & 0.87\\
165 | Precision per la classe \textit{negative} & 0.86\\
166 | Recall per la classe \textit{positive} & 0.86\\
167 | Recall per la classe \textit{negative} & 0.88\\
168 | \end{tabular}
169 | \caption{Metriche risultate dell'esecuzione della cross validation su Logistic Regression}
170 | \end{table}
171 |
172 |
173 | \par
174 | Le due ROC in Figura \ref{roc_nb} e in Figura \ref{roc_lr} mostrano l'andamento del rapporto tra True Positive Rate e False Positive Rate al variare del valore di cut-off di ogni modello: anche in questo grafico possiamo vedere una performance migliore da parte della Logistic Regression poichè la sua curva si avvicina di più a quella "ideale" e la sua inclinazione è più verticale.
175 |
176 | \begin{figure}[H]
177 | \centering
178 | \includesvg[width=0.9\linewidth]{figures/2_roc_nb.svg}
179 | \caption{ROC per Naive Bayes}
180 | \label{roc_nb}
181 | \end{figure}
182 |
183 | \begin{figure}[H]
184 | \centering
185 | \includesvg[width=0.9\linewidth]{figures/2_roc_lr.svg}
186 | \caption{ROC per Logistic Regression}
187 | \label{roc_lr}
188 | \end{figure}
189 |
190 |
--------------------------------------------------------------------------------
/scripts/ver_counts.json:
--------------------------------------------------------------------------------
1 | {
2 | "count": [
3 | [
4 | {
5 | "data": [
6 | 47,
7 | 5
8 | ],
9 | "name": 1
10 | },
11 | {
12 | "data": [
13 | 32,
14 | 2
15 | ],
16 | "name": 2
17 | },
18 | {
19 | "data": [
20 | 79,
21 | 5
22 | ],
23 | "name": 3
24 | },
25 | {
26 | "data": [
27 | 231,
28 | 9
29 | ],
30 | "name": 4
31 | },
32 | {
33 | "data": [
34 | 1189,
35 | 35
36 | ],
37 | "name": 5
38 | }
39 | ],
40 | [
41 | {
42 | "data": [
43 | 64,
44 | 8
45 | ],
46 | "name": 1
47 | },
48 | {
49 | "data": [
50 | 72,
51 | 9
52 | ],
53 | "name": 2
54 | },
55 | {
56 | "data": [
57 | 90,
58 | 17
59 | ],
60 | "name": 3
61 | },
62 | {
63 | "data": [
64 | 155,
65 | 28
66 | ],
67 | "name": 4
68 | },
69 | {
70 | "data": [
71 | 580,
72 | 41
73 | ],
74 | "name": 5
75 | }
76 | ],
77 | [
78 | {
79 | "data": [
80 | 22,
81 | 3
82 | ],
83 | "name": 1
84 | },
85 | {
86 | "data": [
87 | 14,
88 | 1
89 | ],
90 | "name": 2
91 | },
92 | {
93 | "data": [
94 | 37,
95 | 4
96 | ],
97 | "name": 3
98 | },
99 | {
100 | "data": [
101 | 141,
102 | 2
103 | ],
104 | "name": 4
105 | },
106 | {
107 | "data": [
108 | 774,
109 | 25
110 | ],
111 | "name": 5
112 | }
113 | ],
114 | [
115 | {
116 | "data": [
117 | 35,
118 | 3
119 | ],
120 | "name": 1
121 | },
122 | {
123 | "data": [
124 | 31,
125 | 3
126 | ],
127 | "name": 2
128 | },
129 | {
130 | "data": [
131 | 35,
132 | 1
133 | ],
134 | "name": 3
135 | },
136 | {
137 | "data": [
138 | 93,
139 | 3
140 | ],
141 | "name": 4
142 | },
143 | {
144 | "data": [
145 | 800,
146 | 24
147 | ],
148 | "name": 5
149 | }
150 | ],
151 | [
152 | {
153 | "data": [
154 | 11,
155 | 2
156 | ],
157 | "name": 1
158 | },
159 | {
160 | "data": [
161 | 14,
162 | 1
163 | ],
164 | "name": 2
165 | },
166 | {
167 | "data": [
168 | 38,
169 | 1
170 | ],
171 | "name": 3
172 | },
173 | {
174 | "data": [
175 | 139,
176 | 12
177 | ],
178 | "name": 4
179 | },
180 | {
181 | "data": [
182 | 773,
183 | 50
184 | ],
185 | "name": 5
186 | }
187 | ],
188 | [
189 | {
190 | "data": [
191 | 132,
192 | 11
193 | ],
194 | "name": 1
195 | },
196 | {
197 | "data": [
198 | 41,
199 | 0
200 | ],
201 | "name": 2
202 | },
203 | {
204 | "data": [
205 | 48,
206 | 1
207 | ],
208 | "name": 3
209 | },
210 | {
211 | "data": [
212 | 120,
213 | 11
214 | ],
215 | "name": 4
216 | },
217 | {
218 | "data": [
219 | 942,
220 | 71
221 | ],
222 | "name": 5
223 | }
224 | ],
225 | [
226 | {
227 | "data": [
228 | 40,
229 | 2
230 | ],
231 | "name": 1
232 | },
233 | {
234 | "data": [
235 | 26,
236 | 0
237 | ],
238 | "name": 2
239 | },
240 | {
241 | "data": [
242 | 36,
243 | 0
244 | ],
245 | "name": 3
246 | },
247 | {
248 | "data": [
249 | 103,
250 | 2
251 | ],
252 | "name": 4
253 | },
254 | {
255 | "data": [
256 | 892,
257 | 18
258 | ],
259 | "name": 5
260 | }
261 | ],
262 | [
263 | {
264 | "data": [
265 | 40,
266 | 5
267 | ],
268 | "name": 1
269 | },
270 | {
271 | "data": [
272 | 21,
273 | 6
274 | ],
275 | "name": 2
276 | },
277 | {
278 | "data": [
279 | 41,
280 | 3
281 | ],
282 | "name": 3
283 | },
284 | {
285 | "data": [
286 | 120,
287 | 8
288 | ],
289 | "name": 4
290 | },
291 | {
292 | "data": [
293 | 954,
294 | 69
295 | ],
296 | "name": 5
297 | }
298 | ],
299 | [
300 | {
301 | "data": [
302 | 33,
303 | 7
304 | ],
305 | "name": 1
306 | },
307 | {
308 | "data": [
309 | 24,
310 | 7
311 | ],
312 | "name": 2
313 | },
314 | {
315 | "data": [
316 | 38,
317 | 5
318 | ],
319 | "name": 3
320 | },
321 | {
322 | "data": [
323 | 89,
324 | 26
325 | ],
326 | "name": 4
327 | },
328 | {
329 | "data": [
330 | 592,
331 | 156
332 | ],
333 | "name": 5
334 | }
335 | ],
336 | [
337 | {
338 | "data": [
339 | 39,
340 | 2
341 | ],
342 | "name": 1
343 | },
344 | {
345 | "data": [
346 | 19,
347 | 3
348 | ],
349 | "name": 2
350 | },
351 | {
352 | "data": [
353 | 41,
354 | 2
355 | ],
356 | "name": 3
357 | },
358 | {
359 | "data": [
360 | 134,
361 | 10
362 | ],
363 | "name": 4
364 | },
365 | {
366 | "data": [
367 | 1006,
368 | 84
369 | ],
370 | "name": 5
371 | }
372 | ],
373 | [
374 | {
375 | "data": [
376 | 97,
377 | 7
378 | ],
379 | "name": 1
380 | },
381 | {
382 | "data": [
383 | 70,
384 | 3
385 | ],
386 | "name": 2
387 | },
388 | {
389 | "data": [
390 | 83,
391 | 2
392 | ],
393 | "name": 3
394 | },
395 | {
396 | "data": [
397 | 148,
398 | 14
399 | ],
400 | "name": 4
401 | },
402 | {
403 | "data": [
404 | 484,
405 | 70
406 | ],
407 | "name": 5
408 | }
409 | ],
410 | [
411 | {
412 | "data": [
413 | 24,
414 | 3
415 | ],
416 | "name": 1
417 | },
418 | {
419 | "data": [
420 | 19,
421 | 0
422 | ],
423 | "name": 2
424 | },
425 | {
426 | "data": [
427 | 41,
428 | 1
429 | ],
430 | "name": 3
431 | },
432 | {
433 | "data": [
434 | 154,
435 | 13
436 | ],
437 | "name": 4
438 | },
439 | {
440 | "data": [
441 | 1109,
442 | 149
443 | ],
444 | "name": 5
445 | }
446 | ],
447 | [
448 | {
449 | "data": [
450 | 62,
451 | 4
452 | ],
453 | "name": 1
454 | },
455 | {
456 | "data": [
457 | 46,
458 | 2
459 | ],
460 | "name": 2
461 | },
462 | {
463 | "data": [
464 | 67,
465 | 6
466 | ],
467 | "name": 3
468 | },
469 | {
470 | "data": [
471 | 162,
472 | 22
473 | ],
474 | "name": 4
475 | },
476 | {
477 | "data": [
478 | 754,
479 | 102
480 | ],
481 | "name": 5
482 | }
483 | ],
484 | [
485 | {
486 | "data": [
487 | 75,
488 | 8
489 | ],
490 | "name": 1
491 | },
492 | {
493 | "data": [
494 | 46,
495 | 5
496 | ],
497 | "name": 2
498 | },
499 | {
500 | "data": [
501 | 70,
502 | 3
503 | ],
504 | "name": 3
505 | },
506 | {
507 | "data": [
508 | 161,
509 | 8
510 | ],
511 | "name": 4
512 | },
513 | {
514 | "data": [
515 | 609,
516 | 23
517 | ],
518 | "name": 5
519 | }
520 | ],
521 | [
522 | {
523 | "data": [
524 | 152,
525 | 6
526 | ],
527 | "name": 1
528 | },
529 | {
530 | "data": [
531 | 81,
532 | 2
533 | ],
534 | "name": 2
535 | },
536 | {
537 | "data": [
538 | 121,
539 | 4
540 | ],
541 | "name": 3
542 | },
543 | {
544 | "data": [
545 | 137,
546 | 10
547 | ],
548 | "name": 4
549 | },
550 | {
551 | "data": [
552 | 467,
553 | 26
554 | ],
555 | "name": 5
556 | }
557 | ],
558 | [
559 | {
560 | "data": [
561 | 25,
562 | 2
563 | ],
564 | "name": 1
565 | },
566 | {
567 | "data": [
568 | 20,
569 | 1
570 | ],
571 | "name": 2
572 | },
573 | {
574 | "data": [
575 | 29,
576 | 4
577 | ],
578 | "name": 3
579 | },
580 | {
581 | "data": [
582 | 169,
583 | 8
584 | ],
585 | "name": 4
586 | },
587 | {
588 | "data": [
589 | 1355,
590 | 69
591 | ],
592 | "name": 5
593 | }
594 | ],
595 | [
596 | {
597 | "data": [
598 | 14,
599 | 5
600 | ],
601 | "name": 1
602 | },
603 | {
604 | "data": [
605 | 11,
606 | 1
607 | ],
608 | "name": 2
609 | },
610 | {
611 | "data": [
612 | 31,
613 | 3
614 | ],
615 | "name": 3
616 | },
617 | {
618 | "data": [
619 | 120,
620 | 18
621 | ],
622 | "name": 4
623 | },
624 | {
625 | "data": [
626 | 1133,
627 | 177
628 | ],
629 | "name": 5
630 | }
631 | ],
632 | [
633 | {
634 | "data": [
635 | 80,
636 | 4
637 | ],
638 | "name": 1
639 | },
640 | {
641 | "data": [
642 | 45,
643 | 1
644 | ],
645 | "name": 2
646 | },
647 | {
648 | "data": [
649 | 60,
650 | 0
651 | ],
652 | "name": 3
653 | },
654 | {
655 | "data": [
656 | 121,
657 | 5
658 | ],
659 | "name": 4
660 | },
661 | {
662 | "data": [
663 | 668,
664 | 17
665 | ],
666 | "name": 5
667 | }
668 | ],
669 | [
670 | {
671 | "data": [
672 | 23,
673 | 3
674 | ],
675 | "name": 1
676 | },
677 | {
678 | "data": [
679 | 18,
680 | 0
681 | ],
682 | "name": 2
683 | },
684 | {
685 | "data": [
686 | 41,
687 | 1
688 | ],
689 | "name": 3
690 | },
691 | {
692 | "data": [
693 | 156,
694 | 12
695 | ],
696 | "name": 4
697 | },
698 | {
699 | "data": [
700 | 1111,
701 | 141
702 | ],
703 | "name": 5
704 | }
705 | ],
706 | [
707 | {
708 | "data": [
709 | 37,
710 | 5
711 | ],
712 | "name": 1
713 | },
714 | {
715 | "data": [
716 | 39,
717 | 3
718 | ],
719 | "name": 2
720 | },
721 | {
722 | "data": [
723 | 36,
724 | 0
725 | ],
726 | "name": 3
727 | },
728 | {
729 | "data": [
730 | 102,
731 | 4
732 | ],
733 | "name": 4
734 | },
735 | {
736 | "data": [
737 | 1216,
738 | 66
739 | ],
740 | "name": 5
741 | }
742 | ]
743 | ],
744 | "products": [
745 | "B005NF5NTK",
746 | "B0092KJ9BU",
747 | "B00AANQLRI",
748 | "B00BT8L2MW",
749 | "B00D856NOG",
750 | "B00G7UY3EG",
751 | "B00IGISUTG",
752 | "B00M51DDT2",
753 | "B00M6QODH2",
754 | "B00MQSMDYU",
755 | "B00MXWFUQC",
756 | "B00P7N0320",
757 | "B00QN1T6NM",
758 | "B00UCZGS6S",
759 | "B00UH3L82Y",
760 | "B00VH88CJ0",
761 | "B00X5RV14Y",
762 | "B014EB532U",
763 | "B018JW3EOY",
764 | "B019PV2I3G"
765 | ]
766 | }
--------------------------------------------------------------------------------
/webapp/public/lda_B00VH88CJ0.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/scripts/data_exploration.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ### Import libraries ###
4 |
5 | import numpy as np
6 | import pandas as pd
7 | from pandas import Grouper
8 | import matplotlib.pyplot as plt
9 | import seaborn as sns
10 | color = sns.color_palette()
11 | sns.set_style(style="darkgrid")
12 | from data_utils import most_reviewed_products
13 | from pathlib import Path
14 | from matplotlib import rcParams
15 | import json
16 |
17 | # Default text styling for figures
18 | rcParams['font.family'] = 'sans-serif'
19 | rcParams['font.sans-serif'] = ['Inter']
20 | rcParams['font.weight'] = 500
21 | rcParams['xtick.labelsize'] = 13
22 | rcParams['ytick.labelsize'] = 13
23 |
24 | figOutputPath = Path("../figures/")
25 |
26 | ### Functions ###
27 |
28 | def label_typography(ax):
29 | ax.xaxis.label.set_fontweight(500)
30 | ax.yaxis.label.set_fontsize(15)
31 | ax.yaxis.label.set_fontweight(500)
32 | ax.xaxis.label.set_fontsize(15)
33 | return
34 |
35 | def most_active_reviewers(df, n_reviewers):
36 | n_reviews = df['reviewerID'].value_counts()
37 | most_reviews = n_reviews.nlargest(n_reviewers)
38 | most_reviews = most_reviews.reset_index()
39 | most_reviews = most_reviews.drop('reviewerID', axis=1)
40 | definitive = df.merge(most_reviews, left_on='reviewerID', right_on='index')
41 | definitive = definitive.drop('index', axis=1)
42 | return definitive
43 |
44 | def analyze_reviews(df, df_attribute, name_file, xlabel):
45 | fig, ax = plt.subplots(figsize=(10, 10))
46 | sns.countplot(df_attribute, ax=ax)
47 | label_typography(ax)
48 |
49 | # Set and style the title, and move it up a bit (1.02 = 2%)
50 | #ax.set_title(title, fontname='Inter', fontsize=20, fontweight=500, y=1.02)
51 |
52 | ax.xaxis.label.set_text(xlabel)
53 | ax.yaxis.label.set_text("Review count")
54 | if (name_file=="review_distribution_per_day"):
55 | ax.set_xticklabels(["Sunday", "Monday", "Thuesday", "Wednesday", "Thursday", "Friday", "Saturday"])
56 | ax.xaxis.label.set_fontsize(13)
57 | ax.set_yticks([0, 100000, 200000])
58 | ax.set_yticklabels(["0", "100K", "200K"])
59 | elif (name_file=="review_distribution_per_month"):
60 | ax.set_xticklabels(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"])
61 | ax.xaxis.label.set_fontsize(13)
62 | ax.set_yticks([0, 100000, 200000])
63 | ax.set_yticklabels(["0", "100K", "200K"])
64 | elif (name_file=="review_distribution_per_year"):
65 | ax.set_xticklabels([2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018])
66 | ax.xaxis.label.set_fontsize(13)
67 | ax.set_yticks([0, 100000, 200000])
68 | ax.set_yticklabels(["0", "100K", "200K"])
69 | elif (name_file=="unverified_overall_distribution"):
70 | ax.set_yticks([0, 50000, 100000])
71 | ax.set_yticklabels(["0", "50K", "100K"])
72 | elif (name_file=="verified_overall_distribution"):
73 | ax.set_yticks([0, 300000, 600000])
74 | ax.set_yticklabels(["0", "300K", "600K"])
75 | else:
76 | ax.set_yticks([0, 100000, 500000, 1000000])
77 | ax.set_yticklabels(["0", "100K", "500K", "1M"])
78 |
79 |
80 |
81 | ax.figure.savefig(figOutputPath / '1_{0}.svg'.format(name_file), format='svg')
82 | print('Exported 1_{}.svg'.format(name_file))
83 |
84 | def run(df):
85 | # 1 - Countplot: overall distribution
86 | analyze_reviews(df, df.overall, 'overall_distribution', 'Overall')
87 |
88 | # 2 - Countplot: opinion distribution
89 | analyze_reviews(df, df.opinion, 'opinion_distribution', 'Opinion')
90 |
91 | # 3 - Distribution of words
92 | reduced_df = df.copy()
93 | reduced_df = reduced_df[reduced_df['n_words'] <= 1000]
94 | fig, ax5 = plt.subplots()
95 | ax5 = sns.violinplot(x=reduced_df['opinion'], y=reduced_df['n_words'])
96 | #ax5.set_title('Distribution of words in review for each opinion')
97 | ax5.xaxis.label.set_text("Opinion")
98 | ax5.yaxis.label.set_text("Number of words")
99 | label_typography(ax5)
100 | ax5.figure.savefig(figOutputPath / '1_correlation_words_opinion.svg', format='svg')
101 |
102 | # 4 - Review distribution per day
103 | analyze_reviews(df, df.week_day, 'review_distribution_per_day', 'Day')
104 |
105 | # 5 - Top 20 products
106 | fig, ax3 = plt.subplots(figsize=(15, 15))
107 | top_products = most_reviewed_products(df, 20)
108 | r = list(top_products['asin'].unique())
109 | positive = list(top_products.loc[top_products['opinion'] == 'positive', 'asin'].value_counts().reindex(top_products['asin'].unique(), fill_value=0))
110 | neutral = list(top_products.loc[top_products['opinion'] == 'neutral', 'asin'].value_counts().reindex(top_products['asin'].unique(), fill_value=0))
111 | negative = list(top_products.loc[top_products['opinion'] == 'negative', 'asin'].value_counts().reindex(top_products['asin'].unique(), fill_value=0))
112 | raw_data = {'positive': positive, 'neutral': neutral, 'negative': negative}
113 | raw_data = pd.DataFrame(raw_data)
114 |
115 | totals = [i+j+k for i,j,k in zip(raw_data['positive'], raw_data['neutral'], raw_data['negative'])]
116 | positive_percentage = [i / j * 100 for i, j in zip(raw_data['positive'], totals)]
117 | neutral_percentage = [i / j * 100 for i, j in zip(raw_data['neutral'], totals)]
118 | negative_percentage = [i / j * 100 for i, j in zip(raw_data['negative'], totals)]
119 |
120 | bar_width = 0.85
121 |
122 | ax3.bar(r, positive_percentage, color='#b5ffb9', edgecolor='white', width=bar_width, label='positive')
123 | ax3.bar(r, neutral_percentage, bottom=positive_percentage, color='#f9bc86', edgecolor='white', width=bar_width, label='neutral')
124 | ax3.bar(r, negative_percentage, bottom=[i + j for i, j in zip(positive_percentage, neutral_percentage)], color='#a3acff', edgecolor='white', width=bar_width, label='negative')
125 | ax3.set_xticklabels(r, rotation=90)
126 | ax3.set_xlabel('Unique product')
127 | ax3.set_xticks([])
128 | ax3.set_ylabel('Percentage')
129 | ax3.set_xticks([])
130 | label_typography(ax3)
131 | #legend = ax3.legend(loc='lower left', shadow=True, fontsize='large')
132 | #legend.get_frame().set_facecolor('#00FFCC')
133 | #ax3.set_title('Opinion for besteller products')
134 | ax3.figure.savefig(figOutputPath / '1_sentiment_reviews_bestseller_products.svg', format='svg')
135 | print("Exported 1_sentiment_reviews_besteller_products.svg")
136 |
137 | # 6 - Top 50 reviewers
138 | fig, ax4 = plt.subplots(figsize=(15, 15))
139 | top_reviewers = most_active_reviewers(df, 50)
140 | sns.countplot(top_reviewers.reviewerID, ax=ax4, order=top_reviewers['reviewerID'].value_counts().index)
141 | r = list(top_reviewers['reviewerID'].unique())
142 | ax4.set_xticklabels(r, rotation=90)
143 | ax4.set_ylabel('Review count')
144 | ax4.set_xlabel('Unique Reviewers')
145 | ax4.set_xticks([])
146 | label_typography(ax4)
147 | #ax4.set_title('Reviewers with most reviews')
148 | ax4.figure.savefig(figOutputPath / '1_reviewers_most_reviews.svg', format='svg')
149 |
150 | # 7 - Opinion of top reviewers
151 | fig, ax6 = plt.subplots(figsize=(15, 15))
152 | top_reviewers = most_active_reviewers(df, 50)
153 | r = list(top_reviewers['reviewerID'].unique())
154 | positive = list(top_reviewers.loc[top_reviewers['opinion'] == 'positive', 'reviewerID'].value_counts().reindex(top_reviewers['reviewerID'].unique(), fill_value=0))
155 | neutral = list(top_reviewers.loc[top_reviewers['opinion'] == 'neutral', 'reviewerID'].value_counts().reindex(top_reviewers['reviewerID'].unique(), fill_value=0))
156 | negative = list(top_reviewers.loc[top_reviewers['opinion'] == 'negative', 'reviewerID'].value_counts().reindex(top_reviewers['reviewerID'].unique(), fill_value=0))
157 | raw_data = {'positive': positive, 'neutral': neutral, 'negative': negative}
158 | raw_data = pd.DataFrame(raw_data)
159 |
160 | #print("Opinions ",raw_data)
161 |
162 | totals = [i+j+k for i,j,k in zip(raw_data['positive'], raw_data['neutral'], raw_data['negative'])]
163 | #totals = list(top_products['asin'].value_counts().reindex(top_products['asin'].unique(), fill_value=0))
164 | positive_percentage = [i / j * 100 for i, j in zip(raw_data['positive'], totals)]
165 | neutral_percentage = [i / j * 100 for i, j in zip(raw_data['neutral'], totals)]
166 | negative_percentage = [i / j * 100 for i, j in zip(raw_data['negative'], totals)]
167 |
168 | bar_width = 1
169 |
170 | ax6.bar(r, positive_percentage, color='#b5ffb9', edgecolor='white', width=bar_width, label='positive')
171 | ax6.bar(r, neutral_percentage, bottom=positive_percentage, color='#f9bc86', edgecolor='white', width=bar_width, label='neutral')
172 | ax6.bar(r, negative_percentage, bottom=[i + j for i, j in zip(positive_percentage, neutral_percentage)], color='#a3acff', edgecolor='white', width=bar_width, label='negative')
173 | ax6.set_xticklabels(r, rotation=90)
174 | ax6.set_xlabel('Unique Reviewers')
175 | ax3.set_xticks([])
176 | ax6.set_xticks([])
177 | ax6.set_ylabel('Percentage')
178 | label_typography(ax6)
179 | label_typography(ax3)
180 | #legend = ax6.legend(loc='lower left', shadow=True, fontsize='large')
181 | #legend.get_frame().set_facecolor('#00FFCC')
182 | #ax6.set_title('Opinion of top reviewers')
183 | #plt.show()
184 | ax6.figure.savefig(figOutputPath / '1_opinion_top_reviewers.svg', format='svg')
185 | print("Exported 1_opinion_top_reviewers.svg")
186 |
187 | # 8 - Unverified reviews
188 | unverified = df[df['verified'] == False]
189 | analyze_reviews(unverified, unverified.overall, 'unverified_overall_distribution', 'Overall')
190 |
191 | # 9 - Verified reviews
192 | verified = df[df['verified'] == True]
193 | analyze_reviews(verified, verified.overall, 'verified_overall_distribution', 'Overall')
194 |
195 | # 10 - verified vs unverified of top 50 reviewers
196 | fig, ax7 = plt.subplots(figsize=(15, 15))
197 | r = list(top_reviewers['reviewerID'].unique())
198 | verified = list(top_reviewers.loc[top_reviewers['verified'] == True, 'reviewerID'].value_counts().reindex(top_reviewers['reviewerID'].unique(), fill_value=0))
199 | unverified = list(top_reviewers.loc[top_reviewers['verified'] == False, 'reviewerID'].value_counts().reindex(top_reviewers['reviewerID'].unique(), fill_value=0))
200 | raw_data = {'verified': verified, 'unverified': unverified}
201 | raw_data = pd.DataFrame(raw_data)
202 |
203 | totals = [i+j for i,j in zip(raw_data['verified'], raw_data['unverified'])]
204 | verified_percentage = [i / j * 100 for i, j in zip(raw_data['verified'], totals)]
205 | unverified_percentage = [i / j * 100 for i, j in zip(raw_data['unverified'], totals)]
206 |
207 | bar_width = 1
208 |
209 | ax7.bar(r, verified_percentage, color='#b5ffb9', edgecolor='white', width=bar_width, label='verified')
210 | ax7.bar(r, unverified_percentage, bottom=verified_percentage, color='#f9bc86', edgecolor='white', width=bar_width, label='unverified')
211 | ax7.set_xticklabels(r, rotation=90)
212 |
213 | ax7.set_xlabel('Unique Reviewers')
214 | ax7.set_xticks([])
215 | ax3.set_xticks([])
216 | ax7.set_ylabel('Percentage')
217 | label_typography(ax3)
218 | label_typography(ax7)
219 | #legend = ax7.legend(loc='upper right', shadow=True, fontsize='large')
220 | #legend.get_frame().set_facecolor('#00FFCC')
221 | #ax7.set_title('Verified vs Unverified reviews of top reviewers')
222 | #plt.show()
223 | ax7.figure.savefig(figOutputPath / '1_verified_unverified.svg', format='svg')
224 | print("Exported 1_verified_unverified.svg")
225 |
226 |
227 | # Exporting raw data for the web demo
228 |
229 | def top_50_products_verified_unverified_both(df):
230 | print("top_50_products_verified_unverified_both")
231 | top_products = most_reviewed_products(df, 5)
232 | r = list(top_products['asin'].unique())
233 | products = []
234 | verified_series = []
235 | unverified_series = []
236 | overall_series = []
237 |
238 | for asin in r:
239 | print("Product: ", asin)
240 | products.append(asin)
241 | verified = df.loc[(df['asin'] == asin) & (df['verified'] == True), 'overall'].mean()
242 | print("-verified: ",verified)
243 | verified_series.append(verified)
244 | unverified = df.loc[(df['asin'] == asin) & (df['verified'] == False), 'overall'].mean()
245 | unverified_series.append(unverified)
246 | print("-unverified: ", unverified)
247 | aall = df.loc[(df['asin'] == asin), 'overall'].mean()
248 | overall_series.append(aall)
249 | print("-all: ", aall)
250 |
251 | obj = [
252 | {"name": "products",
253 | "data": products},
254 | {"name": "verified",
255 | "data": verified_series},
256 | {"name": "unverified",
257 | "data": unverified_series},
258 | {"name": "all",
259 | "data": overall_series
260 | }]
261 |
262 | with open('ver_unver.json', 'w') as outfile:
263 | json.dump(obj, outfile, indent=2, sort_keys=True)
264 |
265 | print(products)
266 |
267 | def count_reviews(df):
268 | top_products = most_reviewed_products(df, 20)
269 | r = list(top_products['asin'].unique())
270 | products = []
271 | # One element per product
272 | verified_score_qty = []
273 | unverified_score_qty = []
274 | n = 0
275 |
276 | for asin in r:
277 | print("Product: ", asin)
278 | products.append(asin)
279 | dataseries_ver = []
280 | dataseries_unver = []
281 |
282 | for i in range(1,6):
283 | key = { "name" : int(i), "data": [int(df.loc[(df['asin'] == asin) & (df['verified'] == True) & (df['overall'] == i), 'overall'].count()), int(df.loc[(df['asin'] == asin) & (df['verified'] == False) & (df['overall'] == i), 'overall'].count())]}
284 | dataseries_ver.append(key)
285 |
286 | verified_score_qty.append(dataseries_ver)
287 | n = n+1
288 |
289 | obj = {'products': products, 'count':verified_score_qty,}
290 |
291 | with open('ver_counts.json', 'w') as outfile:
292 | json.dump(obj, outfile, indent=2, sort_keys=True)
293 |
294 |
295 | def year_month_day_reviews(df):
296 | analyze_reviews(df, df.week_day, 'review_distribution_per_day', 'Day')
297 | analyze_reviews(df, df.month, 'review_distribution_per_month', 'Month')
298 | analyze_reviews(df, df.year, 'review_distribution_per_year', 'Year')
299 |
300 | def export_week_day(df):
301 | for i in range(1,6):
302 | print(i, df.loc[df['overall']==i].groupby(['week_day']).size())
303 |
304 | def export_month(df):
305 | for i in range(1,6):
306 | print(i, df.loc[df['overall']==i].groupby(['month']).size().values.tolist())
307 |
308 | def export_year(df):
309 | for i in range(1,6):
310 | print(i, df.loc[df['overall']==i].groupby(['year']).size().values.tolist())
311 |
--------------------------------------------------------------------------------
/scripts/topic_analysis.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ### Import libraries ###
4 |
5 | import nltk
6 | from nltk.sentiment.vader import SentimentIntensityAnalyzer
7 | nltk.download('vader_lexicon')
8 | nltk.download('wordnet')
9 | import gensim
10 | import matplotlib.pyplot as plt
11 | import pandas as pd
12 | import pyLDAvis.gensim
13 | from data_utils import most_reviewed_products
14 | from pathlib import Path
15 |
16 | figures_folder = Path("../figures/")
17 | dataframes_folder = Path("../dataframes/")
18 |
19 | ### Functions ###
20 |
21 | def worst_products_asin(df, n_worst):
22 | if n_worst == 0:
23 | return []
24 | top_products = most_reviewed_products(df, 20)
25 | overall_mean = top_products.groupby(['asin'], as_index=False)['overall'].mean()
26 | overall_mean = overall_mean.sort_values('overall', ascending=True)
27 | worst_n_products = overall_mean['asin'].iloc[:n_worst].tolist()
28 | return worst_n_products
29 |
30 |
31 | def best_products_asin(df, n_best):
32 | if n_best == 0:
33 | return []
34 | top_products = most_reviewed_products(df, 20)
35 | overall_mean = top_products.groupby(['asin'], as_index=False)['overall'].mean()
36 | overall_mean = overall_mean.sort_values('overall', ascending=False)
37 | best_n_products = overall_mean['asin'].iloc[:n_best].tolist()
38 | return best_n_products
39 |
40 |
41 | def products_to_analyze(df, n_best=0, n_worst=0):
42 | worst = worst_products_asin(df, n_worst)
43 | best = best_products_asin(df, n_best)
44 | products = worst + best
45 | if products == []:
46 | # Most reviewed product
47 | product_id = df.asin.mode().iloc[0]
48 | return [product_id]
49 | else:
50 | return products
51 |
52 |
53 | def create_dictionary(texts):
54 | dictionary = gensim.corpora.Dictionary(texts)
55 | #dictionary.filter_extremes(no_below=10, no_above=0.5, keep_n=100000)
56 | dictionary.filter_extremes(keep_n=10000)
57 | dictionary.compactify()
58 | return dictionary
59 |
60 |
61 | def make_bigrams(texts):
62 | bigram = gensim.models.Phrases(texts, min_count=5, threshold=100)
63 | bigram_mod = gensim.models.phrases.Phraser(bigram)
64 | return [bigram_mod[doc] for doc in texts]
65 |
66 |
67 | def bag_of_words(texts, dictionary):
68 | corpus = [dictionary.doc2bow(text) for text in texts]
69 | return corpus
70 |
71 |
72 | def compute_lda_model(corpus, num_topics, dictionary, texts, alpha, beta):
73 | lda_models, coherences = [], []
74 | for n in num_topics:
75 | model = gensim.models.LdaModel(corpus=corpus,
76 | num_topics=n,
77 | random_state=42,
78 | chunksize=100,
79 | id2word=dictionary,
80 | passes=10,
81 | alpha=alpha,
82 | eta=beta)
83 | lda_models.append(model)
84 | cm = gensim.models.ldamodel.CoherenceModel(model=model,
85 | dictionary=dictionary,
86 | coherence='c_v',
87 | texts=texts)
88 | coherences.append(cm.get_coherence())
89 | print('\nNumber of topic:', n)
90 | return coherences, lda_models
91 |
92 |
93 | def compute_multiple_lda_models(alphas, betas, num_topics, corpus, texts, dictionary):
94 | all_coherences, all_lda_models, all_parameters = [], [], []
95 | for alpha in alphas:
96 | for beta in betas:
97 | coherences, lda_models = compute_lda_model(corpus=corpus,
98 | num_topics=num_topics,
99 | dictionary=dictionary,
100 | texts=texts,
101 | alpha=alpha,
102 | beta=beta)
103 | all_coherences.append(coherences)
104 | all_lda_models.append(lda_models)
105 | all_parameters.append([alpha, beta])
106 | return all_coherences, all_lda_models, all_parameters
107 |
108 |
109 | def plot_coherence(num_topics, coherence, product_asin):
110 | x_axis = range(2, 2+num_topics)
111 | fig, ax0 = plt.subplots()
112 | ax0.plot(x_axis, coherence)
113 | ax0.set_xlabel("Number of topics")
114 | ax0.set_ylabel("Coherence score")
115 | ax0.figure.savefig(figures_folder / '3_coherence_plot_{0}.svg'.format(product_asin), format='svg')
116 |
117 |
118 | def show_topics(model, ideal_topics, num_words, product_asin):
119 | topics = model.show_topics()
120 | for topic in topics:
121 | print(topic)
122 |
123 | word_dict = {};
124 | for i in range(ideal_topics):
125 | words = model.show_topic(i, topn = num_words)
126 | word_dict['Topic # ' + '{:02d}'.format(i+1)] = [i[0] for i in words]
127 | topic_df = pd.DataFrame(word_dict)
128 | topic_df.to_pickle(dataframes_folder / 'topics_{}.pkl'.format(product_asin))
129 | print(topic_df)
130 |
131 |
132 | def topic_visualization(model, corpus, dictionary, product_asin):
133 | lda_display = pyLDAvis.gensim.prepare(model, corpus, dictionary, sort_topics=True)
134 | pyLDAvis.save_html(lda_display, 'lda_{0}.html'.format(product_asin))
135 |
136 | '''
137 | def format_topics_sentences(model, corpus, texts):
138 | # Get main topic reviews
139 | # Init output
140 | df = pd.DataFrame()
141 |
142 | # Get main topic in each document
143 | for i, row in enumerate(model[corpus]):
144 | row = sorted(row, key=lambda x: (x[1]), reverse=True)
145 | # Get the Dominant topic, Perc Contribution and Keywords for each document
146 | for j, (topic_num, prop_topic) in enumerate(row):
147 | if j == 0: # => dominant topic
148 | # probability pairs for the most relevant words generated by the topic
149 | wp = model.show_topic(topic_num)
150 | topic_keywords = ", ".join([word for word, prop in wp])
151 | df = df.append(pd.Series([int(topic_num), round(prop_topic,4), topic_keywords]), ignore_index=True)
152 | else:
153 | break
154 | #df.columns = ['Dominant_Topic', 'Topic_Perc_Contribution', 'Topic_Keywords']
155 |
156 | # Add original text to the end of the output
157 | contents = pd.Series(texts)
158 | df = pd.concat([df, contents], axis=1)
159 | df = df.reset_index()
160 | df.columns = ['review', 'topic_num', 'topic_perc_contribution', 'keywords', 'text']
161 | return df
162 |
163 |
164 | def sentiment_polarity(df):
165 | sentiment = pd.DataFrame()
166 | sentiment = pd.concat([sentiment, df], ignore_index=True)
167 | analyser = SentimentIntensityAnalyzer()
168 | sentiment['sentiments'] = sentiment['text'].str.join(' ').apply(lambda x:
169 | analyser.polarity_scores(x))
170 | sentiment = pd.concat([sentiment.drop(['sentiments'], axis=1),
171 | sentiment['sentiments'].apply(pd.Series)],
172 | axis=1)
173 | # Numbers of words
174 | sentiment['words_nb'] = sentiment["text"].apply(lambda x: len(x))
175 | sentiment_final = sentiment.groupby(['topic_num',
176 | 'keywords']).agg({'neg':'mean',
177 | 'neu':'mean',
178 | 'pos':'mean',
179 | 'compound':'mean',
180 | 'topic_perc_contribution':'count'}).reset_index()
181 | return sentiment_final, sentiment
182 |
183 |
184 | def most_representative_document(df):
185 | # Most representative document for each topic
186 | sent_topics_sorted_df = pd.DataFrame()
187 | sent_topics_outdf_grpd = df.groupby('topic_num')
188 | for i, grp in sent_topics_outdf_grpd:
189 | sent_topics_sorted_df = pd.concat([sent_topics_sorted_df,
190 | grp.sort_values(['topic_perc_contribution'],
191 | ascending=[0]).head(1)],
192 | axis=0)
193 | sent_topics_sorted_df.reset_index(drop=True, inplace=True)
194 | sent_topics_sorted_df.columns = ['review', 'topic_num', 'topic_perc_contribution', 'keywords', 'text']
195 | sent_topics_sorted_df.drop(['review'], axis=1, inplace=True)
196 | return sent_topics_sorted_df
197 |
198 |
199 | def topic_distribution_across_documents(df, sentiment):
200 | # Number of Documents for Each Topic
201 | sentiment.rename(columns={'dominant_topic':'topic'})
202 | topic_counts = df['topic_num'].value_counts()
203 | topic_contribution = round(topic_counts/topic_counts.sum(), 4)
204 | topic_contribution.rename(columns={'topic_num':'perc_contribution'})
205 | df_dominant_topics = pd.concat([sentiment, topic_contribution], axis=1)
206 | # Change Column names
207 | #df_dominant_topics.columns = ['Dominant_Topic', 'Topic_Keywords', 'Num_Documents', 'Perc_Documents']
208 | # Show
209 | return df_dominant_topics
210 |
211 |
212 | '''
213 |
214 | def run_for_custom_analysis(df):
215 | print("CUSTOM LDA ANALYSIS!")
216 | df = df.head(15000)
217 | product = "PORTABLECHARGERS"
218 | reviews_product = [r.split(' ') for r in df['preprocessedReview']]
219 | bigram_reviews = make_bigrams(reviews_product)
220 | dictionary = create_dictionary(bigram_reviews)
221 | bow_corpus = bag_of_words(bigram_reviews, dictionary)
222 | max_topics = 10
223 | alpha_list = [0.1, 1]
224 | beta_list = [0.01, 0.1, 1]
225 | num_topics = list(range(2, max_topics + 1))
226 | all_coherences, all_lda_models, all_parameters = compute_multiple_lda_models(alphas=alpha_list,
227 | betas=beta_list,
228 | num_topics=num_topics,
229 | corpus=bow_corpus,
230 | texts=bigram_reviews,
231 | dictionary=dictionary)
232 | # Extract best coherence and index
233 | best_coherence_value, index_best_value = max((x, (i, j))
234 | for i, row in enumerate(all_coherences)
235 | for j, x in enumerate(row))
236 | best_alpha = all_parameters[index_best_value[0]][0]
237 | best_beta = all_parameters[index_best_value[0]][1]
238 | best_model = all_lda_models[index_best_value[0]][index_best_value[1]]
239 | print('Best model has {} coherence with {} alpha value and {} beta value'.format(best_coherence_value,
240 | best_alpha,
241 | best_beta))
242 | best_coherences = all_coherences[index_best_value[0]]
243 | best_num_topics = num_topics[0] + index_best_value[1]
244 | print('Best num of topics: {}'.format(best_num_topics))
245 | plot_coherence(len(num_topics), best_coherences, product)
246 | show_topics(best_model, best_num_topics, 10, product)
247 | topic_visualization(best_model, bow_corpus, dictionary, product)
248 |
249 |
250 | def run(df):
251 | product_list = products_to_analyze(df, n_best=3, n_worst=3)
252 | for product in product_list:
253 | figures_folder = Path("../figures/")
254 | name_file = '3_coherence_plot_{0}.svg'.format(product)
255 | path_file = figures_folder / name_file
256 | if False:
257 | print('{} already computed.'.format(product))
258 | else:
259 | print(product)
260 | df_product = df[df['asin'] == product]
261 | reviews_product = [r.split(' ') for r in df_product['preprocessedReview']]
262 | bigram_reviews = make_bigrams(reviews_product)
263 | dictionary = create_dictionary(bigram_reviews)
264 | bow_corpus = bag_of_words(bigram_reviews, dictionary)
265 | max_topics = 10
266 | alpha_list = [0.1, 1]
267 | beta_list = [0.01, 0.1, 1]
268 | num_topics = list(range(2, max_topics+1))
269 | all_coherences, all_lda_models, all_parameters = compute_multiple_lda_models(alphas=alpha_list,
270 | betas=beta_list,
271 | num_topics=num_topics,
272 | corpus=bow_corpus,
273 | texts=bigram_reviews,
274 | dictionary=dictionary)
275 | # Extract best coherence and index
276 | best_coherence_value, index_best_value = max((x, (i, j))
277 | for i, row in enumerate(all_coherences)
278 | for j, x in enumerate(row))
279 | best_alpha = all_parameters[index_best_value[0]][0]
280 | best_beta = all_parameters[index_best_value[0]][1]
281 | best_model = all_lda_models[index_best_value[0]][index_best_value[1]]
282 | print('Best model has {} coherence with {} alpha value and {} beta value'.format(best_coherence_value,
283 | best_alpha,
284 | best_beta))
285 | best_coherences = all_coherences[index_best_value[0]]
286 | best_num_topics = num_topics[0] + index_best_value[1]
287 | print('Best num of topics: {}'.format(best_num_topics))
288 | plot_coherence(len(num_topics), best_coherences, product)
289 | show_topics(best_model, best_num_topics, 10, product)
290 | topic_visualization(best_model, bow_corpus, dictionary, product)
291 | '''
292 | topic_sents_keywords = format_topics_sentences(best_model, bow_corpus, bigram_reviews)
293 |
294 | topic_sents_keywords.to_pickle('dataframes/topic_sents_keywords.pkl')
295 |
296 | sentiment_df, words = sentiment_polarity(topic_sents_keywords)
297 | pos = words[words["words_nb"] >= 5].sort_values("pos", ascending = False)[["text", "pos"]].head(20)
298 | neg = words[words["words_nb"] >= 5].sort_values("neg", ascending = False)[["text", "neg"]].head(20)
299 |
300 | most_repr_rews = most_representative_document(topic_sents_keywords)
301 | df_dominant_topics = topic_distribution_across_documents(topic_sents_keywords, sentiment_df)
302 | '''
303 |
--------------------------------------------------------------------------------
/report/2_esplorazione.tex:
--------------------------------------------------------------------------------
1 | \chapter{Esplorazione dei dati}
2 | \label{exploration}
3 |
4 | \section{Informazioni preliminari sul dominio}
5 | Prima di addentrarci nell'analisi del nostro dataset, che si limita ad una categoria, abbiamo cercato delle visualizzazioni globali dell'intero marketplace Amazon. La Figura \ref{fig1} ci mostra una carattere fortemente stagionale: gli utenti sono molto più propensi a fornire recensioni nei periodi estivi, nonostante i picchi dei volumi di vendita si verifichino intorno al periodo natalizio \cite{trends}.
6 | \par
7 |
8 | \begin{figure}[H]
9 |
10 | \centering
11 | \includegraphics[width=0.95\linewidth]{figures/ext/1_monthly.png}
12 | \caption{General Amazon ratings per month \cite{plots1}}
13 | \label{fig1}
14 | \end{figure}
15 |
16 | La Figura \ref{fig2} visualizza invece il contributo di un utente, dandoci un'idea di quanto vocale sia la clientela Amazon, in media.
17 |
18 | \begin{figure}[H]
19 |
20 | \centering
21 | \includegraphics[width=1.1\linewidth]{figures/ext/1_peruser.png}
22 | \caption{General Amazon ratings per user \cite{plots1}}
23 | \label{fig2}
24 | \end{figure}
25 |
26 |
27 | \section{Descrizione dataset}
28 | \label{descrizione_dataset}
29 | Il dataset si presenta in formato JSON e viene caricato in memoria in un DataFrame con la libreria Pandas, molto efficiente per la gestione di dati voluminosi.
30 |
31 | La fase di caricamento e preprocessamento del dataset sono le più impegnative computazionalmente, impiegando gran parte del tempo totale.
32 |
33 | Per ovviare a questo problema e muoverci più agevolmente durante lo sviluppo sfruttiamo la funzione \texttt{to\_pickle} di Pandas per salvare su disco una versione "cachata" del dataframe, abbreviando le successive esecuzioni della pipeline.
34 |
35 | \par
36 | Prima del salvataggio sono state effettuate alcune operazioni utili per rendere il dataset conforme agli obiettivi. In particolare:
37 | \begin{itemize}
38 | \item Il campo \texttt{vote} è stato trasformato da tipo \texttt{object} a tipo \texttt{float}
39 | \item Il campo \texttt{reviewText} possedeva alcune recensioni vuote, inutili e perciò eliminate
40 | \end{itemize}
41 | Queste operazioni hanno ridotto il dataset portandolo da un totale di recensioni pari a 1128437 a un totale di 1127654, suddivise fra ben 157195 utenti e 48146 prodotti.
42 | \begin{table}[H]
43 | \small
44 | \centering
45 | \begin{tabular}{|p{0.20\textwidth}||p{0.10\textwidth}||p{0.55\textwidth}|}
46 | \hline
47 | Campo & Tipo & Descrizione \\
48 | \hline
49 | overall & int & Valutazione del prodotto (1-5)\\
50 | verified & bool & Recensione proveniente da acquisto verificato\\
51 | reviewTime & string & Data della recensione in formato string\\
52 | reviewerID & string & Codice univoco del recensore\\
53 | asin & string & Codice univoco del prodotto\\
54 | style & string & Dizionario dei metadati del prodotto\\
55 | reviewerName & string & Nome del recensore\\
56 | reviewText & string & Testo della recensione\\
57 | summary & string & Titolo della recensione\\
58 | unixReviewTime & int & Data della recensione in formato unix\\
59 | vote & float & Numero di voti della recensione \\
60 | image & string & Immagine associata alla recensione\\
61 | \hline
62 | \end{tabular}
63 | \caption{Campi del dataset con tipo e descrizione}
64 | \label{table_dataset_description}
65 | \end{table}
66 |
67 | Il dataset possiede gli attributi mostrati in Tabella \ref{table_dataset_description}. Ogni record del dataset è la rappresentazione di una singola recensione svolta da parte di un utente per un certo prodotto nella data indicata.
68 | \par
69 | Per l'identificazione dell'utente abbiamo a disposizione il campo \texttt{reviewerName} e il campo \texttt{reviewerID}: utilizzeremo solamente quest'ultimo per i nostri scopi.
70 | Per quanto riguarda i campi relativi alla recensione, abbiamo a disposizione sia \texttt{summary} che \texttt{reviewText}.
71 | \par
72 | Per identificare il prodotto abbiamo a disposizione solamente il campo \texttt{asin}, che è un codice univoco da cui si può risalire a maggiori informazioni con l'utilizzo delle API Amazon o software di terze parti.
73 | \par
74 | Le recensioni sono classificate come \textit{verificate} se provengono da un acquisto su Amazon per almeno l'80\% del valore originale dell'articolo. L'utente deve aver inoltre speso almeno 50\$ sul proprio account.
75 |
76 | \section{Estensione del dataset}
77 |
78 | A partire dal dataset originale abbiamo creato dei nuovi campi ritenuti di valore per effettuare una fase di esplorazione più approfondita.
79 |
80 | \subsection{Da \texttt{overall} a \texttt{opinion}}
81 | \label{overall_opinion}
82 | Osservando la distribuzione del campo \texttt{overall}, mostrata in Figura \ref{overall_distribution}, possiamo notare un forte sbilanciamento sul valore 5: questa tendenza è presente anche in dataset Amazon di categorie diverse dalla nostra.
83 |
84 | \begin{figure}[H]
85 | \centering
86 | \includesvg[width=0.9\linewidth]{figures/1_overall_distribution}
87 | \caption{Overall distribution}
88 | \label{overall_distribution}
89 | \end{figure}
90 |
91 | In previsione della fase di sentiment analysis, il campo \texttt{overall} è stato utilizzato per la creazione del campo \texttt{opinion}, così composto:
92 |
93 | \begin{itemize}
94 | \item I valori 1 e 2 vengono trasformati in \textit{negative}
95 | \item Il valore 3 viene trasformato in \textit{neutral}
96 | \item I valori 4 e 5 vengono trasformati in \textit{positive}
97 | \end{itemize}
98 |
99 | In Figura \ref{opinion_distribution} viene mostrata la distribuzione: essa è ovviamente simile a quella già osservata per il campo \texttt{overall} e sarà quindi necessario un bilanciamento del dataset per la fase di sentiment analysis.
100 |
101 | \begin{figure}[H]
102 | \centering
103 | \includesvg[width=0.9\linewidth]{figures/1_opinion_distribution}
104 | \caption{Opinion distribution}
105 | \label{opinion_distribution}
106 | \end{figure}
107 |
108 | \subsection{Conteggio delle parole nelle recensioni}
109 |
110 | Il campo \texttt{reviewText} è di fondamentale importanza per le fasi di sentiment e topic analysis. Ma per la fase di esplorazione, essendo il testo di una recensione un dato qualitativo, non è di alcun valore. Per questo motivo, abbiamo computato direttamente il numero di parole e creato il campo risultante \texttt{n\_words}. In Figura \ref{distribution_words_opinion} viene mostrata la distribuzione del campo \texttt{n\_words} rispetto al campo \texttt{opinion}, tenendo in considerazione solamente le recensioni con meno di 1000 parole per una questione di visibilità che sarebbe venuta meno considerando anche le (poche) recensioni composte da oltre 1000 parole.
111 |
112 | \begin{figure}[H]
113 | \centering
114 | \includesvg[width=0.9\linewidth]{figures/1_correlation_words_opinion}
115 | \caption{Distribution of words in review for each opinion}
116 | \label{distribution_words_opinion}
117 | \end{figure}
118 |
119 | \subsection{Analisi temporale}
120 | Il campo \texttt{unixReviewTime} fornisce la data della recensione in formato unix. Con alcune semplici manipolazioni del suddetto campo abbiamo creato i seguenti:
121 |
122 | \begin{itemize}
123 | \item \texttt{month\_year} nel formato YYYY-MM
124 | \item \texttt{month} nel formato MM
125 | \item \texttt{year} nel formato YYYY
126 | \item \texttt{week\_day} in cui il giorno della settimana è rappresentato con un numero intero (0-6)
127 | \end{itemize}{}
128 |
129 | Il dataset considera recensioni nell'arco di 16 anni circa: più precisamente la prima recensione risale al 23-10-2002, mentre l'ultima al 01-10-2018.
130 | Considerato il dominio trattato, un'analisi di valore è quella di considerare la distribuzione delle recensioni tenendo in considerazione il giorno della settimana cosicché da mettere in risalto pattern di attività. \par
131 | Nel caso specifico, come è possibile osservare in Figura \ref{review_dist}, non vi è una dominanza degna di nota nonostante vi sia una tendenza a produrre meno recensioni nelle giornate di venerdì e sabato.
132 |
133 | \begin{figure}[H]
134 | \centering
135 | \includesvg[width=0.9\linewidth]{figures/1_review_distribution_per_day}
136 | \caption{Review distribution per day}
137 | \label{review_dist}
138 | \end{figure}
139 |
140 | \section{Prodotti più recensiti e recensori più popolari}
141 | Il numero di utenti e di prodotti è nell'ordine delle migliaia (come anticipato nel Capitolo \ref{descrizione_dataset}) ed è impensabile anche solo immaginare di fare analisi esplorative approfondite su ogni singolo utente e su ogni singolo prodotto. Per questo motivo abbiamo deciso di focalizzare l'attenzione su un numero ristretto di utenti e di prodotti.
142 |
143 | \par
144 |
145 | La Figura \ref{opinion_bestseller_products} mostra i 20 prodotti più popolari in termini di recensioni. Possiamo notare come, seppur ogni prodotto abbia perlopiù un maggior numero di recensioni \textit{positive}, per alcuni prodotti in particolare la percentuale di recensioni \textit{neutrali} e \textit{negative} è elevata rispetto alla distribuzione osservata nel Capitolo \ref{overall_opinion}.
146 |
147 | \begin{figure}[H]
148 | \centering
149 | \includesvg[width=0.9\linewidth]{figures/1_sentiment_reviews_bestseller_products}
150 | \caption{Opinion for bestseller products}
151 | \label{opinion_bestseller_products}
152 | \end{figure}
153 |
154 | \par
155 | La Figura \ref{reviewers_most_reviews} mostra i 50 utenti con più recensioni prodotte, mentre la Figura \ref{opinion_top_reviewers} mostra la distribuzione delle valutazioni delle recensioni effettuate. Possiamo notare come la maggior parte degli utenti considerati dia in percentuale una valutazione in linea con la distribuzione osservata nel Capitolo \ref{overall_opinion}, fatta eccezione per casi estremi.
156 |
157 | \begin{figure}[H]
158 | \centering
159 | \includesvg[width=1\linewidth]{figures/1_reviewers_most_reviews}
160 | \caption{Reviewers with most reviews}
161 | \label{reviewers_most_reviews}
162 | \end{figure}
163 |
164 | \begin{figure}[H]
165 | \centering
166 | \includesvg[width=1\linewidth]{figures/1_opinion_top_reviewers}
167 | \caption{Opinion of top reviewers}
168 | \label{opinion_top_reviewers}
169 | \end{figure}
170 |
171 | \section{Natura delle recensioni}
172 | Il campo \texttt{verified} merita una trattazione dettagliata per capire se le recensioni \texttt{non verificate} sono di valore tanto quanto le recensioni \textit{verificate}. In Figura \ref{ver_unver_overall} si può notare che la distribuzione del campo \textit{overall} è praticamente identica.
173 |
174 | \begin{figure}[H]
175 | \centering
176 | \subfigure[Unverified overall distribution]{\includesvg[width=0.4\linewidth]{figures/1_unverified_overall_distribution}}
177 | \subfigure[Verified overall distribution]{\includesvg[width=0.4\linewidth]{figures/1_verified_overall_distribution}}
178 | \caption{Verified - Unverified overall distribution}
179 | \label{ver_unver_overall}
180 | \end{figure}
181 |
182 | In Figura \ref{ver_unver_toprev} è invece possibile notare una particolarità. Come in Figura \ref{opinion_top_reviewers} abbiamo preso i 50 utenti con più recensioni prodotte e la maggior parte delle loro recensioni risulta come \textit{non verificata}.
183 | \par
184 | Alcune riflessioni sono possibili soffermandoci su questa Figura. Come specificato nel Capitolo \ref{descrizione_dataset}, le recensioni sono classificate come \textit{verificate} se provengono da un acquisto su Amazon per almeno l'80\% del valore originale dell'articolo. Una suggestione potrebbe far propendere per l'idea che molti di questi utenti siano i cosiddetti \textit{top recensori} solitamente posizionati in cima alla lista dei commenti che (in teoria) non acquistano direttamente i prodotti recensiti che invece gli vengono prestati per provare il prodotto e scrivere una recensione imparziale.
185 |
186 | \begin{figure}[H]
187 | \centering
188 | \includesvg[width=1\linewidth]{figures/1_verified_unverified}
189 | \caption{Verified - Unverified reviews of top reviewers}
190 | \label{ver_unver_toprev}
191 | \end{figure}
192 |
193 | \section{Correlazioni temporali e relative al traffico}
194 |
195 | Aggregando temporalmente i dati abbiamo ottenuto alcuni grafici che suggeriscono correlazioni interessanti: la Figura \ref{figtime1} ci mostra come grande parte del traffico attivo sulle recensioni (ovvero gli utenti che votano e danno rilevanza alle recensioni esistenti) si distribuisce su quelle già più popolari, mentre la grande rimanenza rimane quasi intoccata da grossi picchi di attività di questo tipo.
196 |
197 | \begin{figure}[H]
198 |
199 | \centering
200 | \includesvg[width=1.1\linewidth]{figures/1_avg_help_25_100_traffic}
201 | \caption{Average "helpfulness" of 25 and 200 most relevant reviews over time and traffic}
202 | \label{figtime1}
203 | \end{figure}
204 |
205 | \par
206 |
207 | La Figura \ref{figtime2} mostra un fenomeno interessante: nonostante la quantità di recensioni cambi notevolmente nel tempo, la quantità di recensioni non verificate in rapporto al totale sembra rimanere (quasi) invariata, suggerendo un qualche tipo di moderazione.
208 |
209 | \begin{figure}[H]
210 |
211 | \centering
212 | \includesvg[width=1.1\linewidth]{figures/1_ver_unver_time_traffic.svg}
213 | \caption{Verified - Unverified reviews over time and traffic}
214 | \label{figtime2}
215 | \end{figure}
216 |
217 |
218 | \par
219 |
220 | Incrociando la lunghezza media delle recensioni con il loro voto, abbiamo ottenuto la Figura \ref{figtime3}. Con il passare del tempo (e l'aumentare vertiginoso del traffico) le recensioni sono generalmente più lunghe e meno generose con la valutazione che esprimono.
221 |
222 | \par
223 |
224 | Basandosi su alcuni di questi aspetti, Amazon ha sviluppato un modello di apprendimento automatico per assegnare un valore di rilevanza alle recensioni, in modo da poterle mettere in primo piano. In particolare, i fattori considerati sono: punteggio "utilità" della recensioni (voti), recensione verificata/non verificata, età della recensione.
225 |
226 | \par
227 | Non è noto nel dettaglio come funzioni e in che modo questi fattori vengano pesati ma è certamente importante rilevare come un approccio di questo tipo permette ad Amazon di sfruttare i contributi degli utenti e capitalizzarci, promuovendo recensioni convincenti e prodotti che riescono a produrre (legittimamente o no, altro aspetto importante) feedback così positivi e virali.
228 |
229 |
230 | \begin{figure}[H]
231 |
232 | \centering
233 | \includesvg[width=1.1\linewidth]{figures/1_rew_len_over_time.svg}
234 | \caption{Review length VS overall score over time}
235 | \label{figtime3}
236 | \end{figure}
237 |
238 | \newpage
239 | \section{Polarizzazione delle valutazioni}
240 |
241 | Uno degli aspetti fondamentali e poco chiaro delle recensioni è quanto esse siano polarizzate attorno un singolo voto numerico, in modo spesso estremo. È un fenomeno che si estende per ogni categoria di ogni marketplace in modo praticamente omogeneo.
242 | Nel caso di Amazon, la gran parte delle recensioni riporta una valutazione numerica massima.
243 | \par
244 | In letteratura, abbiamo trovato un recente lavoro \cite{schoenmuller2018extreme} che investiga dettagliatamente la questione, descrivendo la \textit{polarity self-selection} come fattore trainante di questo fenomeno. È tendenza dei consumatori a recensire esperienze estreme. Si discute inoltre il fatto che le distribuzioni estreme di queste valutazioni ne riducono l'informatività, su larga scala.
245 |
246 | \par
247 | I seguenti grafici danno un'idea di questo comportamento: la figura \ref{disp1} confronta recensori abituali ed occasionali del sito Yelp, mostrando come utenti che producono più recensioni distribuiscono meglio le proprie valutazioni, senza esagerare con valutazioni massime nella maggior parte dei casi. \ref{disp2} affronta invece l'aspetto dell'incipit della recensione: quando siamo forzati a valutare un elemento, è più probabile che distribuiremo intro al 4 la nostra valutazione, mentre quando lasciamo una recensione di nostra spontanea volontà si tende a recensire ottime esperienze.
248 |
249 | \begin{figure}[htbp]
250 | \centering
251 | \includegraphics[width=1.1\linewidth]{figures/ext/1_frequentInfrequentYelp.png}
252 | \caption{Review Distribution of Frequent and Infrequent Yelp Reviewers \cite{schoenmuller2018extreme}}
253 | \label{disp1}
254 | \end{figure}
255 |
256 | \begin{figure}[htbp]
257 | \centering
258 | \includegraphics[width=1.1\linewidth]{figures/ext/1_selfVSforced.png}
259 | \caption{Empirical Distributions for Self-Selection versus Forced Reviews \cite{schoenmuller2018extreme}}
260 | \label{disp2}
261 | \end{figure}
--------------------------------------------------------------------------------
/figures/1_opinion_distribution.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
577 |
--------------------------------------------------------------------------------