Ishodi učenja:

  • Primijeniti leksikonski sentiment i izračunati agregatne metrike.

  • Uočiti probleme u podacima i ublažiti ih.

  • Koristiti rezultate sentiment analize kao atribute u prikazu mreža.

  • Interpretirati nalaze sentiment analize.

Što je sentiment u tekstu

Analiza teksta i analiza sentimenata sastavni su dijelovi šireg interdisciplinarnog polja koje se naziva NLP (Natural Language Processing) ili obrada prirodnog jezika. NLP kombinira lingvistiku, statistiku i strojno učenje kako bi omogućio računalima da ‘razumiju’ ljudski jezik. Dok se tradicionalna statistika bavi brojevima, NLP pretvara rečenice u matematičke objekte (vektore) koje potom možemo analizirati modelima koje smo učili u prethodnim lekcijama.

Analiza sentimenata predstavlja postupak procjene emocionalne ili evaluativne orijentacije teksta. Cilj analize je utvrditi izražava li tekst pozitivan, negativan ili neutralan stav prema nekoj temi, događaju ili objektu. U tom smislu govori se o sentimentu kao općoj emocionalnoj procjeni teksta, dok se polaritet odnosi na smjer tog sentimenta, odnosno na to je li on pozitivan ili negativan (ono što se u psihologiji naziva hedonistički ton).

U najjednostavnijem obliku analiza sentimenata razlikuje tri osnovne kategorije: pozitivan sentiment, negativan sentiment i neutralan sentiment. Pozitivan sentiment označava tekst u kojem prevladavaju pozitivni izrazi, evaluacije ili emocije. Negativan sentiment označava tekst s prevladavajućim negativnim izrazima ili kritikama. Neutralan sentiment označava tekst koji ne sadrži jasnu evaluaciju, nego se primarno sastoji od informativnih ili deskriptivnih tvrdnji.

Važno je razlikovati sentiment od pojma subjektivnosti. Subjektivnost označava mjeru u kojoj tekst izražava osobni stav, mišljenje ili procjenu, za razliku od objektivnog iznošenja činjenica. Tekst može biti subjektivan, ali bez jasnog polariteta. Primjerice, rečenica „Ovaj film je zanimljiv” izražava subjektivnu procjenu, ali nije jasno je li ona pozitivna ili negativna.

U proširenim modelima analize sentimenata ne promatra se samo polaritet, nego i emocionalne kategorije poput radosti, straha, ljutnje ili iznenađenja. U tom slučaju analiza prelazi iz binarne procjene polariteta prema detaljnijoj analizi emocionalnog sadržaja teksta.




Priprema podataka

Prije bilo kakve analize sentimenata potrebno je odrediti što je zapravo dokument koji analiziramo. Ovdje ćemo analizirati mali nastavni korpus sastavljen od About us opisa vodećih poduzeća u Hrvatskoj prema Lider Media popisu najvećih poduzeća od 12. ožujka 2026. (bitno je navesti datum, jer se stranica redovito ažurira). Početni popis je proširen na više od deset poduzeća kako bi nakon čišćenja i isključivanja problematičnih stranica ostalo približno deset upotrebljivih organizacijskih opisa.

Kod korporativnih web-stranica određivanje dokumenta nije uvijek trivijalno jer se odjeljak O nama često sastoji od više međusobno povezanih podstranica. Na primjer, INA unutar odjeljka About INA ima zasebne stranice za profil kompanije, povijest, core business, misiju i vrijednosti, etičko poslovanje i privatnost, a HEP unutar O HEP grupi ima zasebne stranice za misiju, strateške ciljeve, povijest, publikacije i upravljačku strukturu. Slično tome, Lidl i Zagrebačka banka imaju više podstranica unutar korporativnog predstavljanja, dok Petrol odvaja korporativno predstavljanje od stranice o ekologiji i društvu.

Zbog toga je prvi korak ručno upoznavanje sa sadržajem. To znači da ne treba odmah “pokupiti sve”, nego pregledati koje stranice doista čine organizacijski samopis, a koje su pravne, tehničke ili administrativne naravi. Za potrebe ove lekcije razumno je uključiti stranice koje opisuju identitet organizacije, djelatnosti, misiju, vrijednosti, povijest i strateško usmjerenje, a isključiti stranice poput politike privatnosti, certificiranja, publikacija, detaljne upravljačke strukture i sličnih sadržaja koji nisu primarno namijenjeni samopredstavljanju.

Drugim riječima, prije automatizacije radimo analitičku selekciju izvora. Kvaliteta analize teksta u velikoj mjeri ovisi o tome što je uključeno u korpus, a ne samo o tome kako se tekst poslije obrađuje.

U sljedećem koraku izrađujemo tablicu svih kandidatskih URL-ova i ručno označavamo treba li ih uključiti u korpus. Ovdje ne pokušavamo biti “potpuno automatizirani”; upravo suprotno, demonstriramo da je ručna kuracija često nužna i metodološki opravdana.

U ovoj fazi preporučljivo je ručno otvoriti nekoliko URL-ova i odgovoriti na tri pitanja:

  • opisuje li stranica organizaciju, njezinu misiju, djelatnosti ili vrijednosti?
  • sadrži li kontinuirani tekst koji ima smisla analizirati kao narativ?
  • je li sadržaj primarno pravni, tehnički ili administrativni?

Ako je odgovor na prva dva pitanja da, a na treće ne, takvu stranicu obično vrijedi uključiti. Priprema podataka nije samo “čišćenje stringova”, nego i konceptualno određivanje jedinice analize.

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.0     ✔ readr     2.1.6
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.2     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(rvest)
## 
## Attaching package: 'rvest'
## 
## The following object is masked from 'package:readr':
## 
##     guess_encoding
library(stringr)
library(purrr)
library(tidytext)
pages <- tribble(
  ~company, ~url, ~section, ~include,
  "INA", "https://www.ina.hr/en/about-ina/profil-kompanije/", "profil kompanije", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/profil-kompanije/povijest/", "povijest", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/profil-kompanije/certificates/", "certifikati", FALSE,
  "INA", "https://www.ina.hr/en/about-ina/core-business/", "core business", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/core-business/exploration-and-production/", "istrazivanje i proizvodnja", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/core-business/refining-and-marketing/", "prerada i marketing", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/core-business/consumer-services-retail/", "maloprodaja i usluge", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/mission-vision-and-core-values/", "misija vizija vrijednosti", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/ethical-business-and-reporting-irregularities/", "eticko poslovanje", TRUE,
  "INA", "https://www.ina.hr/en/about-ina/privacy-policy/", "politika privatnosti", FALSE,

  "HEP", "https://www.hep.hr/o-hep-grupi/25", "o hep grupi", TRUE,
  "HEP", "https://www.hep.hr/o-hep-grupi/misija-vizija-i-temeljne-vrijednosti/37", "misija vizija vrijednosti", TRUE,
  "HEP", "https://www.hep.hr/o-hep-grupi/strateski-ciljevi/51", "strateski ciljevi", TRUE,
  "HEP", "https://www.hep.hr/o-hep-grupi/povijest/54", "povijest", TRUE,
  "HEP", "https://www.hep.hr/drustva-hep-grupe/29", "drustva u grupi", FALSE,
  "HEP", "https://www.hep.hr/o-hep-grupi/hep-d-d-upravljacka-struktura/53", "upravljacka struktura", FALSE,
  "HEP", "https://www.hep.hr/o-hep-grupi/publikacije/55", "publikacije", FALSE,

  "PPD", "https://www.ppd.hr/upoznajte-nas", "upoznajte nas", TRUE,

  "KONZUM", "https://tvrtka.konzum.hr/", "korporativna stranica", TRUE,

  "MET", "https://hr.met.com/en/about-us/about-our-company/", "about our company", TRUE,

  "LIDL", "https://tvrtka.lidl.hr/o-nama", "o nama", TRUE,
  "LIDL", "https://tvrtka.lidl.hr/o-nama/temeljna-nacela-tvrtke", "temeljna nacela", TRUE,
  "LIDL", "https://tvrtka.lidl.hr/o-nama/compliance", "compliance", TRUE,
  "LIDL", "https://tvrtka.lidl.hr/o-nama/povijest", "povijest", TRUE,

  "PETROL", "https://www.petrol.eu/hr/petrol-d-o-o/predstavitev", "predstavljanje", TRUE,
  "PETROL", "https://www.petrol.eu/hr/ekologija-i-drustvo", "ekologija i drustvo", TRUE,

  "SPAR", "https://www.spar.hr/o-nama", "o nama", TRUE,

  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama", "o nama", TRUE,
  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama/pregled", "pregled", TRUE,
  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama/misija-i-vrijednosti", "misija i vrijednosti", TRUE,
  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama/povijest", "povijest", TRUE,
  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama/nas-brend", "nas brend", TRUE,
  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama/struktura", "struktura", FALSE,
  "ZABA", "https://www.zaba.hr/home/o-nama/o-nama/doprinos-zajednici", "doprinos zajednici", TRUE,

  "PLODINE", "https://www.plodine.hr/o-nama", "o nama", TRUE,
  
  "HT", "https://www.hrvatskitelekom.hr/ht-grupa/o-nama/profil-grupe", "o nama", TRUE,
  
  "KAUFLAND", "https://tvrtka.kaufland.hr/kaufland.html", "o nama", TRUE,
  "KAUFLAND", "https://tvrtka.kaufland.hr/kaufland/nase-vrijednosti.html", "nase vrijednosti", TRUE,
  "KAUFLAND", "https://tvrtka.kaufland.hr/kaufland/nagrade-priznanja.html", "nagrade i priznanja", TRUE,
  "KAUFLAND", "https://tvrtka.kaufland.hr/kaufland/kronika.html", "povijest", TRUE,
  "KAUFLAND", "https://tvrtka.kaufland.hr/kaufland/tu-smo-za-tebe.html", "ostalo", TRUE
  
)

Ova tablica je već važan dio istraživačkog procesa. Ona ne služi samo tehničkoj organizaciji URL-ova, nego i bilježi istraživačku odluku o tome što ulazi u korpus.

Nakon ručne selekcije dohvaćamo samo one stranice koje smo odlučili uključiti. Budući da se HTML strukture razlikuju među web-stranicama, najpraktičnije je primijeniti generičku funkciju koja pokušava izvući tekst iz tipičnih sadržajnih elemenata.

# Za ovaj dio koda korišten je Claude Sonnet 4.6, uz manje naknadne dorade
extract_page_text <- function(url) {
  tryCatch({
    response <- httr::GET(
      url,
      httr::add_headers(
        `Accept-Language` = "hr,en;q=0.9",
        `User-Agent` = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
      )
    )
    
    # Dohvati raw bajtove i eksplicitno dekodiraj kao UTF-8
    raw_bytes <- httr::content(response, as = "raw")
    html_string <- rawToChar(raw_bytes)
    Encoding(html_string) <- "UTF-8"
    
    page <- xml2::read_html(html_string, encoding = "UTF-8")
    
    nodes <- page |>
      html_elements(
        "main p, main li, article p, article li,
         .content p, .content li,
         .main-content p, .main-content li,
         .page-content p, .page-content li,
         .entry-content p, .entry-content li,
         h1, h2, h3"
      )
    
    txt <- nodes |>
      html_text2() |>
      str_squish()
    
    txt <- txt[!is.na(txt) & txt != ""] |>
      str_c(collapse = " ")
    
    if (is.na(txt) || txt == "") NA_character_ else txt
    
  }, error = function(e) {
    message("Greška za URL: ", url, " — ", conditionMessage(e))
    NA_character_
  })
}

Sada dohvaćamo tekst samo za uključene stranice.

raw_pages <- pages |>
  filter(include) |>
  mutate(
    text_raw = map_chr(url, extract_page_text),
    n_char = str_length(text_raw)
  )
library(stringi)
raw_pages <- raw_pages |>
  mutate(
    text_raw = stri_enc_toutf8(text_raw)
  )
glimpse(raw_pages)
## Rows: 35
## Columns: 7
## $ X        <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18…
## $ company  <chr> "INA", "INA", "INA", "INA", "INA", "INA", "INA", "INA", "HEP"…
## $ url      <chr> "https://www.ina.hr/en/about-ina/profil-kompanije/", "https:/…
## $ section  <chr> "profil kompanije", "povijest", "core business", "istrazivanj…
## $ include  <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, T…
## $ text_raw <chr> "About INA Company profile INA Group Corporate governance Pol…
## $ n_char   <int> 4453, 5730, 3900, 3407, 5399, 3995, 4285, 4392, 11, 72, 17, 3…

Dobro je odmah provjeriti jesu li sve stranice uspješno dohvaćene i kolika je duljina dobivenog teksta (n_char).

library(DT)
datatable_preview(raw_pages, text_col = "text_raw", n = 100, page_len = 5)

Kvaliteta dohvaćenog teksta nije jednaka za sve stranice. To je očekivano i upravo zato je sljedeći korak kontrola kvalitete. Nakon scrapinga treba ručno pregledati barem nekoliko zapisa. Cilj je provjeriti sadrži li text_raw doista opis kompanije ili su u njemu ostali elementi navigacije, izbornika i tehničkog sadržaja.

Ako neka stranica vraća previše šuma, postoje dvije mogućnosti. Prva je da se za tu domenu napiše precizniji CSS selektor. Druga je da se stranica isključi iz korpusa.

Na primjer, vidimo da redovi 1-8 svi počinju s istim navigacijskim tekstom “About INA Company profile…”. To je header/navigacija stranice. Provjerimo koji dio je jednak:

library(stringr)

texts <- raw_pages$text_raw[1:8]

# Usporedi znak po znak
min_len <- min(nchar(texts))
common <- substr(texts[1], 1, min_len)

for (txt in texts[-1]) {
  while (!startsWith(txt, common)) {
    common <- substr(common, 1, nchar(common) - 1)
  }
}

