Увод в обработката на данни с Raku

Въведение

Една от настоящите ми житейски мисии е да ускоря идването на следващата Зима на Изкуствения Интелект (ИИ). Решил съм да използвам Raku, за изпълването на тази мисия. (Е, поне в началото…)

Искам да отбележа, че считам термина “изкуствен интелект” да е една брилянтна маркетингова фраза за извличане на пари от военния комплекс на САЩ и всякакви финансови инвеститори. Когато хора, които твърдят, че са, да речем, “професионалисти в областта на данновата наука” използват тази фраза в професионална среда, аз ставам силно подозрителен към техния “професионализъм”. (Ползвайки тази фраза те се бележат като вероятни набедени подражатели.)

Но нека да се върнем към ускоряването на идването на следващaта зима на ИИ. Ето го моят план:

  1. Научаването на много хора как да бъдат самозвани даннови учени
  2. Изработване на програмни инструменти, които да улеснят баристите на ИИ-кодове
  3. Изчакване на достатъчно много ИИ-хора да се ударят в достатъчно много ИИ-стени
  4. Настъпване на следващата ИИ зима
  5. Печалба

Вижте презентацията “Raku for Prediction”, [AAv2], която описва моите усилия по точка 2 с помощта на Raku.

Дългата версия на точка 3 е “изчакайте ИИ инвеститорите и мениджърите да се ударят в достатъчно много стени, използвайки голям, безталантен контингент от практикуващи ИИ”.

Печалбата в точка 5 идва от това, че “ИИ полето” е “изчистено”, следователно по-малка конкуренция за получаване на инвестиции и финансиране. (Дори след това уточнение някои читатели все още могат да смятат, че по-горният ми списък е твърде сходен с този, измислен от гномите в South Park’s “Gnomes”; и това въобще не ме притеснява.)

Повечето даннови учени (“data scientists”), прекарват по-голямата част от времето си в събиране и обработка на данни. Не в данново анализаране (сиреч, статистика), или изкуствения интелект, или каквато и да е там “наистина научна” работа… Така че, ако сериозно искам да повлияя на кривите на развитие на ИИ, тогава трябва да се заема сериозно с повлияването на формолировките на заклинания за събиране и преобразуване на данни на различни езици за програмиране; [AAv2]. Тъй като аз твърдо вярвам, че от време на време е добре човек да яде храната, която готви, аз програмирах (в края на 2021) пакети за обработка на данни на Raku. Кой знае, това може да е начин за ракунизираме на изкуствения интелект и – да перифразирам Лари Уол – някои потребители могат да получат своята поправителна доза. (Вижте също [FB1].)

Що се отнася до събирането на данни – аз развивам проекта Data Acquisition Engine project, [AAr1, AAv6], който има разговорен агент за генериране на код чрез Raku пакети, [AAp7]. За да можем да обсъждаме и да дадем примери за обработка на данни, трябва да използваме определени функционалности за добиване на данни. Поради тази причина, по-долу са дадени обяснения и примери за използване на Raku пакети за генериране на случайни даннови таблици и за извличане на популярни, учебни, добре познати таблици (или масиви) от данни.

Този документ е доста техничен – читателите могат просто бегло да прочетат следващата секция, да прочетат секцията “Правим го като Cro”, и да приключат (с четенето). Някои може да искат да погледнат и прегледат супертехническата версия “Data wrangling with Raku”, [AA1].

Забележка: Понякога кодът по-долу може да има израза на Raku ==>encode-to-wl(). Това е за сериализиране на Raku обекти като изрази на Wolfram Language (WL).

Забележка: Този документ е написан като тетрадка (“notebook”) на Mathematica.

Забележка: Целевата аудитория на този документ се състои предимно от хора, запознати с културите и култовете на Perl и Raku. Но, по-голямата част от този документ би трябвало да е достъпна и да представлява интерес и за не-перлирани програмисти или даннови учени.

Забележка: Както може да се види в (дългия, на английски) видео-запис на презентацията “Многоезичен разговорен агент за събиране на данни (разширена версия)”, [AAv6], на един писан на Raku разговорен агент за добив на данни ще са му значително от полза възможности на Raku за обработка на данни.

Забележка: Изречения с “универсални”, проверими или възпроизводими твърдения и кодове използват формата “ние”. Изявления с лични мнения и решения на автора използват формата “аз”. Алтернативно, просто използвах каквато форма ми се стори, че е по-лесна или по-естествена за писане. (С цел “бързото” написване на оригиналната статия на английски език.)

Речник

Този документ е превод (от английски на български) на документа “Introduction to Data Wrangling with Raku”.

Преводът на някои понятия и фрази от английски на български е, може би, спорен. (И, може би, напълно раздразнителен за някои читатели.) Поради тази причина прилагам речник на технически думи и фрази в тази под-секция.

  • Даннов масив : Dataset
  • Даннова наука : Data Science
  • Даннова рамка : Data frame
  • Даннови учени : Data Scientists
  • Данново анализиране : Data analysis
  • Добив на данни : Data acquisition
  • Изкустен Интелект (ИИ) : Artificial Intelligence (AI)
  • Канал : Socket
  • Латентно Семантичен Анализ (ЛСА) : Latent Semantic Analysis (LSA)
  • Масив : Array
  • Мемоаризация : Memoization
  • Модулни тестове : Unit tests
  • Обработка на данни : Data wrangling
  • Регулярен израз : Regex
  • Самозвани даннови учени : Data scientist impostors
  • Специфичен за домейн език : Domain-Specific Language (DSL)
  • Събиране на данни : Data acquisition
  • Тетрадка : Notebook
  • Уеб : Web, (World Wide Web)
  • Хеш : Hash
  • Хранилище : Repository

Програмните езици за изписани на английски език, но нека да посочим българските съответстия:

  • Волфрамски Език : Wolfram Language
  • Математика : Mathematica
  • Пайтон : Python
  • Пърл : Perl
  • Раку : Raku

Забележка: Вижте също секцията “Поправяне на телефона” за допълнителни пояснения. (“Бележки на преводача”.)


楽-за ÷ чрез-楽

или “Раку-за” срещу “чрез-Раку” (също така, може би, “хармонизаране на” срещу “с хармония”.)

Първо, нека направим следното разграничение:

  • Raku за обработка на данни означава използване на Raku за улесняване на обработката на данни в други програмни езици и системи.
  • Обработка на данни чрез Raku означава използване на структурите от данни и езика за програмиране на Raku за работа с данни.

