Чуть подумав рука сама набрала «q», так как это было единственное доступное на компьютере здесь и сейчас. Что про него знал: что первый и последний раз запускал год назад, минут на 30, простой задачки по разбору и поиску по файлу.
Дальше будет много q, а именно ascii последователя подмножества языков APL’а и языка Scheme, а именно k и его расширения k-sql, переродившихся в продукт с именем Q — тесной связке языка и встроенной в него базы данных.
C:\Users\unknown\Dropbox\j>q KDB+ 3.0 2013.02.06 Copyright (C) 1993-2013 Kx Systems w32/ 2()core 2972MB unknown win-d2om7les24v 192.168.1.2 PLAY 2013.05.07
Немного лирики: качаю отчёт с сайта оператора в csv и чуть поправляю заголовок:
Сервис;Дата звонка;tel;time;Длит-ть;Баланс до;money;Баланс после; Входящий звонок внутри группы;22.02.2013 20:38:14;79064014328;00:00:13;0;114,9175;0,0000;114,9175; Входящий звонок;22.02.2013 20:03:49;79094445182;00:12:05;0;114,9175;0,0000;114,9175; Исходящий звонок внутри группы;22.02.2013 17:04:39;79064014328;00:01:15;0;115,8175;-0,9000;114,9175; Исходящий звонок внутри группы;22.02.2013 13:18:22;79064014328;00:01:36;0;116,7175;-0,9000;115,8175; Списание за услугу Сообщники;22.02.2013 01:35:00;;00:00:00;0;119,3675;-2,6500;116,7175; Запрос информации;21.02.2013 23:40:42;*102;00:00:01;0;119,3675;0,0000;119,3675;
Дальше читаем мануал, чтение текстового файла 0:, паралелльно можно разделить файл на поля указав в левой части формат колонок и разделитель, если разделитель список, то колонки таблицы будут именованы из первой строки. Выбираем только нужные поля, раз два и таблица готова.
Кому скучно читать про подговку данных — можно прыгнуть сразу к анализу данных.
q)clog:select tel,time,money from ("SSSTSSSS";enlist ";") 0: `:tel.txt q)clog tel time money --------------------------------- 79064014328 00:00:31.000 0,0000 79263883922 00:02:06.000 0,0000 79064014328 00:01:15.000 -0,9000 79064014328 00:01:36.000 -0,9000 00:00:00.000 -2,6500 *102 00:00:01.000 0,0000 ..
Так как q — векторная база данных, то по сути clog — это словарь, имя колонки — список значений.
q)clog.money `0,0000`0,0000`-0,9000`-0,9000`-2,6500`0,0000`0,0000`0,0000`0,0000`0,0000`-0,..
Чуть подготовлю данные. Видно, что money не в числовом формате, надо бы преобразовать в число: ssr — это oracle replace. Термин $ (cast) занимается различными конвертациями и преобразованиями типов, в данном случае читает число из строки:
each — это map
{"F"$ssr[string x;",";"."]} each clog.money
Ну и запишем это всё в таблицу, используя update. Тут есть небольшая особенность. Если использовать имя таблицы clog, то результатом выполнения функции update будет новая таблица с обновлёнными значениями. но можно указать имя таблицы как `clog, тогда изменения будут сохранены. Телефон тоже сделаем строкой, изначально «S» — это не строка а символьный тип.
q)update string tel, {"F"$ssr[string x;",";"."]} each money from `clog `clog
Почти все слова в данном q-sql — это обычные функции с небольшой порцией синтаксического сахара. Их можно использовать отдельно, например, where просто преобразовывает битовую строку в список индексов.
сравнение работает со списками. результат — битовая строка, из которой where извлекает индексы, ну а select по этим индексам извлекает соответствующие элементы списков из таблицы.
q)15<40 10 20 30 1011b q)where 15<40 10 20 30 0 2 3
В файле есть как исходящие так входящие звонки и оплаты разных сервисов, выбираю строки где списывали деньги и есть номер телефона и присваиваю этому старое имя:
q)clog:select from clog where money<0,not tel like "" q)clog tel time money -------------------------------- "79064014328" 00:01:15.000 -0.9 "79064014328" 00:01:36.000 -0.9 "79064014328" 00:01:33.000 -0.9 "79104652109" 00:01:23.000 -11.9 "79265996349" 00:00:12.000 -5.95 ..
Определяем коды, которые используются, чтобы впоследствии классифицировать по операторам:
уже чуть позже я понял что надо вытянуть извлечение кода в функцию.
q)gcode:1_ 4# / get code from tel q)gcode each clog.tel "906" "906" "906" "910" "926" ..
q)distinct gcode each clog.tel "906" "910" "926" ..
тут более sql-подобную запись с exec. exec — это тот же select, но который не возвращает словарь таблицы, а возвращает значения или значение результата запроса или таблицы.
q)codes:exec distinct gcode each tel from clog q)codes "906" "910" "926" ..
Далее переходим к словарям, описываются они просто <ключи>! <значения>. Создаю словарь код<>оператор.
q)ops:codes ! `beeline`mts`megafon`beeline`mts`beeline`beeline`mts`moscow q)ops "906"| beeline "910"| mts "926"| megafon "909"| beeline "495"| moscow ..
Многие тарифы округляют минуту до полной, ввожу поле для удобства, которое будет просто целым количеством минут. Я не сохраняю его в таблицу, просто получаю результат, так как позже создам view с этим полем. Время в миллисикундах, так что делю на 1000.
q)update ctime:ceiling (time%1000)%60 from clog tel time money ctime -------------------------------------- "79064014328" 00:01:15.000 -0.9 2 "79064014328" 00:01:36.000 -0.9 2 "79064014328" 00:01:33.000 -0.9 2 "79104652109" 00:01:23.000 -11.9 2 "79265996349" 00:00:12.000 -5.95 1 ..
Создаю view с оператором и целыми минутами, если бы написал t:, то создал бы таблицу t. Напоминаю, что update сохраняет исходные колонки.
q)t::update op:ops@gcode each tel, ctime:ceiling (time%1000)%60 from clog q)t tel time money op ctime ---------------------------------------------- "79064014328" 00:01:15.000 -0.9 beeline 2 "79064014328" 00:01:36.000 -0.9 beeline 2 "79064014328" 00:01:33.000 -0.9 beeline 2 "79104652109" 00:01:23.000 -11.9 mts 2 "79265996349" 00:00:12.000 -5.95 megafon 1 ..
всё необходимое набрал, сохраняю результат t в файл на всякий случай, правильнее конечно было бы сохранить clog и описание view `t, но лень:
q)save `:t `:t
Всё что выше — просто подготовка данных, теперь чуть интереснее: разбор.
Посмотрим кому звонил больше всего, тут начинает группировка. Группировка — параметр функции select, которая создаёт списки для каждого вхождения ключа:
q)select ctime by tel from t tel | ctime .. -------------| --------------------------------------------------------------.. "74956471602"| ,1 .. "79031398210"| 7 3 .. "7903X" | ,2 .. "79064014328"| 2 2 2 2 1 2 1 1 1 3 1 2 2 3 1 1 1 1 2 2 3 3 3 1 3 2 1 1 0 2 1 .. ..
после чего выполняем функции с параметром в виде этого списка, desc — функция обратной сортировки, она сортирует как обычный список так и таблицу, умолчательно сортируется по последней колонке.
q)desc select sum ctime by tel from t tel | ctime -------------| ----- "79064014328"| 126 "79094445182"| 36 "79652650530"| 30 ..
Заметив, что много звонков на один номер, я добавил колонку «любимый номер», чуть позже я решил просто обозначить это в поле оператора, назначил старой view новое имя, а «t» теперь это новая view на основе старой:
q)t2::update op:ops@gcode each tel, ctime:ceiling (time%1000)%60 from clog q)t::update op:`lub from t2 where tel like "79064014328" q)t tel time money op ctime ---------------------------------------------- "79064014328" 00:01:15.000 -0.9 lub 2 "79064014328" 00:01:36.000 -0.9 lub 2 "79064014328" 00:01:33.000 -0.9 lub 2 "79104652109" 00:01:23.000 -11.9 mts 2 "79265996349" 00:00:12.000 -5.95 megafon 1 ..
Теперь пора задуматься о деньгах, конкретно о тарифах мегафона.
Какой-то там по 3 копейки, описать функцией просто:
q)meg3:{0.03*sum x}
посмотрим что там с деньгами по каждому оператору:
q)select meg3 time%1000 by op from t op | time -------| ------ beeline| 111.93 lub | 148.05 megafon| 29.1 moscow | 0.93 mts | 24.45
Нужно вводить опции тарифа, если номер `lub, то делим цену на два и прибавляем 30р.
q)lub:{$[x=`lub;30+y%2;y]} / [op;time]
Собственно всё, функция для подсчёта будет следующая, тут для lub использует карринг:
q){lub[x] meg3[y]}
К сожалению, я не нашёл как передать ключ и значение результата «by» в функцию, так что оформляю это как подзапрос. Так как op и time это не два значения какой-то строки из таблицы как в обычной db, то в функцию будут передавать целые списки (в данном случае список и список списков), но функция, описанная мной выше, ожидает только два параметра: оператор и список времен, так что проходится использовать функцию eachboth, которая обозначается как ‘ (кавычка) по сути это zipWith, но без ограничения количества списков. Запрос, в отличие от обычной db, при этом усложняется только на ‘:
q)select money:{lub[x] meg3[y]}'[op;time] from select time%1000 by op from t money ------ 259.98 29.1 0.93 24.45
Суммирую, тут можно написать как exec sum так и sum exec — просуммируется результат exec или exec просуммирует результат — роли не играет:
q)exec sum {lub[x] meg3[y]}'[op;time] from select time%1000 by op from t 314.46
Понятно сколько бы я потратил, перейдя на данный тариф. Теперь подсчитаем другой, где минута округляется, a дальше посекундно. Считать приходится для каждого указанного времени, что я и делаю используя each:
q)mego:sum {1.20+$[x<=60;0;1.20*(x-60)%60]} each q)exec sum {lub[x] mego[y]}'[op;time] from select time%1000 by op from t 258.06
Полный код:
clog:select tel,time,money from ("SSSTSSSS";enlist ";") 0: `:tel.txt {"F"$ssr[string x;",";"."]} each clog.money update string tel, {"F"$ssr[string x;",";"."]} each money from `clog clog:select from clog where money<0,not tel like "" gcode:1_ 4# codes:exec distinct gcode each tel from clog ops:codes ! `beeline`mts`megafon`beeline`mts`beeline`beeline`mts`moscow t2::update op:ops@gcode each tel, ctime:ceiling (time%1000)%60 from clog t::update op:`lub from t2 where tel like "79060414294" meg3:{0.03*sum x} mego:sum {1.20+$[x<=60;0;1.20*(x-60)%60]} each lub:{$[x=`lub;30+y%2;y]} exec sum {lub[x] meg3[y]}'[op;time] from select time%1000 by op from t exec sum {lub[x] mego[y]}'[op;time] from select time%1000 by op from t
Оформить этот текст было значительно сложнее чем написать эти 14 строк. Понятное дело, что тут нет неподъёмных для любой другой базы вещей, но написать это меня сподвигла простота использования и очевидность написания некоторых конструкций. В начале было чуть сложно переключиться с обычного sql, но после понимания того, что таблица тут хранит данные в списках, а функции, как правило работают практически с любыми встроенными типами данных, стало значительно понятнее. Именно идиоматическая простота и простота реализации этой db, а по сути это помесь scheme и APL, позволяет использовать этот инструмент эффективно. Впечатления от этого — это APL и функциональщина, сдвинутая в сторону sql и баз данных.
Вся база состоит из одного файла q.exe, размером ~400kb байт. Скептики улыбнутся после этого, но тогда посмотрите на список заказчиков данного продукта http://kx.com/end-user-customers.php.
Поиграть с этим можно скачав тут http://kx.com/software-download.php
ссылка на оригинал статьи http://habrahabr.ru/post/198912/
Добавить комментарий