Popularność literek w polskich słowach

Zainspirowany grafiką obrazującą rozkład położenia liter w słowach (angielskich) znalezioną na reddicie postanowiłem sprawdzić jak to samo wygląda w języku polskim. Aby sprawdzić na których miejscach w słowach występują poszczególne litery alfabetu potrzebujemy przede wszystkim słów. Bo alfabet znamy. Skąd wziąć słowa? Ze słownika, ktoś powie. Oczywiście to najlepsze miejsce - w słownikach są słowa, po to właśnie są słowniki. Ale słowniki mają jedną wadę - zawierają podstawowe formy słów (rzeczowniki w mianowniku, czasowniki w bezokoliczniku), co znacząco ogranicza liczbę dostępnych słów. A język polski jest bardzo bogaty w różne formy fleksyjne. Spróbujcie wymienić wszystkie możliwe wariacje na temat jakiegoś czasownika - najwięcej zabawy i rubasznego śmiechu będzie przy wulgaryzmach. Do każdego właściwie można dodać na początek wy-, przy-, za-, pod-, s-, roz-. Można dodać też się. Można odmienić czasownik przez osoby i liczby. Podobnie z rzeczownikami, przymiotnikami i innymi częściami mowy. Wystarczy spojrzeć na pierwszy z brzegu link znaleziony w Google. I właśnie na taki worek słów (bardzo lubię angielskie określenie bag of words) się nastawimy. Bo im więcej kombinacji tym bliższe życiu wyniki użycia literek. Dlatego nie skorzystamy ze słownika, a z gotowych tekstów - w tym przypadku z ośmiu książek, które już analizowaliśmy w odcinku poświęconym sprawie kryminalnej poszukiwania autora. Im więcej tekstów, najlepiej różnych autorów i z różnych dziedzin, tym większy zbiór słów. W poszukiwaniu danych do dzisiejszego postu natrafiłem na stronę PWN o korpusie języka polskiego - mowa tam o stu milionach słów! Na pewno z uwzględnioną fleksją, bo słów w naszym języku jest zapewne mniej (w formach podstawowych). Ale nie mamy tych 100 milionów słów, musimy polegać na tym co mamy. Zaczniemy od potrzebnych pakietów:

library(tidyverse) # %>%, dplyr, ggplot2
library(stringr)   # operacje na ciągach znaków
library(tm)        # usunięcie znaków przestankowych

Następny krok to wczytanie tekstów. Jak wspomniałem - wykorzystamy książki Zygmunta Miłoszewskiego oraz Remigiusza Mroza (po cztery), skonwertowane do formatu TXT. Każdą z książek wczytujemy do tabeli:

# lista plików z książkami
pliki <- list.files("ksiazki/")

books <- vector()
for(i in 1:length(pliki)) {
   book <- read_lines(paste0("ksiazki/", pliki[i]))
   book <- book[nchar(book) != 0]

   books <- c(books, book)
}

rm(book, pliki, i) # tego już nie potrzebujemy

Trochę czyścimy dane pozbywając się znaków przestankowych, cudzysłowów i sprowadzając wszystko do małych liter. Na koniec każde ze słów traktujemy specjalną funkcją, która ponumeruje nam literki w słowie:

# bez znaków przestankowych i innych znaków
books <- gsub("„", " ", books)
books <- gsub("”", " ", books)
books <- gsub("–", " ", books)
books <- gsub("-", " ", books)
books <- removePunctuation(books)

# rozbicie na pojedyncze słowa
slowa <- str_split(books, pattern = " ")
slowa <- unlist(slowa)

# usunięcie zbędnych odstępów przed i po słowie
slowa <- trimws(slowa)

# tylko unikalne słowa
slowa <- unique(slowa)

# z co najmniej 1 literą
slowa <- slowa[nchar(slowa) >= 1]

