Labádi Gergely </filológia>

Távoli olvasó továbbolvas

Mit és hogyan, eddig?

Az előző blogposzt egy egyszerű, gyorstalpaló bevezetés a distant reading-be gyakorlatába. Néhány példán keresztül bemutattam/bevezettem néhány alapeszközt: egy magyar nyelvre készített morfológiai elemzőt (magyarlanc), az R-t, egy statisztikai programot, ennek felhasználóbarátabb felületét az RStudio-t – és megemlítettem a Stylo-t is, ami az R-hez készített programcsomag. A példák egyszerűek voltak, leginkább néhány alapművelet, nem sok köze van a distant reading tényleges gyakorlatához, bár néhány dolog így is egyértelmű: iszonyú gyorsan lehet a szövegeket manipulálni. Most egy újabb ízelítő. Olyan műveletek, amelyek azért már közelebb vannak hozzá. Jockers ezeket az elemzéseket zömében microanalysisnek nevezi – megkülönböztetve még a mezo- és az macroanalysist.

Az előző blogposztban létrehozott változókat adottnak veszem. Amikor ki akarunk lépni az RStudio-ból, akkor mindig rákérdez, el akarjuk-e menteni. Mondjuk neki nyugodtan igent. Akkor legközelebb betölti az addigi munkánkat. A különböző munkákat érdemes külön-külön menteni, ehhez a legjobb külön projektek indítása/mentése – értelemszerűen a File menüből.

1. További alapműveletek és egy kevés elemzés

1.1. Kisbetűsítés

És akkor megszakítom az adást: én eredetileg az .epub-verziót konvertáltam .txt formátumúra a Calibre-vel. Amikor a változók közti különbségek okát próbáltam megérteni, mellékesen rájöttem, hogy az egyik fejezet, Dódi levele hibás, egy másik fejezet szövegét is tartalmazza. Tehát ha az .epub alapján csinálta valaki – mint én – akkor érdemes mindent újracsinálni.

  • Ha megnézzük az elejét (emlékeztetőül: head(aranyember.v,n=3)), láthatjuk, hogy ez tényleg bekezdésenként tárolja a szöveget, valamint, hogy kis- és nagybetűk még megmaradtak, szemben a magyarlanc-cal végzett elemzés után behívott fájllal. Kisbetűsítsük tehát:
kisbetus.aranyember.v = tolower(aranyember.v)

