As frequências relativas de cada uma das letras no conjunto de palavras de uma língua constituem uma característica única dessa língua.
Sabemos, por exemplo que a letra a tem uma frequência relativa de 14.6% no português mas cerca de 6.0% no alemão.
Se conhecermos estas frequências relativas para várias línguas, um vector de percentagens para cada língua, basta-nos calcular o vector de frequências relativas das letras do texto que pretendemos analisar e tentar identificar de qual dos vectores de língua mais se aproxima.
É o que pretendo fazer, sem fazer uso de nenhum tipo de repositório lexical (dicionário).
O exercício será executado em linguagem R mas não tem nada de particular, aconselhado ou desaconselhado, que impeça a sua realização noutra linguagem qualquer.
Frequência de letras de idiomas
O repositório de dados que vai ser utilizado é algo reduzido mas suficiente para o conceito que se pretende demonstrar. Representa 15 idiomas (inglês, francês, alemão, espanhol, português, italiano, turco, sueco, polaco, neerlandês, dinamarquês, islandês, finlandês, checo e húngaro) e pode ser encontrado na Wikipedia no verbete Letter frequency.
A extração da tabela foi feita de forma manual, simples copy/paste
com edição também manual mínima, ou até nenhuma, se a seleção for feita com cuidado.
Embora a tabela inclua todas as letras usadas nos idiomas representados, incluindo caracteres acentuados ou específicos, vão ser usadas apenas as letras de [a-z].
Os dados obtidos assim diretamente da página constituem linhas de valores separados por TAB, pelo que o ficheiro resultante é um ficheiro de texto.tsv (tab separated values).
Note-se que os valores são representados de maneira formatada para leitura, em pontos percentuais e incluindo o caractere %. Alguns valores incluem ainda outro tipo de caracteres não numéricos como '(', ')', '~' e '*', assim como a linha de título inclui algumas indicações de notas de rodapé [dd]; tudo isto será retirado no script de forma automatizada.
O processo
1) Ler o conteúdo do ficheiro
Para tal vai usar-se a package readr (Read Rectangular Text Data) que contém variadas funções de leitura de ficheiros de texto em vários formatos, nomeadamente o tsv.
library(readr)
df <- readr::read_tsv("data/letter_frequencies.tsv", col_names = TRUE)
O que resulta numa estrutura do tipo data-frame com o seguinte aspecto:
2) Limpeza e normalização de dados
Como já referido, há caracteres que devem ser retirados. Para tal vai usar-se a substituição por expressão regular gsub.
Primeiramente a linha de cabeçalho
colnames(df) <- gsub('([a-zA-Z]*)(.*)', '\\1', colnames(df))
De seguida uma função geral de retirada dos caracteres %, ~, ( e )
clean_values <- function(data) {
as.numeric(gsub('([0-9\\.]*)([%~\\*\\(\\)])', '\\1', data))
}
aplicada através da função sapply às colunas de valores df[2:ncol(df)]
df <- data.frame(df[1], sapply(df[2:ncol(df)], clean_values))
Após estas operações, o data-frame de frequências relativas por idioma fica com o seguinte aspecto:
3) Analisar o texto de que queremos identificar o idioma
O objectivo aqui é calcular um vector de frequências relativas das letras presentes no texto a analisar.
Começamos então por calcular as frequências absolutas:
letter_count <- (
text
%>% tolower
%>% strsplit(split="")
%>% unlist
%>% `[`(!. %in% c("", " ", ".", ","))
%>% table
%>& data.frame)
Nota: usamos aqui o operador
forward-pipe %>% apenas como curiosidade; é possível executar as mesmas operações usando a idiomática funcional típica.
text %>% tolower é equivalente a
tolower(text).
Aplicando então à frase "as armas e os baroes assinalados que da ocidental praia lusitana" resulta no data-frame:
Precisamos de tornar este
data-frame comparável com os vectores de frequências dos idiomas pelo que temos de incluir nele também as letras que não fazem parte do texto.
Para tal usamos a função
merge que faz a junção de dois vectores (de forma semelhante ao JOIN do SQL) com a opção de incluir todos os valores do vector de letras dos idiomas (uma espécie de LEFT JOIN em SQL).
merge(x = frequency_df[1], y = letter_count, by = c("Letter"), all.x = TRUE)
com o resultado:
letter_count[is.na(letter_count)] <- 0
e calculadas as frequências relativas
letter_count$Percentage <- 100 * letter_count$Frequency /
sum(letter_count$Frequency)
obtemos o vector de frequências relativas que será usado para comparação com os vectores de idioma; a coluna Percentage do data-frame:
A coluna
Percentage é directamente comparável com as colunas de idioma, já que tem o mesmo número de valores e dispostos pela mesma ordem (alfabética das letras respectivas), pelo que podemos calcular a distância entre cada uma destas e aquela e verificar então qual dos idiomas se encontra mais próximo.
Notar que cada um destes vectores de 26 valores contém, nem mais nem menos, do que as coordenadas de um ponto de um hipotético espaço 26-dimensional, pelo que é aplicável qualquer uma das fórmulas típicas de distância.
com a instrução
t(data.frame(lapply(df[2:ncol(df)], manhattan, letter_count$Percentage)))
em que se verifica que idioma mais próximo é o Português.
4) Teste
Partimos então da frase em português "as armas e os baroes assinalados que da ocidental praia lusitana" a qual traduzimos com o google translator para cada um dos restantes 14 idiomas (sem preocupações poéticas):
phrase_pt <- "as armas e os baroes assinalados que da ocidental praia lusitana"
phrase_en <- "the weapons and barons marked from the western Lusitanian beach"
phrase_tk <- "Batı Lusitanya sahilinden işaretlenmiş silahlar ve baronlar"
phrase_fr <- "les armes et les barons marquaient celui de la plage ouest de la Lusitanienne"
phrase_de <- "Die Waffen und Barone markierten das vom westlichen lusitanischen Strand aus"
phrase_es <- "las armas y los barones marcaron eso desde la playa lusitana occidental"
phrase_it <- "le armi e i baroni lo segnavano dalla spiaggia lusitana occidentale"
phrase_se <- "vapnen och baronerna markerade det från den västra lusitanska stranden"
phrase_pl <- "broń i baronowie oznaczali to z zachodniej plaży Lusitanii"
phrase_nl <- "de wapens en baronnen markeerden dat vanaf het westelijke Lusitaanse strand"
phrase_dk <- "våbnene og baronerne påpegede, at der fra den vestlige lusitanske"
phrase_is <- "vopnin og barónarnir merktu það frá vesturhluta Lusitanian ströndinni"
phrase_fi <- "aseet ja paronit merkitsivät sen Länsi-Lusitanian rannalta"
phrase_ck <- "Zbraně a baroni označili,To ze západní pláže Lusitana"
phrase_hu <- "A fegyverek és a bárók megjelölték, A nyugati Lusitana strandé"
agrupadas numa estrutura apropriada:
test_df <- data.frame(
rbind(
c("English", phrase_en),
c("French", phrase_fr),
c("German", phrase_de),
c("Spanish", phrase_es),
c("Portuguese", phrase_pt),
c("Italian", phrase_it),
c("Turkish", phrase_tk),
c("Swedish", phrase_se),
c("Polish", phrase_pl),
c("Dutch", phrase_nl),
c("Danish", phrase_dk),
c("Icelandic", phrase_is),
c("Finnish", phrase_fi),
c("Czech", phrase_ck),
c("Hungarian", phrase_hu)))
colnames(test_df) <- c("Idiom", "SampleText")
e encapsulamos numa função as operações referidas acima:
idiom_inference <- function(text, frequency_df, distance_fn) {
# getting a data frame of the absolute frequencies of individual letters
# present on `text`
letter_count <- (
text
%>% tolower
%>% strsplit(split="")
%>% unlist
%>% `[`(!. %in% c("", " ", ".", ","))
%>% table
%>% data.frame)
# giving the letters data frame a proper nice structure
colnames(letter_count) = c("Letter", "Frequency")
# and merging it with the original all-letters columns
# in order to get a complete vector
letter_count <- merge(x = frequency_df[1], y = letter_count, by = c("Letter"), all.x = TRUE)
# letter missing in `text` get a NA value so we transform that in 0
letter_count[is.na(letter_count)] <- 0
# and calculate the relative frequency
letter_count$Percentage <- 100 * letter_count$Frequency / sum(letter_count$Frequency)
# finally we apply the distance function between out text relative frequency vector
# to each of the idioms relative frequency
distances <- t(data.frame(lapply(frequency_df[2:ncol(frequency_df)], distance_fn, letter_count$Percentage)))
# and return then rowname, which is the idiom name, of the lesser
rownames(distances)[which(distances == min(distances), arr.ind=TRUE)[,1]]
}
que se executa com:
infered <- lapply(X=test_df$SampleText, FUN=idiom_inference, df, abdiv::manhattan)
ontem os seguintes resultados:
5) Conclusões
A eficiência não é total já que falhou dois idiomas, "confundindo" dinamarquês com holandês e checo com italiano.
Para tal poderá ter contribuído a relativamente reduzida dimensão do texto bem como a não consideração de caracteres acentuados ou específicos em alguns idiomas, como é precisamente o caso do dinamarquês e do checo embora não se tenha confundido no húngaro em que tal também se verifica.
Não sabemos também como se comportará com outro tipo de textos, mais longos ou mais curtos, pelo que há espaço para evolução.
Espero que tenham gostado do exercício e parece que já estou a ouvir: "em Python faz-se em 3 linhas".
Venham elas.
6) Código fonte
O projecto encontra-se no GitHub.