# funkcja rozbijająca słowo na literki
# i nadająca im numerki wg kolejności w słowie
znaki_df <- function(slowo) {
   slowo <- str_to_lower(slowo)
   litery <- unlist(str_split(slowo, pattern = ""))
   znaki <- tibble(slowo = slowo, znak = litery, pos = 1:length(litery))
   return(znaki)
}

# dla każdego słowa: podział na literki, wszystko w jedną długą tabelę
znaki <- bind_rows(lapply(slowa, znaki_df))
znaki <- distinct(znaki)

Dane już gotowe (mamy nieco ponad 88 i pół tysiąca słów), więc możemy sobie coś pooglądać. Ale najpierw coś dla fanów Sherlocka Holmesa i opowiadania “Tańczące sylwetki”. To moje ulubione opowiadanie z serii o Holmesie; gdzieś w młodości przeczytane u ciotki na strychu. Idealnie wpasowuje się w chęć młodzieńca lubiącego poszukiwania, rozwiązywanie zagadek, bardzo klarownie pokazuje logiczne i metodyczne podejście do rozwiązywania problemów. Tego wymaga się od analityków (rozumianych jako data scientist - Wiki twierdzi, że polski odpowiednik to mistrz danych… co za bullshit). Dla tych, którzy nie znają opowiadania mały skrót z fabuły (tej kluczowej tutaj części). Sherlock zajmuje się sprawą tajemniczych zaszyfrowanych liścików - zamiast liter na kartkach narysowane są postacie z różnym ułożeniem rączek i nóżek. Listów jest coraz więcej (coraz więcej danych!), dzięki czemu Sherlock jest w stanie przyporządkować sylwetką odpowiednie litery alfabetu. Wychodzi od wiedzy, że litera E jest najpopularniejsza w angielskim słowie pisanym oraz długości poszczególnych słów - bardzo łatwo rozpoznać “the”, a jak się ma “the” to ma się już trzy symbole. Z czego dwa użyte w “have”, co prowadzi do kolejnych dwóch. Które to a możemy zweryfikować po “at” (oraz samym “a” przed rzeczownikami), zaś po “to” można odszukać sylwetkę dla “o”. “O” oraz “a” pozwolą na weryfikację “n” (na podstawie “on” oraz “an”). I tak dalej, i tak dalej. A wymieniłem tylko kilka podstawowych słów: the, have, on, a, an, at, co daje już siedem liter (z 26 używanych w języku angielskim) - ponad 1/4. To trochę jak rozpoznawanie kotów na obrazach, czyż nie? Koty Sieci neuronowe trzeba nakarmić (nauczyć) dużą ilością danych - mamy dużo liścików, dużo sylwetek. Później już “tylko” pozostaje odnalezienie odpowiednich patternów. Sprawdźmy zatem czy wiedza Sherlocka Holmesa o najpopularniejszej literze E ma zastosowanie w języku polskim:

znaki %>%
   filter(znak %in% pl_znaki) %>%
   count(znak) %>%
   ungroup() %>%
   mutate(p = 100*n/sum(n)) %>%
   ggplot() +
   geom_bar(aes(znak, p, fill=p), stat="identity", show.legend = FALSE) +
   geom_text(aes(znak, p, label=sprintf("%.2f%%", p)), angle=90, hjust=-0.2) +
   expand_limits(y = c(0, 11)) +
   scale_y_continuous(breaks = 1:10)

I Sherlock by się pomylił. Najpopularniejsze jest A, E zaś jest dopiero czwarte. Widać też coś, czego zapewne sobie nie uświadamiamy: najpopularniejsze są samogłoski (kolejno: a, i, o, e). Co więcej, około 42% wykorzystywanych liter to samogłoski (rozumiane jako jedna z a, ą, e, ę, i, o, ó, u, y). Reszta to oczywiście spółgłoski. Sherlockowi pomogło the - słowo trzyliterowe. A jak wygląda rozkład długości polskich słów?