1.2. Szógazdagság – egyedi szavak száma

  • Ennek lekérdezése már egy valódi elemzési szempont. Ti. nemcsak az lehet egy szöveggel kapcsolatban kérdés, hogy hány szóból (jelből) áll, hanem az is, hány egyedi kifejezésből, vagy az előző blogposzt terminológiája nyomán hány lemmából (jeltípusból).
    • Ennek rendes tudományos neve is van: Type-Token Ratio, vagy magyarul Antal László nyomán Bencziktől átvéve: típus–jel-viszony (a fordítottja pedig a jel–típus-viszony). Erről (ezekről) sokféle elképzelést olvashatunk, hogy ti. melyik milyen arányok jellemzők inkább a szóbeli, szóbeliséget imitáló szövegekre, a költői alkotásokra stb. Mindenki döntse el maga – az 1960-as és ‘70-es években sok ilyen szöveget írtak magyarul is (pl. Zsilka Tibor). Annyival viszont beljebb vagyunk, hogy gyorsabban tudjuk ezeket a vizsgálatokat elvégezni, illetve mivel megadjuk, melyik programot használjuk, ellenőrizhetők is vagyunk. És többféle szempontot lehet kombinálni (majd egy másik blogposztban).
    • Maga a parancs roppant egyszerű, az eredményeket el is tudjuk menteni. Ám előtte az aranyember.v változóval is végezzük el azt a feladatot, amelyet a magyarlanc-cal elemzett szöveggel is megtettünk (a > ismét az RStudio konzolján jelenik meg, a # pedig az én kommentárom, egyiket sem kell begépelni/bemásolni).
# mivel bekezdésenként tárolja, ezért előbb összemásoljuk a bekezdéseket
> ae.v = paste(aranyember.v, collapse = " ")
# kisbetűsítjük a változót
> ae.lower.v = tolower(ae.v)
# a kisbetűs változatból kivesszük az írásjeleket – az előző posztban ezt a parancsot használtam: `ae.lower.l = strsplit(ae.lower.v, "\\W")`, de ezután nagyon sokat kell még gépelni (kivenni az üres helyeket), egyszerűbb a következő parancs, amely az összes íásjelet kiejti, de a szavakat meghagyja
> ae.lower.l = strsplit(ae.lower.v, "(\\W+)")
# a listaformátumú változót visszaalakítjuk karaktervektorrá
> ae2.v = unlist(ae.lower.l)
# egyedi szavak
> egyedi.aranyember.v = unique(szavak.ae2.v)
# a `magyarlanc`-cal elemzett változó már eleve kisbetűs, ha a lemmával dolgozunk, de előtte távolítsuk el belőle az írásjelek üres helyét – ha az előző alkalommal nem tettük volna meg –, ezt most nem írom ide, ott úgyis elolvasható a metódus
> egyedi.szavak.v = unique(szavak.v)
  • Ha ellenőrizzük az elkészült két változó hosszúságát (length(egyedi.aranyember.v), length(egyedi.szavak.v)), akkor azt találjuk, hogy nem ugyanolyan hosszúak. Nagyon nem. Az okok banálisak.
    • A közvetlenül a szövegfájlból, előzetes elemzés nélkül behívott regényszöveg szavainak egyedisége a szóalak egyeediségét jelenti. Tehát a „kaput”, „kapunak” két külön alak, míg a magyarlanc-cal előkészített, és a csak lemma-oszlopot tartalmazó szöveg esetében ez egy alak, a „kapu”. De van más oka is.
    • Ha az írásjelek eltávolításának Jockersnél olvasható parancsát használtuk (ae.lower.l = strsplit(ae.lower.v, "\\W")), akkor azt mondtuk, minden nem-karakter írásjelnél kezdjen új elemet. De ez akkor is új elemet kezd, ha a kifejezés történetesen az „1.”, ti. „első”. Ez tehát eszerint a parancs szerint rögtön két elem, a magyarlanc-cal ellenőrizve viszont, helyesen, egy. (Nézzük meg: ` head(ae2.v, n=40) az összes írásjelnél üres elemet fogunk látni.) Ezt ugyan a fenti paranccsal (ae.lower.l = strsplit(ae.lower.v, “(\W+)”)) kikerültük, ti. a regex`-kifejezés azt jelenti, minden nem szónál kezdjen új elemet, de legalább az írásjelek nem lesznek külön elemek. Viszont maradt még problémás hely.
    • Az „Athalie-nak” ugyanis két elem lesz. Ugyanakkor látni kell, hogy a magyarlanc még nem képes korrekten kezelni a kötőjeles alakokat („törte-e”, „Athalie-nak”), ti. nem elemzi jól, bár egy szónak látja őket. Ezért érdemes megfontolni, hogy a szöveget előzetesen preparáljuk, és ezeket a kötőjeles alakokat eltávolítjuk („törte e”, „Athalienak” – én végül a „törte-e” típusú alakokat szétválasztottam, de a többit [pl. „egy-egy”] meghagytam).
  • Végül számoljuk ki a különböző TTR-arányokat!
# a lemmákkal számolt értékek
tjv.aranyember = length(egyedi.szavak.v)/length(szavak.v)
jtv.aranyember = length(szavak.v)/length(unique(szavak.v))
# a nyers regényszövegekből számolt értékek (a `valtozo.df`-ből nem a 2., hanem az 1. oszlopot exportáltam, majd írásjeltelenítettem)
tjv.regeny.aranyember = length(unique(regenyszavak.v))/length(regenyszavak.v)
jtv.regeny.aranyember = length(regenyszavak.v)/length(unique(regenyszavak.v))
  • Ha ellenőrizzük az egyes értékeket (kiíratjuk magát a változót a változó nevének begépelésével vagy az RStudio environment fülére kattintva megnézzük az értékeket), akkor láthatjuk, hogy jelentősen eltérnek. Ha felhasználjuk az adatokat, akkor érdemes pontosan jelezni, mit és hogyan számoltunk. Esetleg miért.

1.3. Szókeresés

  • Egy szót megkeresni roppant egyszerű… Egy sima szövegfájlban – legyen bármilyen formátumban is – a szókeresés alapvetően egy karaktersor keresését jelenti, tehát az „arannyal”, „aranyat” és az „Aranyban” nem feltétlenül jelenik meg az „arany” keresésére. A kisbetű–nagybetű probléma persze könnyen orvosolható, de az „arannyal” problémája már nem. Hiszen ha rövidítem, hogy csak „aran”-ra keressen és ne teljes szavakban, akkor a „parancsol” is megjelenhet, többek között. Két megoldást mutatok, R-ben hogyan keressünk (nyilván: lehetne még máshogy is).
arany.v = grep("arany", szavak.v)
arany2.v = which(szavak.v=="arany")
  • A legnagyobb problémát már kiküszöböltük azzal, hogy a magyarlanc-cal végzett elemzés szövegét használjuk kiindulópontnak, ti. a ragozott alakokat is megtaláljuk, hiszen a lemma szerepel a szövegünkben. Ugyanakkor van különbség a kettő között, mivel a which paranccsal pontosan ezt a karaktersort keresi meg a program, a grep esetében azonban már a szó belsejében is keres. Így például megtalálja az „aranysárgá”-t is. Hogy melyikre van szükségünk, döntse el mindenki maga.

1.4. Szóeloszlás

  • Nézzünk egy egyszerű szóeloszlási példát. Az arany emberből vegyük az elemzések néhány sokat emlegetett elemét, motívumát – kinek mi tetszik –, az „arany” és a „[vörös] hold” kifejezéseket.
    1. Hozzunk létre egy változót, amely olyan hosszú, mint maga a regény (a regény hosszúság az alábbi példában csak a regény szavainak egymásutánját jelenti, tehát az írásjeleket kihagytam – persze dönthetünk máshogy, de akkor ne a szavak.v, hanem a szoveg.v változó legyen az alap)!
      regenyido.v = seq(1:length(szavak.v))
      
    2. Keressük meg az „arany”, „hold”, „üstökös” kifejezéseket a regényben!
      arany.v = grep("arany", szavak.v)
      hold.v = grep("hold", szavak.v)
      voros.v = grep("vörös", szavak.v)
      
    3. Hozzunk létre egy-egy változót, amely olyan hosszú, mint a regény, nem ugyanaz, mint a regenyido.v, és a helyek üres elemként jelennek meg (NA)!
      a.count.v = rep(NA, length(szavak.v))
      h.count.v = rep(NA, length(szavak.v))
      v.count.v = rep(NA, length(szavak.v))
      
    4. Ezután azok a helyek, ahol a korábbiak szerint az „arany”, a „hold” és a „vörös” kifejezések előfordulnak, kapja meg az 1 értéket!
      a.count.v[arany.v] = 1
      h.count.v[hold.v] = 1
      v.count.v[voros.v] = 1
      
    5. És akkor végre ábrázoljuk egy-egy grafikonon a találatokat! A vízszintes tengely a regényidő, azaz ahogy múlnak a szavak…, a függőleges tengelynek mindössze két értéke van, 0 és 1, ha a keresett érték („arany” stb.) előfordul az adott szóban vagy maga az adott szó, akkor annak helyén van vonal, ha nem, akkor nincs.
plot(a.count.v, main="Az 'arany' eloszlása Az arany emberben",xlab="Regényidő", ylab="arany", type="h", ylim=c(0,1), yaxt='n')
plot(h.count.v, main="A 'hold' eloszlása Az arany emberben", xlab="Regényidő", ylab="hold", type="h", ylim=c(0,1), yaxt='n')
plot(v.count.v, main="A 'vörös' eloszlása Az arany emberben", xlab="Regényidő", ylab="vörös", type="h", ylim=c(0,1), yaxt='n')
  • És így fognak kinézni: arany_plot hold_plot voros_plot
  • Természetesen lehetőségünk van szó- vagy betűkapcsolatokra is keresni, valamint azokat hasonló módon megjeleníteni.

1.5. Korreláció

  • A fenti szóeloszlás ugyan jól mutat, esetenként néhány következtetést már ennyiből is le lehet vonni, de adódik a kérdés, van-e valódi összefüggés a kifejezések felbukkanása között.
    • Ehhez viszont a regényidő ilyen felosztása/felfogása nem megfelelő. Jockers az általa vizsgált regények esetében a fejezetenkénti ellenőrzést javasolja. A fejezetek automatikus megkerestetése az angolszász regényhagyományban némileg egyszerűbb, mert ott a fejezetek rendszerint úgy kezdődnek, hogy „Chapter X.”, ahol „X” értelemszerűen a fejezet száma. A magyar regényhagyományban ha egyáltalán valahogy, akkor számokkal jelölik. Az arany ember esetében legalább ennyi segítségünk van. Két megoldást találtam ki – talán több is van –, mindkettőt bemutatom.

    A) Az első jobban követi Jockerst, de előtte preparálni kell a .txt fájlt. Minden fejezet/rész – ki mit akar elemezni – elejére be kell szúrni egy olyan karaktersort, amely egyébként nem fordul elő a regényben (én a fejezetek esetén az „FFF”-et, a részek esetén az „RRR”-t használtam). Ezután kell beolvastatni a regény szövegét, a példában egy text.v nevű változóba történt a beolvasás (ezt a már leírt scan paranccsal, vagy a readLines("aranyember_kritikai.txt", encoding = "UTF-8") paranccsal kell megtenni). Mindezek után edig következnek a következő parancsok – magyarázatuk a kódrészlet után.

chap.positions.v <- grep("FFF", text.v)

text.v <- c(text.v, "END")
last.position.v <- length(text.v)
chap.positions.v <- c(chap.positions.v , last.position.v)

chapter.raws.l <- list()
chapter.freqs.l <- list()

for(i in 1:length(chap.positions.v)){
if(i != length(chap.positions.v)){
chapter.title <- text.v[chap.positions.v[i]]
start <- chap.positions.v[i]+1
end <- chap.positions.v[i+1]-1
chapter.lines.v <- text.v[start:end]
chapter.words.v <- tolower(paste(chapter.lines.v, collapse=" "))
chapter.words.l <- strsplit(chapter.words.v, "\\W")
chapter.word.v <- unlist(chapter.words.l)
chapter.word.v <- chapter.word.v[which(chapter.word.v!="")]
chapter.freqs.t <- table(chapter.word.v)
chapter.raws.l[[chapter.title]] <- chapter.freqs.t
chapter.freqs.t.rel <- 100*(chapter.freqs.t/sum(chapter.freqs.t))
chapter.freqs.l[[chapter.title]] <- chapter.freqs.t.rel
}
}
  • A text.v változó bekezdésenként tárolja a regény szövegét (mindegy melyik parancsot használtuk). A grep paranccsal megkerestük és elmentettük azokat az elemeket, amelyek az „FFF” karaktersort tartalmazták. Némileg elaboráltabb ha a sima „FFF” helyett a következő regex-kifejezést használjuk Jockers nyomán: ^FFF \\d. Ekkor ugyanis azt mondjuk, hogy azoknak a bekezdéseknek a pozícióját jelöljük, amelyek „FFF”-fel kezdődnek, amely után egy számjegy áll. Ez persze a legutolsó fejezetnél, „Utóhangok…” éppenséggel nem fog működni, vagy legfeljebb ha még egy csillagot is teszünk a „d” mögé, amellyel azt jelenti a kifejezés, hogy „FFF”-fel kezdődik, és utána 0 vagy több számjegy következik – aminek viszont nincs sok értelme.
  • Az importált szöveget tartalmazó szövegfájl végére még egy elemet kell illeszteni, „END”, hogy az utolsó fejezetet is ki tudjuk jelölni, majd az utolsó fejezet végét is elmentjük. Végül egyetlen változóba mentjük az összes pozíciót, amelyre a fejezetek kijelölésénél szükség van.
  • Létrehozunk két üres listát, az egyik majd a szavak nyers listáját tartalmazza, a másik a szavak gyakorisági adatait.
  • Ezután a for-ral kezdődő parancsciklus végrehajt egy csomó parancsot, amíg a ciklus végére nem ér – jelen esetben meg kell keresnie az egyes fejezeteket, és minden egyes fejezeten végrehajtani a következő parancsokat:
    1. találja meg az egyes fejezetkezdő pozíciókat
    2. azt a bekezdést mentse el a fejezet címeként
    3. a fejezet a következő bekezdésnél kezdődik, mentse el ezt a pozíciót, és egészen addig tart, ameddig a következő fejezetpozíciót meg nem találja (pontosabban annál eggyel visszább van a vége), és mentse el a végét is
    4. a fejezet sorai a most elmentett pozíciók között vannak, ezeket kisbetűsítse, másolja össze őket, majd az írásjeleket vegye ki belőlük, egyúttal mentse egy listába a szavakat, majd a listából törölje az üres helyeket (ha emlékszünk, följebb mutattam egy másik parancsot, amellyel kiiktathatjuk az üres helyek törlésének plusz sorait [azokkal a változókkal idézem]: ae.lower.l = strsplit(ae.lower.v, "(\\W+)"))
    5. a most már csak a szavakat tartalmazó változóból (chapter.word.v) csináljon egy táblázatot és mentse el
    6. a nyers lista fejezetcíméhez rendelve mentse el a gyakorisági táblázatot
    7. a nyers gyakoriságot százalékos arányba fordítja át
  • Egy probléma van ezzel, nem a magyarlanc-cal elemzett listát használtuk, azaz csak a szóalakokról, és nem a típusokról/lemmákról tudunk meg adatokat. Természetesen ez is használható.

B) A másik módszer a fejezetek preparálására kissé nehézkes. A magyarlanc-os elemzést akarjuk használni. Ez viszont szavanként/írásjelenként menti el az adatokat, tehát nehéz például kiíratni a címet, mert nem tudjuk, hogy a megkeresett szám után hány további van, ráadásul a regényben is vannak számok („november 23-án”), azaz a fejezetek pozícióját nehéz megkeresni a lemmatizált alakoknál. Ezt ugyan áthidalhatjuk azzal, hogy a nyersszövegben, azaz a data.frame első oszlopánál keressük meg a fejezeteket kezdetét, hiszen az érték pont ugyanaz lesz, mint a lemmáknál.

