4. cvičení

Práce s textem, regulární výrazy a kategorizace

Cíle 4. cvičení:

Znát základní funkce pro práci s textem, porozumět práci s regulárními výrazy a osvojit si funkce pro vyhledávání a zpracování (nahrazování) regulárních výrazů a textu. Naučit se provádět kategorizaci nefaktorových proměnných.


Základy práce s textem

Datová třída character je typická množinou specifických funkcí pro spojování a rozdělování textových řetězců (též pro vyhledávání a nahrazování textu, jak je popsáno níže u regulárních výrazů). Text zřejmě nelze sčítat nebo násobit, jak to umožňují čísla nebo logické hodnoty, lze však operovat s délkou řetězců a pořadím znaků v nich.

Mezi nejjednodušší funkce pro práci s textovými řetězci (a jejich vektory) patří funkce pro zjištění délky řetězce nchar() a ořezání řetězce podle zadané polohy prvního a posledního znaku substr() (pořadí udávají druhý a třetí argument funkce):

t0<-"kyselina"
t1<-"disírová"
nchar(t1)
t2<-substr(t1,1,2)
t3<-substr(t1,3,8)

Opačným procesem je spojování textu, pro které nabízí jazyk R dvojici podobných funkcí paste() a paste0(), přičemž první vkládá mezi spojované řetězce mezeru, zatímco druhá nikoliv:

paste(t0,t1)
paste(t2,t3)
paste0(t2,t3)

V základním nastavení pracují obě funkce paste() a paste0() tzv. po prvcích vektoru, tj, pokud jsou jejich argumenty vektory s více prvky, spojují vždy dvojici prvků, nikoliv však prvky jednoho vektoru mezi sebou:

v1<-c("kyselina","hydroxid")
v2<-c("fluorovodíková","lithný")
paste(v1,v2) # stejně dlouhé vektory fungují správně
v3<-c("kyselina","hydroxid","karbid")
paste(v3,v2) # pokud je jeden vektor delší, kratší vektor se recykluje od začátku

Naopak je-li zapotřebí spojit text z jednotlivých prvků textového vektoru, je třeba specifikovat argument collapse, který obsahuje znak použitý pro oddělení prvků zadaného vektoru:

v4<-c("V","uzavřené","soustavě","se","součet","hmotností","látek,","které","vstupují","do","reakce,","rovná","součtu","hmotností","látek,","které","reakcí","vznikají.")
paste(v4,collapse=" ")


Regulární výrazy

Jako regulární výrazy se označují specifické textové řetězce, obsahující kromě samotných textových znaků také speciální značky (rovněž složené z vyjmenovaných sekvencí znaků), které mohou zastupovat jiné znaky. Obecně může jeden regulární výraz odpovídat celé množině textových řetězců.

Regulární výrazy se běžně používají pro vyhledávání a nahrazování částí textu (řetězců) v mnoha programech a programovacích jazycích, přičemž obvykle každý jazyk používá mírně odlišnou syntaxi. V R existují dva režimy práce s regulárními výrazy - tzv. rozšířené (extended) regulární výrazy a regulární výrazy převzaté z jazyka Perl (Perl-like). V následujícím textu se budeme zabývat pouze prvním druhem regulárních výrazů v R, který je podstatně používanější. U většiny funkcí pro práci s regulárními výrazy lze rovněž nastavit hodnotu argumentu fixed=TRUE, což znamená, že daný regulární výraz je vyhodnocen jako čistý text bez speciálních značek.


Rozšířené regulární výrazy