Ето един пример за използване на Raku за обработка на данни – генерира се код на Python:

use DSL::Shared::Utilities::ComprehensiveTranslation;
ToDSLCode("
dsl target Python-pandas; 
зареди таблицата iris; 
групирай по колоната Species;
покажи размерите", language=>'Bulgarian'):code;

# obj = example_dataset('iris')
# obj = obj.groupby(["Species"])
# print(obj.size())

Следва пример за обработка на данни чрез Raku:

my $obj = example-dataset(/ iris $ /);
$obj = group-by($obj, "Species");
say $obj>>.elems

# {setosa => 50, versicolor => 50, virginica => 50}

Следната диаграма:

  • Обобщава моите дейности по обработка на данни
  • Обозаначава бъдещи планове с прекъснати линии
  • Представя с шестоъгълника усилията “Raku-за”
  • Представлява в долния десен ъгъл усилията “с Raku”.

За да илюстрираме многоезичието на подхода, ето пример за превод на български спецификации за обработване на данни към английски и руски спецификации за обработване на данни:

for <English Russian> -> $l {
    say '-' x 30;
    say ToDSLCode("dsl module DataQuery; dsl target " ~ $l ~ ";
    зареди таблицата iris; 
    филтрирай чрез Sepal.Width е по-голямо от 2.4;
    групирай чрез колоната Species;
    покажи размерите", language=>'Bulgarian'):code
}

# ------------------------------
# load the data table: "iris"
# filter with the predicate: ((Sepal.Width greater than 2.4))
# group by the columns: Species
# show the count(s)
# ------------------------------
# загрузить таблицу: "iris"
# фильтровать с предикатом: ((Sepal.Width больше 2.4))
# групировать с колонками: Species
# показать число

Както показва горната диаграма, аз възнамерявам с конструкцията в нея да обясня (и разкажа) Raku кодовете за обработка на данни чрез естествени езици. Също така, разбира се, да превеждам от Raku към други програмни езици.

Забележка: Дълго време използвах принципа “дрехите нямат император”, [AAv1, CARH1], който, разбира се, принадлежи към подхода “Raku-за”. Чрез снабдяването на Raku с (i) възможности за обработка на данни, и (ii) възможности за генериране на Raku код за обработка на данни, бих казал, че тези дрехи могат да бъдат подходящо облечени. И обратно – децата на обущаря ще щъкат наоколо подходящо обути.

Забележка: И една друга надежда имам: наблюдението “дрехите правят човека” ще бъде напълно приложимо към даннови пощрапулници и самозванци комбинирани с тази система.


Datum fundamentum

или “Структури от данни и методология” (Също така на латински означава “дадена основа”.)

Тази секция дава дефиниции за основните понятия на нашия подход за обработка на данни в Raku.

Даннов масив срещу Даннова рамка

Ето някои интуитивни определения на “даннови масиви” (“datasets”) и “даннови рамки” (“data frames”):

  • Даннов масив е таблица, която като структура от данни най-естествено се интерпретира като масив от хешове, като всеки хеш представлява ред в таблицата.
  • Даннова рамка е таблица, която като структура от данни най-естествено се интерпретира като масив от хешове, като всеки хеш представлява колона в таблицата.

Mathematica използва даннови масиви. S, R и пакета pandas от Python използват даннови рамки.

Raku системата, представена в този документ, използва даннови масиви. Ето пример на даннов масив с 3 реда и 2 колони:

srand(128);
my $tbl=random-tabular-dataset(3,2).deepmap({ $_ ~~ Str ?? $_ !! round($_, 0.01) });
.say for |$tbl

# {controlling => -4.84, unlace => means}
# {controlling => 7.83, unlace => thyrotropin}
# {controlling => 11.92, unlace => parfait}

Ето структурата на съответната даннова рамка:

transpose($tbl)

# {controlling => [-4.84 7.83 11.92], unlace => [means thyrotropin parfait}]}

Минималистична перспектива

Аз не искам да създавам специален тип (клас) за даннови масиви или даннови рамки – искам да използвам стандартните структури от данни на Raku. (Поне на този етап от работата ми с данни в Raku.)

Причините за това са:

  1. Данните могат да се разберат и обработят с типични, вградени команди.
    • Тоест, без придържане към определена методология за обработка на данни или специализирани пакети.
  2. Използването на стандартни, вградени структури е вид решение за “потребителски интерфейс”.
  3. Добро “потребителско изживяване” може да се постигне или осигури чрез други парадигми и пакети за обработка на данни.

Точки 2 и 3 са, разбира се, последици от точка 1.

Структури от данни в Raku

Структурите от данни, върху които се фокусираме, са даннови масиви, а конкретно в Raku имаме следните представяния на даннови масиви:

  1. масив от хешове;
  2. хeш от хешове;
  3. масив от масиви;
  4. хeш от масиви.

Редът в по-горният списък отговаря на приоритетите по време на имплементирането на съответните функционалности:

  • Функционалностите за първите две структури са основни и имат модулни тестове (“unit tests”).
  • Освен това, ние се съобразяваме с използването на последните две структури.

Когато системата и съзвездието от функционалности за обработка на данни узреят, всичките четири структури от данни ще се третират по правилен и непротиворечив начин.

Целеви потребители

Целевите потребители са даннови учени (на щат, на непълно работно време, или напълно начинаещи), които искат:

  • да обработват данни от типични даннови масиви от данновата наука с Raku;
  • да знаят, че усилията им за обработка на данни в Raku се възпроизвеждат сравнително лесно в други програмни езици или системи.

Алтернативно, можем да кажем, че целевите потребители са:

  1. Хора лъжещи, че са даннови учени
  2. Баристи на кодове
  3. Опитни даннови учени, които искат да ускорят работата си
  4. Даннови учени, които искат да научат Raku
  5. Програмисти на Raku.

Разглеждани работни процеси

Следващата блок-схема обхваща работните процеси за обработка на данни, които разглеждаме:

Забележка: Методологията, представена в блок-схемата по-горе, ще наричаме Модел на работните процеси за обработка на данни и ще използваме съкращението “DTWM” на съответния английски израз “Data Transformation Workflows Model”. В настоящия документ разглеждаме тази методология и блок-схемата като синоними.

Ето някои свойства на методологията (блок-схемата):

  • Блок-схемата е за даннови масиви или за списъци (масиви) или речници (хешове) от даннови масиви.
  • В блок-схемата само зареждането на данните и обобщеният анализ не са по избор.
  • Всички други стъпки не са задължителни.
  • Трансформации като вътрешни съединения (“inner joins”) са представени чрез блока “Комбинирай групи”.
  • Предполага се, че в реалните приложения трябва да се извършат няколко итерации (цикли) върху блок-схемата.

В света на езика за програмиране R оранжевите блокове представляват така наречения модел Разделяне-Трансформиране-Съединяване (“Split-Transform-Combine”); вижте статията [“The Split-Apply-Combine Strategy for Data Analysis”] (https://www.jstatsoft.org/article/view/v040i01) от Хадли Уикам (Hadley Wickham), [HW1].

Забележка: R беше (и вероятно все още е) доста непоследователен и труден език за програмиране, така че изричното въвеждане на модела Разделяне-Трансформиране-Комбиниране е от голяма полза за програмистите на R. От друга страна, този модел е доста стар и добре познат: той е присъщ на SQL и е основен в паралелното програмиране. (Например, вижте WL функцията ParallelCombine, [WRI2].)

Тук представяме един прост сценарий за проиграване по тази блок схема:

  1. Получете табличени данни от данново хранилище.
  2. Обобщете и разгледайте набора от данни и решете, че той няма желаната форма и съдържание.
    • Т.е. данните трябва да бъдат обработени.
  3. Изберете само колоните, които съдържат данните, които ви интересуват.
  4. Филтрирайте редовете в съответствие с определен оперативен критерий.
  5. Разделете редовете – т.е. групирайте ги по стойностите в една от колоните.
  6. Преобразувайте всяка група, като комбинирате по някакъв начин стойностите на всяка колона.
    • Например намиране на средни стойности или стандартни отклонения на цифрови колони.
  7. Комбинирайте трансформираните групи (в една “плоска” таблична съвкупност от данни).
  8. Преобразувайте данните в дълъг формат и ги експортирайте.

Да повторим, горният списък от стъпки е само един от възможните работни процеси. За по-подробни примери и обяснения вижте [AA1, AAv2, AAv3, AAv4].

Основни операции

Тези операции са основни:

  • Избор на колони, преименуване и изтриване
  • Избор и изтриване на редове
  • Транспониране
  • Вътрешни, леви, и десни съединения
  • Групиране (или разделяне) по критерии.

(Транспонирането на таблични данни, матрици или пълни масиви, е също основна операция във функционалното програмиране.)

При обработката и анализа на данни следните три операции не са основни, но все пак са много важни и можем да кажем, че са фундаментални на “макро-ниво”:

За повече подробности вижте [AA1, Wk1, Wk2, AAv1-AAv4].

Забележка: Пакетът “Data::Reshapers”, [AAp2], предоставя всички функции, споменати в тази секция.


Размерът на магическите данни не определя колко магически са те

или “Придобиване на добре познати набори от данни в Raku”.

Трябва да имаме достъп до някои типични набори от данни, използвани в учебните часове по статистика, или в книги и пакети, които илюстрират концепции от областта на статистиката или обясняват свързаните с тях софтуерни проекти и ноу-хау. Също така, разбира се, имайки под ръка тези набори от данни би било от голяма полза за самозваните даннови учени и баристите на кодове във взаимодействието им с клиенти, инженери, или други даннови учени (били те реални или самозванци).

Raku-пакетът “Data::ExamplesDatasets”, [AAp3], предоставя функции за получаване на (сравнително добре познати, учебни) даннови масиви. Самият пакет съдържа само метаданни за данновите масиви – тези масиви се изтеглят от хранилището “Rdatasets”, [VAB1].

Тук получаваме един известен даннов масив с помощта на регулярен израз (“regex”):

my $iris=example-dataset(/iris $/);
$iris==>encode-to-wl

Ето размерите на току-що получения даннов масив:

dimensions($iris)

# (150 5)

Можем да получим URL адреса на документацията за този даннов масив, като използваме функцията item-to-doc-url:

get-item-to-doc-url()<datasets::iris3>

# "https://vincentarelbundock.github.io/Rdatasets/doc/datasets/iris3.html"

Тук използваме WL, за да покажем документацията:

WebImage[StringTrim@Out[145]]

Обобщение на колекцията от даннови масиви

Ето обобщение на данновия масив с метаданни без колоната със заглавия и колоните с URL адреси на данни и документации:

records-summary(delete-columns(get-datasets-metadata(),<Title CSV Doc>), max-tallies=>12)

# +---------------------+------------------+--------------------+------------------+--------------------+--------------------+-----------------------+--------------------+---------------------+
# | Cols                | Package          | n_factor           | Item             | n_logical          | n_binary           | Rows                  | n_character        | n_numeric           |
# +---------------------+------------------+--------------------+------------------+--------------------+--------------------+-----------------------+--------------------+---------------------+
# | Min    => 1         | Stat2Data => 211 | Min    => 0        | salinity => 3    | Min    => 0        | Min    => 0        | Min    => 2           | Min    => 0        | Min    => 0         |
# | 1st-Qu => 3         | openintro => 206 | 1st-Qu => 0        | Grunfeld => 3    | 1st-Qu => 0        | 1st-Qu => 0        | 1st-Qu => 35          | 1st-Qu => 0        | 1st-Qu => 2         |
# | Mean   => 13.021203 | Ecdat     => 134 | Mean   => 1.290544 | housing  => 3    | Mean   => 0.030372 | Mean   => 1.940401 | Mean   => 3860.679656 | Mean   => 0.311175 | Mean   => 11.338109 |
# | Median => 5         | DAAG      => 121 | Median => 0        | smoking  => 3    | Median => 0        | Median => 0        | Median => 108         | Median => 0        | Median => 3         |
# | 3rd-Qu => 9         | AER       => 107 | 3rd-Qu => 2        | Mroz     => 3    | 3rd-Qu => 0        | 3rd-Qu => 2        | 3rd-Qu => 601.5       | 3rd-Qu => 0        | 3rd-Qu => 7         |
# | Max    => 6831      | MASS      => 87  | Max    => 64       | bmt      => 2    | Max    => 11       | Max    => 624      | Max    => 1414593     | Max    => 17       | Max    => 6830      |
# |                     | datasets  => 84  |                    | titanic  => 2    |                    |                    |                       |                    |                     |
# |                     | stevedata => 71  |                    | aids     => 2    |                    |                    |                       |                    |                     |
# |                     | carData   => 63  |                    | npk      => 2    |                    |                    |                       |                    |                     |
# |                     | boot      => 49  |                    | Hitters  => 2    |                    |                    |                       |                    |                     |
# |                     | HistData  => 46  |                    | Hedonic  => 2    |                    |                    |                       |                    |                     |
# |                     | HSAUR     => 41  |                    | goog     => 2    |                    |                    |                       |                    |                     |
# |                     | (Other)   => 525 |                    | (Other)  => 1716 |                    |                    |                       |                    |                     |
# +---------------------+------------------+--------------------+------------------+--------------------+--------------------+-----------------------+--------------------+---------------------+

Ето хистограма на разпределението на броя на редовете в учебните данновите масиви (данните са получени в Raku, хистограмата е изчертана с WL):

select-columns(get-datasets-metadata(),"Rows")
==>transpose()
==>encode-to-wl()
Histogram[Log10@Normal[%["Rows"]], PlotRange -> All, PlotTheme -> "Detailed", FrameLabel -> {"lg of number of rows", "count"}, PlotLabel -> "Distribution of the number of rows of the example datasets"]

Стойностите на графиката по-горе са логаритми с основа 10. Виждаме, че по-голямата част от данновите масиви имат между 10 и 1000 реда, което се “потвърждава” и от обобщената таблица по-горе.

Идентификатори на даннови масиви

Идентификаторът на даннов масив се състои от име на пакет и име на елемент, разделени с "::". Както може да се види в обобщаващата таблица по-горе, един пакет може да има няколко елемента и едно и също име на елемент може да бъде намерено в няколко пакета. Поради тези причини, при някои спецификации на даннови масиви функцията example-dataset, без да извлича данни, дава предупреждение, че има множество отговарящи масиви.

Например:

example-dataset(/ .* smoking .* /)

#ERROR: Found more than one dataset with the given spec: 
#ERROR: openintro::smoking  https://vincentarelbundock.github.io/Rdatasets/csv/openintro/smoking.csv
#ERROR: HSAUR::smoking  https://vincentarelbundock.github.io/Rdatasets/csv/HSAUR/smoking.csv
#ERROR: COUNT::smoking  https://vincentarelbundock.github.io/Rdatasets/csv/COUNT/smoking.csv(Any)

Тук извличаме конкретен даннов масив, като използваме идентификатор, който се състои от името на пакета и името на елемента. (разделени с “::”):

example-dataset('COUNT::smoking')
==>to-pretty-table()

# +---+--------+-----+------+-----+
# |   | smoker | age | male | sbp |
# +---+--------+-----+------+-----+
# | 1 |   1    |  34 |  1   | 131 |
# | 2 |   1    |  36 |  1   | 132 |
# | 3 |   0    |  30 |  1   | 122 |
# | 4 |   0    |  32 |  0   | 119 |
# | 5 |   1    |  26 |  0   | 123 |
# | 6 |   0    |  23 |  0   | 115 |
# +---+--------+-----+------+-----+

Мемоаризация (“memoization”)

Основната функция на пакета, example-dataset, има наречието keep. Ако това наречие е дадено, тогава example-dataset съхранява извлечените от уеб данни в директорията XDG_DATA_HOME, и впоследствие ги извлича оттам. Вижте “Freedesktop.org Specifications” и [JS1] за повече подробности относно това, каква е конкретната стойност на променливата XDG_DATA_HOME във вашата операционна система.


ГСДМ

или “Генериране на собствени даннови масиви”. (Не “Гледай си досадната мастия”.)

Вместо извличане на учебни данни и разрешаването на потенциално свързани проблеми, като например извличането им или просто намирането на един или два, или пет даннови масива, които отговарят на това, с което искаме да експериментираме, защо просто да не генерираме случайни даннови масиви?!

Функцията random-tabular-dataset от пакета “Data::Generators”, [AAp4], генерира случайни даннови масиви, като използва аргументи специфициращи размери и генератори.

Напълно случайни данни

Ето един пример за “напълно случаен” набор от данни:

srand(5);
random-tabular-dataset()
==>encode-to-wl

Зададени имена на колони и генератори на стойности на колони

Можем също така да генерираме случайни даннови масиви, като посочим имена на колоните и генератор на стойности за всяка колона:

srand(32);
random-tabular-dataset(10, <Task Story Epic>, generators=>{Task=>&random-pet-name, Epic=>&random-word})
==>encode-to-wl

Забележка: Колоната “Story” няма зададен генератор, затова за нея e избран случаен генератор.

Използване на генериращи множества вместо генериращи функции

Вместо да използваме функции за генераторите на колони, можем да използваме списъци от обекти: random-tabular-dataset генерира автоматично съответните функции за вземане на случайни извадки.

Тук генерираме случаен даннов масив с 10 реда, и с колоните “Eva”, “Jerry” и “Project”, като за всяка колона се задава по един малък набор от стойности:

srand(1);
my $tblWork=random-tabular-dataset(10, 
                                   <Eva Jerry Project>, 
                                   generators=>{
                                         Eva=><Task Story Epic>, 
                                         Jerry=><Task Story Epic>, 
                                         Project=>(haikunate(tokenLength=>4) xx 4).List});
$tblWork==>encode-to-wl


Обработка на данни за глупаци (справочник за останалите нас)

или “Еспресо машина за баристи на кодове” или “Генериране на кодове за обработка на данни”.

Вместо да очакваме хората да знаят как да използват определени пакети и команди на Raku за обработка на данни, защо не “просто” да генерирате кода на Raku за тях, като използвате спецификации на естествен език? Тогава добрите баристи на код могат да модифицират тези кодове според изискванията на клиента.

Тук зареждаме пакета за “глобален” превод [AAp8]:

use DSL::Shared::Utilities::ComprehensiveTranslation;

(*"(Any)"*)

Тук дефинираме команда-низ, която задава работен процес за обработка на данни:

my $command='
load dataset starwars;
replace missing with "NA";
group by homeworld;
show counts';

Тук транслираме командата към Raku:

ToDSLCode('dsl target Raku::Reshapers;'~$command):code

# my $obj = example-dataset('starwars') ;
# $obj = $obj.deepmap({ ( ($_ eqv Any) or $_.isa(Nil) or $_.isa(Whatever) ) ?? \"NA\" !! $_ }) ;
# $obj = group-by( $obj, \"homeworld\") ;
# say \"counts: \", $obj>>.elems"*)

В случай че сте любопитни, ето какво се получава от горния код:

my $obj = example-dataset('starwars') ;
$obj = $obj.deepmap({ ( ($_ eqv Any) or $_. isa(Nil) or $_. isa(Whatever) ) ?? "NA" !! $_ }) ;
$obj = group-by( $obj, "homeworld") ;
say "counts: ", $obj>>.elems

# counts: {Alderaan => 3, Aleen Minor => 1, Bespin => 1, Bestine IV => 1, Cato Neimoidia => 1, Cerea => 1, 
# Champala => 1, Chandrila => 1, Concord Dawn => 1, Corellia => 2, Coruscant => 3, Dathomir => 1, Dorin => 1,
# Endor => 1, Eriadu => 1, Geonosis => 1, Glee Anselm => 1, Haruun Kal => 1, Iktotch => 1, Iridonia => 1, Kalee => 1, 
# Kamino => 3, Kashyyyk => 2, Malastare => 1, Mirial => 2, Mon Cala => 1, Muunilinst => 1, NA => 10, Naboo => 11, 
# Nal Hutta => 1, Ojom => 1, Quermia => 1, Rodia => 1, Ryloth => 2, Serenno => 1, Shili => 1, Skako => 1, Socorro => 1,
# Stewjon => 1, Sullust => 1, Tatooine => 10, Toydaria => 1, Trandosha => 1, Troiken => 1, Tund => 1, Umbara => 1, 
# Utapau => 1, Vulpter => 1, Zolan => 1}

Забележка: С една и съща команда на естествен език можем да генерираме код за обработка на данни за много програмни езици: Julia, Python, R, Raku, Wolfram Language.

Преводачът на команди за обработка на данни (задедени с естествен език) се основава на методологията DTWM, описана в раздела “Datum fundamentum” по-горе. За по-обширни примери за използването му вижте презентацията “Multi-language Data-Wrangling Conversational Agent” (на английски език).


Правим го като Cro

или “Използване на уеб услуга, създадена на Cro, за генериране на код за обработка на данни”.

Мислейки по-нататък за професионалния живот на самозваните даннови учени и баристите на кодове, можем да предоставим уеб услуга, която превежда DSL на естествен език в изпълним програмен код. Аз реализирах такава уеб услуга чрез съзвездието от Raku библиотеки Cro. По-долу ще наричаме тази уеб услуга Cro Web Service (CWS). Вижте видеото [AAv5] за демонстрация.

Получаване на код чрез Web API

Raku код за обработка на данни

Ето един пример за използване на CWS чрез една от WL функциите за уеб взаимодействие: URLRead, [WRI3]:

command = "dsl target Raku::Reshapers; 
include setup code;
зареди таблицата iris;
групирай чрез Species;
покажи размерите";

res = Import@
    URLRead[<|"Scheme" -> "http", "Domain" -> "accendodata.net",
      "Port" -> "5040", "Path" -> "translate",
      "Query" -> <|"command" -> command, "from-lang" -> "Bulgarian"|>|>]

(*"{\"DSLTARGET\": \"Raku\",\"USERID\": \"\",\"CODE\": \"use Data::Reshapers;\\nuse Data::Summarizers;\\nuse Data::ExampleDatasets;\\n\\nmy $obj = example-dataset('iris') ;\\n$obj = group-by( $obj, \\\"Species\\\") ;\\nsay \\\"counts: \\\", $obj>>.elems\",\"SETUPCODE\": \"use Data::Reshapers;\\nuse Data::Summarizers;\\nuse Data::ExampleDatasets;\\n\",\"STDERR\": \"\",\"DSL\": \"DSL::English::DataQueryWorkflows\",\"DSLFUNCTION\": \"proto sub ToDataQueryWorkflowCode (Str $command, Str $target = \\\"tidyverse\\\", |) {*}\",\"COMMAND\": \"dsl target Raku; include setup code; load the dataset iris; group by Species; show counts\"}"*)

Тук преобразуваме JSON резултата от CWS и го показваме в табличен
вид:

ResourceFunction["GridTableForm"][List @@@ ImportString[res, "JSON"], TableHeadings -> {"Key", "Value"}]

R код за Латентно семантичен анализ

CWS предоставя код на други езици за програмиране, както и други видове работни процеси. Ето един пример за генерирането на R-код изпълняващ работен процес за латентно семантичен анализ (“latent semantic analysis”):

command = "USER ID BaristaNo12;
dsl module LSAMon;
include setup code;
създай с данните aAbstracts;
направи документ-термин матрицата;
приложи LSI функции IDF, None, Cosine;
добий 40 теми с метод SVD;
покажи таблицата на темите";

res = Import@
    URLRead[<|"Scheme" -> "http", "Domain" -> "accendodata.net",
      "Port" -> "5040", "Path" -> "translate",
      "Query" -> <|"command" -> command, "from-lang" -> "Bulgarian"|>|>]

ResourceFunction["GridTableForm"][List @@@ ImportString[res, "JSON"], TableHeadings -> {"Key", "Value"}]

Забележка: Както се вижда по-горе, на CWS могат да бъдат дадени потребителски идентификатори, което позволява допълнителна персонализация на резултатите от интерпретацията.

Получаване на код “на място”

Това е схема, която показва компонентите на системата, използвани чрез Apple’s Shortcuts:

В тази диаграма можем да проследим следните стъпки на изпълнение от Shortcuts:

  1. В тетрадка (“notebook”) на Mathematica (или във файл във VS Code) извикваме Shortcuts.
  2. Въвеждаме команди за текстова обработка.
  3. Shortcuts извиква CWS.
  4. Резултатът се връща в JSON формат към Shortcuts.
  5. Shortcuts анализира резултата от CWS.
  6. Ако парсирането е успешно:
    1. Shortcuts издава съответно съобщение;
    2. слага съответният код в клипборда.
  7. Ако парсирането не е успешно:
    1. Shortcuts издава съответно известие;
    2. показва пълния JSON изход от CWS.

(Видеото “Doing it like a Cro (Raku data wrangling Shortcuts demo)”, [AAv5], демонстрира горните стъпки чрез диктовка на английски език.)

Забележка: На английски използвайки Siri ние можем да преобразуваме диктувана реч в текст. В момента на писането на тази статия, Siri не приема диктовка на български език.


Единственият начин на правене

или “Използване на универсалността на естествения език и простотата на модела за обработка на данни”.

Принципът “има повече от един начин правене” често се оказва твърде ограничаващ или
твърде блокиращ. Според моя опит баристите на кодове и мениджърите на информационно-технологични проекти предпочитат един начин за правене на нещата. Също така, да не бъдат изложени прекалено много на Парадокса на избора. И изобщо, предпочитат се доброволното
опростяване
и предвидимата посредственост. Което нас съвсем ни устройва, тъй като ние имаме
решение, което добре служи на мало-акълните, когато са праволинейни.

Ето елементите на предложеното решение:

  • Специфичен за проблемната област естесвен език (домейн-специфичен език или DSL).
    • Породен от “стандартен” български език. (Или английски.)
  • Преводачи на този DSL към най-популярните езици за обработка на данни.
  • Адекватна и лесна за научаване методология за обработка на данни.
  • Предполжението, че генерираните програмни кодове са “добри отправни точки”.
    • Сиреч, очаква се, че кодовете ще бъдат допълнително натъкмени от потребителите в съответствие с желаните резултати.

Можем да перифразираме и обобщим горния списък по следния начин:

  • Бързото специфициране на работни процеси за обработка на данни се
    постига чрез използване на абстрактно граматичмо представяне на
    командите на естествения език за обработка на данни, което позволява да
    се създават различни преводачни имплементации към всеки представляващ
    интерес език за програмиране.

Нека също така да посочим, че предложеното DSL решение за обработка на данни не обхваща всички възможни процеси за обработка на данни. Но, аз твърдя, че за колекции от таблични данни ние можем до голяма степен да представим по стандартизиран и достатъчно прост начин всички сложни процеси за обработка на данни.

Да перефразирам, предполагам, че 60÷80 % от вашите работни процеси за обработка на данни могат да бъдат сборени с това решение. (Да, YMMV и принципът на Парето в комбинация.)

Позоваване към авторитети

Разбира се, добре е да обосновем предложеното решение, като посочим как то съвпада с твърденията на определени авторитетни фигури. Да речем, Лари Уол или Лао Дзъ.

Лари Уол

В интервюто с Лари Уол, поместено в книгата “Masterminds of programming”, [FB1], Лари Уол описва Perl 6 (Raku) като:

[…] Той ще бъде снабден с копчета за регулиране на многото му
различни измерения, включително възможността да скриете всички онези
измерения, за които в момента не ви е интересно да мислите, в зависимост
от това коя парадигма ви допада за решаване на текущия проблем.

Решението за обработла на данни чрез DSL се придържа към твърдението на Лари:

  • Многото му измерения са операторите и синтактичните елементи на различните езици за програмиране и свързаните с тях библиотеки.
  • Скриването на тези измерения се постига чрез използване на спецификации на DSL породен от естествен език, които генерират изпълним код за тези измерения.
  • Дизайнът на граматики-и-действия внедрен в Raku осигурява възможността за скриване на измеренията.

Лао Дзъ

Подходът може да бъде допълнително обоснован чрез позоваване към книгата на Лао Дзъ “Дао Дъ Дзин”, Книга 1, глава 11.

Ето превод на тази глава (превод на Ленин Димитров):

Тридесет спици обхващат главината;
в нейната вътрешна празнота
е полезността на колелото.

От глината правят прибори;
в тяхната вътрешна празнота
е пригодността на съда.

На дома пробиват врати и прозорци,
в неговата вътрешна празнота
е полезността на стаята.

Ето защо
битието е полезно,
небитието — пригодно.

Ето някои точки, които изясняват как решението за обработка на данни чрез DSL може да се разглежда като проява на изложения по-горе принцип:

  • Ние използваме даден жаргон не само заради “готиния” начин на произнасяне на думите му, но и заради нещата които не трябва да обясняваме, като използваме жаргона.
    • Тоест “готините думи” оформят жаргона, но полезността на жаргона идва от това, което не е в изреченията му.
  • DSL породен от естествен език за обработка на данни позволява бързо специфициране на съответните работни процеси на различни езици за програмиране, защото:
    • Съществува абстрактен, до голяма степен универсален модел на работните процеси за обработка на данни. (DTWM е обяснен по-горе.)
    • Естественият език скрива много семантични и синтактични детайли на езиците за програмиране.
  • “Небитието – пригодно” се постига чрез използването на естествения език: премахват се синтаксиса, непоследователността, и неестествеността на езиците за програмиране.
  • “Тридесет спици обхващат главината” са представени от множеството езици за програмиране и библиотеки към които се превежда DSL.

Забележка: Аз използвах подобна обосновка за превода на изрази от Mathematica към High Performance Fortran (HPF). Вижте [AA3, AAn3]. По принцип “небитиетата” в изразите на функционалния език на Mathematica и този на HPF са донякъде сходни, и това позволи да се напише транслатор от Mathematica към HPF.


Тежък мозък вместо леко сърце

или “Ръководството на потребителя за преоразуване на данни е доста скучно”.

Аз имах намерение да публикувам този документ в “Raku Advent Calendar”. (Не се случи.)

Един от организаторите на “Raku Advent Calendar”, след като видя една от моите съвсем първоначални чернови, ме помоли да напиша нещо “по-леко”. (В сърдечен смисъл.)

(Няма да казвам имена, но ще кажа, че той използва инициалите “JJ” и е написал готварска книга за Raku).

Е, този документ е моят лек вариант на това, което исках да кажа за усилията си да даря екосистемата на Raku с възможности за обработка на данни, които приличат на подходите в други, добре познати системи.

Оригиналната, “тежко-мозъчна” версия е [AA1]. Тежко-мозъчната версия събира всички обяснения и примери за използване, дадени в README файловете на Raku пакетите [AAp1 ÷ AAp4].

Забележка: Има специализаран проект “Advent of Raku 2021”, който е свързан с по-общия проект “Advent of Code”.

Приказка за вълка, овена, и миещата мечка

или “Свързване на Raku с Mathematica”.

Кратката версия на приказката е следната:

Вълкът, овенът и миещата мечка разговаряли през едно каналче. Каналчето е било предоставено от ZeroMQ. (Край.)

Дългата версия е дадена в “Connecting Mathematica and Raku”, [AA2]. Ползвайки ZeroMQ и Raku-to-WL сериализатор, [AAp6], ние можем да изпълняваме команди на Raku в тетрадки на Mathematica.

Защо това е полезно? Mathematica е най-мощната математическа софтуерна система и има едно от най-старите, най-зрелите решения за тетрадки. Следователно свързването на Raku с Mathematica позволява лесното използване на някои интересни синергии.

Ето и някои ориентирани към Raku последици от последното твърдение:

  • Използването на тетрадки улеснява интерактивни разработки или изследвания (с Raku).
  • Възможност за визуализиране и изчертаване на резултати (получени с Raku).
  • Комбиниране на изчисления с други езици за програмиране.
    • Които могат да се изпълняват в тетрадките на Mathematica: Python, R, Julia, и др.
  • Грамотно програмиране.
  • Сравнително тестване на коректността на резултатите.
    • Проверка на това дали новите реализации на Raku правят “правилното нещо”.
    • Сравнение с други езици, които “правят същото нещо”.

Ето пример за използване на сериализиращата команда encode-to-wl за конвертиране на даннов масив, генериран в Raku с командата random-tabular-dataset и извеждането му в
Mathematica като собствен (“native”) Dataset обект, [WRI1]:

say to-pretty-table(random-tabular-dataset(3,5))

# +---------------------+-----------+-----------+------------+-------------+
# |       appalled      |  slammer  |    aura   | anglophile | accompanied |
# +---------------------+-----------+-----------+------------+-------------+
# |     salaciously     | 14.966161 | 91.331654 | 16.961175  |  15.061875  |
# | unconscientiousness | -1.617324 |  7.872224 | -4.440601  |   7.161489  |
# |         fey         |  8.157817 | 65.334798 |  2.785762  |   7.945464  |
# +---------------------+-----------+-----------+------------+-------------+
random-tabular-dataset(3,5)==>encode-to-wl()

Забележка: В този документ използвам Mathematica и Wolfram Language (WL) като синоними. Ако трябва да сме точни, трябва да кажем нещо от рода на “Mathematica е софтуерна система (продукт) и WL е езикът за програмиране в тази софтуерната система.” Или нещо подобно.


Твърде зелено, за да бъде червено

или “Не имплементирах обработка на данни чрез пакета Red, щото не можах да го инсталирам”.

В този документ използвам стандартни структури от данни на Raku. Щеше да е хубаво да покажа примери с помощта на Raku пакета Red, [FCO1]. Причините да не го направя могат да се обобщят като: “твърде много незрялост навсякъде”. По-точно:

  • Нямах време да имлементирам Raku-действията за Red в модула “DSL::English::DataQueryWorkflows”, [AAp6].
    • Също така не бях “разбрал” достатъчно добре Red.
  • Опитах се да инсталирам Red няколко пъти и не успях.
    • Предполагам, че причината е в мен, но може да е и в Raku, или в Red, или в бързия ми скок към macOS Monterey

Правейки бъдещето по-равномерно разпределено

или “Непосредствени и дългосрочни бъдещи планове”.

Разбира се, като създаваме и улесняваме самозваните даннови учени и баристите на кодове, ние, поне донякъде, правим бъдещето, което е вече пристигнало, по-равномерно разпределено.

Ето някои от моите предстоящи планове за събиране, генериране, и обработка на данни с помощта на Raku, които ще разпределят пристигналото бъдеще още по-равномерно:

  • Имплементации на функционалности за събиране и генериране на данни.
  • Имплементации на функционалности за събиране и генериране на данни чрез разговарни агенти.
    • Реализирах такав агент първо на WL, [AAv6], след това на Raku, [AAp9, AAv7].
    • Raku агента трябва да има по-пълно и универсално поведение.
    • (Не планирам да развивам WL агента по-нататък.)
  • По-обширни и по-обстойни модулни тестове.
    • Досега модулните тестовете са на минимално ниво.
  • Изследване и подобряване на производителността.
    • На този етап не съм разглеждал, колко бързи или бавни са функциите за обработка на данни в Raku.
    • Разбира се, това трябва да се проучи достатъчно подробно.
  • Преводи на команди на естествен език към системи за обектно-релационно картографиране като Red, [FCO1].
  • Транслация към Raku на модулни тестове за обработка на данни писани на Mathematica.
    • Това се отнася както за тестовете за генерирането на код от команди на естествен език, така и за тестовете за обработка на данни.

Забележка: Цитатът, перифразиран по-горе, и който се приписва на Уилям Гибсън е: > Бъдещето е пристигнало – просто все още не е равномерно разпределено.


Поправяне на телефона

или “Бележки на преводача”.

Както беше споменато във въведението, тази статия е превод от английски език. Аз се опитах преведеният текст да е хем “решително” български, хем следващ английският оригинал доста буквално (и разбира се по дух). Поради стремежа към буквалност някои изрази и алюзии сигурно не са много ясни. Надявам се бележките в тази секция да са от помощ.

Бележките за раздели според секциите със съответните текстове.

Въведение

  • “Данново анализиране” е буквален превод от “data analysis”. По
    принцип ако се занимаме с данни ние се занимаве със статистика. Но
    терминът “data analysis” има (вече) самостоятелно значение в английския
    език, така че ние използвамае “данново анализаране” вместо
    “статистика”.

楽-за ÷ с-楽

  • Буквалните преводи на поговорката “Децата на обущаря боси ходят”
    съществуват като поговорки в немския и английския. По-познати съответни
    български поговорки са:

    • “Обущарят бос ходи.”
    • “Вода гази, жаден ходи.”

Размерът на магическите данни не определя колко магически са те

  • “Добре познати набори от данни” и “учебни даннови масиви” се
    позовават към един и същ вид от данни.

Обработка на данни за глупаци (справочник за останалите нас)

  • Изразът “обработка на данни за глупаци (справочник за останалите
    нас)” се позовава към шаблона за именуване добре известен клас от
    справочни книги на английския език. Например:

    • “Data wrangling for dummies (a reference for the rest of us).”
  • “Comprehensive translation” може буквално да се преведе като
    “цялостна транслация” или “пълна транслация”. Съответният Raku пакет се
    опитва да направи преводи на спецификации от различен род: далеч не само
    за обработка на данни, но също и спецификации за класифициране, латенто
    семантичен анализ, регресия, препоръчителни системи, и други. Поради
    тази причина се използва превода “глобална транслация”.

Единственият начин на правене

  • “Your Mileage May Vary” (YMMV) може да се преведе като “вашият пробег може да варира”. Но, аз мисля, че е по-добре да използваме YMMV, тъй като това съкращение е достатъчно популярно.

Тежък мозък вместо леко сърце

  • Изразът “тежък мозък вместо леко сърце” се позовава към английската дума “lighthearted”, която се превежда буквално като “леко-сърдечно”.

Приказка за вълка, овена, и миещата мечка

  • Думата “wolfram” може да бъде разбита като “wolf” (вълк) и “ram” (овен). “Raccoon” се използва често за обозначаване на програмисти на Raku. На български “raccoon” се превежда като “миеща мечка” или “енот”.

Поправяне на телефона


Кодова подготовка

Тази секция съдържа код на Mathematica и Raku за да се изпълнят примерите в тази тетрадка на Mathematica.

Зареждане на пакетите на Mathematica

Import["https://raw.githubusercontent.com/antononcube/ConversationalAgents/master/Packages/WL/RakuMode.m"]
Import["https://raw.githubusercontent.com/antononcube/ConversationalAgents/master/Packages/WL/RakuEncoder.m"]
Import["https://raw.githubusercontent.com/antononcube/ConversationalAgents/master/Packages/WL/RakuDecoder.m"]

Import["https://raw.githubusercontent.com/antononcube/ConversationalAgents/master/Packages/WL/DSLMode.m"]

Стартиране на Raku процес

KillRakuProcess[]
SetOptions[RakuInputExecute, Epilog -> (FromRakuCode[#, DisplayFunction -> (Dataset[#, MaxItems -> {Automatic, All}] &)] &)];
StartRakuProcess["Raku" -> "/Applications/Rakudo/bin/raku"]

RakuMode[]

Зареждане на Raku пакети

use Data::Generators;
use Data::Reshapers;
use Data::Summarizers;
use Data::ExampleDatasets;
use Haikunator;
use Mathematica::Serializer;

(*"(Any)"*)
use DSL::Shared::Utilities::ComprehensiveTranslation;

(*"(Any)"*)

Библиография

Статии и книги

[AA1] Anton Antonov, “Data wrangling in Raku”, (2021), RakuForPrediction-book at GitHub.

[AA2] Anton Antonov, “Connecting Raku to Mathematica”, (2021), RakuForPrediction-book at GitHub.

[AA3] Anton Antonov, “Translating Mathematica expressions to High Performance Fortran”, (1999), HiPer’99, Tromsoe, Norway.

[AAn3] Anton Antonov, “Translating Mathematica expression to High Performance Fortran”, from the Notebook Archive (2004), https://notebookarchive.org/2018-10-10qgpsl .

[CARH1] Charles Antony Richard Hoare, (1980), “The emperor’s old clothes” ACM Turing award lectures January 2007 Year Awarded: 1980.
https://doi.org/10.1145/1283920.1283936.

[HW1] Hadley Wickham, “The Split-Apply-Combine Strategy for Data Analysis”, (2011), Journal of Statistical Software.

[FB1] Federico Biancuzzi and Shane Warden, (2009), Masterminds of Programming: Conversations with the Creators of Major Programming Languages. ISBN-978-0596515171. See page. 385.

[LT1] Лао Дзъ, “Дао Дъ Дзин”, 1990, (превод: Ленин Димитров).

Финкции, пакети, хранилища

[AAp1] Anton Antonov, Data::Reshapers, (2021), Raku Modules.

[AAp2] Anton Antonov, Data::Summarizers, (2021), Raku Modules.

[AAp3] Anton Antonov, Data::ExampleDatasets, (2021), Raku Modules.

[AAp4] Anton Antonov, Data::Generators, (2021), Raku Modules.

[AAp5] Anton Antonov, Mathematica::Serializer Raku package, (2021), GitHub/antononcube.

[AAp6] Anton Antonov, DSL::English::DataQueryWorkflows Raku package, (2020-2021), GitHub/antononcube.

[AAp7] Anton Antonov, DSL::English::DataAcquisitionWorkflows Raku package, (2021), GitHub/antononcube.

[AAp8] Anton Antonov, DSL::Utilities::ComprehensiveTranslation, (2020-2021), GitHub/antononcube.

[AAp9] Anton Antonov, DSL::FiniteStateMachines, (2022), GitHub/antononcube.

[AAr1] Anton Antonov, Data Acquisition Engine project, (2021), GitHub/antononcube.

[FCOp1] Fernando Correa de Oliveira, Red, (last updated 2021-11-22), Raku Modules.

[JSp1] Jonathan Stowe, XDG::BaseDirectory, (last updated 2021-03-31), Raku Modules.

[WRI1] Wolfram Research, (2014), Dataset, Wolfram Language function, https://reference.wolfram.com/language/ref/Dataset.html (updated 2021).

[WRI2] Wolfram Research, (2008), ParallelCombine, Wolfram Language function, https://reference.wolfram.com/language/ref/ParallelCombine.html (updated 2010).

[WRI3] Wolfram Research, (2016), URLRead, Wolfram Language function, https://reference.wolfram.com/language/ref/URLRead.html.

Видео записи на доклади

[AAv1] Anton Antonov, “Raku for Prediction”, (2021), The Raku Conference 2021.

[AAv2] Anton Antonov, “Multi-language Data-Wrangling Conversational Agent”, (2020), Wolfram Technology Conference 2020, YouTube/Wolfram.

[AAv3] Anton Antonov, “Data Transformation Workflows with Anton Antonov, Session #1”, (2020), YouTube/Wolfram.

[AAv4] Anton Antonov, “Data Transformation Workflows with Anton Antonov, Session #2”, (2020), YouTube/Wolfram.

[AAv5] Anton Antonov, “Doing it like a Cro (Raku data wrangling Shortcuts demo)”, (2021), YouTube/Anton.Antonov.Antonov.

[AAv6] Anton Antonov, “Multi language Data Acquisition Conversational Agent (extended version)”, (2021), YouTube/Anton.Antonov.Antonov.

[AAv7] Anton Antonov, “FOSDEM2022 Multi language Data Wrangling and Acquisition Conversational Agents (in Raku)”, (2022), YouTube/Anton.Antonov.Antonov.

3 thoughts on “Увод в обработката на данни с Raku

  1. Този блог пост е публикуван днес с цел да отбележа 24-ти май, “Денят на светите братя Кирил и Методий, на българската азбука, просвета и култура и на славянската книжовност”.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s