common
## [1] "About INA Company profile INA Group Corporate governance Policies and documents Company information History Certificates INA Group Corporate governance Policies and documents Policies and documents Company information History Certificates Core business From production and processing to the sale of gas and petroleum products Exploration & Production Exploration Geothermal Energy and New Energy Field Development Drilling & Well Workover Production E&P Project Management & Permitting Exploration & Production Laboratory Refining & Marketing Development Logistics Fuels New and Sustainable Businesses Value Chain Management INA has completed the construction of key systems for the new refinery unit Additional information Internal acts Professional Training Centre INA, d.d. Consumer Services & Retail About The pure power of INA<U+2019>s fuels Grab a quick bite or a drink at the Fresh Corner A wide range of services Save money with INA Loyalty From production and processing to the sale of gas and petroleum products Exploration & Production Exploration Geothermal Energy and New Energy Field Development Drilling & Well Workover Production E&P Project Management & Permitting Exploration & Production Laboratory Exploration Geothermal Energy and New Energy Field Development Drilling & Well Workover Production E&P Project Management & Permitting Exploration & Production Laboratory Refining & Marketing Development Logistics Fuels New and Sustainable Businesses Value Chain Management INA has completed the construction of key systems for the new refinery unit Additional information Internal acts Professional Training Centre INA, d.d. Development Logistics Fuels New and Sustainable Businesses Value Chain Management INA has completed the construction of key systems for the new refinery unit Additional information Internal acts Professional Training Centre INA, d.d. Internal acts Professional Training Centre INA, d.d. Consumer Services & Retail About The pure power of INA<U+2019>s fuels Grab a quick bite or a drink at the Fresh Corner A wide range of services Save money with INA Loyalty About The pure power of INA<U+2019>s fuels Grab a quick bite or a drink at the Fresh Corner A wide range of services Save money with INA Loyalty Mission, vision and values Ethical business and reporting irregularities Privacy policy VIDEO SURVEILLANCE POLICY AT INA GROUP SITES MANAGED BY INA, d.d. Access Control Policy at INA Group locations managed by INA d.d. Archive of modifications and completions Cookie Policy VIDEO SURVEILLANCE POLICY AT INA GROUP SITES MANAGED BY INA, d.d. Access Control Policy at INA Group locations managed by INA d.d. Archive of modifications and completions Cookie Policy News "

Provjerimo prvo preklapaju li se opisi u potpunosti, pa možda možemo jednostavno ukloniti retke 2 - 8.

length(unique(raw_pages$text_raw[1:8])) == 1
## [1] FALSE

Nisu u potpunosti jednaki. Pristupamo uklanjanju teksta koji se ponavlja.

raw_pages$text_raw[2:8] <- gsub(common, "", raw_pages$text_raw[2:8], fixed = TRUE)
# ponovimo uvid
substring(raw_pages$text_raw[1:8], 1, 100) # samo prvih 100 znakova za svaki dokument
## [1] "About INA Company profile INA Group Corporate governance Policies and documents Company information "
## [2] "History INA was founded on January 1, 1964 through the merger of Naftaplin Zagreb, the Rijeka Oil Re"
## [3] "Core business Core business of INA, d.d. and its subsidiaries The principal activities of INA and it"
## [4] "Exploration & Production INA Group Exploration & Production has more than 70 years of experience and"
## [5] "Refining & Marketing Refining and Marketing is in charge of the refining operations in Rijeka, activ"
## [6] "Consumer Services & Retail Consumer Services and Retail operate a modernized regional network of mor"
## [7] "Mission, vision and values INA is a modern, socially responsible and transparent company in constant"
## [8] "Ethical business and reporting irregularities INA Group Code of Ethics INA Group Code of Ethics (CoE"

Ponavljamo postupak za retke [16:19].

library(stringr)

texts <- raw_pages$text_raw[16:19]

# Usporedi znak po znak
min_len <- min(nchar(texts))
common <- substr(texts[1], 1, min_len)

for (txt in texts[-1]) {
  while (!startsWith(txt, common)) {
    common <- substr(common, 1, nchar(common) - 1)
  }
}

common
## [1] "Lidl.hr Karijera Nekretnine O nama Kvaliteta dostupna svima Odr<U+017E>ivost u Lidlu Press centar Kontakt O nama Pregled Povijest tvrtke Temeljna na<U+010D>ela tvrtke Compliance Odr<U+017E>ivost u Lidlu Pregled Dobro za planet Dobro za tebe Na<U+010D>ela kompanije Slu<U+017E>bena stajali<U+0161>ta Izvje<U+0161>taji o odr<U+017E>ivosti Ostale publikacije WWF partnerstvo Dobro za ljude Dobro za planet Pregled Za<U+0161>tita klime Po<U+0161>tovanje bioraznolikosti O<U+010D>uvanje resursa Na<U+010D>ela kompanije Pregled Kodeks pona<U+0161>anja za poslovne partnere Ostale publikacije Pregled Transparentnost u lancu opskrbe neprehrambenim proizvodima UN Global Compact Dobro za ljude Pregled Vo<U+0111>enje dijaloga Po<U+0161>teno postupanje Osvije<U+0161>tena prehrana Promicanje zdravlja O nama "
raw_pages$text_raw[17:19] <- gsub(common, "", raw_pages$text_raw[17:19], fixed = TRUE)
# ponovimo uvid
substring(raw_pages$text_raw[16:19], 1, 100) # samo prvih 100 znakova za svaki dokument
## [1] "Lidl.hr Karijera Nekretnine O nama Kvaliteta dostupna svima Odr<U+017E>ivost u Lidlu Press centar Ko"
## [2] "Temeljna na<U+010D>ela tvrtke Primjetili smo da Java Script nije aktiviran. Kako biste nesmetano mog"
## [3] "Compliance Primjetili smo da Java Script nije aktiviran. Kako biste nesmetano mogli koristiti na<U+0"
## [4] "Povijest tvrtke Primjetili smo da Java Script nije aktiviran. Kako biste nesmetano mogli koristiti n"

Uočavamo simpatičan komentar “NE stavljamo link za zadnji breadcrumb O nama”. Uklonimo ga.

raw_pages$text_raw <- gsub("NE stavljamo link za zadnji breadcrumb O nama", "", raw_pages$text_raw, fixed = TRUE)
substring(raw_pages$text_raw[23:28], 1, 100) # samo prvih 500 znakova za svaki dokument
## [1] "O nama Banka  Pregled Tko smo, kako i za<U+0161>to smo tu ve<U+0107> vi<U+0161>e od 100 godina Misij"
## [2] "Pregled O nama  Profil kompanije Financijski podaci Zagreba<U+010D>ka banka vode<U+0107>a je banka u"
## [3] "Misija i vrijednosti O nama  Misija Mi zaposlenici Zagreba<U+010D>ke banke kao dio Grupe UniCredit p"
## [4] "Povijest O nama  Povijest Vremenski trezor Monografija Zagreba<U+010D>ka banka zapo<U+010D>ela je s "
## [5] "Na<U+0161> brend O nama  Na<U+0161>a strategija Dugoro<U+010D>no stvaranje vrijednosti kroz stabilno"
## [6] "Doprinos zajednici O nama  Sponzorstva i donacije S ciljem osna<U+017E>ivanja razvoja zajednice Zagr"

Također, uočili smo i da su dokumenti 22 i 30 prazni, pa ih uklanjamo.

raw_pages <- raw_pages[!is.na(raw_pages$text_raw), ]
glimpse(raw_pages)
## Rows: 33
## Columns: 7
## $ X        <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18…
## $ company  <chr> "INA", "INA", "INA", "INA", "INA", "INA", "INA", "INA", "HEP"…
## $ url      <chr> "https://www.ina.hr/en/about-ina/profil-kompanije/", "https:/…
## $ section  <chr> "profil kompanije", "povijest", "core business", "istrazivanj…
## $ include  <lgl> TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, T…
## $ text_raw <chr> "About INA Company profile INA Group Corporate governance Pol…
## $ n_char   <int> 4453, 5730, 3900, 3407, 5399, 3995, 4285, 4392, 11, 72, 17, 3…

U nekim opisima uočavamo neuobičajene znakove, pa ćemo te znakove ukloniti.

library(stringi)

# Pretvorimo <U+xxxx> u prave znakove
raw_pages$text_raw <- stri_replace_all_regex(
  raw_pages$text_raw,
  "<U\\+([0-9A-Fa-f]+)>",
  "\\\\u$1"
)
raw_pages$text_raw <- stri_unescape_unicode(raw_pages$text_raw)
raw_pages$text_raw <- stri_trans_general(raw_pages$text_raw, "Latin-ASCII")

datatable_preview(raw_pages, text_col = "text_raw", n = 100, page_len = 5)

Vidimo da neke stranice imaju opis na hrvatskom, a neke na engleskom.

library(cld2)

raw_pages <- raw_pages |>
  mutate(
    language = detect_language(text_raw)
  )
raw_pages$language
##  [1] "en" "en" "en" "en" "en" "en" "en" "en" NA   "bs" NA   "hr" "hr" "hr" "en"
## [16] "hr" "hr" "hr" "hr" "hr" "hr" NA   "hr" "hr" "hr" "hr" "hr" "hr" "hr" "hr"
## [31] "hr" "hr" "hr"

Za prijevod koristimo polyglotr s Google Translate free endpointom. Generalno je stabilan, ali Google povremeno blokira botove. Rate limiting je obavezan. Bez Sys.sleep() Google će nas blokirati već nakon ~10 poziva. S 35 redova i pauzama od 0.3s, cijeli dataset će biti gotov za ~20 sekundi (još uvijek vremenski izvediv za manji nastavni skup podataka). Dugi tekstovi su problem. Web scrapani tekstovi su vjerojatno dugi — strwrap() u kodu će ih rezati na chunkove od 4500 znakova. Provjerimo koliko su dugački:

raw_pages |> summarise(max_chars = max(n_char, na.rm = TRUE), 
                        mean_chars = mean(n_char, na.rm = TRUE))
##   max_chars mean_chars
## 1     10948   2959.667

Provodimo tekst na engleski:

# za ovaj dio koda korišten je Claude Sonnet 4.6, uz naknadno ručno dodane korekcije
# install.packages("polyglotr")
library(polyglotr)
library(dplyr)
library(purrr)

# Wrapper s rate limitingom i error handlingom
translate_to_english <- function(text, source_lang = NULL) {
  if (is.na(text) || nchar(text) == 0) return(NA_character_)
  
  tryCatch({
    # Google Translate free endpoint - max ~5000 znakova po pozivu
    # Ako je tekst duži, treba ga razdvojiti
    if (nchar(text) > 4500) {
      # Razdvaja na rečenice, prevodi u chunkovima
      chunks <- strwrap(text, width = 4500, simplify = TRUE)
      translated <- map_chr(chunks, ~ {
        Sys.sleep(0.5)  # rate limiting
        google_translate(.x, target_language = "en", source_language = source_lang %||% "auto")
      })
      return(paste(translated, collapse = " "))
    }
    
    result <- google_translate(text, target_language = "en", source_language = source_lang %||% "auto")
    Sys.sleep(0.3)  # rate limiting - važno da ne dobiješ blokadu
    result
    
  }, error = function(e) {
    message("Prijevod neuspješan: ", conditionMessage(e))
    NA_character_
  })
}

# Primjena - prevodi samo ako nije već engleski
raw_pages <- raw_pages |>
  mutate(
    text_en = case_when(
      is.na(text_raw)              ~ NA_character_,
      language == "en"             ~ text_raw,       # već engleski, preskoči
      TRUE                         ~ map_chr(text_raw, translate_to_english)
    )
  )

Provjera:

raw_pages <- raw_pages |>
  mutate(
    language = detect_language(text_en)
  )
raw_pages$language
##  [1] "en" "en" "en" "en" "en" "en" "en" "en" "en" "en" NA   "en" "en" "en" "en"
## [16] "en" "en" "en" "en" "en" "en" "en" "en" "en" "en" "en" "en" "en" "en" "en"
## [31] "en" "en" "en"
datatable_preview(raw_pages[,c(2,3,5,10)], text_col = "text_en", n = 100, page_len = 5)

Kad smo zadovoljni dohvatom sadržaja, tekst treba standardizirati tako da bude prikladan za tokenizaciju i kasniju sentiment analizu. Još jednom “provlačimo” kroz čišćenje.

clean_pages <- raw_pages |>
  mutate(
    text_clean = text_en |>
      str_to_lower() |>
      str_replace_all("[^\\p{L}\\s]", " ") |>
      str_replace_all("\\s+", " ") |>
      str_squish()
  )

Provjerimo kako tekst izgleda:

substring(raw_pages$text_en, 1, 100)
##  [1] "About INA Company profile INA Group Corporate governance Policies and documents Company information "                    
##  [2] "History INA was founded on January 1, 1964 through the merger of Naftaplin Zagreb, the Rijeka Oil Re"                    
##  [3] "Core business Core business of INA, d.d. and its subsidiaries The principal activities of INA and it"                    
##  [4] "Exploration & Production INA Group Exploration & Production has more than 70 years of experience and"                    
##  [5] "Refining & Marketing Refining and Marketing is in charge of the refining operations in Rijeka, activ"                    
##  [6] "Consumer Services & Retail Consumer Services and Retail operate a modernized regional network of mor"                    
##  [7] "Mission, vision and values INA is a modern, socially responsible and transparent company in constant"                    
##  [8] "Ethical business and reporting irregularities INA Group Code of Ethics INA Group Code of Ethics (CoE"                    
##  [9] "That HEP group"                                                                                                          
## [10] "Mission, vision and core values â\u0080\u008bâ\u0080\u008bMission Vision Core values"                                    
## [11] "Strategic goals"                                                                                                         
## [12] "History The first alternating power system in Croatia HPP Krka - Sibenik HPP Kraljevac was built, th"                    
## [13] "Get to know us Get to know us Our cooperation with strong international partners brings safety and r"                    
## [14] "Home About us Responsibility Cooperation Media Careers konzum.hr a year with you throughout life as "                    
## [15] "About our company MET Group About our company Company data MET CROATIA AT A GLANCE"                                      
## [16] "Lidl.hr Career Real Estate About us Quality accessible to everyone Sustainability at Lidl Press cent"                    
## [17] "Company Fundamentals We noticed that Java Script is not activated. In order to be able to use our si"                    
## [18] "Compliance We noticed that Java Script is not activated. In order to be able to use our site smoothl"                    
## [19] "Company History We noticed that Java Script is not activated. In order to be able to use our site sm"                    
## [20] "Representation of Petrol d.o.o. Presentation of the company Petrol d.o.o. Seeing the future, before "                    
## [21] "Ecology and society Ecology and society Petrol and ecology Petrol is a company that operates on the "                    
## [22] "About us Bank Overview Who we are, how and why we have been here for more than 100 years Mission and"                    
## [23] "Overview About us Company profile Financial data Zagrebacka banka is a leading bank in Croatia and a"                    
## [24] "Mission and values â\u0080\u008bâ\u0080\u008bAbout us Mission We Zagrebacka banka employees as part of the UniCredit Gro"
## [25] "History About us History Vremenski trezor Monograph Zagrebacka banka started operating as early as 1"                    
## [26] "Our brand About us Our strategy Long-term value creation through stability, innovation and trust. Aw"                    
## [27] "Contribution to the community About us Sponsorships and donations With the aim of strengthening comm"                    
## [28] "Company About us Company Plodine d.d. was founded in 1993 in Rijeka, where the first sales center wa"                    
## [29] "About Kaufland Kaufland is a store chain for which efficiency is very important. The results we have"                    
## [30] "Our values: customer satisfaction and fair treatment Your satisfaction is a key value in our daily w"                    
## [31] "Awards and recognition Product quality and customer satisfaction are our top priority, so we want to"                    
## [32] "Chronicle How was Kaufland created? When was the first business unit opened? Who founded the company"                    
## [33] "We are here for you. Our activities are always focused on efficiency, dynamism and fairness. Whether"