chap.positions.v <- grep("(I\\.)|(II)|(IV)|(V\\.)|(VI)|(X)|(Utóhangok)", regeny.v)

# az eredményben ugyanakkor lesz néhány olyan elem, amelyet törölni kell (a „II. Rákóczi Ferenc” „II.”-ja, például). Ezt sajnos egyesével ellenőrizni kell – én megtettem.
x = c(31706, 44813, 72729, 154284, 179096)

# ezek után kivonjuk a törlendőket a `chap.positions.v`-ből, majd visszamentjük a jó helyeket (ezt lehet egyben is, de két részletre bontottam)
y = setdiff(chap.positions.v, x)
chap.positions.v = y

#És akkor megvannak a pozíciók, innentől kezdve mindent lehet ugyanúgy, mint feljebb (a `text.v <- c(text.v, "END")`-től).
  • A következő lépés a korrelációk vizsgálatához, hogy az egyes szavakra kérdezzük le az adatokat. Az egyes szavak („arany”, „hold”, „vörös”) előfordulását minden fejezetből megkerestetjük és elmentjük egy listába, majd mindegyik szónak létrehozunk egy táblázatot (valójában egy mátrix típusú változó).
arany.l <- lapply(chapter.freqs.l, '[', 'arany')
hold.l <- lapply(chapter.freqs.l, '[', 'hold')
voros.l <- lapply(chapter.freqs.l, '[', 'vörös')

