1 file changed, 66 insertions(+), 40 deletions(-) final.py | 106 +++++++++++++++++++++++++++++++++++++++------------------------ modified final.py @@ -49,8 +49,18 @@ from nltk.corpus import CategorizedPlaintextCorpusReader, CategorizedCorpusReade # In[4]: huvel = nltk.corpus.CategorizedPlaintextCorpusReader("/home/ivankartac/edu/python/huvel", r".*\.txt", encoding="utf-8", cat_pattern=r".*-(.*?)\,") # Čistě formátovací záležitost: velmi dlouhé řádky můžeme uvnitř závorek rozdělit na více řádků, # přičemž obsah závorek většinou odsazujeme na úroveň otevírací závorky... huvel = nltk.corpus.CategorizedPlaintextCorpusReader("/home/ivankartac/edu/python/huvel", r".*\.txt", encoding="utf-8", cat_pattern=r".*-(.*?)\,") # ... nebo (zvláště v případě, že by takto vznikly velmi krátké řádky) odřádkujeme hned za otevírací # závorkou a odsadíme o standardní čtyři mezery: huvel = nltk.corpus.CategorizedPlaintextCorpusReader( "/home/ivankartac/edu/python/huvel", r".*\.txt", encoding="utf-8", cat_pattern=r".*-(.*?)\,") # In[5]: @@ -103,10 +113,14 @@ import csv def load_fdist(path): fdist = nltk.FreqDist() with open(path, newline='') as file: reader = csv.reader(file, delimiter = ';', quotechar = '"') # V Pythonu je zvykem, že kolem `=` ve volání funkce se nedělají mezery, kdežto kolem `=` # při přiřazování proměnné ano (z obou stran). reader = csv.reader(file, delimiter=';', quotechar='"') for row in reader: fdist[row[1]] = int(row[2]) return(fdist) # `return` není funkce, je to speciální jazykový konstrukt, takže nepotřebuje závorky (ne že by # něčemu vadily, ale tím, že tam nemusí být, se zde obvykle nepíšou). return fdist # **ZADÁNÍ:** Následně tuto funkci využijte k nahrání následujících dvou referenčních frekvenčních distribucí do Pythonu, např. do proměnných `syn2015_fdist` a `totalita_fdist` (jedná se o frekvenční distribuce slovních tvarů, case insensitive): @@ -244,9 +258,16 @@ fd.N() # In[9]: def is_keyword_candidate(word, text_fd, ref_fd): # Pozor, je-li frekvence v textu *menší* než v ref. korpusu, měli bychom rovnou rozhodnout, že # slovo nebude klíčové (jinak nám jako kandidáti na klíčová slova vyvstanou i slova, jejichž # frekvence je statisticky významně nižší v textu než v ref. korpusu). Musíme porovnávat # relativní frekvence, protože text a korpus budou skoro jistě jinak dlouhé: if text_fd.freq(word) < ref_fd.freq(word): return False p = chi2_contingency([[text_fd[word], text_fd.N() - text_fd[word]], [ref_fd[word], ref_fd.N() - ref_fd[word]]], lambda_="log-likelihood")[1] is_candidate = (p < 0.001) return(is_candidate) is_candidate = p < 0.001 return is_candidate # Případně lze taky rovnou `return p < 0.001`. # **OVĚŘENÍ:** Otestujte si, zda vaše funkce dobře funguje. V následující buňce jsou proporce v textu i v referenčním korpusu stejné, výsledek by tedy měl být `False`. @@ -273,8 +294,13 @@ is_keyword_candidate("kočka", text_fd_test, ref_fd_test) text_fd_test = nltk.FreqDist() ref_fd_test = nltk.FreqDist() text_fd_test["kočka"] = 90 text_fd_test["zbytek"] = 1910 # Zkuste zde schválně upravit hodnoty tak, aby relativní frekvence v textu byla *nižší* než v # referenčním korpusu; uvidíte, že původní verze funkce `is_keyword_candidate()` vrátí `True` i v # tomto případě (bude-li rozdíl ve frekvencích dostatečně velký na to, aby byl statisticky # významný). text_fd_test["kočka"] = 10 text_fd_test["zbytek"] = 1990 ref_fd_test["kočka"] = 3000 ref_fd_test["zbytek"] = 197000 @@ -300,8 +326,11 @@ is_keyword_candidate("kočka", text_fd_test, ref_fd_test) def din(word, text_fd, ref_fd): relf_text = text_fd[word] / text_fd.N() relf_ref = ref_fd[word] / ref_fd.N() index = 100 * ((relf_text - relf_ref) / (relf_text + relf_ref)) return(index) # V případě nejistot stran vyhodnocení složitějšího matematického výrazu s vícero operátory # pochopitelně ničemu neuškodí seskupení zaznamenat explicitně pomocí závorek (dokonce bych # řekl, better safe than sorry), jen pro úplnost doplním, že ty vnější nutné nejsou :) index = 100 * (relf_text - relf_ref) / (relf_text + relf_ref) return index # **OVĚŘENÍ:** Následující buňka by měla vrátit -100 -- cílové slovo se v textu vůbec nevyskytuje. @@ -390,26 +419,27 @@ husak75 = list(tagger.tag(test)) # In[18]: def keywords(text, ref_fd): frekvence = nltk.FreqDist() kandidati = [] for token in text: word = token.word if word in frekvence: frekvence[word] += 1 else: frekvence[word] = 1 # Původní kód je naprosto v pořádku a funguje, ale inicializaci frekvenční distribuce můžeme v # tomto případě provést jednodušeji tak, že konstruktoru objektu (= funkci `nltk.FreqDist()`) # předáme kolekci nebo generátor objektů, z nichž chceme f.d. vytvořit. Zápis je kratší a # myslím, že by to takhle mělo být i o něco rychlejší (lze ověřit, kdyby vás to zajímalo) :) frekvence = nltk.FreqDist(token.word for token in text) kandidati = set() # Velmi pěkné, čisté a úsporné řešení! Jen drobnost: asi bych do proměnné `kandidati` uložil # rovnou množinu a přidával prvky do ní, čímž si ušetříme dodatečnou deduplikaci v samostatném # kroku (konverzi seznamu v množinu). for token in text: word = token.word if re.match(r"^[^RJTZ]", token.tag) and frekvence[word] > 2: if is_keyword_candidate(word, frekvence, ref_fd): index = din(word, frekvence, ref_fd) kandidati.append((word, index, frekvence[word])) kandidati.add((word, index, frekvence[word])) kandidati = set(kandidati) return sorted(kandidati, key=lambda kandidati: (kandidati[1], kandidati[2]), reverse=True) # Po dvojtečce v rámci `lambda` odřádkování není nutné. Jinak nevím, jestli jste jméno # `kandidati` pro parametr anonymní funkce (`lambda`) použil jen tak nebo cíleně, každopádně to # není potřeba, parametr se může jmenovat jakkoli, typicky se volí nějaké krátké jméno. return sorted(kandidati, key=lambda x: (x[1], x[2]), reverse=True) # In[19]: @@ -493,8 +523,9 @@ def fdist(text): return(frekvence) husak_fdist = fdist(husak_komplet) havel_fdist = fdist(havel_komplet) # Viz výše, zde by bylo asi nejjednodušší rovnou použít konstruktor `nltk.FreqDist()`. husak_fdist = nltk.FreqDist(t.word for t in husak_komplet) havel_fdist = nltk.FreqDist(t.word for t in havel_komplet) # In[124]: @@ -529,37 +560,29 @@ keywords(husak_komplet, syn2015_fdist) # 'minfreq' určuje minimální frekvenci slova def keywords(text, ref_fd, pos = r"^[^RJTZ]", minfreq = 3, alpha = 0.001): frekvence = nltk.FreqDist() kandidati = [] for token in text: word = token.word if word in frekvence: frekvence[word] += 1 else: frekvence[word] = 1 def keywords(text, ref_fd, pos=r"^[^RJTZ]", minfreq=3, alpha=0.001): frekvence = nltk.FreqDist(token.word for token in text) kandidati = set() for token in text: word = token.word if re.match(pos, token.tag) and frekvence[word] >= minfreq: if is_keyword_candidate(word, frekvence, ref_fd, alpha): index = din(word, frekvence, ref_fd) kandidati.append((word, index, frekvence[word])) kandidati = set(kandidati) kandidati.add((word, index, frekvence[word])) return sorted(kandidati, key=lambda kandidati: (kandidati[1], kandidati[2]), reverse=True) (kandidati[1], kandidati[2]), reverse=True) # In[28]: # fakultativním argumentem 'alpha' je případně možné zvolit práh statistické významnosti def is_keyword_candidate(word, text_fd, ref_fd, alpha = 0.001): def is_keyword_candidate(word, text_fd, ref_fd, alpha=0.001): p = chi2_contingency([[text_fd[word], text_fd.N() - text_fd[word]], [ref_fd[word], ref_fd.N() - ref_fd[word]]], lambda_="log-likelihood")[1] is_candidate = (p < alpha) return(is_candidate) return is_candidate # In[39]: @@ -568,3 +591,6 @@ def is_keyword_candidate(word, text_fd, ref_fd, alpha = 0.001): # s defaultním nastavením tu nejsou zarhnuty roky keywords(husak75, syn2015_fdist, pos = r"^[^RJTZC]") # Pěkné postřehy k datům a velmi dobré nápady na vylepšení funkce `keywords()`! A obzvlášť oceňuju, # že ty nápady jsou převážně implementovány velmi idiomaticky, čistě a přehledně :)