Budući da pojedine kompanije imaju više uključenih podstranica, jedna korisna odluka jest spojiti ih u jedan dokument po kompaniji. Time dobivamo upravo onaj tip inputa koji je praktičan za sentiment analizu na razini organizacije.

company_corpus <- clean_pages |>
  group_by(company) |>
  summarise(
    url_n = n(),
    pages_included = str_c(section, collapse = "; "),
    text_clean = str_c(text_clean, collapse = " "),
    .groups = "drop"
  ) |>
  mutate(
    doc_id = row_number(),
    n_char = str_length(text_clean)
  ) |>
  select(doc_id, company, url_n, pages_included, n_char, text_clean)

Sada je svaki redak jedna kompanija, a sve relevantne podstranice spojene su u jedan tekst.

datatable_preview(company_corpus, text_col = "text_clean", n = 100, page_len = 5)

Ako želimo prijeći na leksikonsku sentiment analizu, tekst treba rastaviti na riječi i dodatno očistiti te provesti lematizaciju i stemming. Prvi korak je tokenizacija.

tokens <- company_corpus |>
  select(doc_id, company, text_clean) |>
  unnest_tokens(word, text_clean)

Dobivena tablica tokens sadrži barem ova tri stupca:

  • doc_id
  • company
  • word
head(tokens, 10)
## # A tibble: 10 × 3
##    doc_id company word   
##     <int> <chr>   <chr>  
##  1      1 HEP     that   
##  2      1 HEP     hep    
##  3      1 HEP     group  
##  4      1 HEP     mission
##  5      1 HEP     vision 
##  6      1 HEP     and    
##  7      1 HEP     core   
##  8      1 HEP     values 
##  9      1 HEP     â      
## 10      1 HEP     â

<U+00E2> je tzv. mojibake - pojava kad se tekst enkodiran u jednom standardu (npr. UTF-8) pročita kao da je u drugom (npr. Latin-1/ISO-8859-1). U ovom slučaju <U+00E2> je â, što je tipičan znak mojibakea - najčešće se pojavljuje kad UTF-8 navodnici ili crtice (“, –) nisu ispravno pročitani.

tokens %>% filter(!stri_enc_isascii(word)) %>% count(word, sort = TRUE)
## # A tibble: 2 × 2
##   word         n
##   <chr>    <int>
## 1 â           32
## 2 zagrebaä     6
tokens <- tokens %>% filter(stri_enc_isascii(word))

#ponovimo provjeru
tokens %>% filter(!stri_enc_isascii(word)) %>% count(word, sort = TRUE)
## # A tibble: 0 × 2
## # ℹ 2 variables: word <chr>, n <int>

Preostaje još samo ukloniti stop_words. To su najčešće riječi u jeziku koje same po sebi ne nose značenje — prijedlozi, veznici, zamjenice i slično (npr. the, a, is, in, of, and).

U analizi teksta uklanjamo ih jer ne doprinose razumijevanju sadržaja. Bez uklanjanja, dominirali bi u svakoj analizi frekvencije, ali nam ne bi rekli ništa korisno o temi ili tonu teksta.

library(tidytext)
head(stop_words)
## # A tibble: 6 × 2
##   word      lexicon
##   <chr>     <chr>  
## 1 a         SMART  
## 2 a's       SMART  
## 3 able      SMART  
## 4 about     SMART  
## 5 above     SMART  
## 6 according SMART

No, pritom trebamo paziti da ne uklonimo negacije, jer one imaju svoj sentiment.

stop_words_custom <- stop_words %>%
  filter(!word %in% c("no", "not", "nor", "never"))

tokens <- tokens %>%
  anti_join(stop_words_custom, by = "word")

Provjera:

str(tokens)
## tibble [6,050 × 3] (S3: tbl_df/tbl/data.frame)
##  $ doc_id : int [1:6050] 1 1 1 1 1 1 1 1 1 1 ...
##  $ company: chr [1:6050] "HEP" "HEP" "HEP" "HEP" ...
##  $ word   : chr [1:6050] "hep" "mission" "vision" "core" ...

Provodimo lematizaciju.

library(textstem)
## Loading required package: koRpus.lang.en
## Loading required package: koRpus
## Loading required package: sylly
## For information on available language packages for 'koRpus', run
## 
##   available.koRpus.lang()
## 
## and see ?install.koRpus.lang()
## 
## Attaching package: 'koRpus'
## The following object is masked from 'package:readr':
## 
##     tokenize
tokens <- tokens %>%
  mutate(word = lemmatize_words(word))

Za leksikonsku sentiment analizu najčešće je najbolje eventualno raditi samo lematizaciju (bez stemminga, pa ćemo preskočiti taj korak). Razlog je što su leksikoni poput bing, afinn i nrc već zadani u konkretnim oblicima riječi. Ako ih previše “izrežemmo” stemmingom, možemo izgubiti podudaranja.

Sad možemo nastaviti s analizom. Prije nego krenemo sa sentiment analizom, pogledajmo najčešće riječi.

tokens %>%
  count(word, sort = TRUE) %>%
  head(20)
## # A tibble: 20 × 2
##    word           n
##    <chr>      <int>
##  1 company      102
##  2 ina           89
##  3 business      78
##  4 croatia       70
##  5 award         55
##  6 product       52
##  7 kaufland      50
##  8 zagreb        50
##  9 bank          49
## 10 employee      45
## 11 banka         44
## 12 lidl          43
## 13 oil           42
## 14 market        39
## 15 quality       39
## 16 croatian      36
## 17 service       36
## 18 production    34
## 19 customer      33
## 20 project       33

Najčešće riječi u korpusu upućuju na to da se organizacijski opisi pretežno temelje na samopredstavljanju, poslovnoj djelatnosti i reputacijskim signalima. Dominiraju opći korporativni pojmovi poput company, business, market, service, production i customer, što pokazuje da tekstovi naglašavaju identitet organizacije, područje poslovanja i odnos prema tržištu. Istodobno se među najučestalijim riječima pojavljuju i pojmovi poput award, quality i employee, što sugerira da organizacije u svojim narativima ne opisuju samo vlastitu djelatnost, nego i nastoje oblikovati reputacijsku sliku o sebi, tj. istaknuti kvalitetu, priznanja i važnost zaposlenika kao dio pozitivne slike o sebi. Prisutnost naziva kompanija i lokacijskih oznaka, poput ina, kaufland, lidl, zagreb i croatia, također pokazuje da dio najčešćih riječi proizlazi iz specifičnosti korpusa, pa takve riječi valja interpretirati oprezno jer više govore o identitetu izvora nego o emocionalnom tonu teksta.

Tokens je standardni ulaz za spajanje s leksikonima poput bing, afinn ili nrc.

Kako bi analiza bila reproducibilna, dobro je pohraniti korištene tekstove i korpuse teksta za naknadno korištenje ili validaciju.

write_csv(clean_pages, "about_pages_raw_cleaned.csv")
write_csv(company_corpus, "about_company_corpus.csv")
write_csv(tokens, "about_company_tokens.csv")

Nakon što smo identificirali relevantne stranice, dohvatili tekst, ručno provjerili njegovu kvalitetu, očistili ga i po potrebi spojili više podstranica u jedan dokument, imamo korpus spreman za analizu. Tek sada ima smisla prijeći na pitanje kako sentiment mjerimo i koje su razlike između leksikonskog pristupa i pristupa temeljenih na modelima.

Pristupi analizi sentimenata

U analizi sentimenata najčešće se razlikuju dva osnovna metodološka pristupa: leksikonski pristup i pristup temeljen na strojnom učenju.

Leksikonski pristup koristi unaprijed definirane rječnike u kojima su riječima pridružene oznake sentimenta ili numeričke vrijednosti. Jednostavan je za implementaciju i interpretaciju, no ne uzima u obzir kontekst u kojem se riječ pojavljuje. Ovo je i dalje jedan od najčešće korištenih pristupa.

Pristup temeljen na strojnom učenju koristi algoritme koji uče prepoznavati sentiment iz označenih primjera. Fleksibilniji je i često točniji, osobito za složenije tekstove, ali zahtijeva označene podatke za treniranje i manje je transparentan od leksikonskog pristupa.

Izračun sentiment scorea

Nakon identifikacije riječi iz sentiment leksikona potrebno je izračunati sentiment score, odnosno numeričku mjeru ukupnog sentimenta teksta.

Jednostavan način izračuna temelji se na razlici između broja pozitivnih i negativnih riječi:

\[ Sentiment = N_{pos} - N_{neg} \]

gdje je:

  • \(N_{pos}\) broj pozitivnih riječi u tekstu
  • \(N_{neg}\) broj negativnih riječi u tekstu.

Dobivena vrijednost predstavlja ukupni polaritet teksta. Ako je rezultat pozitivan, tekst ima prevladavajući pozitivan sentiment. Ako je rezultat negativan, prevladava negativan sentiment. Vrijednosti blizu nule upućuju na neutralan tekst ili uravnotežen odnos pozitivnih i negativnih izraza.

Ako leksikon sadrži numeričke vrijednosti sentimenta, ukupni sentiment može se izračunati kao zbroj svih sentiment vrijednosti:

\[ Sentiment = \sum_{i=1}^{n} w_i \]

gdje je:

  • \(w_i\) sentiment vrijednost riječi \(i\)
  • \(n\) broj riječi u tekstu koje se pojavljuju u leksikonu.

Ovaj pristup uvodi pojam valencije, odnosno intenziteta emocionalne procjene. Riječi s većom apsolutnom vrijednošću imaju snažniji utjecaj na konačni sentiment score.




Agregacija sentimenta

Nakon izračuna sentimenta na razini pojedinih riječi ili rečenica, rezultati se obično agregiraju kako bi se dobila sažeta procjena sentimenta na višoj razini.

Agregacija može biti provedena na različitim razinama analize:

  • po dokumentu (npr. recenziji ili članku)
  • po autoru ili korisniku
  • po vremenskom razdoblju
  • po tematskoj kategoriji.

Na primjer, u analizi društvenih mreža moguće je izračunati prosječni sentiment svih objava pojedinog korisnika ili prosječni sentiment komunikacije između dvije skupine aktera.

Agregacija omogućuje sažimanje velikih količina tekstualnih podataka u nekoliko ključnih indikatora koji se mogu vizualizirati i dalje analizirati.

Leksikonski pristup

Leksikonski pristup temelji se na unaprijed definiranom rječniku riječi kojem su pridružene informacije o sentimentu. Takav rječnik naziva se sentiment leksikon. Svaka riječ u leksikonu ima pridruženu oznaku polariteta (npr. pozitivno ili negativno) ili numeričku vrijednost koja označava intenzitet sentimenta.

Analiza se provodi tako da se tekst tokenizira na pojedinačne riječi, nakon čega se te riječi uspoređuju s rječnikom. Ako se riječ nalazi u leksikonu, njezin sentiment doprinosi ukupnoj procjeni sentimenta teksta.

Prednost leksikonskog pristupa je njegova transparentnost i jednostavnost implementacije. Analitičar može jasno vidjeti koje su riječi utjecale na konačni rezultat. Nedostatak je što takav pristup često ne uzima u obzir širi kontekst u kojem se riječ pojavljuje.

U R okruženju često se koriste sljedeći sentiment leksikoni:

  • bing – klasifikacija riječi na pozitivan i negativan polaritet
  • afinn – numeričke vrijednosti sentimenta (npr. od −5 do +5)
  • nrc – kategorizacija riječi prema emocijama i polaritetu

Usporedba riječnika - po prvih 100 riječi

library(textdata)
library(tidytext)
library(tidyverse)

afinn <- get_sentiments("afinn")
bing <- get_sentiments("bing")
nrc <- get_sentiments("nrc")

comparison <- data.frame(
  afinn_word = get_sentiments("afinn")[1:100,1],
  afinn_value = get_sentiments("afinn")[1:100,2],
  bing_word = get_sentiments("bing")[1:100,1],
  bing_sentiment = get_sentiments("bing")[1:100,2],
  nrc_word = get_sentiments("nrc")[1:100,1],
  nrc_sentiment = get_sentiments("nrc")[1:100,2]
)

colnames(comparison) <- c("afinn_word", "afinn_value", "bing_word", "bing_sentiment", "nrc_word", "nrc_sentiment")

library(DT)

datatable(
  comparison,
  options = list(
    pageLength = 5,
    autoWidth = TRUE,
    scrollX = TRUE
  ),
  rownames = FALSE
)

Bing leksikon

Bing leksikon klasificira svaku riječ kao pozitivnu ili negativnu. Spajanjem s tokenima dobivamo broj pozitivnih i negativnih riječi po tvrtki.

library(tidytext)
library(tidyverse)

bing <- get_sentiments("bing")

sentiment_bing <- tokens %>%
  inner_join(bing, by = "word") %>%
  count(company, sentiment) %>%
  pivot_wider(names_from = sentiment, values_from = n, values_fill = 0) %>%
  mutate(score = positive - negative)

sentiment_bing
## # A tibble: 9 × 4
##   company  negative positive score
##   <chr>       <int>    <int> <int>
## 1 HEP             5        3    -2
## 2 INA            11       81    70
## 3 KAUFLAND       10      175   165
## 4 KONZUM          0       22    22
## 5 LIDL           23       43    20
## 6 PETROL          9       11     2
## 7 PLODINE         2       29    27
## 8 PPD             1        7     6
## 9 ZABA           11       89    78

Napomena: inner_join zadržava samo one riječi koje se nalaze i u našem tekstu i u rječniku sentimenta. Sve neutralne riječi ili riječi koje rječnik ne poznaje bit će odbačene iz ove analize.

library(tidyr)
library(reshape2)
## 
## Attaching package: 'reshape2'
## The following object is masked from 'package:tidyr':
## 
##     smiths
library(wordcloud)
## Loading required package: RColorBrewer
tokens %>%
  inner_join(bing, by = "word") %>%
  count(word, sentiment, sort = TRUE) %>%
  acast(word ~ sentiment, value.var = "n", fill = 0) %>%
  comparison.cloud(colors = c("firebrick3", "deepskyblue3"),
                   max.words = 150, 
                   title.size = 1.5)

Jedna tvrtka vjerojatno nema niti jednu riječ koja se podudara s bing leksikonom - provjera:

tokens %>%
  filter(company == "MET") %>%
  inner_join(bing, by = "word")
## # A tibble: 0 × 4
## # ℹ 4 variables: doc_id <int>, company <chr>, word <chr>, sentiment <chr>
sentiment_bing %>%
  pivot_longer(cols = c(positive, negative), names_to = "sentiment", values_to = "n") %>%
  ggplot(aes(x = reorder(company, n), y = n, fill = sentiment)) +
  geom_col(position = "dodge") +
  coord_flip() +
  scale_fill_manual(values = c("positive" = "#2ecc71", "negative" = "#e74c3c")) +
  labs(title = "Bing sentiment po tvrtki",
       x = NULL, y = "Broj riječi", fill = "Sentiment") +
  theme_minimal()


AFINN leksikon

AFINN leksikon dodjeljuje svakoj riječi numeričku vrijednost između −5 (vrlo negativno) i +5 (vrlo pozitivno). Zbrajanjem vrijednosti dobivamo ukupni sentiment score po tvrtki. Pogledajmo prvo output spajanja s riječima iz leksikona.

library(tidyr)
library(reshape2)
library(wordcloud)

afinn <- get_sentiments("afinn")

tokens %>%
  inner_join(afinn, by = "word") %>%
  # KLJUČNI KORAK: Pretvaramo brojčani 'value' u kategoriju
  mutate(sentiment = ifelse(value > 0, "positive", "negative")) %>% 
  count(word, sentiment, sort = TRUE) %>%
  acast(word ~ sentiment, value.var = "n", fill = 0) %>%
  comparison.cloud(colors = c("firebrick3", "deepskyblue3"),
                   max.words = 150, 
                   title.size = 1.5)

Ovdje već vidimo da različiti rječnici daju različiti output.

library(textdata)

sentiment_afinn <- tokens %>%
  inner_join(afinn, by = "word") %>%
  mutate(sentiment = ifelse(value > 0, "positive", "negative")) %>%
  group_by(company, sentiment) %>%
  summarise(score = sum(value), .groups = "drop") %>%
  pivot_wider(names_from = sentiment, values_from = score, values_fill = 0) %>%
  mutate(total = positive + negative) %>%
  arrange(desc(total))

sentiment_afinn
## # A tibble: 9 × 4
##   company  negative positive total
##   <chr>       <dbl>    <dbl> <dbl>
## 1 KAUFLAND      -27      334   307
## 2 ZABA           -8      160   152
## 3 INA           -10      138   128
## 4 PLODINE        -6       53    47
## 5 LIDL          -14       58    44
## 6 KONZUM         -1       26    25
## 7 PETROL         -9       32    23
## 8 PPD             0       17    17
## 9 HEP            -7        5    -2

U slučaju AFINN leksikona varijabla total predstavlja neto ukupni sentiment score, odnosno očekivan konačan prevladavajući dojam nakon zbrajanja pozitivnih i negativnih vrijednosti riječi u tekstu.

sentiment_afinn %>%
  pivot_longer(cols = c(positive, negative), names_to = "sentiment", values_to = "n") %>%
  ggplot(aes(x = reorder(company, total), y = n, fill = sentiment)) +
  geom_col(position = "dodge") +
  coord_flip() +
  scale_fill_manual(values = c("positive" = "#2ecc71", "negative" = "#e74c3c")) +
  labs(title = "AFINN sentiment po tvrtki",
       x = NULL, y = "Score", fill = "Sentiment") +
  theme_minimal()


NRC leksikon

NRC leksikon kategorizira riječi prema osam emocija (strah, radost, povjerenje, iznenađenje…) i dvama polaritetima. Prvo ćemo pogledati učestalost riječi po emociji u oblaku riječi.

nrc <- get_sentiments("nrc")

# filtriranje (izbacujemo opće kategorije 'positive' i 'negative' 
# kako bismo se fokusirali samo na specifične emocije)
nrc_emotions <- nrc %>%
  filter(!sentiment %in% c("positive", "negative"))

library(RColorBrewer)

# izrada oblaka
tokens %>%
  inner_join(nrc_emotions, by = "word", relationship = "many-to-many") %>%
  count(word, sentiment, sort = TRUE) %>%
  acast(word ~ sentiment, value.var = "n", fill = 0) %>%
  comparison.cloud(colors = brewer.pal(8, "Dark2"), # Koristimo paletu s 8 boja
                   max.words = 150, 
                   title.size = 1.2,
                   scale = c(3, 0.5)) # Prilagodba veličine riječi

Prikazujemo distribuciju emocija po tvrtki.

nrc <- get_sentiments("nrc")

sentiment_nrc <- tokens %>%
  inner_join(nrc, by = "word", relationship = "many-to-many") %>% # NRC leksikon dodjeljuje jednoj riječi više emocija istovremeno - "many-to-many"
  count(company, sentiment)

# sentiment_nrc  - ispis nije pregledan
sentiment_nrc_za_graf  <- sentiment_nrc %>%
  filter(!sentiment %in% c("positive", "negative"))  # zadržimo samo emocije

sentiment_nrc_za_graf %>%
  ggplot(aes(x = sentiment, y = n, fill = sentiment)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~company, scales = "free_y") +
  coord_flip() +
  labs(title = "NRC emocije po tvrtki",
       x = NULL, y = "Broj riječi") +
  theme_minimal()

Usporedba rezultata

Usporedba rezultata dobivenih koristeći različite leksikone jest jedan način validacije rezultata.

# Bing već imamo u prikladnom obliku
bing_compare <- sentiment_bing %>%
  transmute(
    company,
    bing_negative = negative,
    bing_positive = positive,
    bing_score = score
  )

# AFINN preimenujemo radi preglednosti
afinn_compare <- sentiment_afinn %>%
  transmute(
    company,
    afinn_negative = negative,
    afinn_positive = positive,
    afinn_score = total
  )

# NRC: zadržavamo samo pozitivno i negativno
nrc_compare <- sentiment_nrc %>%
  filter(sentiment %in% c("positive", "negative")) %>%
  pivot_wider(names_from = sentiment, values_from = n, values_fill = 0) %>%
  mutate(score = positive - negative) %>%
  transmute(
    company,
    nrc_negative = negative,
    nrc_positive = positive,
    nrc_score = score
  )

# objedinjena tablica
sentiment_compare <- full_join(bing_compare, afinn_compare, by = "company") %>%
  full_join(nrc_compare, by = "company") %>%
  arrange(company)

sentiment_compare
## # A tibble: 9 × 10
##   company  bing_negative bing_positive bing_score afinn_negative afinn_positive
##   <chr>            <int>         <int>      <int>          <dbl>          <dbl>
## 1 HEP                  5             3         -2             -7              5
## 2 INA                 11            81         70            -10            138
## 3 KAUFLAND            10           175        165            -27            334
## 4 KONZUM               0            22         22             -1             26
## 5 LIDL                23            43         20            -14             58
## 6 PETROL               9            11          2             -9             32
## 7 PLODINE              2            29         27             -6             53
## 8 PPD                  1             7          6              0             17
## 9 ZABA                11            89         78             -8            160
## # ℹ 4 more variables: afinn_score <dbl>, nrc_negative <int>,
## #   nrc_positive <int>, nrc_score <int>

Usporedna tablica pokazuje rezultate sentiment analize dobivene korištenjem triju različitih leksikona: Bing, AFINN i NRC. Iako svaki leksikon koristi različitu metodologiju procjene sentimenta, opći obrasci u rezultatima su vrlo slični.

Prvo, smjer sentimenta uglavnom je konzistentan među leksikonima. Sve kompanije, osim HEP-a, imaju pozitivan neto sentiment u svim leksikonima. HEP je jedina organizacija kod koje Bing i AFINN pokazuju blago negativan rezultat, dok NRC pokazuje pozitivan rezultat. Takva razlika može nastati jer NRC klasificira riječi u više emocionalnih kategorija, a neke riječi koje Bing ili AFINN prepoznaju kao negativne mogu se u NRC-u pojaviti u kontekstu drugih emocija.

Drugo, relativni poredak kompanija vrlo je sličan među leksikonima. Primjerice, kompanije poput Kauflanda, ZABA-e i INA-e imaju najviše pozitivnih rezultata u svim metodama, dok kompanije poput PPD-a, Petrol-a i Konzuma imaju znatno umjerenije vrijednosti. To sugerira da različiti leksikoni prepoznaju sličan emocionalni ton u tekstovima.

Treće, intenzitet sentimenta razlikuje se među leksikonima. Bing koristi binarnu klasifikaciju (riječi su samo pozitivne ili negativne), dok AFINN dodjeljuje numeričke težine sentimentu, a NRC broji pojave polariteta unutar šireg emocionalnog leksikona. Zbog toga su vrijednosti u NRC-u i AFINN-u često veće od Bing rezultata, ali razlike u apsolutnim vrijednostima ne znače nužno i razliku u interpretaciji.

Općenito, usporedba pokazuje da su rezultati relativno stabilni među različitim leksikonima, što povećava pouzdanost interpretacije sentimenta u analiziranom korpusu.

sentiment_compare_scores <- data.frame(
  company = sentiment_compare$company,
  
  bing_afinn_neg = abs(sentiment_compare$bing_negative - abs(sentiment_compare$afinn_negative)),
  afinn_nrc_neg = abs(abs(sentiment_compare$afinn_negative)-sentiment_compare$nrc_negative),
  nrc_bing_neg = abs(sentiment_compare$nrc_negative - sentiment_compare$bing_negative),
  
  bing_afinn_pos = abs(sentiment_compare$bing_positive - abs(sentiment_compare$afinn_positive)),
  afinn_nrc_pos =  abs(abs(sentiment_compare$afinn_positive)-sentiment_compare$nrc_positive),
  nrc_bing_pos = abs(sentiment_compare$nrc_positive - sentiment_compare$bing_positive)
)

sentiment_compare_scores
##    company bing_afinn_neg afinn_nrc_neg nrc_bing_neg bing_afinn_pos
## 1      HEP              2             4            2              2
## 2      INA              1            22           21             57
## 3 KAUFLAND             17             4           21            159
## 4   KONZUM              1             2            3              4
## 5     LIDL              9            13            4             15
## 6   PETROL              0             3            3             21
## 7  PLODINE              4             2            2             24
## 8      PPD              1             0            1             10
## 9     ZABA              3             5            2             71
##   afinn_nrc_pos nrc_bing_pos
## 1            34           36
## 2           105          162
## 3             6          153
## 4            23           27
## 5           110          125
## 6            18           39
## 7            16           40
## 8             9           19
## 9            55          126

Analiza apsolutnih odstupanja između rezultata dobivenih različitim leksikonima pokazuje da u nekim slučajevima postoje relativno velike razlike u broju prepoznatih pozitivnih i negativnih riječi. Takve razlike su očekivane jer svaki leksikon koristi različit skup riječi i različita pravila klasifikacije sentimenta.

Leksikoni se razlikuju po veličini i strukturi rječnika. NRC leksikon, primjerice, uključuje velik broj riječi povezanih s emocijama, pa često prepoznaje znatno više pojavnica pozitivnog ili negativnog sentimenta nego Bing. Zbog toga je u nekim slučajevima razlika između NRC i Bing rezultata relativno velika.

Osim toga, leksikoni se razlikuju po logici vrednovanja riječi. Bing klasificira riječi isključivo kao pozitivne ili negativne, dok AFINN koristi numeričke vrijednosti sentimenta, a NRC osim polariteta uključuje i emocijske kategorije. Posljedica toga je da isti tekst može generirati različite apsolutne vrijednosti sentimenta ovisno o korištenom leksikonu.

Također, čak važnije od apsolutnih razlika u broju riječi jest slaganje u smjeru sentimenta i relativnom poretku dokumenta. U ovom primjeru većina kompanija ima pozitivan neto sentiment u svim leksikonima, a kompanije s najvišim vrijednostima (npr. Kaufland, ZABA i INA) pojavljuju se među najpozitivnijima u svim metodama. Takva konzistentnost sugerira da leksikoni, unatoč razlikama u apsolutnim vrijednostima, prepoznaju sličan emocionalni ton u tekstovima.

Zbog toga velike razlike u apsolutnim vrijednostima ne znače nužno da su rezultati nepouzdani. Problem bi nastao tek kada bi različiti leksikoni davali suprotan smjer sentimenta ili potpuno različit relativni poredak dokumenata.

Kakve su razlike zapravo problem?

Ne postoji univerzalni “prag”, ali u praksi gledamo tri stvari:

1. znak sentimenta (najvažnije): ako jedan leksikon kaže: +50, a drugi -40, to je problem.

2. korelacija između leksikona: Ako su rezultati korelirani, analiza je stabilna.

cor(sentiment_compare$bing_negative, abs(sentiment_compare$afinn_negative))
## [1] 0.5764037
cor(abs(sentiment_compare$afinn_negative), sentiment_compare$nrc_negative)
## [1] 0.7990541
cor(sentiment_compare$nrc_negative, sentiment_compare$bing_negative)
## [1] 0.7594557
cor(sentiment_compare$bing_positive, sentiment_compare$afinn_positive)
## [1] 0.9944219
cor(sentiment_compare$afinn_positive, sentiment_compare$nrc_positive)
## [1] 0.9262679
cor(sentiment_compare$nrc_positive, sentiment_compare$bing_positive)
## [1] 0.9523314

U ovom primjeru korelacije između leksikona pokazuju visok stupanj slaganja, osobito za pozitivni sentiment. Korelacija između Bing i AFINN pozitivnih rezultata iznosi 0.99, dok su korelacije između AFINN i NRC (0.93) te NRC i Bing (0.95) također vrlo visoke. Takve vrijednosti upućuju na to da različiti leksikoni prepoznaju vrlo sličan obrazac pozitivnog sentimenta u analiziranim tekstovima.

Za negativni sentiment korelacije su nešto niže, ali i dalje relativno visoke. Korelacija između NRC i Bing negativnih rezultata iznosi 0.76, dok korelacija između AFINN i NRC negativnih rezultata iznosi 0.80. Korelacija između Bing i AFINN negativnih rezultata je niža - umjereno izražena (0.58), što može biti posljedica različite strukture leksikona i činjenice da AFINN koristi numeričke težine sentimenta, dok Bing klasificira riječi isključivo kao pozitivne ili negativne.

Općenito, visoke korelacije među leksikonima sugeriraju da se opći obrazac sentimenta u tekstovima prepoznaje konzistentno, iako se apsolutni broj prepoznatih riječi može razlikovati. Takva analiza predstavlja dodatnu provjeru stabilnosti rezultata sentiment analize.

3. relativni poredak: Ako isti tekstovi imaju sličan poredak sentimenta, rezultati su validni.