znaki %>%
   filter(znak %in% pl_znaki) %>%
   group_by(slowo) %>%
   filter(pos == max(pos)) %>%
   ungroup() %>%
   ggplot() +
   geom_histogram(aes(pos), binwidth = 1)

Wybierając informacje z powyższego histogramu bez ogona można zgodnie z testem Shapiro-Wilka stwierdzić, że to rozkład bliski rozkładowi normalnemu. Pojawia się ów ogon - najdłuższe słowo użyte w 8 badanych tekstach ma 52 znaki (jest to MuszeToKomusWyznacKochamKsiedzaMarkaNieMogeZycPomocy, które jako treść SMSa pada w Uwikłaniu Miłoszewskiego). Cała reszta słów jest krótsza niż 25 znaków (wszystkie dłuższe słowa to: siedemdziesięcioprocentowy, trzydziestosiedmiocalowego, osiemdziesięciosześcioletnia, trzydziestodziewięciostopniową, dziewięćdziesięcioprocentowego oraz wspomniany SMS), zatem dalsze rozważania ograniczymy do słów do 25 znaków włącznie. Dochodzimy do zamierzonego celu, czyli odpowiedzi na pytanie na jakiej pozycji w słowie występują poszczególne litery?. Zobaczmy to dla każdej z liter z osobna:

znaki %>%
   filter(pos <= 25) %>%
   filter(znak %in% pl_znaki) %>%
   ggplot() +
   geom_histogram(aes(pos), binwidth = 1) +
   facet_wrap(~znak, ncol = 5, scales="free_y")

Co tutaj widać? Przede wszystkim różną skalę dla każdego z wykresów, na to należy zwrócić uwagę. Ale nas interesuje każdy z tych wykresów z osobna, a tutaj widzimy, że S występuje najczęściej na początku słowa jako jedna z pierwszych liter (pierwsza prawdę powiedziawszy), podobnie W, P, F czy M. Samogłoski są na kolejnych pozycjach. Tyle tylko, że są to wartości bezwzględne, a słowa jak widzieliśmy mają różną długość - raz pozycja numer trzy to środek słowa, a raz jego koniec. Musimy zatem przejść na wartości bezwzględne - na przykład na procent długości słowa:

znaki %>%
   # dla każdego słowa policz w którym procencie jego długości jest literka
   group_by(slowo) %>%
   mutate(l = max(pos)) %>%
   ungroup() %>%
   mutate(proc = 100*pos/l) %>%
   # podział na bloki co 5%
   mutate(proc = cut(proc, breaks = seq(0, 100, 5))) %>%
   # przygotuj histogram dla słów krótszych niż 25 znaków
   filter(pos <= 25) %>%
   filter(znak %in% pl_znaki) %>%
   count(znak, proc) %>%
   ggplot() +
   geom_bar(aes(proc, n), stat="identity") +
   facet_wrap(~znak, ncol = 5, scales="free_y")

Z przeliczeniem na procenty jest pewien kłopot - dla słowa trzyliterowego każda z liter to już krok o 33%. Ale abstrahując od tego problemu widzimy, że:

  • to co poprzednio: F, P, S, W oraz Z rozpoczynają słowa
  • A, Ą, E, Ę, I, J, M, U kończą słowa
  • Ć jest tutaj niesamowite - właściwie tylko jako ostatni znak. W połączeniu z Ś (też dominacja na końcu) mamy końcówkę -ść
  • podobnie jest z Ń i Y - najczęściej są ostatnimi literami
  • H podobnie - końcówka -ch
  • ciekawostką jest X - pojawia się rzadko, zapewne w nazwach leków, marek lub firm (niepokonany schemat nazewnictwa firm z lat ’90 XX wieku - Pol-Ex). Ale występuje też w numerach rozdziałów (teksty nie były czyszczone z tego typu elementów)
  • analogicznie jest z Q - występuje jeszcze rzadziej (słowa: tequila, squash i aquapark)
  • Ó występuje najczęściej pod koniec - to wszystkie końcówki -ów
  • B, D, K, L, O, R, T, Z, Ź i Ż dominują w środkach wyrazów