Obvyklejším způsobem práce s regulárními výrazy je využití tzv. rozšířených regulárních výrazů. Kromě standardních znaků tyto výrazy využívají rovněž značky složené z tzv. metaznaků (metacharacters). V prostředí R jde o následující znaky: . \ | ( ) [ { ^ $ * + ?. Jejich význam nicméně závisí na kontextu, ve kterém jsou v regulárním výrazu využity. Kromě těchto jednoduchých metaznaků lze podle pravidel uvedených níže skládat komplikovanější značky, včetně některých využívajících textové pojmenování (například značka [:lower:] zastupuje libovolný počet malých písmen dle nastaveného národního prostředí).

Escapování

Samozřejmě se naskýtá otázka, jak zapsat text, obsahující některý z těchto znaků, který ovšem nemá být vyhodnocen jako metaznak (a tedy značka se speciálním významem), ale pouze jako sám prostý znak. Nejčastěji k takovému problému dochází u běžných interpunkčních znamének jako jsou tečky a otazníky, které se běžně vyskytují v psaném textu, nicméně v regulárním výrazu mají speciální význam (tečka zastupuje libovolný znak a otazník zastupuje nejvýše jeden výskyt předchozího znaku/značky). Řešením je tzv. escapování, tj. uvození metaznaku opačným lomítkem. Místo řetězce "." tak v regulárním výrazu uvedeme "\.". Jiným řešením je pak výše uvedený argument fixed=TRUE, který však má za následek i zbavení všech ostatních značek jejich funkce.

Pomocí escapování lze navíc zadat i některé netisknutelné znaky do textu, interpretace se však může u různých funkcí lišit. Běžně lze využít značky "\a" namísto znaku BEL (zvonek), "\e" jako ESC (escape), "\f" jako FF (konec stránky typu form feed), "\n" jako LF (konec řádku typu line feed), "\r" jako CR (konec řádku typu návrat vozíku) a "\t" jako TAB (tabelátor).

Významy metaznaků

. nahrazuje libovolný jeden znak/značku,
? předchozí znak/značka je volitelný ale vyskytuje se maximálně jednou,
* předchozí znak/značka je volitelný a vyskytuje se libovolněkrát za sebou,
+ předchozí znak/značka se vyskytuje nejméně jednou,
{n} předchozí znak/značka se vyskytuje přesně n-krát,
{n,} předchozí znak/značka se vyskytuje nejméně n-krát,
{n,m} předchozí znak/značka se vyskytuje nejméně n-krát a nejvýše m-krát.

Znakové třídy

Speciální metaznaky "[]" a "^" slouží k vyjmenování rozsahů znaků, tzv. znakových tříd (character classes). Jde o značky, které odpovídají libovolnému znaku v rozsahu. Například značka "[0123456789]" odpovídá libovolné číslici od 0 do 9. Vzhledem k tomu, že použití pořadí písmen je závislé na národním prostředí, doporučuje se jej nevyužívat, s výjímkou základní anglické abecedy v kódování ASCII. Například značka "[abc]" odpovídá libovolnému ze znaků "a", "b" nebo "c". Negace rozshau se provádí znakem stříšky "^", tedy např. značka "[^abc]" odpovídá libovolnému znaku kromě znaků "a", "b" a "c".

Mimo těchto univerzálních znakových tříd jsou v R předdefinovány některé znakové třídy zadávané pomocí svých názvů, hranatých závorek a dvojice dvojteček:

[:alnum:] alfanumerické znaky, vlastně sjednocení množin [:alpha:] a [:digit:],
[:alpha:] písmena, vlastně sjednocení množin [:lower:] a [:upper:],
[:blank:] netisknutelné znaky: mezera, tabelátor, tvrdá mezera a další podobné znaky (závislé na národním prostředí),
[:cntrl:] řídící znaky - v ASCII s osmičkovými kódy od 000 do 037 a 177 (DEL) nebo ekvivalentní znaky v jiných znakových sadách,
[:digit:] desítkové číslice: 0 1 2 3 4 5 6 7 8 9,
[:graph:] grafické znaky, vlastně sjednocení množin [:alnum:] a [:punct:],
[:lower:] malá písmena dle národního prostředí,
[:print:] tisknutelné znaky, vlastně sjednocení množin [:alnum:] a [:punct:] a také mezera,
[:punct:] interpunkční znaménka: ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~,
[:space:] odsazení a konce: tabelátor, nový řádek, vertikální odsazení, konec stránky typu FF (form feed), posun vozíku (konec řádku typu CR), mezera a další podobné znaky závislé na národním prostředí,
[:upper:] velká písmena dle národního prostředí,
[:xdigit:] šestnáctkové číslice: 0 1 2 3 4 5 6 7 8 9 A B C D E F a b c d e f.

Tyto speciální třídy je posléze nutné uvádět, pokud stojí samostatně ještě znovu v hranatých závorkách, např. "[[:upper:]]".

Mimo výše uvedených značek existují ještě tyto další (tzv. rozšířené) značky:

^ odpovídá prázdnému řetězci na začátku řádku,
$ odpovídá prázdnému řetězci na začátku řádku,
\w jedno slovo mezi netisknutelnými znaky, synonymum pro "[[:alnum:]_]",
\W opak předchozího, synonymum pro "[^[:alnum:]_]",
\d číslice, synonymum pro "[:digit:]",
\D opak předchozího, synonymum pro "[^[:digit:]]",
\s odsazení a konce, synonymum pro "[:space:]",
\S opak předchozího, synonymum pro "[^[:space:]]",
\b prázdný řetězec na začátku nebo konci slova,
\B opak předchozího,
() uzávorkování negací a kvantifikátorů ve výrazech
| logický součet dvou regulárních výrazů (spojka nebo), nefunguje uvnitř znakových tříd,
\1, \2,…,\9 odkaz na 1., 2.,…, 9. uzávorkovaný regulární podvýraz regulárního výrazu.

Rozšířeným regulárním výrazům se podrobněji věnujeme v ukázkách konkrétních aplikací v následujícím textu.


Vyhledávání zadaných řetězců (funkce grep(), grepl(), regexpr(), gregexpr() a regexec())

Pro vyhledávání řetězců v textu/textovém vektoru má jazyk R pět funkcí, podobných v argumentech, nicméně lišících se návratovou strukturou. V následujících příkladech postupně rozebereme jednotlivé funkce s jejich specifiky. K tomu nám poslouží jednoduchá datová tabulky slouceniny, kterou si nyní vytvoříme a budeme používat po celé cvičení:

slouceniny<-data.frame(vzorec=c("H3PO4","Ca(OH)2","KCl","H4SiO4","HPO3","NaHCO3","I2","KCN","H2S2O7"),
nazev=c("k. trihydrogenfosforečná","hydroxid vápenatý","chlorid draselný","k. tetrahydrogenkřemičitá","k. fosforečná","hydrogenuhličitan sodný","jód","kyanid draselný","k. disírová"),
trivialni=c("k. ortofosforečná","vápno","drasloš","k. ortokřemičitá","k. metafosforečná","jedlá soda",NA,"cyankáli",NA),
hmotnost=c(98.0,74.1,74.6,227.0,80.0,84.0,126.9,65.1,178.1))

grep()

Nejjednodušší funkcí pro vyhledání zadaného textového řetezce je funkce grep(), která má dva povinné argumenty pattern, tedy hledaný řetězec znaků a x, tedy textový vektor, ve kterém je provádeno vyhledávání. Návratovou strukturou je pak celočíselný vektor, udávající pořadová čísla prvků původního vektoru x, u kterých došlo ke shodě.

Pokusme se nyní začít s vyhledáváním naivním způsobem tak, že ve sloupci nazev vyhledáme všechny kyseliny, tj. látky obsahující v názvu zkratku „k.“.

attach(slouceniny)
grep("k.",nazev)

Výsledkem bude vektor obsahující pořadová čísla 1, 4, 5, 8 a 9. Na první pohled správný výsledek nicméně ukrývá kromě kyselin trihydrogenfosforečné, tetrahydrogenkřemičité, fosforečné a disírové také prvek s pořadovým číslem 8 neboli kyanid draselný. Ve svém naivním přístupu jsme si totiž neuvědomili, že znak tečka "." je současně znaménkem regulárního výrazu, který zastupuje libovolné následující znaky - jinými slovy námi vyhledávaný řetězec "k." odpovídá v jazyce regulárních výrazů jakémukoliv řetezci začínajícímu písmenem "k".

Řešení nepříjemné sitauce jsou dvě - buď tečku escapujeme pomocí zpětného lomítka, jak bylo uvedeno výše, nebo použijeme volitelný argument fixed funkce grep(), tak, abychom pracovali místo regulárních výrazů pouze s čistými textovými řetězci:

grep("k\.",nazev) # nefunguje
grep("k\\.",nazev) # tečku je nutné escapovat dvěma lomítky
grep("k.",nazev,fixed=TRUE)

Stejně jako většina funkcí v R, je rovněž funkce grep() citlivá na velikost písmen (tzv. case sensitive). Například následující funkce vyhledají ve vzorcích látek v tabulce slouceniny vzorce obsahující velké I, malé i a obě varianty:

grep("*I+",vzorec)
grep("*i+",vzorec)
grep("*I+|*i+",vzorec)
grep("[I,i]",vzorec)

Podobně bychom mohli například pro vyhledání všech látek, v jejichž vzorci se (ne)vyskytují číslice využít příkaz:

grep("[[:digit:]]",vzorec) # vzorce s číslicemi
which(!is.element(1:nrow(slouceniny),grep("[[:digit:]]",vzorec))) # vzorce bez číslic

Zde už je ale syntaxe s využitím funkce grep() poněkud krkolomná. S výhodou lze proto využít alternativní funkci grepl() vracející logické hodnoty.

grepl()

Narozdíl od funkce grep() vrací obdobná funkce grepl() logický vektor o stejné délce jako vektor v argumentu x (l na konci názvu funkce značí „logical“), který obsahuje hodnotu TRUE pokud se v prvku na dané pozici vyskytuje vyhledávaný regulární výraz a FALSE v opačném případě:

is.element(1:nrow(slouceniny),grep("*fosfor+",nazev))
grepl("*fosfor+",nazev)

Pro zjištění, ve kterých názvech měsíců se vyskytuje alespoň dvakrát písmeno „e“ pak lze použít příkaz:

format(ISOdate(2000,1:12,1),"%B") # výpis měsíců v roce (R konstanta)
grepl("e.{2,}|e{2,}",format(ISOdate(2000,1:12,1),"%B"))

regexpr()

Ještě složitější návratovou strukturu vrací funkce regexpr(), jejímž výsledkem je seznam o dvou prvcích ve formě celočíselných vektorů, kde první prvek obsahuje hodnotu -1, pokud se v daném prvku vektoru x nevyskytuje hledaný řetězec nebo pořadové číslo znaku, od kterého začíná v prvku hledaný řetězec. Druhý prvek návratového seznamu opět obsahuje hodnotu -1, pokud se v daném prvku vektoru x nevyskytuje hledaný řetězec nebo délku (tj. počet znaků) všech řetězců, které se shodují s hledaným řetězcem v daném prvku:

regexpr("hydro",nazev)

gregexpr()

Stejný obsah, avšak v jiné formě vrací funkce gregexpr(), jejíž návratovou strukturou je seznam seznamů o délce totožné s délkou vstupního vektoru x. Každý dílčí seznam pak obsahuje dvojici prvků, totožnou s prvky vektorů navrácených funkcí regexpr().

gregexpr("hydro",nazev)

regexec()

Poslední z pětice funkcí pro vyhledávání je funkce regexec(), která vrací seznam seznamů o délce totožné s délkou vstupního vektoru x. Každý dílčí seznam pak obsahuje trojici prvků, obsahující pořadí a délky celého hledaného řetězce a všech jeho uzávorkovaných podřetězců:

regexec("(hydr)(o)",nazev)


Nahrazování zadaných řetězců (funkce grep(), grepl(), regexpr(), gregexpr() a regexec())

Krokem následujícím po nalezení konkrétního textového řetězce může být jeho nahrazení (substituce) jiným řetězcem, případně jeho vymazání (tj. nahrazení prázdným řetězcem). Syntaxe funkcí pro nahrazování textu je velice podobná syntaxi pro jeho vyhledávání, kromě argumentů pattern a x ovšem přibývá nový argument replacement umístěný doprostřed mezi ně a obsahující řetězec, kterým má být původní text nahrazen. Pokud ponecháme argument replacement prázdný (tj. nastavíme jej na "" nebo character(0)), dojde ke smazání hledaného textu.

gsub()

Funkce gsub() vyhledá všechny výskyty zadaného řetězce a nahradí je zadaným textem pro nahrazení:

gsub("tekutiny","kapaliny",c("Těleso ponořené do tekutiny je nadlehčováno silou rovnající se tíze tekutiny tělesem vytlačené."))
gsub("k.","kyselina",nazev,fixed="TRUE")

sub()

Naproti tomu funkce sub() nahradí v každém řetězci (tj. v každém prvku vektoru) pouze první výskyt hledaného řetězce:

sub("tekutiny","kapaliny",c("Těleso ponořené do tekutiny je nadlehčováno silou rovnající se tíze tekutiny tělesem vytlačené."))
sub("k.","kyselina",nazev,fixed="TRUE")


Kategorizace

Zejména za účelem vytváření faktorových vektorů byla do balíku car přidána funkce recode(), která umožňuje na základě zadaných kritérií kategorizovat textové nebo číselné vektory (výsledkem je kategoriální, případně numerický vektor).

Funkce má dva povinné argumenty, přičemž první argument var je vektor, který má být kategorizován, druhý argument recodes uvádí ve formě určitého pseudokódu pravidla pro zařazení prvků vektoru var do jednotlivých kategorií. Tento pseudokód je zadán ve formě textového řetězce, kdy všechny prvky z dané kategorie jsou vyjmenovány ve formě pseudovektoru (c()) nebo rozsahu (zadaného pomocí dvojtečky) a název kategorie do které patří je připojen za rovnítkem v jednoduchých uvozovkách. Pravidla pro jednotlivé kategorie jsou v pseudokódu navzájem oddělená středníky.

Pro využití funkce recode() je nutné mít nainstalovaný a načtený balík ,code>car:

install.packages("car")
library("car")

Demonstrujme si funkci recode() na následujícím příkladu: vektor iso ISO kódů států budeme kategorizovat podle kontinentů, na kterých státy leží:

iso<-c("FR","VN","RU","CZ","NP")
recode(iso,"c('FR','CZ','RU')='Evropa';c('RU','VN','NP')='Asie'")

Funkce zřejmě funguje správně, její výsledek se nicméně může lišit v případě, že některý z prvků původního vektoru spadá do více kategorií, jako je tomu u kódu "RU", který spadá současně do Evropy i do Asie. V případě obdobného konfliktu se uplatní ta kategorie, která je uvedena jako první při čtení pseudokódu zleva:

recode(iso,"c('FR','CZ','RU')='Evropa';c('RU','VN','NP')='Asie'")
recode(iso,"c('RU','VN','NP')='Asie';c('FR','CZ','RU')='Evropa'")

Dvě užitečné poznámky se týkají využití formulky "else" pro všechny doposud nenadefinované hodnoty vektoru var a zdůraznění, že hodnoty NA lze zpracovat zcela totožným způsobem jako textové nebo číselné hodnoty:

iso<-c("FR","VN","RU","CZ","NP",NA)
recode(iso,"c('FR','CZ','RU')='Evropa';c('RU','VN','NP')='Asie'")
recode(iso,"c('FR','CZ','RU')='Evropa';c('RU','VN','NP',NA)='Asie'")
recode(iso,"c('FR','CZ','RU')='Evropa';else='Asie'")

Na závěr uveďme příklad zpracování číselného vektoru (použijeme zde již výše nadefinovanou datovou tabulku slouceniny, resp. její sloupec hmotnost. Zde je možné s výhodou využít rozsahů definovaných pomocí znaménka dvojtečky ":", užitečnou maličkostí je rovněž využití kódů "lo" a "hi" pro nejvyšší a nejnižší prvek vektoru, které tak není třeba složitě hledat a zadávat do pseudokódu pomocí funkce paste0():

slouceniny<-data.frame(vzorec=c("H3PO4","Ca(OH)2","KCl","H4SiO4","HPO3","NaHCO3","I2","KCN","H2S2O7"),
nazev=c("k. trihydrogenfosforečná","hydroxid vápenatý","chlorid draselný","k. tetrahydrogenkřemičitá","k. fosforečná","hydrogenuhličitan sodný","jód","kyanid draselný","k. disírová"),
trivialni=c("k. ortofosforečná","vápno","drasloš","k. ortokřemičitá","k. metafosforečná","jedlá soda",NA,"cyankáli",NA),
hmotnost=c(98.0,74.1,74.6,227.0,80.0,84.0,126.9,65.1,178.1))
attach(slouceniny)
recode(hmotnost,"0:100='lehká';100:200='střední';200:300='těžká'")
recode(hmotnost,"lo:100='lehká';100:200='střední';200:hi='těžká'")


Cvičení

  1. Pracujte se souborem incidence.xlsx. Načtete tento soubor do datové tabulky pojmenované incidence a zkontrolujte, zda je její obsah korektní (zejména odstraňte chyby v kódování češtiny).
  2. Připojte tabulku pomocí funkce attach() pro práci s jejím jediným sloupcem - textovým vektorem incidence.
  3. Zjistěte, zda se někde ve vektoru choroba vyskytuje písmeno „ň“.
  4. Nalezněte pomocí funkce which() prázdné prvky ve vektoru choroba a uložte jejich pořadová čísla do vektoru prazdne1.
  5. Nalezněte pomocí funkce grep() a vhodného regulárního výrazu (využít můžete např. začátky a konce řádků) prázdné prvky ve vektoru choroba a uložte jejich pořadová čísla do vektoru prazdne2.
  6. Porovnejte vektory prazdne1 a prazdne2. Využít můžete operátor == a posléze najít minimum vytvořeného logického vektoru.
  7. Vyjměte z datové tabulky incidence řádky obsahující ve sloupci choroba prázdné prvky. Pokud se přitom datová tabulka změní na vektor, konvertujte jej zpět na datovou tabulku o jediném sloupci.
  8. Protože se datová tabulka incidence změnila, je třeba ji odpojit a znovu připojit pomocí funkcí detach() a attach().
  9. Nahraďte ve vektoru choroby všechny řetězce "St.p." řetězcem "Status praesens ".
  10. Nahraďte ve vektoru choroby všechny výskyty tečky, za kterými nenásleduje mezera - vyjma teček na konci řetězce - tečkami, za kterými následuje mezera.
  11. Nalezněte všechny prvky vektoru choroba, které obsahují číslice a vypište je.
  12. Nalezněte všechny prvky vektoru choroba, které obsahují pouze číslice a uložte jejich pořadová čísla do vektoru zuby.
  13. Připojte na začátek prvků s pořadím vyšším než první zub a nižším než druhý zub číselné oéznačení prvního zubu („18“) a mezeru („ “). Poté opakujte postup i pro další zuby (k tomu můžete využít efektivně znalost for cyklu, který je popsán v šestém cvičení.
  14. Překódujte všechny prvky vektoru choroba rovné hodnotě "Mrtvý zub" na 1 a ostatní hodnoty na 0. Spočtěte, kolikrát se vyskytovala hodnota "Mrtvý zub".
 
Článek ze dne 27. 12. 2018 byl naposledy upraven dne 9. 1. 2019 a zobrazen celkem 982×, naposledy dne 20. 9. 2019 v 18:04.
 
 

Komentáře:

Jméno autora:
Email (nebude zveřejněn):
Komentář:
Sem napiš slovo Adamov:




Stránka:
 
Citace: Kalina, J., Sloupová, K., Vérteši, M., Správným směrem [online]. Jiří Kalina, 2014 [cit. 2019-09-21]
Dostupné z: http://spravnym.smerem.cz/Tema/4.%20cvi%C4%8Den%C3%AD.
 
Desktopová verze | Mobilní verze