Ako se leksikoni uglavnom slažu u predznaku sentimenta, pokazuju sličan relativni poredak dokumenata i umjereno do visoko koreliraju, tada rezultate možemo smatrati dovoljno stabilnima za interpretaciju.




Klasteriranje sentiment profila

Kao što je ranije navedeno, sljedeći korak je agregacija outputa leksikonske analize. NRC leksikon omogućuje analizu emocionalne strukture teksta, a ne samo ukupnog polariteta. Svaka riječ može biti povezana s jednom ili više emocionalnih kategorija, poput joy, trust, fear ili anger. Nakon tokenizacije i spajanja s NRC leksikonom dobivamo broj pojavljivanja pojedine emocije u tekstu.

U tablici sentiment_nrc svaki red predstavlja jednu kombinaciju tvrtke i emocije, a varijabla n označava koliko se puta riječ iz te emocionalne kategorije pojavljuje u tekstu.

str(sentiment_nrc)
## tibble [83 × 3] (S3: tbl_df/tbl/data.frame)
##  $ company  : chr [1:83] "HEP" "HEP" "HEP" "HEP" ...
##  $ sentiment: chr [1:83] "anger" "anticipation" "fear" "joy" ...
##  $ n        : int [1:83] 3 21 11 16 3 39 1 8 29 12 ...

Kako bismo identificirali tipične obrasce emocionalnog tona, možemo primijeniti klastersku analizu. Jedna od najčešće korištenih metoda je k-means klasteriranje, koje grupira objekte prema sličnosti njihovih numeričkih obilježja. No, pritom prolazimo kroz nekoliko klasičnih koraka koji obuhvaćaju dodatne uvide i pripremu podataka.

Kako bismo mogli usporediti tvrtke prema njihovom emocionalnom profilu, potrebno je podatke transformirati u matricu emocija, gdje svaki red predstavlja tvrtku, a svaki stupac jednu emociju, odnosno ukupan pozitivni i negativni sentiment.

emotion_matrix <- sentiment_nrc %>%
  pivot_wider(names_from = sentiment, values_from = n, values_fill = 0)

emotion_matrix
## # A tibble: 9 × 11
##   company  anger anticipation  fear   joy negative positive sadness surprise
##   <chr>    <int>        <int> <int> <int>    <int>    <int>   <int>    <int>
## 1 HEP          3           21    11    16        3       39       1        8
## 2 INA         12           89    22    36       32      243       3       10
## 3 KAUFLAND     7          136     4   135       31      328       3       55
## 4 KONZUM       3            8     1    12        3       49       0        3
## 5 LIDL        22           58    22    27       27      168      19       12
## 6 PETROL       3           11     9     8       12       50       2        2
## 7 PLODINE      4           34     2    23        4       69       2        7
## 8 PPD          0            7     1     4        0       26       2        0
## 9 ZABA         5          102     9    64       13      215      12       34
## # ℹ 2 more variables: trust <int>, disgust <int>

U ovom obliku svaka tvrtka ima vlastiti emocionalni vektor.

Budući da se ukupna duljina tekstova razlikuje među tvrtkama, korisno je opažanja standardizirati kako bi bila usporedive. Koristimo funkciju scale() koja primijenjuje izraz za standardizirano obilježje po svakoj varijabli, tj. stupcu: \(z_{ij}=\frac{x_{ij}- \overline{x_j}}{s_j}\):

emotion_features <- emotion_matrix %>%
  select(-company)
emotion_features
## # A tibble: 9 × 10
##   anger anticipation  fear   joy negative positive sadness surprise trust
##   <int>        <int> <int> <int>    <int>    <int>   <int>    <int> <int>
## 1     3           21    11    16        3       39       1        8    29
## 2    12           89    22    36       32      243       3       10   116
## 3     7          136     4   135       31      328       3       55   204
## 4     3            8     1    12        3       49       0        3    26
## 5    22           58    22    27       27      168      19       12    98
## 6     3           11     9     8       12       50       2        2    23
## 7     4           34     2    23        4       69       2        7    58
## 8     0            7     1     4        0       26       2        0    15
## 9     5          102     9    64       13      215      12       34   141
## # ℹ 1 more variable: disgust <int>
emotion_scaled <- scale(emotion_features)
emotion_scaled
##             anger anticipation       fear          joy    negative   positive
##  [1,] -0.53134451   -0.6508887  0.2425356 -0.487427716 -0.84613338 -0.8462434
##  [2,]  0.81362129    0.7871759  1.5764816 -0.002692971  1.40734430  1.0122528
##  [3,]  0.06641806    1.7811324 -0.6063391  2.396744020  1.32963818  1.7866262
##  [4,] -0.53134451   -0.9258129 -0.9701425 -0.584374665 -0.84613338 -0.7551406
##  [5,]  2.30802773    0.1315876  1.5764816 -0.220823606  1.01881367  0.3289822
##  [6,] -0.53134451   -0.8623688  0.0000000 -0.681321615 -0.14677824 -0.7460303
##  [7,] -0.38190387   -0.3759646 -0.8488747 -0.317770555 -0.76842726 -0.5729351
##  [8,] -0.97966645   -0.9469609 -0.9701425 -0.778268564 -1.07925177 -0.9646769
##  [9,] -0.23246322    1.0621000  0.0000000  0.675935673 -0.06907211  0.7571651
##          sadness   surprise      trust    disgust
##  [1,] -0.6140351 -0.3605832 -0.7632330 -1.1039746
##  [2,] -0.2982456 -0.2505748  0.5677502  0.9283422
##  [3,] -0.2982456  2.2246152  1.9140320  0.9283422
##  [4,] -0.7719298 -0.6356043 -0.8091290 -1.1039746
##  [5,]  2.2280702 -0.1405663  0.2923743  0.7025293
##  [6,] -0.4561404 -0.6906086 -0.8550250  0.7025293
##  [7,] -0.4561404 -0.4155874 -0.3195720 -1.1039746
##  [8,] -0.4561404 -0.8006170 -0.9774142 -0.8781616
##  [9,]  1.1228070  1.0695265  0.9502166  0.9283422
## attr(,"scaled:center")
##        anger anticipation         fear          joy     negative     positive 
##     6.555556    51.777778     9.000000    36.111111    13.888889   131.888889 
##      sadness     surprise        trust      disgust 
##     4.888889    14.555556    78.888889     4.888889 
## attr(,"scaled:scale")
##        anger anticipation         fear          joy     negative     positive 
##     6.691620    47.285774     8.246211    41.259679    12.868998   109.766166 
##      sadness     surprise        trust      disgust 
##     6.333333    18.180423    65.365213     4.428443

Sada svaka vrijednost predstavlja odstupanje od prosjeka stupca.

Prije nego odaberemo broj klastera, korisno je provjeriti kako se mijenja suma kvadrata unutar grupa (within-group sum of squares, WSS) za različite vrijednosti \(k\). Ideja elbow metode jest pronaći točku nakon koje dodatno povećanje broja klastera više ne donosi znatno poboljšanje.

wss <- sapply(1:8, function(k) {
  kmeans(emotion_scaled, centers = k, nstart = 50, iter.max = 15)$tot.withinss
})

plot(
  1:8, wss, type = "b",
  xlab = "Broj klastera (k)",
  ylab = "Within-group sum of squares"
)

Na temelju elbow metode uobičajeno možemo odabrati razuman broj klastera. Ovdje se ne uočava posve jasno najizraženija promjena nagiba na krivulji (specifično, jesu li bolja dva ili tri klastera), pa ćemo, u ovom primjeru pomalo proizvoljno koristiti tri klastera, što je često dovoljno da se razlikuju osnovni tipovi emocionalnih profila bez pretjerane fragmentacije uzorka, osobito zbog malog uzorka.

set.seed(123)

km_result <- kmeans(emotion_scaled, centers = 3, nstart = 25)

library(factoextra)
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
fviz_cluster(
  km_result,
  data = emotion_scaled,
  geom = c("point", "text"),
  repel = TRUE,
  ellipse.type = "convex",
  labelsize = 4,
  ggtheme = theme_minimal()
)

Dobivamo grupiranje tvrtki prema sličnosti njihovog emocionalnog profila. Izvršit ćemo brzu usporedbu s dva i četiri klastera.

set.seed(123)

km_result2 <- kmeans(emotion_scaled, centers = 2, nstart = 25)
km_result4 <- kmeans(emotion_scaled, centers = 4, nstart = 25)

fviz_cluster(
  km_result2,
  data = emotion_scaled,
  geom = c("point", "text"),
  repel = TRUE,
  ellipse.type = "convex",
  labelsize = 4,
  ggtheme = theme_minimal()
)

fviz_cluster(
  km_result4,
  data = emotion_scaled,
  geom = c("point", "text"),
  repel = TRUE,
  ellipse.type = "convex",
  labelsize = 4,
  ggtheme = theme_minimal()
)

Kako bismo procijenili kvalitetu rješenja, možemo usporediti nekoliko vrijednosti \(k\) i izračunati više kriterija validacije. U nastavku koristimo:

  • silhouette – veće vrijednosti upućuju na bolje razdvojene klastere
  • Calinski–Harabasz indeks – veće vrijednosti upućuju na bolju strukturu klastera
  • Davies–Bouldin indeks – manje vrijednosti upućuju na bolju razdvojenost klastera.
library(cluster)
library(clusterCrit)

X <- emotion_scaled
ks <- 2:6

metrics <- lapply(ks, function(k) {
  set.seed(123)
  km <- kmeans(X, centers = k, nstart = 50)

  sil_avg <- mean(silhouette(km$cluster, dist(X))[, 3])

  int <- intCriteria(
    as.matrix(X),
    as.integer(km$cluster),
    c("Calinski_Harabasz", "Davies_Bouldin")
  )

  data.frame(
    k = k,
    silhouette = sil_avg,
    calinski_harabasz = int$calinski_harabasz,
    davies_bouldin = int$davies_bouldin
  )
})

metrics <- do.call(rbind, metrics)
metrics
##   k silhouette calinski_harabasz davies_bouldin
## 1 2  0.4969981          10.89200      0.7355693
## 2 3  0.4884855          11.99750      0.7325656
## 3 4  0.3817078          11.17148      0.4686847
## 4 5  0.3550601          13.64961      0.2009238
## 5 6  0.2008689          21.49235      0.2085681

Ovakva tablica pomaže u argumentiranju izbora broja klastera. U praksi različiti kriteriji ne moraju uvijek sugerirati isto rješenje, pa je izbor broja klastera često kompromis između statističke kvalitete i interpretativne smislenosti.

Kako bi se procijenio optimalan broj klastera, analizirano je nekoliko standardnih pokazatelja kvalitete klasteriranja: silhouette indeks, Calinski–Harabasz indeks i Davies–Bouldin indeks. Svaki od tih pokazatelja mjeri različite aspekte strukture klastera, poput njihove međusobne razdvojenosti i unutarnje kompaktnosti.

Silhouette indeks mjeri koliko je svaki objekt sličan vlastitom klasteru u odnosu na ostale klastere. Veće vrijednosti upućuju na bolje razdvojene i kompaktnije klastere. U ovom slučaju najveća vrijednost silhouette indeksa dobivena je za k = 2 (0.497), što sugerira da bi dva klastera mogla predstavljati relativno jasno razdvojene skupine. Međutim, vrijednost za k = 3 (0.488) također je visoka i pokazuje da i rješenje s tri klastera zadržava dobru razinu razdvojenosti.

Calinski–Harabasz indeks uspoređuje varijaciju između klastera i varijaciju unutar klastera. Veće vrijednosti ukazuju na bolju strukturu klastera. U dobivenim rezultatima ovaj indeks ima relativno bliske vrijednosti za k = 2 (10.89) i k = 3 (11.998), što znači da oba rješenja pokazuju sličnu razinu kvalitete u smislu diferencijacije skupina (iako se najviša razina postiže sa šest klastera - pri čemu bi se, za samo 9 opažanja, izgubio smisao klasteriranja).

Davies–Bouldin indeks mjeri prosječnu sličnost između klastera, pri čemu su niže vrijednosti poželjne jer upućuju na veću međusobnu razdvojenost skupina. Najniža vrijednost u ovom primjeru dobivena je za k = 6 (0.21), dok je vrijednost za k = 3 (0.73) gotovo ista kao rješenje s dva klastera.

Budući da različiti kriteriji ne sugeriraju jednoznačno isti broj klastera, izbor optimalnog rješenja u praksi često predstavlja kompromis između statističkih pokazatelja i interpretativne smislenosti rezultata. U ovom primjeru odabiremo tri klastera, jer takvo rješenje omogućuje jasnu diferencijaciju između emocionalnih profila kompanija, a pritom ostaje interpretativno pregledno i prikladno za analizu komunikacijskih stilova.

Rezultat klasteriranja možemo pridružiti natrag tablici tvrtki.

clustered_companies <- emotion_matrix %>%
  select(company) %>%
  mutate(cluster = km_result$cluster)

clustered_companies
## # A tibble: 9 × 2
##   company  cluster
##   <chr>      <int>
## 1 HEP            2
## 2 INA            3
## 3 KAUFLAND       1
## 4 KONZUM         2
## 5 LIDL           3
## 6 PETROL         2
## 7 PLODINE        2
## 8 PPD            2
## 9 ZABA           1

Nakon primjene k-means klasteriranja moguće je analizirati prosječne vrijednosti emocija unutar svakog klastera kako bi se razumjelo koje karakteristike definiraju pojedinu skupinu. Tablica prosječnih vrijednosti emocija po klasteru omogućuje uvid u tipične emocionalne obrasce korporativnih narativa:

cluster_profiles <- as.data.frame(emotion_scaled) %>%
  mutate(cluster = factor(km_result$cluster)) %>%
  group_by(cluster) %>%
  summarise(across(everything(), mean), .groups = "drop")

cluster_profiles
## # A tibble: 3 × 11
##   cluster   anger anticipation   fear    joy negative positive sadness surprise
##   <fct>     <dbl>        <dbl>  <dbl>  <dbl>    <dbl>    <dbl>   <dbl>    <dbl>
## 1 1       -0.0830        1.42  -0.303  1.54     0.630    1.27    0.412    1.65 
## 2 2       -0.591        -0.752 -0.509 -0.570   -0.737   -0.777  -0.551   -0.581
## 3 3        1.56          0.459  1.58  -0.112    1.21     0.671   0.965   -0.196
## # ℹ 2 more variables: trust <dbl>, disgust <dbl>

Na grafu svaka točka predstavlja jednu tvrtku, a boja označava klaster kojem pripada.

