$$ \newcommand{\floor}[1]{\left\lfloor{#1}\right\rfloor} \newcommand{\ceil}[1]{\left\lceil{#1}\right\rceil} \renewcommand{\mod}{\,\mathrm{mod}\,} \renewcommand{\div}{\,\mathrm{div}\,} \newcommand{\metar}{\,\mathrm{m}} \newcommand{\cm}{\,\mathrm{cm}} \newcommand{\dm}{\,\mathrm{dm}} \newcommand{\litar}{\,\mathrm{l}} \newcommand{\km}{\,\mathrm{km}} \newcommand{\s}{\,\mathrm{s}} \newcommand{\h}{\,\mathrm{h}} \newcommand{\minut}{\,\mathrm{min}} \newcommand{\kmh}{\,\mathrm{\frac{km}{h}}} \newcommand{\ms}{\,\mathrm{\frac{m}{s}}} \newcommand{\mss}{\,\mathrm{\frac{m}{s^2}}} \newcommand{\mmin}{\,\mathrm{\frac{m}{min}}} \newcommand{\smin}{\,\mathrm{\frac{s}{min}}} $$

Prijavi problem


Obeleži sve kategorije koje odgovaraju problemu

Još detalja - opišite nam problem


Uspešno ste prijavili problem!
Status problema i sve dodatne informacije možete pratiti klikom na link.
Nažalost nismo trenutno u mogućnosti da obradimo vaš zahtev.
Molimo vas da pokušate kasnije.

Анализа текстуалних података - припрема књиге за анализу

У овој радној свесци настављамо са преузимањем и припремом текстуалних података тако да можемо да се бавимо анализом како смо и до сада навикли. Велика база књига које су бесплатне и доступне онлајн у txt формату налази се на платформи пројекта Гутенберг, и ми ћемо у наставку преузети и припремити податке за анализу по књизи Ана Карењина.

Као и радна свеска са преузимањем текстова са веб сајта и ова превазилази оквире планираних тема у домену обраде података и није неопходна за разумевање садржаја приказаних у свесци која обрађује податке о књизи Ана Карењина. Свеска је остављена овде у случају да вас занима како се дошло до података који се обрађују у централној радној свесци или да послужи за инспирацију за неко даље анализирање неких других књига.

In [1]:
import pandas as pd
import numpy as np
import re
import string

Текстуални фајл преузет са платформе Гутенберг пројекта (https://www.gutenberg.org/ebooks/1661) налази се у фолдеру са подацима и учитаћемо га комплетног. Користићемо функцију open да отворимо фајл, readlines да прочитамо све линије текста из фајла, након чега ћемо затворити фајл close:

In [4]:
f = open('data/tekst data/Anna.txt', 'r', encoding='UTF-8')
lines = f.readlines()
f.close()

Прегледамо првих 5 линија текста да кренемо у упознавање са подацима које смо преузели:

In [3]:
lines[:5]
Out[3]:
['\ufeff\n',
 'The Project Gutenberg EBook of Anna Karenina, by Leo Tolstoy\n',
 '\n',
 'This eBook is for the use of anyone anywhere at no cost and with\n',
 'almost no restrictions whatsoever.  You may copy it, give it away or\n']

Уочвавамо да се свака линија завшава ознаком за нови ред '\n' али и да има елемената листе који не садрже ништа више од тога, њих ћемо одмах уклонити:

In [4]:
lines=[l for l in lines if l!='\n']

Уочавамо такође и да почетак фајла садржи податке о доступности књиге, верзијама и сличне техничке информације које нису део текста који желимо да анализирамо. Погледајмо мало више линија текста да детектујемо у којој линији креће текст књиге:

In [5]:
lines[:40]
Out[5]:
['\ufeff\n',
 'The Project Gutenberg EBook of Anna Karenina, by Leo Tolstoy\n',
 'This eBook is for the use of anyone anywhere at no cost and with\n',
 'almost no restrictions whatsoever.  You may copy it, give it away or\n',
 're-use it under the terms of the Project Gutenberg License included\n',
 'with this eBook or online at www.gutenberg.org\n',
 'Title: Anna Karenina\n',
 'Author: Leo Tolstoy\n',
 'Release Date: July 01, 1998 [EBook #1399]\n',
 'Last Updated: July 28, 2019\n',
 'Language: English\n',
 'Character set encoding: UTF-8\n',
 '*** START OF THIS PROJECT GUTENBERG EBOOK ANNA KARENINA ***\n',
 'Produced by David Brannan, Andrew Sly and David Widger.\n',
 ' ANNA KARENINA \n',
 ' by Leo Tolstoy \n',
 ' Translated by Constance Garnett \n',
 'Contents\n',
 ' PART ONE\n',
 ' PART TWO\n',
 ' PART THREE\n',
 ' PART FOUR\n',
 ' PART FIVE\n',
 ' PART SIX\n',
 ' PART SEVEN\n',
 ' PART EIGHT\n',
 'PART ONE\n',
 'Chapter 1\n',
 'Happy families are all alike; every unhappy family is unhappy in its\n',
 'own way.\n',
 'Everything was in confusion in the Oblonskys’ house. The wife had\n',
 'discovered that the husband was carrying on an intrigue with a French\n',
 'girl, who had been a governess in their family, and she had announced\n',
 'to her husband that she could not go on living in the same house with\n',
 'him. This position of affairs had now lasted three days, and not only\n',
 'the husband and wife themselves, but all the members of their family\n',
 'and household, were painfully conscious of it. Every person in the\n',
 'house felt that there was no sense in their living together, and that\n',
 'the stray people brought together by chance in any inn had more in\n',
 'common with one another than they, the members of the family and\n']

Први елемент од интереса је наслов књиге. Користећи index можемо издвојити индекс елемента листе који садржи наслов:

In [6]:
lines.index(' ANNA KARENINA \n')
Out[6]:
14

Док за почетак књиге можемо прескочити садржај и кренути од почетка првог дела:

In [7]:
pocetak = lines.index('PART ONE\n')
pocetak
Out[7]:
26

Слично као што је почетак фајла означен информацијама о пројекту Гутенберг, постоји и ознака за крај текста, након које следе детаљи о пројекту, правима и слично. Последњу линију од интереса налазимо на следећи начин:

In [8]:
kraj = lines.index('End of The Project Gutenberg Etext of Anna Karenina by Leo Tolstoy\n')
kraj
Out[8]:
31877
In [9]:
knjiga = lines[pocetak:kraj]

Као део припреме текста у наставку ће нам бити потрбна и функција за уклањање знака интерпункције:

In [10]:
znaci_interpunkcije = string.punctuation + '”“’‘—'
def ukloni_znake_interpunkcije(text):
    for ch in znaci_interpunkcije:
        text=text.replace(ch,' ')
    return text

Раздвајање листе редова на делове и поглавља:

У нашем кратком прегледу текста уочили смо да сваком делу књиге претходи линија која садржи стринг "PART", као и да поглавља садрже стринг "Chapter", што ћемо искористити за идентификацију одговарајућих сегмената текста у наставку:

In [1]:
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) # додавање броја поглавља последњег дела у листу
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-76892899879a> in <module>
      5 i = 0
      6 
----> 7 for line in knjiga:
      8     if 'PART' in line: # провера да ли је разматрана линија почетак новог дела књиге
      9         if i>0: # бројач i расте са сваким новим поглављем, тако да изузев у првмом делу књиге овај услов ће бити задовољен

NameError: name 'knjiga' is not defined

Кратка провера да смо претходним делом кода урадили шта смо желели је да проверимо дужину листе поглавља, као и суму дужина делова књиге:

In [12]:
(len(chapters),sum(partlens))
Out[12]:
(239, 239)
In [13]:
chapters = [ukloni_znake_interpunkcije(chapter) for chapter in chapters]

Детекција јунака

У овом делу наш циљ је да препознамо ко су јунаци у тексту и да припремимо сет података о броју појављивања сваког од јунака не бисмо ли успели да само на основу анализе текста наслутимо о коме је реч у тексту. Томе ћемо приступити користећи чињеницу да се властита имена пишу великим почетним словом. За почетак, спојићемо целокупан текст и од њега направити листу речи:

In [14]:
razmak=' '
ceotekst = razmak.join(chapters)
In [15]:
ceotekst[:100]
Out[15]:
' Happy families are all alike  every unhappy family is unhappy in its own way  Everything was in con'

Дужина текста мерена у карактерима:

In [16]:
len(ceotekst)
Out[16]:
1954438

До листе појединачних речи доћи ћемо користећи функцију split(), поређења ради, излистаћемо и првих 10 речи:

In [17]:
reci = ceotekst.split()
reci[:10] 
Out[17]:
['Happy',
 'families',
 'are',
 'all',
 'alike',
 'every',
 'unhappy',
 'family',
 'is',
 'unhappy']

Сачуваћемо и листу речи као још један једноставан сет информација о овој књизи.

In [18]:
reci = [rec for rec in reci if rec.isalpha()]
pd.DataFrame(reci,columns={'Rec'}).to_csv('Ana_df.csv',index=False)

Међутим, у листи речи коју смо овако направили, пуно речи се понавља (што ће бити део анализе у следећој радној свесци) те ћемо припремити и једну листу јединствених речи из овог текста:

In [19]:
jedinstvenereci = list(set(reci))
jedinstvenereci[:10]
Out[19]:
['undoubted',
 'scold',
 'divorces',
 'track',
 'cat',
 'Slavophiles',
 'village',
 'bounces',
 'dolls',
 'quack']

Да бисмо из листе јединствених речи издвојили потенцијалне кандидате за јунаке, користићемо се регуларним изразима. Правилност (енг. pattern) који ћемо тражити је реч почиње великим словом [A-Z] а затим је прати једно или више малих слова [a-z]+ (плус је ту да означи да очекујемо 1 или више слова из угласте заграде).

О регуларним изразима можете прочитати више овде, али суштински, када дефинишемо pattern само филтрирамо листу да добијемо речи које одговарају нашим захтевима:

In [20]:
pattern = re.compile(r'\b[A-Z][a-z]+\b')
potencijalni_junaci = list(filter(pattern.search, jedinstvenereci))

У листи potencijalni_junaci сада се налазе сви кандидати за име јунака, можемо видети колико их је и осмотрити првих 10:

In [21]:
len(potencijalni_junaci)
Out[21]:
1711
In [22]:
potencijalni_junaci[:10]
Out[22]:
['Slavophiles',
 'Ivanov',
 'Nothing',
 'Surely',
 'Elements',
 'Buslaev',
 'Raphaelite',
 'Striving',
 'Casting',
 'Principles']

Међу ових 10 речи, видимо неке речи које нису властита имена. То је зато што наше правило описује и све речи које се налазе на почетку реченице! Међутим, када се нека честа реч нашла на почетку реченице, очекујемо да се иста реч појављује у тексту и на неким другим местима у реченици, па ће се у листи јединствених речи наћи и у верзији исписа малим словима. То није ситуација у којој се може наћи властито име, па ћемо пробати да на тај начин умањимо листу кандидата за ликове. На пример, из претходне листе речи, можете одабрати неку од речи и да испитате да ли се она у тексту налази и када је цела написана малим словима:

In [23]:
i = 2
potencijalni_junaci[i]
Out[23]:
'Nothing'
In [24]:
potencijalni_junaci[i].lower() in jedinstvenereci
Out[24]:
True

Сада ћемо овај метод применити на целу листу речи потенцијалних јунака, чувајући само речи које се не налазе у остатку текста написане малим словима:

In [25]:
potencijalni_junaci = [rec for rec in potencijalni_junaci if rec.lower() not in jedinstvenereci]

Нова дужина листе је:

In [26]:
len(potencijalni_junaci)
Out[26]:
810

Успели смо да преполовимо листу потенцијалних јунака овим једноставним резоновањем!

In [27]:
potencijalni_junaci[:10]
Out[27]:
['Slavophiles',
 'Ivanov',
 'Buslaev',
 'Raphaelite',
 'Gospel',
 'Serbia',
 'Sorokina',
 'Pervozvanny',
 'Slav',
 'Shrinking']

Међутим и даље нису све речи у нашој потенцијалној листи заиста јунаци, стога ћемо осмислити још пар начина да смањимо листу речи које ћемо пратити на даље. На пример, сетићемо се да се на енглеском и називи дана и називи месеци пишу првим великим словом, тако да очекујемо да су се и они нашли у листи наших потенцијалних јунака:

In [28]:
days = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
months = ['January','February','March','April','May','June','July','August','September','October','November','December']
In [29]:
months[1] in potencijalni_junaci
Out[29]:
True

Cа друге стране, нису ни сви ликови једнако битни, па је један од начина да усмеримо нашу пажњу само на ликове који се појављују више пута у текст. Погледајмо за почетак како изгледа број понављања различитих потенцијалних јунака у целом тексту.

Сада ћемо ипак претраживати целокупан текст књиге исписан малим словима (и имена потенцијалних јунака ћемо исписивати на исти начин) за случај да се неки од издвојених јунака помиње у књизи и у комбинацији када су сва слова велика (нпр. неки узвик).

In [30]:
zastupljenostjunaka=dict()
reci_malaslova = [rec.lower() for rec in reci]
for junak in potencijalni_junaci:
    zastupljenostjunaka[junak]=reci_malaslova.count(junak.lower())

Да испитамо исход ове процедуре, сортираћемо и погледати првих 20 фреквентних потенцијалних јунака:

In [31]:
sorted_counts = sorted(list(zastupljenostjunaka.items()), key=lambda x:x[1], reverse=True)
for word, count in sorted_counts[:20]:
    print(word, count)
Levin 1616
Vronsky 862
Anna 822
Kitty 673
Alexey 632
Alexandrovitch 571
Stepan 548
Arkadyevitch 548
Dolly 307
Ivanovitch 302
Sergey 301
Alexandrovna 215
Darya 209
Moscow 173
Varenka 155
Sviazhsky 137
Petersburg 127
Seryozha 122
Oblonsky 115
Konstantin 112

Уколико сте читали књигу, сада вас сигурно радује чињеница да смо дошли до главних јунака (чак и локација на којима се радња дешава) без пуно муке!

У посебну листу убацити само она имена која се у целој књизи појављују 5 или мање пута и елиминисаћемо их из наставка анализе. Ово је параметар који можемо мењати и последично мењати дужину листе потенцијалних ликова.

In [32]:
rarenames = [junak for junak in zastupljenostjunaka.keys() if zastupljenostjunaka[junak]<=5]
In [33]:
len(rarenames)
Out[33]:
643

Има чак 643 јунака који се појављују само пар пута у књизи, што је згодно пошто ћемо фокусирати анализу на пар десетина најактивнијих јунака. Дакле, речи које желимо да уклонимо из листе потенцијалних јунака су називи дана и месеци, као и листу ретко помињаних имена:

In [34]:
wordstoremove = rarenames + days + months
In [35]:
potencijalni_junaci = [junak for junak in potencijalni_junaci if junak not in wordstoremove]
In [36]:
len(potencijalni_junaci)
Out[36]:
164

Сачуваћемо сада и нову верзију речника заступљености јунака, која садржи само изабране јунаке које ћемо пратити у наставку:

In [37]:
zastupljenostjunaka = {junak: zastupljenostjunaka[junak] for junak in potencijalni_junaci}

Направити табеле јунака и њиховог укупног броја појављивања (именом) у тексту:

In [38]:
junaci_df = pd.DataFrame.from_dict(zastupljenostjunaka,orient='index')
junaci_df.head()
Out[38]:
0
Gospel 6
Sorokina 11
Kouzma 22
Katya 9
Oblonsky 115
In [39]:
junaci_df = junaci_df.rename(columns={0:'Broj pojavljivanja'})
In [40]:
junaci_df.head(2)
Out[40]:
Broj pojavljivanja
Gospel 6
Sorokina 11
In [41]:
junaci_df = junaci_df.reset_index()
In [42]:
junaci_df = junaci_df.rename(columns={'index':'Ime'})
In [43]:
#junaci_df.to_csv('Ana_likovi_ukupan_br_pojavljivanja.csv')

Јунаци у различитим поглављима

Да би смо могли у даљој анализи да пратимо да ли су исти ликови популарни све време, или се неки јунаци појављују епизодно, или повремено, забележићемо на следећи начин и број понављања имена сваког од јунака у сваком поглављу појединачно, као и позицију у оквиру поглавља у којој је дати јунак поменут (ово ћемо у будућности користити као меру блискости два јунака - за јунаке који се помињу у истој реченици ћемо претпоставити да су блиски за разлику од јунака који се помињу на различитим странама).

In [44]:
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)

Користећи ове податке, направићемо одговарајућу табелу:

In [45]:
pojavljivanj_df = pd.DataFrame(pojavljivanja_junaka,columns=list(junaci_df.Ime))
pojavljivanj_df.head()
Out[45]:
Gospel Sorokina Kouzma Katya Oblonsky Alexandrovna Lizaveta Lvov Alexander Lvova ... Lily Vrede Mihailov Pava Vozdvizhenskoe Agafea Lidia Flerov Mahotin Gagin
0 0 0 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 4 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
2 0 0 0 0 1 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 4 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0
4 0 0 0 0 21 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0 0

5 rows × 164 columns

У овој табели свака колона одговара једном јунаку, док различити редови садрже информације о броју појављивања сваког од јунака у датом поглављу.

Сачуваћемо ову табелу у csv фајлу како бисмо јој лакше приступали и анализирали даље овај текст.

In [46]:
pojavljivanj_df.to_csv('Ana_likovi_pojavljivanja.csv',index=False)

А сада ћемо направити табелу у којој се налазе сва имена јунака и локације у тексту на којима се појављују:

In [47]:
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)
In [48]:
pozicije_junaka = pd.DataFrame()
In [49]:
pozicije_junaka['Ime junaka'] = junak
pozicije_junaka['Pozicija junaka'] = pozicija
pozicije_junaka.head()
Out[49]:
Ime junaka Pozicija junaka
0 oblonskys 20
1 french 36
2 oblonskys 142
3 english 170
4 stepan 220