Zestawiając oba wykresy (bezwzględną pozycję w słowie i “pozycję” procentową) widzimy, że:

  • Ą jest najczęściej 5-8 literą, a jednocześnie raczej ostatnią - to słowa 5-8 literowe, takich słów mamy ponad 2200
  • podobnie jest z Ę
  • O występuje najczęściej w okolicach 15-20% długości wyrazu jednocześnie będąc najczęściej drugim znakiem. Skoro 2 znaki to 15% (lub 20%) to całość to słowo ma 13 lub 14 znaków (dla 20% - 10 znaków). Znacie takie słowa? Chwila na zastanowienie się… Znowu mamy ich około 2300, a przykładowe to postanowił oraz powinienem albo monopolowy
  • Ł występuje w 85-90% oraz 95-100% długości słowa jednocześnie będąc gdzieś 5-8 literką. Jakieś 8 tysięcy słów, od urodziłam do zdechł

Bardzo przyjemna rozrywka intelektualna: szukanie słów przy znajomości położenia danej litery i długości słowa. Przypomina scrabble albo krzyżówkę, prawda? Wróćmy do rozkładu literek po pozycji w wyrazie i zobaczmy te same dane nieco inaczej - w postaci heatmapy:

znaki %>%
   filter(znak %in% pl_znaki) %>%
   group_by(znak, pos) %>%
   summarise(n=n()) %>%
   ggplot() +
   geom_tile(aes(znak, pos, fill=n))

Widzimy, że żadne słowa nie zaczynają się na Ą, Ę czy Ń. To dość oczywiste. (nieco) Mniej oczywiste jest to, że Ć nie występuje na drugiej pozycji (ale może być pierwsze - chwila na zastanowienie się - ćpać i okolice oraz ćwiczenia i pochodne). Z Ń jest tak samo, jest dopiero na trzecim miejscu. Wiecie w jakim słowie? W wielu:

znaki %>%
   filter(znak == "ń") %>%
   filter(pos == min(pos)) %>% # w tym przypadku może być również pos = 3
   select(slowo) %>%
   distinct() %>%
   arrange(slowo)
##   [1] "bańki"          "bińczyk"        "bińczyka"       "bońka"         
##   [5] "buńczucznym"    "dań"            "duńczycy"       "duńczyk"       
##   [9] "duńczyka"       "duńczykami"     "duńczykom"      "duńczyków"     
##  [13] "duńska"         "duńską"         "duński"         "duńskich"      
##  [17] "duńskie"        "duńskiego"      "duńskiej"       "duńskim"       
##  [21] "duńsku"         "fiński"         "fińskich"       "goń"           
##  [25] "gończe"         "gończy"         "gończym"        "hańbić"        
##  [29] "hańbił"         "jońskiej"       "koń"            "końca"         
##  [33] "końcach"        "końce"          "końcem"         "końcowa"       
##  [37] "końcową"        "końcowe"        "końcowej"       "końcowy"       
##  [41] "końcowych"      "końców"         "końcówce"       "końcówka"      
##  [45] "końcówkami"     "końcówkę"       "końcówki"       "końcu"         
##  [49] "końcu…"         "kończą"         "kończąc"        "kończącą"      
##  [53] "kończące"       "kończącego"     "kończący"       "kończę"        
##  [57] "kończmy"        "kończy"         "kończyć"        "kończyli"      
##  [61] "kończył"        "kończyła"       "kończyłby"      "kończyło"      
##  [65] "kończyłoby"     "kończyły"       "kończymy"       "kończyn"       
##  [69] "kończyna"       "kończynach"     "kończynami"     "kończynę"      
##  [73] "kończyny"       "kończysz"       "końmi"          "koński"        
##  [77] "końskim"        "łańcuch"        "łańcucha"       "łańcuchach"    
##  [81] "łańcuchami"     "łańcuchem"      "łańcuchowym"    "łańcuchów"     
##  [85] "łańcuchu"       "łańcuchy"       "mońkach"        "nań"           
##  [89] "pań"            "pańscy"         "pańska"         "pańską"        
##  [93] "pański"         "pańskich"       "pańskie"        "pańskiego"     
##  [97] "pańskiej"       "pańskim"        "państewku"      "państw"        
## [101] "państwa"        "państwach"      "państwem"       "państwie"      
## [105] "państwo"        "państwo…"       "państwowa"      "państwową"     
## [109] "państwowe"      "państwowego"    "państwowej"     "państwowy"     
## [113] "państwowych"    "państwowym"     "państwowymi"    "państwu"       
## [117] "pańszczyzna"    "pańszczyźniani" "pończoch"       "pończochach"   
## [121] "pończochy"      "sińców"         "tańca"          "tańce"         
## [125] "tańcu"          "tańczą"         "tańczące"       "tańczącego"    
## [129] "tańczący"       "tańczącym"      "tańczenie"      "tańczy"        
## [133] "tańczyć"        "tańczyli"       "tańczył"        "tańczymy"      
## [137] "tańsze"         "tańszych"       "tańszymi"       "wańkowicza"    
## [141] "wiń"            "woń"            "żeńską"         "żeńskiego"     
## [145] "żeńskim"