Rezultati pokazuju da se analizirane kompanije mogu podijeliti u tri skupine prema emocionalnom tonu njihovih organizacijskih opisa. Klasteri se razlikuju prvenstveno prema intenzitetu pozitivnih emocija, razini anticipacije i povjerenja te relativnoj prisutnosti negativnih emocija.

Prvi klaster, u kojem se nalaze tvrtke Kaufland i Zagrebačka banka, karakterizira izrazito visok udio pozitivnih emocija, uz izbjegavanje negativnih emocija (anger i fear). Prosječne vrijednosti za kategorije positive, joy, trust i anticipation u ovom su klasteru znatno više nego u ostalim skupinama. Takav emocionalni profil sugerira komunikacijski stil koji snažno naglašava uspjeh, razvoj i organizacijske vrijednosti. U ovakvim narativima organizacije često ističu rast poslovanja, inovacije, zadovoljstvo kupaca ili pozitivne učinke poslovanja na zajednicu. Zbog snažnog naglašavanja pozitivnih elemenata ovaj se tip komunikacije može opisati kao optimistični ili promotivni narativ, u kojem organizacija aktivno gradi pozitivan identitet i naglašava vlastitu uspješnost.

Drugi klaster obuhvaća većinu analiziranih organizacija - HEP, Konzum, Petrol, Plodine i PPD. Emocionalni profil u ovom klasteru pokazuje ispodprosječnu izraženost emocija. U usporedbi s prvim i trećim klasterom, intenzitet pozitivnih emocija poput joy, trust i anticipation znatno je niži, dok su negativne emocije također ispodprosječno zastupljene. Takav obrazac upućuje na komunikaciju koja je prvenstveno informativna i institucionalna, s naglaskom na opis organizacije, njezine djelatnosti, povijest ili strukture poslovanja. U tim tekstovima emocionalni ton nije dominantan element komunikacije, nego se organizacija predstavlja kroz činjenice, organizacijsku povijest i osnovne informacije o poslovanju. Ovaj tip narativa može se opisati kao neutralni - institucionalni ili tehnički korporativni stil komunikacije.

Treći klaster, koji uključuje kompanije INA i Lidl, karakterizira relativno visok intenzitet emocija općenito, izuzev radosti (joy) i iznenađenja (surprise). U ovom klasteru pozitivne emocije poput trust i anticipation te pozitivnog sentimenta, imaju vrijednosti iznad prosjeka, no niže nego u opisima prvog klastera. Istodobno su povišene i neke negativne emocije poput anger, fear i sadness. Takva kombinacija upućuje na komunikaciju koja koristi širi raspon emocionalnih izraza. Ovakav obrazac može se pojaviti u tekstovima koji ne naglašavaju samo pozitivne aspekte organizacije, nego također referiraju na izazove, odgovornost, sigurnost, regulatorne okvire, upravljanje rizicima ili druge kompleksnije teme poslovanja. Takav stil komunikacije može se opisati kao emocionalno intenzivan ili kontrastni narativ, u kojem se pozitivne poruke o razvoju i uspjehu kombiniraju s referencama na rizike, odgovornost, održivost ili društvene izazove.

Dobiveni klasteri ne predstavljaju strogo definirane kategorije, nego analitičke skupine temeljene na sličnosti emocionalnih profila. Budući da se analiza temelji na relativno malom uzorku organizacija, rezultate prije svega treba promatrati kao ilustraciju metodološkog pristupa. Ipak, klasteriranje jasno pokazuje da se korporativni narativi razlikuju u načinu na koji koriste emocionalni ton, čime sentiment analiza prelazi iz jednostavne procjene polariteta prema tipologiji organizacijskih komunikacijskih stilova.

Strojno učenje

Drugi pristup analizi sentimenata temelji se na modelima strojnog učenja. U tom slučaju model uči prepoznavati sentiment iz skupa tekstova.

U slučaju primjene strojnog učenja, model se trenira na skupu tekstova koji su ručno označeni (engl. labeled data). Na primjer, svaka recenzija (ili tekst) može biti označena kao pozitivna ili negativna. Na temelju takvih primjera model uči koje riječi i obrasci u tekstu najčešće razlikuju pozitivne i negativne tekstove. Takav pristup naziva se nadzirano učenje (supervised learning), jer model uči uz pomoć unaprijed poznatih oznaka.

Suprotno tome, nenadzirane metode (unsupervised learning) pokušavaju otkriti strukturu u tekstu bez prethodno označenih podataka. Takve metode mogu identificirati, primjerice, tematske skupine dokumenata ili latentne strukture u tekstu.

U praksi se oba pristupa često koriste zajedno. U ovoj lekciji fokus je na leksikonskom pristupu, jer je metodološki jednostavniji i transparentniji. Ipak, kako bi se razumjela logika naprednijih metoda analize teksta, u nastavku ćemo ilustrirati dva osnovna pristupa strojnog učenja.

Ilustracija: nadzirani pristup

Kako bismo ilustrirali osnovnu ideju nadziranog učenja, koristimo mali skup tekstova s unaprijed označenim sentimentom.

Cilj modela je naučiti koje riječi i kombinacije riječi najčešće signaliziraju pozitivan ili negativan sentiment.

Za demonstraciju koristimo naivni Bayesov klasifikator, jedan od klasičnih i često korištenih modela u obradi teksta.

library(tidyverse)
library(tidytext)
library(caret)
library(e1071)
# Primjer označenih tekstova (pozitivno = 1, negativno = 0)
primjeri <- tibble(
  tekst = c(
    "excellent quality and great service",
    "wonderful experience highly recommend",
    "outstanding performance and reliable",
    "poor quality and bad experience",
    "terrible service would not recommend",
    "disappointing and unreliable product"),
  sentiment = factor(c(1, 1, 1, 0, 0, 0), labels = c("negativno", "pozitivno"))
)

primjeri
## # A tibble: 6 × 2
##   tekst                                 sentiment
##   <chr>                                 <fct>    
## 1 excellent quality and great service   pozitivno
## 2 wonderful experience highly recommend pozitivno
## 3 outstanding performance and reliable  pozitivno
## 4 poor quality and bad experience       negativno
## 5 terrible service would not recommend  negativno
## 6 disappointing and unreliable product  negativno
# Tokenizacija i document-term matrix
dtm <- primjeri %>%
  mutate(company = row_number()) %>%
  unnest_tokens(word, tekst) %>%
  anti_join(stop_words_custom, by = "word") %>%
  count(company, word) %>%
  pivot_wider(names_from = word, values_from = n, values_fill = 0)

dtm
## # A tibble: 6 × 18
##   company excellent quality service experience highly recommend wonderful
##     <int>     <int>   <int>   <int>      <int>  <int>     <int>     <int>
## 1       1         1       1       1          0      0         0         0
## 2       2         0       0       0          1      1         1         1
## 3       3         0       0       0          0      0         0         0
## 4       4         0       1       0          1      0         0         0
## 5       5         0       0       1          0      0         1         0
## 6       6         0       0       0          0      0         0         0
## # ℹ 10 more variables: outstanding <int>, performance <int>, reliable <int>,
## #   bad <int>, poor <int>, not <int>, terrible <int>, disappointing <int>,
## #   product <int>, unreliable <int>
# Dodaj oznake sentimenta
dtm_labeled <- dtm %>%
  left_join(primjeri %>% mutate(company = row_number()) %>% select(company, sentiment),
            by = "company") %>%
  select(-company)

# Naivni Bayes klasifikator
model_nb <- naiveBayes(sentiment ~ ., data = dtm_labeled)
predikcije <- predict(model_nb, dtm_labeled)

# Matrica konfuzije
confusionMatrix(predikcije, dtm_labeled$sentiment)
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  negativno pozitivno
##   negativno         3         0
##   pozitivno         0         3
##                                      
##                Accuracy : 1          
##                  95% CI : (0.5407, 1)
##     No Information Rate : 0.5        
##     P-Value [Acc > NIR] : 0.01563    
##                                      
##                   Kappa : 1          
##                                      
##  Mcnemar's Test P-Value : NA         
##                                      
##             Sensitivity : 1.0        
##             Specificity : 1.0        
##          Pos Pred Value : 1.0        
##          Neg Pred Value : 1.0        
##              Prevalence : 0.5        
##          Detection Rate : 0.5        
##    Detection Prevalence : 0.5        
##       Balanced Accuracy : 1.0        
##                                      
##        'Positive' Class : negativno  
## 

Matrica konfuzije prikazuje usporedbu između stvarnih oznaka sentimenata (Reference) i predikcija modela (Prediction).

U ovom primjeru redovi predstavljaju predikcije modela, dok stupci predstavljaju stvarne oznake u podacima.

stvarno negativno stvarno pozitivno
predviđeno negativno 3 0
predviđeno pozitivno 0 3

Iz tablice vidimo da je model:

  • točno klasificirao 3 negativna teksta
  • točno klasificirao 3 pozitivna teksta
  • nije napravio nijednu pogrešku

Drugim riječima, sve se vrijednosti nalaze na glavnoj dijagonali matrice, što znači da su predikcije potpuno točne.

Accuracy = 1 - Accuracy predstavlja udio točnih klasifikacija. Vrijednost 1 znači da je model točno klasificirao sve tekstove u uzorku.

Sensitivity = 1 - Sensitivity (osjetljivost) pokazuje koliki udio stvarno negativnih tekstova model uspijeva ispravno prepoznati.

Specificity = 1 - Specificity pokazuje koliki udio pozitivnih tekstova model ispravno klasificira kao pozitivne.

Kappa = 1 - Kappa mjeri koliko je model bolji od slučajnog pogađanja. Vrijednost 1 označava savršeno slaganje između predikcija i stvarnih oznaka.

Savršeni rezultat u ovom primjeru ne znači da je model nužno vrlo dobar. Budući da je skup podataka vrlo mali (samo šest tekstova), model može lako zapamtiti obrasce u podacima. U stvarnim analizama modeli se treniraju i testiraju na znatno većim skupovima podataka.

Važno je naglasiti da je ovaj primjer vrlo mali i služi isključivo za ilustraciju logike modela. U stvarnim primjenama modeli se treniraju na znatno većim korpusima tekstova.




Ilustracija: nenadzirani pristup (LDA)

Nesupervizirane metode ne koriste unaprijed označene podatke, nego pokušavaju identificirati latentne strukture u tekstu.

Jedna od najpoznatijih metoda u analizi teksta je Latent Dirichlet Allocation (LDA). LDA pretpostavlja da se svaki dokument sastoji od kombinacije nekoliko tema, dok je svaka tema definirana distribucijom riječi.

Drugim riječima, model pokušava odgovoriti na pitanje: Koje se skupine riječi u tekstu pojavljuju zajedno i mogu predstavljati istu temu?

Važno je naglasiti da LDA ne analizira sentiment, nego tematsku strukturu teksta. Ili, još preciznije - LDA je ovdje kao ilustracija nenadziranog text mining pristupa, a ne sentiment metode. Ipak, takve metode često predstavljaju prvi korak u istraživanju većih tekstualnih korpusa, pa će se ovdje prikazati mini-primjer.

library(topicmodels)
library(tidytext)
# Priprema document-term matrice iz tokens
dtm_lda <- tokens %>%
  count(company, word) %>%
  cast_dtm(company, word, n)

dtm_lda
## <<DocumentTermMatrix (documents: 10, terms: 1548)>>
## Non-/sparse entries: 2607/12873
## Sparsity           : 83%
## Maximal term length: 16
## Weighting          : term frequency (tf)
# LDA s k temama
set.seed(42)
lda_model <- LDA(dtm_lda, k = 3, control = list(seed = 42))

# Najvažnije riječi po temi
teme <- tidy(lda_model, matrix = "beta")

top_words <- teme %>%
  group_by(topic) %>%
  slice_max(beta, n = 10) %>%
  ungroup()

top_words
## # A tibble: 30 × 3
##    topic term        beta
##    <int> <chr>      <dbl>
##  1     1 kaufland 0.0235 
##  2     1 award    0.0202 
##  3     1 company  0.0168 
##  4     1 product  0.0160 
##  5     1 quality  0.0136 
##  6     1 food     0.0108 
##  7     1 croatia  0.0108 
##  8     1 employee 0.00984
##  9     1 business 0.00971
## 10     1 croatian 0.00795
## # ℹ 20 more rows
top_words %>%
  mutate(term = reorder_within(term, beta, topic)) %>%
  ggplot(aes(x = term, y = beta, fill = factor(topic))) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~topic, scales = "free_y") +
  coord_flip() +
  scale_x_reordered() +
  labs(title = "Top 10 riječi po LDA temi",
       x = NULL, y = "Beta (vjerojatnost riječi u temi)") +
  theme_minimal()

Nakon treniranja modela možemo izdvojiti riječi koje imaju najveću vjerojatnost pripadnosti pojedinoj temi. Te riječi pomažu u interpretaciji tema koje je model identificirao.

Preciznije, rezultati LDA modela prikazuju riječi koje imaju najveću vjerojatnost pripadnosti pojedinoj temi. Na grafu svaki stupčasti dijagram predstavlja jednu temu, dok stupci prikazuju riječi s najvećom vrijednošću parametra \(\beta\), koji označava vjerojatnost da se određena riječ pojavljuje unutar određene teme. Što je vrijednost \(\beta\) veća, to je riječ tipičnija za tu temu.

Važno je naglasiti da LDA ne daje izravne nazive tema. Model identificira skupine riječi koje se statistički često pojavljuju zajedno, a istraživač zatim interpretira značenje tih skupina. Također, nazivi kompanija mogu dominirati temama i da ih je u ozbiljnijoj analizi često korisno ukloniti iz tokena prije modeliranja. Budući da se na grafu pojavljuju riječi poput ina, lidl, kaufland, netko bi mogao pomisliti da je to “prava tema”, a zapravo je dijelom i artefakt korpusa.

U prvoj temi pojavljuju se riječi poput company, business, overview, quality, compliance i principles. Ove riječi često se nalaze u dijelovima korporativnih stranica koji opisuju organizaciju, njezine vrijednosti ili načela poslovanja. Takav skup riječi upućuje na temu koja se može interpretirati kao opći organizacijski opis i korporativna načela poslovanja.

U takvim tekstovima organizacije obično predstavljaju svoju strukturu, poslovnu filozofiju, standarde kvalitete i regulatorne okvire u kojima posluju.

Druga tema uključuje riječi poput oil, production, business, company te nazive organizacija poput ina ili banka. Takav skup pojmova sugerira tekstove koji se odnose na osnovne poslovne aktivnosti organizacija, poput proizvodnje, energetike ili financijskih usluga. Ova tema može se interpretirati kao operativni ili sektorski opis poslovanja, odnosno dio korporativnog narativa u kojem organizacije opisuju svoje ključne djelatnosti i tržišni kontekst.

