Анализа текстуалних података - припрема књиге за анализу¶
У овој радној свесци настављамо са преузимањем и припремом текстуалних података тако да можемо да се бавимо анализом како смо и до сада навикли. Велика база књига које су бесплатне и доступне онлајн у txt формату налази се на платформи пројекта Гутенберг, и ми ћемо у наставку преузети и припремити податке за анализу по књизи Ана Карењина.
Као и радна свеска са преузимањем текстова са веб сајта и ова превазилази оквире планираних тема у домену обраде података и није неопходна за разумевање садржаја приказаних у свесци која обрађује податке о књизи Ана Карењина. Свеска је остављена овде у случају да вас занима како се дошло до података који се обрађују у централној радној свесци или да послужи за инспирацију за неко даље анализирање неких других књига.
import pandas as pd
import numpy as np
import re
import string
Текстуални фајл преузет са платформе Гутенберг пројекта (https://www.gutenberg.org/ebooks/1661) налази се у фолдеру са подацима и учитаћемо га комплетног. Користићемо функцију open да отворимо фајл, readlines да прочитамо све линије текста из фајла, након чега ћемо затворити фајл close:
f = open('data/tekst data/Anna.txt', 'r', encoding='UTF-8')
lines = f.readlines()
f.close()
Прегледамо првих 5 линија текста да кренемо у упознавање са подацима које смо преузели:
lines[:5]
Уочвавамо да се свака линија завшава ознаком за нови ред '\n' али и да има елемената листе који не садрже ништа више од тога, њих ћемо одмах уклонити:
lines=[l for l in lines if l!='\n']
Уочавамо такође и да почетак фајла садржи податке о доступности књиге, верзијама и сличне техничке информације које нису део текста који желимо да анализирамо. Погледајмо мало више линија текста да детектујемо у којој линији креће текст књиге:
lines[:40]
Први елемент од интереса је наслов књиге. Користећи index можемо издвојити индекс елемента листе који садржи наслов:
lines.index(' ANNA KARENINA \n')
Док за почетак књиге можемо прескочити садржај и кренути од почетка првог дела:
pocetak = lines.index('PART ONE\n')
pocetak
Слично као што је почетак фајла означен информацијама о пројекту Гутенберг, постоји и ознака за крај текста, након које следе детаљи о пројекту, правима и слично. Последњу линију од интереса налазимо на следећи начин:
kraj = lines.index('End of The Project Gutenberg Etext of Anna Karenina by Leo Tolstoy\n')
kraj
knjiga = lines[pocetak:kraj]
Као део припреме текста у наставку ће нам бити потрбна и функција за уклањање знака интерпункције:
znaci_interpunkcije = string.punctuation + '”“’‘—'
def ukloni_znake_interpunkcije(text):
for ch in znaci_interpunkcije:
text=text.replace(ch,' ')
return text
Раздвајање листе редова на делове и поглавља:¶
У нашем кратком прегледу текста уочили смо да сваком делу књиге претходи линија која садржи стринг "PART", као и да поглавља садрже стринг "Chapter", што ћемо искористити за идентификацију одговарајућих сегмената текста у наставку:
partlens = [] # листа у којој ћемо сачувати број полавља у оквиру сваког дела књиге
chapters = [] # листа у којој ћемо сачувати текстове свих поглавља
tempchapter = ' ' # овде ћемо чувати садржај поглавља, додајући ред по ред
i = 0
for line in knjiga:
if 'PART' in line: # провера да ли је разматрана линија почетак новог дела књиге
if i>0: # бројач i расте са сваким новим поглављем, тако да изузев у првмом делу књиге овај услов ће бити задовољен
partlens.append(i) # у листу partlens додајемо укупан број поглавља у окивру претходног дела књиге
i = 0 # ресетујемо бројач
elif 'Chapter' in line: # провера да ли је у питању почетак новог поглавља
i += 1
if (tempchapter != ' '): # ако текст поглавља није празан (што је тачно за све осим за прво поглавље)
chapters.append(tempchapter) # додајемо текст поглавља у листу
tempchapter = ' ' # ресетујемо променљиву у којој чувамо текст поглавља
else: # нова линија текста која није наслов поглавља или дела и треба је додати у текст поглавља
templine = line.replace('\n',' ')# у наредна два реда изацујемо \n и \t који су нам непотребни за даљу анализу
templine = templine.replace('\t',' ')
tempchapter += templine # додавање очишћене линије у текст поглавља
chapters.append(tempchapter) # додавање текста последњег поглавља у листу
partlens.append(i) # додавање броја поглавља последњег дела у листу
Кратка провера да смо претходним делом кода урадили шта смо желели је да проверимо дужину листе поглавља, као и суму дужина делова књиге:
(len(chapters),sum(partlens))
chapters = [ukloni_znake_interpunkcije(chapter) for chapter in chapters]
Детекција јунака¶
У овом делу наш циљ је да препознамо ко су јунаци у тексту и да припремимо сет података о броју појављивања сваког од јунака не бисмо ли успели да само на основу анализе текста наслутимо о коме је реч у тексту. Томе ћемо приступити користећи чињеницу да се властита имена пишу великим почетним словом. За почетак, спојићемо целокупан текст и од њега направити листу речи:
razmak=' '
ceotekst = razmak.join(chapters)
ceotekst[:100]
Дужина текста мерена у карактерима:
len(ceotekst)
До листе појединачних речи доћи ћемо користећи функцију split(), поређења ради, излистаћемо и првих 10 речи:
reci = ceotekst.split()
reci[:10]
Сачуваћемо и листу речи као још један једноставан сет информација о овој књизи.
reci = [rec for rec in reci if rec.isalpha()]
pd.DataFrame(reci,columns={'Rec'}).to_csv('Ana_df.csv',index=False)
Међутим, у листи речи коју смо овако направили, пуно речи се понавља (што ће бити део анализе у следећој радној свесци) те ћемо припремити и једну листу јединствених речи из овог текста:
jedinstvenereci = list(set(reci))
jedinstvenereci[:10]
Да бисмо из листе јединствених речи издвојили потенцијалне кандидате за јунаке, користићемо се регуларним изразима. Правилност (енг. pattern) који ћемо тражити је реч почиње великим словом [A-Z] а затим је прати једно или више малих слова [a-z]+ (плус је ту да означи да очекујемо 1 или више слова из угласте заграде).
О регуларним изразима можете прочитати више овде, али суштински, када дефинишемо pattern само филтрирамо листу да добијемо речи које одговарају нашим захтевима:
pattern = re.compile(r'\b[A-Z][a-z]+\b')
potencijalni_junaci = list(filter(pattern.search, jedinstvenereci))
У листи potencijalni_junaci сада се налазе сви кандидати за име јунака, можемо видети колико их је и осмотрити првих 10:
len(potencijalni_junaci)
potencijalni_junaci[:10]
Међу ових 10 речи, видимо неке речи које нису властита имена. То је зато што наше правило описује и све речи које се налазе на почетку реченице! Међутим, када се нека честа реч нашла на почетку реченице, очекујемо да се иста реч појављује у тексту и на неким другим местима у реченици, па ће се у листи јединствених речи наћи и у верзији исписа малим словима. То није ситуација у којој се може наћи властито име, па ћемо пробати да на тај начин умањимо листу кандидата за ликове. На пример, из претходне листе речи, можете одабрати неку од речи и да испитате да ли се она у тексту налази и када је цела написана малим словима:
i = 2
potencijalni_junaci[i]
potencijalni_junaci[i].lower() in jedinstvenereci
Сада ћемо овај метод применити на целу листу речи потенцијалних јунака, чувајући само речи које се не налазе у остатку текста написане малим словима:
potencijalni_junaci = [rec for rec in potencijalni_junaci if rec.lower() not in jedinstvenereci]
Нова дужина листе је:
len(potencijalni_junaci)
Успели смо да преполовимо листу потенцијалних јунака овим једноставним резоновањем!
potencijalni_junaci[:10]
Међутим и даље нису све речи у нашој потенцијалној листи заиста јунаци, стога ћемо осмислити још пар начина да смањимо листу речи које ћемо пратити на даље. На пример, сетићемо се да се на енглеском и називи дана и називи месеци пишу првим великим словом, тако да очекујемо да су се и они нашли у листи наших потенцијалних јунака:
days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
months = ['January','February','March','April','May','June','July','August','September','October','November','December']
months[1] in potencijalni_junaci
Cа друге стране, нису ни сви ликови једнако битни, па је један од начина да усмеримо нашу пажњу само на ликове који се појављују више пута у текст. Погледајмо за почетак како изгледа број понављања различитих потенцијалних јунака у целом тексту.
Сада ћемо ипак претраживати целокупан текст књиге исписан малим словима (и имена потенцијалних јунака ћемо исписивати на исти начин) за случај да се неки од издвојених јунака помиње у књизи и у комбинацији када су сва слова велика (нпр. неки узвик).
zastupljenostjunaka=dict()
reci_malaslova = [rec.lower() for rec in reci]
for junak in potencijalni_junaci:
zastupljenostjunaka[junak]=reci_malaslova.count(junak.lower())
Да испитамо исход ове процедуре, сортираћемо и погледати првих 20 фреквентних потенцијалних јунака:
sorted_counts = sorted(list(zastupljenostjunaka.items()), key=lambda x:x[1], reverse=True)
for word, count in sorted_counts[:20]:
print(word, count)
Уколико сте читали књигу, сада вас сигурно радује чињеница да смо дошли до главних јунака (чак и локација на којима се радња дешава) без пуно муке!
У посебну листу убацити само она имена која се у целој књизи појављују 5 или мање пута и елиминисаћемо их из наставка анализе. Ово је параметар који можемо мењати и последично мењати дужину листе потенцијалних ликова.
rarenames = [junak for junak in zastupljenostjunaka.keys() if zastupljenostjunaka[junak]<=5]
len(rarenames)
Има чак 643 јунака који се појављују само пар пута у књизи, што је згодно пошто ћемо фокусирати анализу на пар десетина најактивнијих јунака. Дакле, речи које желимо да уклонимо из листе потенцијалних јунака су називи дана и месеци, као и листу ретко помињаних имена:
wordstoremove = rarenames + days + months
potencijalni_junaci = [junak for junak in potencijalni_junaci if junak not in wordstoremove]
len(potencijalni_junaci)
Сачуваћемо сада и нову верзију речника заступљености јунака, која садржи само изабране јунаке које ћемо пратити у наставку:
zastupljenostjunaka = {junak: zastupljenostjunaka[junak] for junak in potencijalni_junaci}
Направити табеле јунака и њиховог укупног броја појављивања (именом) у тексту:
junaci_df = pd.DataFrame.from_dict(zastupljenostjunaka,orient='index')
junaci_df.head()
junaci_df = junaci_df.rename(columns={0:'Broj pojavljivanja'})
junaci_df.head(2)
junaci_df = junaci_df.reset_index()
junaci_df = junaci_df.rename(columns={'index':'Ime'})
#junaci_df.to_csv('Ana_likovi_ukupan_br_pojavljivanja.csv')
Јунаци у различитим поглављима¶
Да би смо могли у даљој анализи да пратимо да ли су исти ликови популарни све време, или се неки јунаци појављују епизодно, или повремено, забележићемо на следећи начин и број понављања имена сваког од јунака у сваком поглављу појединачно, као и позицију у оквиру поглавља у којој је дати јунак поменут (ово ћемо у будућности користити као меру блискости два јунака - за јунаке који се помињу у истој реченици ћемо претпоставити да су блиски за разлику од јунака који се помињу на различитим странама).
lista_junaka_malaslova = [junak.lower() for junak in potencijalni_junaci]
pojavljivanja_junaka = []
for chapter in chapters:
pojavljivanja_temp = []
reciupoglavlju = ukloni_znake_interpunkcije(chapter).lower().split()
for junak in lista_junaka_malaslova:
pojavljivanja_temp.append(reciupoglavlju.count(junak))
pojavljivanja_junaka.append(pojavljivanja_temp)
Користећи ове податке, направићемо одговарајућу табелу:
pojavljivanj_df = pd.DataFrame(pojavljivanja_junaka,columns=list(junaci_df.Ime))
pojavljivanj_df.head()
У овој табели свака колона одговара једном јунаку, док различити редови садрже информације о броју појављивања сваког од јунака у датом поглављу.
Сачуваћемо ову табелу у csv фајлу како бисмо јој лакше приступали и анализирали даље овај текст.
pojavljivanj_df.to_csv('Ana_likovi_pojavljivanja.csv',index=False)
А сада ћемо направити табелу у којој се налазе сва имена јунака и локације у тексту на којима се појављују:
junak = []
pozicija = []
for i in range(len(reci_malaslova)):
if reci_malaslova[i] in lista_junaka_malaslova:
junak.append(reci_malaslova[i])
pozicija.append(i)
pozicije_junaka = pd.DataFrame()
pozicije_junaka['Ime junaka'] = junak
pozicije_junaka['Pozicija junaka'] = pozicija
pozicije_junaka.head()
Kако смо у овом процесу све преписали малим словима - овде ћемо то исправити користећи функцију capitalize:
pozicije_junaka['Ime junaka'] = pozicije_junaka['Ime junaka'].apply(str.capitalize)
pozicije_junaka.head(2)
Коначно сачуваћемо ове податке за даљу анализу:
pozicije_junaka.to_csv('Ana_pozicije_junaka.csv',index=False)
У овој радној свесци:
- учитали смо и пречистили текст књиге
- користили смо регуларне изразе да издвојимо јунаке
- избројали смо појављивања јунака и позиције појављивања у оквиру књиге.
Детекција ликова у тексту није једноставан задатак зато што су властита имена рановрсна, мењају се с временом и врло често могу бити у питању речи које имају своје значење, те се појављују у тексту и мимо именовања ликова. Међутим, када нам није потребна изузетна прецизност, једноставне процедуре попут ових приказаних могу бити довољне. Данас, задатак детекције јунака је најчешће намењен алгоритмима заснованим на машинском учењу за које је потребно припремити изузетно добре тренинг сетове у којима су детекцију ликова урадили људи.
Задатак¶
- Одлука да се фокусирамо само на ликове који се појављују више од 5 пута у тексту је помоћна процена. Са једне стране, желимо да смањимо скуп ликова које ћемо анализирати у наставку, са друге стране не желимо да изгубимо неке ликове који иако се појављују мало, могу играти битну улогу у повезивању јунака. Стога предлажемо да варирате број појављивања у тексту који користите као филтер и избројте колико потенцијалних јунака остаје у којој од опција. Нацртајте график броја потенцијалних јунака за различита филтрирања од 0 (без филтрирања по броју појављивања имена) до 10 (где елиминишете сва имена која су поменута мање од 10 пута). Можете сачувати и неке од табела које су настале овим другачијим филтрирањем да видите како се резултати у наредној радној свесци разликују када користите њих у односу на ове које смо припремили заједно.