arany.m = do.call(rbind, arany.l)
hold.m = do.call(rbind, hold.l)
voros.m = do.call(rbind, voros.l)

ah

  • Ezután a táblázatok közül a minket érdeklők közül összemásoljuk, ahol a szó nem fordul elő, ott az automatikusan beillesztett NA értékeket „0”-ra cseréljük, majd lekérdezzük, talál-e korrelációt.
colnames(ah.m) = c("arany", "hold")
ah.m[which(is.na(ah.m))] <- 0
mycor <- cor(ah.m[,"arany"], ah.m[,"hold"])
  • A korreláció értéke „-1” és „+1” között lehet. Ha az előbbi, akkor nagyon erős a korreláció, de fordított, azaz ha az egyik érték nő, akkor a másik ugyanolyan mértékben csökken, ha az utóbbi, akkor teljesen együttmozognak. Ha „+/-1” és „+/-0.5” közötti az érték, akkor erős a korreláció, ha „+/-0.1” és „+/-0.3” közötti, akkor gyenge a korreláció. Ha „0”, akkor semmilyen korreláció nincs. Az „arany” és a „hold” között „0.2222113”, ami nem túl magas érték, és azt jelenti, hogy gyenge pozitív kapcsolat van köztük, amikor megjelenik az egyik, akkor megjelenik a másik is, és nagyon távolról nézve, kb. hasonló arányban.
  • Az eredményt azonban lehet randomizáltan ellenőrizni, azaz az egyes fejezetek „arany”- és „hold”-értékeit véletlenszerűen egymás mellé rendelni, majd megnézni, akár 10000-szer, hogyan alakulnak az értékek, mert ekkor derül ki, hogy a tényleges eredmény mennyire egyedi, vagy ha úgy tetszik, „tudatos”.
cor.data.df <- as.data.frame(ah.m)
cor(sample(cor.data.df$arany), cor.data.df$hold)
# Futassuk le 10000-szer, és tároljuk el:
mycors.v <- NULL
for(i in 1:10000){
    mycors.v <- c(mycors.v, cor(sample(cor.data.df$arany), cor.data.df$hold))
    }

# jelenítsük meg (minimum- és maximumértékek, különböző átlagok)
h <- hist(mycors.v, breaks=100, col="grey", xlab="Korreláció-együtthatók", main="Véltelenszerű korreláció-együtthatók", plot=T)
    xfit <- seq(min(mycors.v),max(mycors.v),length=1000)
    yfit <- dnorm(xfit,mean=mean(mycors.v),sd=sd(mycors.v))
    yfit <- yfit*diff(h$mids[1:2])*length(mycors.v)
    lines(xfit, yfit, col="black", lwd=2)
  • Ebből kiderült, hogy a 0.22-es érték… Tessék megcsinálni és értelmezni.

Eddig tart Jockersnél a microanalysis. Egy csomó mindent még tanulni kell. Majd máskor.