Treća tema sadrži riječi poput award, products, employees, management i health. Ove riječi često se pojavljuju u dijelovima tekstova koji naglašavaju organizacijska postignuća, razvoj zaposlenika, održivost ili upravljačku strukturu.

Takav skup riječi upućuje na temu koja se može interpretirati kao reputacijski narativ, odnosno dio komunikacije u kojem organizacije ističu nagrade, organizacijsku kulturu ili brigu za zaposlenike i društvenu zajednicu.

Važno je naglasiti da LDA analiza identificira statističke obrasce u raspodjeli riječi, a ne jasno definirane semantičke kategorije. Zbog toga interpretacija tema uvijek uključuje određeni stupanj istraživačke procjene.

U ovom primjeru korpus je relativno malen, pa rezultati služe prvenstveno kao ilustracija metode. U većim tekstualnim zbirkama teme bi obično bile jasnije diferencirane i stabilnije.




Sentiment u mrežama

U kontekstu analize mreža sentiment može biti povezan s odnosima između aktera. U takvim analizama sentiment se ne promatra samo kao svojstvo pojedinog teksta, nego i kao karakteristika komunikacije između čvorova mreže.

Primjerice, moguće je analizirati sentiment poruka koje jedan korisnik upućuje drugome. Ako su poruke pretežno pozitivne, veza između ta dva aktera može se interpretirati kao pozitivna interakcija. Ako su poruke pretežno negativne, veza može ukazivati na konflikt ili kritiku.

Na taj način sentiment analiza omogućuje proučavanje emocionalne strukture mreže, odnosno načina na koji se pozitivni i negativni stavovi šire kroz relacijske strukture. Takve analize mogu otkriti kohezivne zajednice, konfliktne skupine ili utjecajne aktere koji šire određene emocionalne tonove unutar mreže.

Kako bi se ilustrirala ideja sentimenta u mrežama, zamislimo jednostavnu mrežu komunikacije između korisnika društvene mreže. Svaka poruka predstavlja usmjerenu vezu između pošiljatelja i primatelja.

messages <- tibble(
  from = c("A","A","B","C","D","E","F","G","B","D"),
  to   = c("B","C","D","E","F","G","A","C","E","B"),
  text = c(
    "great collaboration on the project",
    "excellent analysis and presentation",
    "this is a terrible decision and consequences will be devastating",
    "good results so far, let's hope for the best",
    "very disappointing outcome",
    "outstanding teamwork and support",
    "bad coordination in the last phase",
    "very promising results",
    "poor communication in the team",
    "great improvement compared to last year"
  )
)

messages
## # A tibble: 10 × 3
##    from  to    text                                                            
##    <chr> <chr> <chr>                                                           
##  1 A     B     great collaboration on the project                              
##  2 A     C     excellent analysis and presentation                             
##  3 B     D     this is a terrible decision and consequences will be devastating
##  4 C     E     good results so far, let's hope for the best                    
##  5 D     F     very disappointing outcome                                      
##  6 E     G     outstanding teamwork and support                                
##  7 F     A     bad coordination in the last phase                              
##  8 G     C     very promising results                                          
##  9 B     E     poor communication in the team                                  
## 10 D     B     great improvement compared to last year

Najprije izračunavamo sentiment svake poruke koristeći leksikonski pristup.

sentiment_messages <- messages %>%
  unnest_tokens(word, text) %>%
  inner_join(get_sentiments("bing"), by = "word") %>%
  mutate(score = if_else(sentiment == "positive", 1, -1)) %>% # samo ukupni score radi jednostavnosti
  group_by(from, to) %>%
  summarise(sentiment_score = sum(score), .groups = "drop")

sentiment_messages
## # A tibble: 10 × 3
##    from  to    sentiment_score
##    <chr> <chr>           <dbl>
##  1 A     B                   1
##  2 A     C                   1
##  3 B     D                  -2
##  4 B     E                  -1
##  5 C     E                   2
##  6 D     B                   2
##  7 D     F                  -1
##  8 E     G                   2
##  9 F     A                  -1
## 10 G     C                   1

Dobiveni rezultat predstavlja sentiment veze između aktera. Pozitivna vrijednost označava pretežno pozitivan ton komunikacije, dok negativna vrijednost upućuje na kritiku ili konflikt.

Takvi se rezultati mogu interpretirati kao emocionalne težine veza u mreži, što omogućuje analizu strukture pozitivnih i negativnih interakcija.

U ovom primjeru sentiment se najprije računa na razini pojedine poruke, a zatim agregira na razini veze između dvaju aktera. Time ista veza može sažeti više poruka i predstavljati njihov prevladavajući emocionalni ton. Riječ je o pojednostavljenoj ilustraciji, ali ona jasno pokazuje kako se tekstualni sadržaj može prevesti u mrežni atribut.

U mrežnoj analizi sentiment se može koristiti kao atribut veze (edge attribute). Na primjer, pozitivne veze mogu predstavljati podršku ili suradnju, dok negativne veze mogu označavati sukob ili kritiku.

Na taj način moguće je analizirati kako se emocionalni ton komunikacije raspoređuje unutar mreže. Takva analiza može otkriti:

  • skupine aktera koje međusobno razmjenjuju pozitivne poruke,
  • konfliktne podskupine u kojima prevladava negativna komunikacija,
  • aktere koji imaju snažan utjecaj na širenje određenog emocionalnog tona.
library(igraph)
## 
## Attaching package: 'igraph'
## The following objects are masked from 'package:lubridate':
## 
##     %--%, union
## The following objects are masked from 'package:dplyr':
## 
##     as_data_frame, groups, union
## The following objects are masked from 'package:purrr':
## 
##     compose, simplify
## The following object is masked from 'package:tidyr':
## 
##     crossing
## The following object is masked from 'package:tibble':
## 
##     as_data_frame
## The following objects are masked from 'package:stats':
## 
##     decompose, spectrum
## The following object is masked from 'package:base':
## 
##     union
g <- igraph::graph_from_data_frame(sentiment_messages, directed = TRUE)
E(g)$sent <- sentiment_messages$sentiment_score
E(g)$col <- ifelse(E(g)$sent >0, "green", "red")

plot(g,
     vertex.size = 15,
     vertex.color = "grey80",
     edge.color = E(g)$col,
     edge.arrow.size = 0.5,
     edge.width = abs(E(g)$sent))

U stvarnim istraživanjima ovakav se pristup često primjenjuje na velikim skupovima komunikacijskih podataka, poput poruka na društvenim mrežama, e-mail komunikacije ili online rasprava. U takvim slučajevima sentiment analiza omogućuje povezivanje tekstualnog sadržaja i mrežne strukture, čime se otvara mogućnost analize širenja emocija i stavova kroz društvene mreže.

Studija slučaja: sentiment profili korporativnog predstavljanja

Kao primjer primjene analize sentimenata može se razmotriti istraživanje predstavljeno u članku “Corporate Self-Representation on Official Websites: Strategic Signifiers and Sentiment Profiles” (Kostelić i Gonan Božac, 2026). Istraživanje analizira način na koji se poduzeća predstavljaju na vlastitim službenim web-stranicama te kakav emocionalni ton koriste u komunikaciji s javnošću.

Autori su prikupili uzorak 100 tekstualnih opisa iz “About us” sekcije službenih web-stranica poduzeća u Hrvatskoj s Liderove liste 1000 najvećih. Kao i u ovoj lekciji, ovi tekstovi predstavljaju posebno zanimljiv izvor podataka jer su namjerno konstruirani komunikacijski sadržaj: organizacije u njima pokušavaju oblikovati vlastiti identitet, naglasiti strateške ciljeve i poslati signal o vrijednostima koje zastupaju.

Cilj istraživanja bio je identificirati:

  • ponavljajuće strateške signale u korporativnim narativima
  • emocionalni ton komunikacije
  • tipične obrasce sentimenta koji se pojavljuju u opisima poduzeća.

U analizi je primijenjen leksikonski pristup sentiment analizi. Tekstovi su najprije prikupljeni na hrvatskom jeziku, a zatim strojno prevedeni na engleski kako bi se mogli koristiti standardni sentiment leksikoni razvijeni za engleski jezik.

Sentiment je analiziran pomoću tri poznata rječnika:

  • AFINN – numerički sentiment score
  • Bing – klasifikacija riječi na pozitivne i negativne
  • NRC – klasifikacija prema emocijama (npr. trust, joy, fear, anticipation)

Na temelju tih rječnika izračunati su:

  • polaritet teksta (pozitivno / negativno)
  • emocionalni profil (raspodjela emocija u tekstu).

Rezultati su zatim agregirani na razini poduzeća, što znači da je svaki dokument (opis poduzeća) dobio vlastiti sentiment profil.

Izvor: Kostelić i Gonan Božac, 2026

Analiza je pokazala da u korporativnim narativima dominira pozitivan emocionalni ton, osobito emocije povjerenja (trust) i anticipacije (anticipation). To upućuje na komunikacijsku strategiju kojom organizacije nastoje naglasiti stabilnost, pouzdanost i budući razvoj.

Istraživanje slijedi relativno jasan pipeline analize teksta, koji se može generalizirati za slične projekte.

Prvi korak je prikupljanje tekstualnog korpusa.

U ovom slučaju:

  • identifikacija 100 najvećih poduzeća
  • preuzimanje tekstova iz sekcija poput About us, Company profile ili Mission & vision
  • formiranje korpusa dokumenata.

Rezultat je skup tekstova u kojem svaki dokument predstavlja jednu organizaciju.

Budući da su dostupni sentiment leksikoni uglavnom razvijeni za engleski jezik, tekstovi su:

  • strojno prevedeni s hrvatskog na engleski
  • standardizirani za daljnju analizu.

Ovaj korak ilustrira važan metodološki problem višejezične analize sentimenata.

Sljedeći korak uključuje tipične postupke pripreme teksta:

  • tokenizacija (razdvajanje teksta na riječi)
  • uklanjanje nepotrebnih znakova
  • uklanjanje stop-riječi.

Ovaj korak omogućuje povezivanje riječi s elementima sentiment leksikona.

Nakon tokenizacije riječi se uspoređuju s rječnicima sentimenta.

Za svaku riječ dobiva se:

  • polaritet (pozitivno / negativno)
  • ili vrijednost sentiment scorea
  • ili pripadnost određenoj emociji.

Rezultati se zatim agregiraju na razini dokumenta kako bi se dobio sentiment profil pojedinog poduzeća.

Za svaku organizaciju formira se vektor emocija, primjerice:

  • joy
  • trust
  • fear
  • anger
  • anticipation.

Vrijednosti tih emocija normaliziraju se kako bi se mogli uspoređivati različiti dokumenti.

U završnoj fazi istraživanja primijenjena je k-means klasterska analiza nad emocionalnim vektorima i izvršena je validacija klasteringa.

Izvor: Kostelić i Gonan Božac, 2026

Na temelju toga identificirana su tri tipična sentiment profila korporativnih narativa:

  1. optimistični narativi orijentirani potrošačima
  2. transparentni emotivni narativi koji adresiraju probleme ili izazove
  3. tehnički opisi organizacije niskog emocionalnog tona.

Ova analiza pokazuje da se organizacije razlikuju u načinu na koji koriste emocionalni ton u komunikaciji.

Rezultati sugeriraju da emocije poput povjerenja i iščekivanja mogu funkcionirati kao strateški komunikacijski signali. Organizacije time nastoje signalizirati pouzdanost, stabilnost i orijentaciju na budućnost.

Istodobno, pojedine organizacije koriste transparentniji ton koji uključuje i negativne aspekte poslovanja, ali bez narušavanja ukupno pozitivnog sentimenta.

Autori zaključuju da analiza sentimenata može poslužiti kao analitički alat za audit korporativne komunikacije, odnosno za procjenu usklađenosti organizacijskog narativa s očekivanjima dionika.

Sirovi podaci i kod dostupni su putem Open Science Framework-a.




Studija slučaja: Može li raspoloženje s Twittera predvidjeti burzu?

Ova je studija korisna jer pokazuje da sentiment ne mora nužno biti samo binarna podjela na pozitivno i negativno, nego se može operacionalizirati i kao višedimenzionalno raspoloženje.

Bollen, Mao i Zeng (2011) polaze od ideje iz bihevioralne ekonomije da emocije utječu na donošenje odluka pojedinaca, pa postavljaju pitanje vrijedi li to i na razini društva. Drugim riječima, ako se raspoloženje velikog broja ljudi može očitati iz objava na društvenim mrežama, može li ono biti povezano s kretanjem burzovnih indeksa? U svom radu analiziraju dnevne objave s Twittera i iz njih konstruiraju vremenske serije kolektivnog raspoloženja, koje potom uspoređuju s dnevnim promjenama indeksa Dow Jones Industrial Average (DJIA).

Za analizu teksta koriste dva različita alata. Prvi je OpinionFinder, koji mjeri opći polaritet poruka, odnosno odnos pozitivnog i negativnog sentimenta. Drugi je Google-Profile of Mood States (GPOMS), koji ne svodi raspoloženje samo na pozitivno i negativno, nego ga razlaže na šest dimenzija: Calm, Alert, Sure, Vital, Kind i Happy. Autori najprije provjeravaju imaju li tako dobivene mjere smisla tako da gledaju reagiraju li očekivano na poznate društvene događaje, poput predsjedničkih izbora i Dana zahvalnosti 2008. godine. Nakon toga ispituju prediktivnu vrijednost tih vremenskih nizova pomoću Grangerove kauzalnosti i modela Self-Organizing Fuzzy Neural Network, kako bi utvrdili poboljšava li uključivanje raspoloženja predviđanje dnevnog smjera kretanja DJIA-a.

Glavni nalaz rada jest da nisu sve dimenzije raspoloženja jednako korisne. Jednostavan pozitivno-negativan sentiment nije se pokazao osobito korisnim za predviđanje, ali su neke specifične dimenzije raspoloženja, osobito one povezane sa smirenošću, dale bolji doprinos modelu. Autori izvještavaju da se uključivanjem određenih dimenzija javnog raspoloženja može poboljšati točnost predviđanja smjera dnevnih promjena DJIA-a te smanjiti pogrešku predviđanja. Kao metodološka pouka, ova studija lijepo pokazuje da je u analizi teksta često korisnije promatrati više latentnih emocionalnih dimenzija nego samo grubi binarni sentiment, osobito kada tekst povezujemo s kompleksnim društvenim ili ekonomskim ishodima.




Problemi i ograničenja analize sentimenata

Unatoč jednostavnosti implementacije, analiza sentimenata suočava se s nizom metodoloških ograničenja.