Kако смо у овом процесу све преписали малим словима - овде ћемо то исправити користећи функцију capitalize:

In [50]:
pozicije_junaka['Ime junaka'] = pozicije_junaka['Ime junaka'].apply(str.capitalize)
pozicije_junaka.head(2)
Out[50]:
Ime junaka Pozicija junaka
0 Oblonskys 20
1 French 36

Коначно сачуваћемо ове податке за даљу анализу:

In [51]:
pozicije_junaka.to_csv('Ana_pozicije_junaka.csv',index=False)

У овој радној свесци:

  • учитали смо и пречистили текст књиге
  • користили смо регуларне изразе да издвојимо јунаке
  • избројали смо појављивања јунака и позиције појављивања у оквиру књиге.

Детекција ликова у тексту није једноставан задатак зато што су властита имена рановрсна, мењају се с временом и врло често могу бити у питању речи које имају своје значење, те се појављују у тексту и мимо именовања ликова. Међутим, када нам није потребна изузетна прецизност, једноставне процедуре попут ових приказаних могу бити довољне. Данас, задатак детекције јунака је најчешће намењен алгоритмима заснованим на машинском учењу за које је потребно припремити изузетно добре тренинг сетове у којима су детекцију ликова урадили људи.

Задатак

  1. Одлука да се фокусирамо само на ликове који се појављују више од 5 пута у тексту је помоћна процена. Са једне стране, желимо да смањимо скуп ликова које ћемо анализирати у наставку, са друге стране не желимо да изгубимо неке ликове који иако се појављују мало, могу играти битну улогу у повезивању јунака. Стога предлажемо да варирате број појављивања у тексту који користите као филтер и избројте колико потенцијалних јунака остаје у којој од опција. Нацртајте график броја потенцијалних јунака за различита филтрирања од 0 (без филтрирања по броју појављивања имена) до 10 (где елиминишете сва имена која су поменута мање од 10 пута). Можете сачувати и неке од табела које су настале овим другачијим филтрирањем да видите како се резултати у наредној радној свесци разликују када користите њих у односу на ове које смо припремили заједно.
In [ ]:
 
© 2021 Petlja.org Creative Commons License