Należy pamiętać, że baza słów pochodzi z kilku książek, a nie ze wszystkich słów w języku polskim. Jest to fajna próba, ale raczej nie 100% reprezentatywna. Zobaczmy jeszcze które litery występują po sobie? Aby to uzyskać grupujemy dane według słów i przesuwamy (w dodatkowej kolumnie) o jeden każdą literkę. Dzięki temu w wierszu otrzymamy literkę oraz jej następnika - tak słowo po słowie. Następnie zliczamy pary literek.

znaki_pary <- znaki %>%
   group_by(slowo) %>%
   mutate(znak_next = lead(znak)) %>%
   ungroup() %>%
   select(znak, znak_next) %>%
   filter(znak %in% pl_znaki) %>%
   filter(znak_next %in% pl_znaki) %>%
   count(znak, znak_next) %>%
   ungroup() %>%
   mutate(p = 100 * n/sum(n)) %>%
   mutate(proc = cut(p, 10))

Mając przygotowane dane możemy pokazać heatmapkę z kolejnością liter - która po której i jak często?

znaki_pary %>%
   ggplot() +
   geom_tile(aes(znak_next, znak, fill=proc), show.legend = FALSE) +
   scale_fill_brewer(palette = "YlOrRd") +
   scale_y_discrete(limits = rev(pl_znaki))

Widać wręcz kilka najmocniejszych połączeń. Najpopularniejsze zbitki to:

znaki_pary %>%
   mutate(para = paste0(znak, znak_next)) %>%
   top_n(30, wt = p) %>%
   arrange(p) %>%
   mutate(para = factor(para, levels=para)) %>%
   ggplot() +
   geom_bar(aes(para, p, fill=para), stat="identity") +
   coord_flip()

Nie. To najpopularniejsza zbitka. Do tego nasze ortograficzne problemy: rz, ch (a takie cz jest bardziej popularne niż ch). Na wykres nie załapało się ść (287 na liście popularności, dla porównania śc - 128), ąc (jest 44 na liście), ów (jest 89) a także uj (94).   Fajne? Podziel się linkiem do tekstu ze znajomymi (tam są takie ikonki na dole) albo wrzuć na Wykop. Napisz co myślisz w komentarzu. Zapraszam też na facebookowy fanpage Dane i Analizy gdzie trailery i teasery nowych wpisów, a także linki do ciekawych treści o analizie danych.