Jedan od čestih problema je ironija i sarkazam. Rečenice poput „Baš odlično, opet je sustav pao” sadrže pozitivne riječi, ali stvarno značenje je negativno. Leksikonske metode često ne prepoznaju takve obrate značenja.

Drugi važan problem su negacije. Negacija može potpuno promijeniti polaritet izraza. Na primjer, riječ „dobro” ima pozitivan sentiment, ali izraz „nije dobro” ima negativno značenje.

Treći problem odnosi se na domenske pomake. Riječi mogu imati različito značenje u različitim kontekstima. Izraz „agresivan marketing” može imati pozitivnu konotaciju u poslovnom kontekstu, dok izraz „agresivno ponašanje” u društvenom kontekstu ima negativnu konotaciju. Slično, na primjer, riječ waste ima negativan predznak sentimenta, a često će se pojavljivati u temama o održivosti i načinima zbrinjavanja otpada i recikliranju - što zapravo predstavlja pozitivne aktivnosti.

Dodatni izazov predstavlja višejezičnost. Sentiment leksikoni najčešće su razvijeni za engleski jezik, dok za druge jezike često postoje ograničeni resursi. Razlike u morfologiji i sintaksi dodatno otežavaju analizu.




Validacija rezultata

Rezultati analize sentimenata trebaju biti validirani kako bi se procijenila njihova pouzdanost.

Jedan od najjednostavnijih pristupa je ručna provjera uzorka tekstova. Analitičar nasumično odabire dio tekstova i uspoređuje automatsku procjenu sentimenta s vlastitom interpretacijom.

Drugi pristup je usporedba različitih sentiment leksikona. Ako različiti leksikoni daju slične rezultate, može se zaključiti da je procjena relativno stabilna.

Validacija je važan korak jer automatske metode analize teksta uvijek uključuju određenu razinu pogreške i aproksimacije.




Memento

Sentiment: Emocionalna/polarna ocjena teksta.

Polaritet: Smjer sentimenta (pozitivno/negativno).

Pozitivan sentiment: Tekst s prevladavajućim pozitivnim izrazima.

Negativan sentiment: Tekst s prevladavajućim negativnim izrazima.

Neutralan sentiment: Tekst bez jasne polarne orijentacije.

Leksikonski pristup: Sentiment iz rječnika označenih riječi.

Strojno učenje: Model uči sentiment iz označenih primjera.

Supervizirana analiza: Učenje na labeliranim podacima.

Nesupervizirana analiza: Učenje bez labela (npr. clustering) ili heuristike.

Sentiment score: Numerička mjera ukupnog sentimenta.

Agregacija: Sažimanje sentimenta po dokumentu/korisniku/vremenu.

Kontekstualni sentiment: Sentiment ovisi o kontekstu i domeni.

Ironija i sarkazam: Obrt značenja koji često ruši leksikonske metode.

Domensko prilagođavanje: Prilagodba rječnika/modela specifičnoj temi.

Višejezični sentiment: Analiza na više jezika; problem rječnika i morfologije.

Valencija: Intenzitet emocije/polariteta.

Emocije: Specifične kategorije (npr. radost, strah) umjesto samo polariteta.

Subjektivnost: Koliko tekst izražava osobni stav vs. činjenice.

Ograničenja leksikona: Nepokrivenost, polisemičnost, kontekst, negacije.

Interpretacija rezultata: Prevođenje metrika u zaključke uz oprez.

Sentiment u mrežama: Povezivanje sentimenta s odnosima.




Pitanja za ponavljanje

Odaberite sve točne odgovore.

  1. Ako želite provesti leksikonsku sentiment analizu korpusa web-stranica kompanija, koji je od ponuđenih koraka najprimjereniji kao prvi analitički korak?
  1. Tokenizirati tekst u pojedinačne riječi i ukloniti stop riječi
  2. Izgraditi neuronsku mrežu za klasifikaciju sentimenta
  3. Izračunati centralnost čvorova u mreži komunikacije
  4. Prevesti sve tekstove u numeričke vektore latentnih tema
  5. Konstruirati regresijski model za procjenu sentimenta
  1. U analizi teksta želite usporediti rezultate više leksikona (Bing, NRC i AFINN). Koji je najprikladniji metodološki pristup?
  1. Izračunati korelaciju između rezultata dobivenih različitim leksikonima
  2. Odbaciti sve leksikone osim onoga s najvećim rječnikom
  3. Usporediti relativni poredak dokumenata prema sentiment scoreu
  4. Zamijeniti leksikonsku analizu dubokim neuronskim modelom
  5. Promatrati slažu li se leksikoni u smjeru sentimenta
  1. Tijekom čišćenja teksta pojavljuju se znakovi poput â ili Ã. Koji je najvjerojatniji uzrok takvih znakova?
  1. Problem kodiranja znakova tijekom dohvaćanja ili spremanja teksta
  2. Pogrešno izračunat sentiment score u analizi
  3. Pogreška u algoritmu za klasteriranje podataka
  4. Posljedica preagresivnog uklanjanja stop riječi
  5. Normalna posljedica tokenizacije teksta
  1. Ako u sentiment analizi koristite Bing leksikon, kako se određuje sentiment pojedine riječi?
  1. Riječ se klasificira kao pozitivna ili negativna bez intenziteta
  2. Riječ dobiva numeričku težinu između −5 i +5
  3. Riječ se raspoređuje u osam emocionalnih kategorija
  4. Riječ dobiva latentni vektor u semantičkom prostoru
  5. Riječ se klasificira pomoću treniranog modela
  1. U AFINN leksikonu svaka riječ ima numeričku vrijednost sentimenta. Kako se obično izračunava ukupni sentiment dokumenta?
  1. Zbrajanjem svih sentiment vrijednosti riječi u dokumentu
  2. Prosjekom svih riječi koje imaju sentiment oznaku
  3. Brojanjem svih pozitivnih i negativnih riječi
  4. Računanjem medijana sentiment vrijednosti riječi
  5. Usporedbom sentimenta s referentnim dokumentom
  1. Ako želite grupirati kompanije prema sličnosti njihovih emocionalnih profila dobivenih iz NRC leksikona, koji su od ponuđenih pristupa prikladni?
  1. Primijeniti k-means klasteriranje na matricu emocija
  2. Izračunati PageRank centralnost kompanija
  3. Izračunati TF-IDF vrijednosti za svaku riječ
  4. Primijeniti regresijski model s emocijama kao varijablama
  5. Primijeniti hijerarhijsko klasteriranje nad dokumentima
  1. Prije klasteriranja emocionalnih profila često se provodi skaliranje varijabli. Zašto?
  1. Kako bi sve emocije imale usporediv raspon vrijednosti
  2. Kako bi se povećao broj emocija u analizi
  3. Kako bi se uklonile rijetke riječi iz korpusa
  4. Kako bi se smanjila dimenzionalnost podataka
  5. Kako bi se eliminirale negativne vrijednosti
  1. Ako želite identificirati latentne teme u velikom korpusu tekstova bez prethodnih oznaka, koju biste metodu koristili?
  1. Latentnu Dirichletovu alokaciju (LDA)
  2. Naivni Bayes klasifikator
  3. Logističku regresiju
  4. Support Vector Machine klasifikator
  5. Random Forest klasifikator
  1. U analizi mreže komunikacije želite pridružiti emocionalni ton svakoj vezi između aktera. Koji je najprikladniji postupak?
  1. Izračunati sentiment poruka koje prolaze između dvaju aktera
  2. Izračunati stupanj centralnosti svakog čvora
  3. Izračunati gustoću mreže komunikacije
  4. Konstruirati regresijski model nad mrežom
  5. Izračunati klaster koeficijent mreže
  1. Ako želite provjeriti slažu li se različiti leksikoni u procjeni sentimenta dokumenata, što biste mogli napraviti?
  1. Izračunati korelaciju između sentiment scoreova leksikona
  2. Izračunati udaljenost između dokumenta i leksikona
  3. Izračunati broj tokena u svakom dokumentu
  4. Izračunati broj stop riječi u dokumentima
  5. Izračunati prosječnu duljinu riječi
  1. Ako Bing i AFINN leksikon za isti dokument daju negativan sentiment, a NRC pozitivan, što je najvjerojatnije objašnjenje?
  1. Različiti leksikoni koriste različite skupove riječi
  2. NRC uključuje šire kategorije emocija
  3. Dokument je pogrešno tokeniziran
  4. Tekst ne sadrži riječi iz leksikona
  5. Analiza je nužno pogrešno provedena
  1. Ako različiti leksikoni daju vrlo sličan poredak kompanija prema sentimentu, što to sugerira?
  1. Emocionalni ton tekstova stabilno se prepoznaje među metodama
  2. Analiza sentimenta ne ovisi o izboru leksikona
  3. Tekstovi su potpuno identični među kompanijama
  4. Metoda sentiment analize nije potrebna
  5. Klasteriranje neće imati smisla
  1. Ako je korelacija između sentiment rezultata dvaju leksikona vrlo visoka (npr. 0.95), što to znači?
  1. Metode prepoznaju vrlo sličan obrazac sentimenta u tekstovima
  2. Apsolutne vrijednosti sentimenta moraju biti identične
  3. Leksikoni koriste potpuno isti rječnik
  4. Analiza sentimenta nije potrebna
  5. Tokenizacija je provedena bez pogrešaka
  1. Ako kompanija ima mnogo riječi povezanih s povjerenjem, anticipacijom i radošću, kako biste opisali njezin emocionalni profil?
  1. Profil naglašava optimističan i afirmativan komunikacijski ton
  2. Profil naglašava neutralan i informativan komunikacijski ton
  3. Profil naglašava konfliktan i kritičan komunikacijski ton
  4. Profil naglašava nesigurnost i zabrinutost
  5. Profil naglašava regulatorni i administrativni stil
  1. Ako su razlike u apsolutnim sentiment scoreovima među leksikonima velike, ali je smjer sentimenta isti, kako to interpretirati?
  1. Rezultati su metodološki konzistentni unatoč razlikama u skali
  2. Analiza sentimenta nije pouzdana
  3. Tekstovi su pogrešno tokenizirani
  4. Leksikoni se ne mogu uspoređivati
  5. Analiza treba biti potpuno ponovljena
  1. Ako klaster analiza emocionalnih profila grupira više kompanija u isti klaster, što to znači?
  1. Kompanije imaju sličan emocionalni obrazac komunikacije
  2. Kompanije imaju identičan poslovni model
  3. Kompanije imaju istu tržišnu strategiju
  4. Kompanije koriste isti rječnik na web-stranicama
  5. Kompanije imaju identičnu duljinu tekstova
  1. Ako jedna kompanija ima izrazito više pozitivnih riječi od ostalih, kako to interpretirati?
  1. Komunikacija kompanije naglašava pozitivne vrijednosti i postignuća
  2. Tekst kompanije je nužno dulji od svih ostalih
  3. Kompanija ima više proizvoda na tržištu
  4. Kompanija koristi više tehničkih izraza
  5. Kompanija ima složeniju organizacijsku strukturu
  1. Ako u analizi emocija jedna kompanija ima relativno visoke vrijednosti i pozitivnih i negativnih emocija, što to može značiti?
  1. Tekst sadrži raznolike emocionalne teme
  2. Komunikacija uključuje različite narativne elemente
  3. Dokument ima vrlo mali broj riječi
  4. Leksikon nije prikladan za analizu
  5. Tekst je pogrešno tokeniziran
  1. Ako LDA analiza otkrije temu u kojoj dominiraju riječi poput “company”, “group” i naziv kompanije, što to najvjerojatnije znači?
  1. Tema opisuje organizacijski identitet i predstavljanje kompanije
  2. Tema opisuje tehnološke inovacije kompanije
  3. Tema opisuje tržišnu konkurenciju
  4. Tema opisuje regulatorni okvir poslovanja
  5. Tema opisuje financijske rezultate kompanije
  1. Ako se sentiment poruka u mreži komunikacije koristi kao atribut veze, što to omogućuje analizirati?
  1. Širenje pozitivnih i negativnih emocija kroz mrežu
  2. Emocionalne zajednice u mreži komunikacije
  3. Geografski raspored čvorova mreže
  4. Broj zaposlenika u organizaciji
  5. Prosječnu duljinu poruka



Korištena literatura

Bollen, J., Mao, H., & Zeng, X. (2011). Twitter mood predicts the stock market. Journal of Computational Science, 2(1), 1-8. https://doi.org/10.1016/j.jocs.2010.12.007

Csárdi, G., & Nepusz, T. (2006). The igraph software package for complex network research. Semantic Scholar.

Grün, B., & Hornik, K. (2011). topicmodels: An R package for fitting topic models. Journal of Statistical Software, 40(13), 1–30.

Kassambara A. & Mundt F. (2026). factoextra: Extract and Visualize the Results of Multivariate Data Analyses. R package version 2.0.0.999. With contributions from Laszlo Erdey (Faculty of Economics and Business, University of Debrecen, Hungary), https://CRAN.R-project.org/package=factoextra.

Kostelić, K., & Gonan Božac, M. (2026). Corporate Self-Representation on Official Websites: Strategic Signifiers and Sentiment Profiles. Administrative Sciences, 16(3), 140. https://doi.org/10.3390/admsci16030140

Kwartler, T. (2017). Text mining in practice with R. Wiley.

Mohammad, S.M. and Turney, P.D. (2013), CROWDSOURCING A WORD–EMOTION ASSOCIATION LEXICON. Computational Intelligence, 29: 436-465. https://doi.org/10.1111/j.1467-8640.2012.00460.x

Nielsen, F. Å. (2011). A new ANEW: Evaluation of a word list for sentiment analysis in microblogs. Proceedings of the ESWC2011 Workshop on ‘Making Sense of Microposts’: Big things come in small packages, 93-98. https://doi.org/10.48550/arXiv.1103.2903

Pedersen, T. L. (2023). ggraph: An implementation of grammar of graphics for graphs and networks. https://CRAN.R-project.org/package=ggraph

Silge, J. & Robinson, D. (2016). tidytext: Text Mining and Analysis Using Tidy Data Principles in R. Journal of Open Source Software, 1(3), 37, doi:10.21105/joss.00037

Silge, J. & Robinson, D. (2025). Text Mining with R. https://www.tidytextmining.com/

Wickham et al. (2019). Welcome to the Tidyverse. Journal of Open Source Software, 4(43), 1686. https://doi.org/10.21105/joss.01686

Wickham, H. (2016). ggplot2: Elegant graphics for data analysis. Springer.




Ključ odgovora

  1. a

  2. a, c, e

  3. a

  4. a

  5. a

  6. a, e

  7. a

  8. a

  9. a

  10. a

  11. a, b

  12. a

  13. a

  14. a

  15. a

  16. a

  17. a

  18. a, b

  19. a

  20. a, b