Страницы

Поиск по вопросам

Показаны сообщения с ярлыком big-data. Показать все сообщения
Показаны сообщения с ярлыком big-data. Показать все сообщения

четверг, 9 апреля 2020 г.

Эффективная работа с большими объемами данных

#алгоритм #big_data

                    
Изначальная архитектура приложения была построена ошибочно - все данные сливались
в один файл, размер которого перевалил теперь за отметку 950ГБ.

Есть ли какой-нибудь эффективный метод (из области big-data) выделения из этого массива
данных групп сущностей, состоящих из одних и тех же символов (так называемых анаграмм)?
    


Ответы

Ответ 1



Стандартная идея вот какая: К каждой сущности дописать её номер в исходном списке. У каждой сущности отсортировать символы. Отсортировать сущности. Теперь анаграммы будут находиться рядом. Теперь для нахождения анаграмм нужен лишь один пробег по данным. Все эти операции хорошо «параллелятся», Кроме, пожалуй сортировки. С учётом этого можно изменить немного алгоритм: Распартиционировать данные как угодно. Пронумеровать сущности и отсортировать их символы на каждом хосте по отдельности (номеру назначать уникальный префикс, чтобы не смешивать) Отсортировать данные каждого хоста. Смёржить все данные. Сначала заливать результат на первый хост, когда 1/n всех данных зальётся — на второй хост и т. д. (Это по сути перепартиционирование.) Далее пробег по данным на каждом хосте. Вместо пункта 4, возможно, более эффективно будет не сливать данные вместе, а делать многохостовый пробег: string currentValue = null; int anagramCount = 0; string[] nextByHost = new string[N]; for (i = 0..n-1) { currentByHost[i] = ""; nextByHost = fetch from host[i]; } while (any of hosts has data) { if any of nextByHost[i] equals to currentValue, anagramCount++ fetch next from host[i] to nextByHost[i] else store currentValue and anagramCount currentValue = min(nextByHost) }

пятница, 13 марта 2020 г.

Big data, оптимизация запросов

#sql_server #big_data


Есть следующая простая структура данных

Id; fk_Security_Id; DateTime; Price


Строка хранит данные по инструменту(активу), дату и время, цену(котировку). Строк
в БД на данный момент ~ 1 млрд. 200 млн. (10 инструментов с историей за прошлые 10 лет)

Задача - выборка данных по указанному fk_Security_Id и промежутку DateTime(например,
июль 2000г.) за адекватный промежуток времени (в идеале меньше минуты).

Сначала, я использовал знакомый мне MSSQL и навесил в лоб clustered index на эти
2 поля. В результате поиск по этим 2 полям занимает в районе 35 минут и сожранные 6.5Gb
RAM. Не совсем то, что конечно хотелось бы.
Какие варианты решения вижу пока я:


Не менять выбранную бд, а изменить саму структуру хранения
данных. Например разнести в разные таблицы данные по разным инструментам. В
этом случае конечно будут абсолютно идентичные таблицы с точки
зрения структуры, но можно будет выиграть некоторое время на поиске и
дальнейшее добавление новых инструментов не будет влиять на то самое время поиска.
И тогда вместо композитного кластерного индекса, индекс
будет состоять из одного поля - datetime. Также возможно здесь
имеет смысл вместо поля datetime в качестве индекса брать некий
timestamp или преобразованный Id. Но не уверен что это даст
существенный прирост в поиске, хотя стоит попробовать думаю.
Использовать какую-нибудь более легковесную бд, например postgres (дружит с необходимым
мне EF, что очень хотелось бы) + есть нативная поддержка Sphinx-а например.
Использовать какое-нибудь NoSql решение. С данными бд дел не имел, но допускаю,что
в моем случае данные укладываются в простую структуру key-value. Правда, наверное те
NoSql которые держат данные в RAM мне не подойдут потому что у меня просто столько
памяти нету. Хотя, если я не ошибась есть и достаточно шустрые дисковые NoSql , Aerospike
например. Но опять же поскольку я с ними не работал я не могу оценить насколько они
дадут выигрыш по времени по сравнению с обыными реляционными бд.


База не распределенная, ресурсы машины - 8 потоков и 8Gb RAM. Буду рад любому совету.
    


Ответы

Ответ 1



Пара мыслей (eсли вы всё же остановитесь на MSSQL). На мой взгляд big-data подразумевает щепетильное отношение к структурам хранения данных и типам хранимых данных. Сравните, к примеру, размеры различных типов данных для хранения дат и чисел: declare @dt datetime = getdate(), @dt2 datetime2(0) = getdate(), @sdt smalldatetime = getdate(), @m money = 1.0, @f float = 1.0, @dec_15_5 decimal(15,5) = 1.0, @r real = 1.0 select [datetime] = datalength(@dt), [datetime2(0)] = datalength(@dt2), [smalldatetime] = datalength(@sdt), [money] = datalength(@m), [float] = datalength(@f), [decimal(15,5)] = datalength(@dec_15_5), [real] = datalength(@r) datetime datetime2(0) smalldatetime money float decimal(15,5) real --------- ------------- -------------- ------ ------ -------------- ----- 8 6 4 8 8 5 4 Если тип столбца DateTime у вас datetime, рассмотрите возможность использования, например, типа smalldatetime (диапазон значений от 1900-01-01 до 2079-06-06 с точностью 1 минута). Если, тип стоблца Price, к примеру, float - рассмотрите возможность использования типов decimal (numeric) или real. Чем меньше размер строки данных, тем больше строк помещается в одну страницу памяти, соответственно легче оперировать ими в запросах. В таблицах с большим числом строк нелишним будет избегать NULL-able столбцов (это также сэкономит немного места). Правда следствием компактного хранения может быть некоторое неудобство в написании запросов, когда, например, при вычислении среднего для сохранения точности приходится делать кастинг в тип с большей точностью, а потом обратно. Да и сам кастинг несколько повысит стоимость запроса. Ваша идея разнести инструменты по таблицам имеет рациональное зерно. Нужно ли их держать в одной таблице, и в самом ли деле нужен Id в таблице, если, к примеру, на неё нет ссылок - решать вам. Однако если разнести данные по таблицам вида create table SomeInstrument ( DateTime smalldatetime not NULL primary key, Rate real not NULL ) то общий объём хранимых данных явно уменьшится, т.к. не будет столбцов Id и fk_Security_Id. Если всё же оставите всё в одной таблице, то fk_Security_Id (вместе с primary key таблицы, на которую он ссылается) имеет смысл перевести на тип tinyint, раз уж инструментов всего около десятка.

четверг, 13 февраля 2020 г.

Индексируется только часть файлов

#база_данных #elasticsearch #big_data


Необходимо проиндексировать большое количество (около 100к) файлов формата json разного
размера (на 100к файлов приходится в общем 1,2ГГб текстовой информации). 

Пишу на python, соответственно использую стандартный модуль для работы с Elasticsearch.
Из состава использую функцию helpers.bulk таким образом:

es = Elasticsearch(ES_CLUSTER)

json_docs = []
for filename in os.listdir(os.getcwd()):
    if filename.endswith('.json'):
        with open(filename) as open_file:
            json_docs.append(json.load(open_file))

helpers.bulk(ES_INDEX, ES_TYPE, json_docs)


В результате работы индексируется только 570 файлов. Причем заметил, что размер индекса
после каждого нового прогона программы сильно колеблется от 2-4мб до 15мб, хотя количество
проиндексированных файлов остается неизменным.

Размер индекса узнаю запросом:

curl 'localhost:9200/_cat/indices?v'


Очищаю так:

curl -XDELETE 'localhost:9200/_all/'


Видимо использую не оптимальный способ индексирования.
    


Ответы

Ответ 1



Очередь на индексацию в Elasticsearch по умолчанию ограничена.Вы пытаетесь скормить ему слишком много записей за раз. Это хоть и Bulk API, но работает он немного по другому: Необходимо отправлять порции в несколько сотен записей (подбирается экспериментально) es = Elasticsearch(ES_CLUSTER) json_docs = [] i=0 bulkSize=500 for filename in os.listdir(os.getcwd()): i++ if filename.endswith('.json'): with open(filename) as open_file: json_docs.append(json.load(open_file)) if len(json_docs) >= bulkSize: print(i, "current file:", datetime.now(), filename) try: helpers.bulk(ES_INDEX, ES_TYPE, json_docs) except Exception as error: print(error) del json_docs json_docs = [] //do not forget to put rest helpers.bulk(ES_INDEX, ES_TYPE, json_docs)

Ответ 2



Мы в своем проекте (не python) тоже индексируем большое кол-во информации. Основные моменты: Мы не индексируем через API - curl из bash Наш скрипт преобразует данные в формат bulk и кусками по 1000 документов складывает в обычный текстовый файл. После обработки всех данных запускаем консольный скрипт: files=(${1}*.txt) total=${#files[@]}; count=0 pstr="[=======================================================================]" echo "Start export to ElasticSearch ${total} files with data" for i in ${1}*.txt; do curl -XPOST http://elastic.domain.conm/_bulk --data-binary @${i} &>/dev/null count=$(( $count + 1 )) pd=$(( $count * 73 / $total )) printf "\r%3d.%1d%% %.${pd}s" $(( $count * 100 / $total )) $(( ($count * 1000 / $total) % 10 )) $pstr done printf "\n"

понедельник, 6 января 2020 г.

FPR (False Positive Rate), ложные положительные срабатывания

#python #машинное_обучение #big_data


Подскажите, пожалуйста, как посчитать число ложных положительных срабатываний (FPR)
относительно третьего класса для следующей матрицы ошибок (confusion matrix):


    


Ответы

Ответ 1



FP FP 14+5 FPR = ---- = ------- = ----------------- = 0.106 N FP + TN 40+50+23+47+14+5 обозначения: FPR: False Positive Rate (FPR) FP: False Positive (FP) N: condition negative (N) TN: True Negative (TN)

Ответ 2



В хелпе sklearn есть пример, но вот не могу понять как его верно использовать. На данный момент у меня csv файл с одним признаком и одним классификаторов (два столбца, y_true, y_pred)

Ответ 3



Число ложных положительных срабатываний для третьего класса = 14 + 5 = 19

воскресенье, 5 января 2020 г.

Чтение и обработка огромных файлов CSV

#python #pandas #csv #big_data


Есть 2 файла в формате .csv, размеры 60 ГБ и 1 ГБ. Мне необходимо создать на их основе
таблицу по ключам из обоих файлов (по типу WHERE и JOIN в SQL), а затем экспортировать
опять в файл .csv. Как мне это сделать? pandas такое не сможет обработать скорее всего,
просто оперативки не хватит. 
    


Ответы

Ответ 1



Можно воспользоваться Dask DataFrame вместо Pandas. Dask умеет обрабатывать DataFrame на диске - т.е. такие, которые не помещаются в память. Обычно Dask работает гораздо медленнее Pandas и его API гораздо беднее. Кроме этого можно прочитать в память только те столбцы, которые участвуют в объединении и фильтрации (если эти данные влезают в память), выбрать только соответствующие строки и дальше читать из CSV файлов только соответствующие строки. Но проще всего для данных задач использовать базы данных, например MySQL или PostgreSQL - они с легкостью обрабатывают данные, которые не помещаются в памяти. Кроме этого они поддерживают индексацию, что может значительно ускорить обработку данных. Кстати в большинстве БД существуют специальные утилиты для быстрой загрузки CSV файлов в БД: PostgreSQL: COPY, MySQL: LOAD DATA

вторник, 31 декабря 2019 г.

Как эффективнее всего распарсить огромный файл логов на слабой машине?

#алгоритм #файлы #логирование #highload #big_data


Есть сервер с 1gb RAM. Есть лог файл nginx (любой другой веб-сервер) на 70gb. Как
максимально быстро собрать статистику по user agent пользователей сайта, учитывая описанные
ограничения по ресурсам.
    


Ответы

Ответ 1



можно воспользоваться StringTokenizer в языке Java, который позволяет считывать файл построчно и не тратить память на хранение всех строк файла. StringTokenizer tok = new StringTokenizer("/path/to/file"); while (tok.hasMoreTokens()) { String line = tok.nextToken(); // работаешь со строкой. } Также можно указывать разделитель в конструкторе, по умолчанию стоит \t\n\r\f

Ответ 2



Считываете файл построчно и анализируете считанную сроку. Практически во всех языках можно указать размер буфера при работе с файловой системой и поэтому, несмотря на то, что вы работаете с файлом построчно, система будет читать файл фиксированными блоками.

среда, 18 декабря 2019 г.

Как найти наибольшую биклику, допуская неполное совпадение?

#алгоритм #графы #big_data


Дан двумерный массив булевых значений. Строки это аккаунты, столбцы – различные свойства
каждого аккаунта: «состоит в группе А», «совершал покупки» и т.п.

Надо найти максимальную биклику (полный двудольный граф) – т.е. тот наибольший набор
аккаунтов×свойств, где свойства true.

Задача NP-полная, и с её реализацией я кое-как в лоб разобрался. Но теперь два усложнения.


хочется сравнивать «нечётко»: допустить вхождение в биклику аккаунтов, у которых
недостаёт 1-2 (задаётся параметром нечеткости) свойств. Лучше посчитаем, как будто
они у них true, если это позволит включить их в наибольший набор. Неужели для каждой
найденной клики нужно зановой перебрать все данные, проверяя гипотезы о каждом свойства,
которое можно бы добавить? Подскажите алгоритм.
размер исследуемых наборов растёт. Может, вы прошли курс Machine Learning или прочие
BigData – посоветуйте методики поиска, альтернативные полному перебору? Я краем уха
слышал, есть такие, эффективно работающие на больших объёмах, пусть, дающие приблизительный
результат – объясните, пожалуйста, «на пальцах», как их можно применить к задаче?


Иллюстрации проблемы



Синие ячейки true, белые false. Нечёткая максимальная биклика обведена красным –
в каждой строке добавили по три «фантомных» ячейки (серые). Порядок строк/столбцов
не имеет значения. Максимальная биклика вовсе не обязательно будет сплошным прямоугольником,
это только для иллюстрации.



Например, здесь светло-синие ячейки были отброшены на этапе оптимизации, а оставшиеся
формируют две би-клики: строки (3,5) × столбцы (1,3,4,6) и (3,4,5)×(1,3). Однако, если
включить fuzziness на 2, то можно добавив в 4-й строке столбцы (4,6), а в строках (3,5)
столбец 5 – получить ещё большую биклику (3,4,5)×(1,3,4,5,6).
    


Ответы

Ответ 1



В случае нечеткого определения понятия "полнота" возможны серьезные отклонения в определении "максимальной" биклики в зависимости от метрики, по которой оценивается максимум, так как наличие ненулевого числа false в строках и столбцах биклики позволяет выбирать метрику так, что "максимальными" для каждой метрики будут разные биклики. Например, в случае метрики "превышение true над false на всей биклике" (количество заполненных против количества незаполненных клеток) и нечеткости 3 на примере с прямоугольником максимумом окажется биклика из столбцов 1,2,6,7 прямоугольника и всех его строк, с метрикой +8. При использовании метрики "всего истин" (число заполненных прямоугольников) выделенный прямоугольник является максимальным. Таким образом, для задачи с нечеткостью метрику нужно жестко задать, причем в зависимости от метрики может варьироваться сам алгоритм выбора биклики. В общм случае (если метрика линейно зависит от количества строк и столбцов, участвующих в биклике) подойдет жадный алгоритм захвата столбцов. Вначале сортируем столбцы по количеству строк с данным атрибутом, установленным в true, потом включаем в множество столбцов по очереди и смотрим изменение метрики. Для параметра нечеткости начальное рассмотрение должно включать минимум (параметр нечеткости)+1 столбцов, иначе биклика с нечеткостью захватит весь массив строк - этого нам явно не надо. После появления набора столбцов добавляем по одному так, чтобы промежуточное значение метрики возрастало. Количество строк в биклике будет убывать, так как не всегда у имеющихся строк будет true в новом столбце, и какие-то из них выпадут по причине превышения false предела нечеткости, в итоге либо добавим все, либо на каком-то этапе ни одной не получится добавить. Проверяем, удастся ли выкинуть хоть один столбец чтобы метрика полезла вверх, если да, выкидываем и продолжаем, пока изменение включения одного столбца не приведет к увеличению метрики. Выдаем её как локальный максимум. Для очистки совести можно рассмотреть наибольшую по метрике стартовую комбинацию из столбцов, не вошедших в найденную биклику и точно так же её обработать. Если сойдется к этому же набору, его выдаем, иначе проверяем, какая из найденных биклик больше, и если новая, проверяем ещё какую-нибудь комбинацию на случай нескольких локальных максимумов, пока либо не придем к уже найденному локальному максимуму, либо к меньшему, тогда возвращаем текущий локальный максимум как ответ.

среда, 11 декабря 2019 г.

База данных для статистических данных

#база_данных #nosql #highload #статистика #big_data


Есть набор быстро пополняющихся данных из большого количества источников, которые
тоже пополняются. Структура предельно простая:


id источника
значение
дата/время


Нужна выборка значений по произвольному промежутку даты и времени, как для одного
источника, так и сумма значений со всех источников сразу. 
Это нужно для возможности динамического построения графиков за определенный промежуток
времени для одного источника; либо для графика, на котором отображается сумма значений
от всех источников. 
Так же, такая выборка потребуется для анализа этих данных, чтобы прогнозировать будущие
значения на определенный период.


Какая база данных лучше всего вписывается в эту задачу и почему?
Подойдут ли облачные NOSQL хранилища (Google, Amazon, Azure), или дешевле поднять
свой сервер с БД из ответа №1?

    


Ответы

Ответ 1



ИМХО. NoSQL - не подойдут от слова "вааще". Они заточены по совсем другой, более "вариабельный" тип данных. При вашей простой структуре вам нужна именно реляционная база данных, так как данные из большого количества источников - то что то серверное: MSSQL, MySQL, Oracl - скорее всего любая из них справится.

Ответ 2



Если у вас данных ОЧЕНЬ много (сотни гигабайт-терабайты/день), то посмотрите на Hadoop, там можно хранить много и считать быстро. Если же данных меньше - то стоит использовать PostgreSQL, Oracle, MS SQL. NoSQL - это не о том, он скорее о слабо структурированных данных, так что в вашем случае это просто не нужно.

четверг, 5 декабря 2019 г.

Среднее арифметическое куууучи чисел

#алгоритм #big_data #java


Дано: пару миллиардов int -ов (короче очень много)
Найти: их среднее арифметическое (оссобой точности не надо, 1-2 знаков после запятой
хватит)
Можно конечно их всех просуммировать в какой-нибудь BigInteger (или собственную реализацию),
но нет ли способов покрасивее и попроще :) ?
p.s. в моём случае скорость не играет роли, но вообще хотелось бы увидеть что-нибудь
не очень тормознутое     


Ответы

Ответ 1



А в чём проблема? Сумма двух милллиардов int'ов поместится в long даже если они все равны максимальному значению. А уж если int'ы нормальные... Так что смело суммируйте всё в long с детектированием переполнения. И делите на количество.

Ответ 2



Всё ещё проще на самом деле... Имеем последовательность чисел x1, x2, x3, x4, x5... k=2; //количество чисел, текущий шаг Находим среднее арифметическое первых двух чисел r=(x1+x2)/k. Далее не будем постоянно считать суммы, а будем брать следующий множитель для следующего среднего арифметического: n=(k*r+x3)/((k+1)* r). //x3 естественно на каждом шаге меняется на следующий элемент Следующее среднее арифметическое: r=r*n. k=k+1 Повторяем шаги 4..6 для нужного числа чисел.

Ответ 3



Предложу свой вариант. Этот миллиард группируется в какой-то класс. Много объектов. Группируется так, чтобы сумма этой группы нечаянно не превыcила Integer.MAX_VALUE. В объект заносится сумма этих чисел и их количество. Сами числа уже не нужны. Далее вычисляется ср. арифметическое для этого объекта. Придётся создать много таких объектов-групп. После разбивания всех входных данных на эти объекты-группы, зная общую сумму и общее количество, делим - и всё. Очень хорошо, если хоть кто-то понял мой алгоритм.

Ответ 4



Страдает точность, но переполнение невозможно. avg = 0; for (int i = 0, n = 1; i < array.length; i++, n++) avg = avg * (1 - 1f / n) + (float) array[i] / n; return avg; А если нам не нужно добавлять новые элементы в среднее арифметическое, мы можем сразу делить каждый элемент на n. avg = 0; for (int i = 0; i < array.length; i++) avg += (float) array[i] / array.length; return avg;

Ответ 5



Среднее арифметическое можно искать не складывая все числа разом. Их можно складывать группами. Например: даны числа [1, 2, 3, 4, 5] среднее арифметическое (1 + 2 + 3 + 4 + 5) / 5 = 3 то же самое можно получить, если складывать пересекающимися парами (1 + 2) / 2 = 1.5 (2 + 3) / 2 = 2.5 (3 + 4) / 2 = 3.5 (4 + 5) / 2 = 4.5 (1.5 + 2.5) / 2 = 2 (2.5 + 3.5) / 2 = 3 (3.5 + 4.5) / 2 = 4 (2 + 3) / 2 = 2.5 (3 + 4) / 2 = 3.5 (2.5 + 3.5) / 2 = 3 // результат или тройками (1 + 2 + 3) / 3 = 2 (3 + 4 + 5) / 3 = 4 (2 + 4) / 2 = 3 // результат Ну или любыми другими количественными группами, главное, чтобы они пересекались, если количество чисел нечетное и не делится по группам ровно. Если же, например, можно сразу разбить все на равные группы, то они могут не пересекаться. [1, 2, 3, 4, 5, 6] -> (1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.5 (1 + 2 + 3) / 3 = 2 (4 + 5 + 6) / 3 = 5 (2 + 5) / 2 = 3.5 // результат И, разумеется, можно комбинировать. То есть, на первом уровне, например, складывать парами, на втором тройками. Группы конечно могут быть и по сто и по тысяче чисел. Но думаю, что это может помочь от переполнения.

суббота, 30 ноября 2019 г.

Как эффективно группировать строки?

#java #коллекции #big_data


Нужное решить такую задачу:


  Разбить множество уникальных строк на непересекающиеся группы по
  следующему критерию:
  
  Если две строчки имеют совпадения непустых значений в одной или более
  колонках, они принадлежат одной группе.
  
  Например, строчки

111;123;222
200;123;100
300;;100

  
  все принадлежат одной группе, так как первые две строчки имеют
  одинаковое значение 123 во второй колонке, а две последние одинаковое
  значение 100 в третьей колонке


Также есть ограничение на время работы программы (30 секунд). Также могу добавить
количество строк - около миллиона.
Вот мой код:

private static Set> findLineGroups(List lines) {
    Set> resultSet = new TreeSet<>((Comparator>)
(trSet1, trSet2) -> {
        int diff = trSet2.size() - trSet1.size();
        if (diff != 0)
            return diff;

        Iterator iterator1 = trSet1.iterator();
        Iterator iterator2 = trSet2.iterator();
        while (iterator1.hasNext()) {
            diff = iterator1.next() - iterator2.next();
            if (diff != 0)
                return diff;
        }

        return 0;
    });

    Map termLineGroupsPairs = new HashMap<>();
    List> lineNumGroups = new ArrayList<>();

    for (int lineNum = 0; lineNum < lines.size(); lineNum++) {
        String line = lines.get(lineNum);
        String[] lineElements = line.replaceAll("\"", "").replaceAll(" ", "").split(";");
        Set termSet = new HashSet<>(Arrays.asList(lineElements));
        termSet.remove("");

        Integer groupNum = null;
        TreeSet tempSet = new TreeSet<>(termLineGroupsPairs.keySet());
        tempSet.retainAll(termSet); //оставляем только общие элементы
        if (!tempSet.isEmpty()) {
            String term = tempSet.first();
            groupNum = termLineGroupsPairs.get(term);
            lineNumGroups.get(groupNum).add(lineNum);
        }

        if (groupNum == null) {
            TreeSet group = new TreeSet<>();
            group.add(lineNum);
            lineNumGroups.add(group);
            groupNum = lineNumGroups.size() - 1;
        }
        for (String term : termSet) {
            termLineGroupsPairs.put(term, groupNum);
        }
        if (lineNumGroups.size() % 1000 == 0)
            System.out.println(lineNumGroups.size());
    }

    resultSet.addAll(lineNumGroups);
    return resultSet;
}


И все мои решения работают слишком долго (а пробовал по-разному решать эту задачу).
Правда, если строк меньше тысячи, то работает быстро (укладываюсь в указанное ограничение),
и практически с любым моим алгоритмом.

Прошу подсказать, как можно решить эту задачку (или что изменить в моём решении,
чтобы работало быстро). 
    


Ответы

Ответ 1



К какому решению "почти в лоб" я пришел: храним результат в виде списка списков: [номер_группы -> [строки_группы]] используем вспомогательный список хэш-таблиц: [позиция_слова -> { слово -> номер_группы }] и вспомогательную хэш-таблицу для хранения какая группа в какую была влита каждую строку разбиваем на слова каждое слово строки ищем в соответствующей (позиции слова в строке) хэш-таблице если слово есть, запоминаем номер группы (значение из хэш-таблицы), в которой оно найдено если слова нет, то добавляем его в список новых слов если строка (а точнее её слова) найдена в группах, то берём первую из "живых" (объяснение этого позже) групп, иначе создаём новую группу добавляем новые слова в соответствующие хэш-таблицы с номером найденной/созданной группы объединяем найденные группы в одну, выбранную ранее. Так как группы хранятся в виде списка строк, то просто объединяем списки строк в один у выбранной группы, а более ненужные группы отмечаем как "мёртвые" (присваиваем null, дабы не перемещать элементы внутри списка) добавляем строку в список строк группы Кода выходит ещё больше, чем слов: //вспомогательный класс для добавления новых слов private static class NewWord { public String value; public int position; public NewWord(String value, int position) { this.value = value; this.position = position; } } private static List> findGroups(List lines) { List> wordsToGroupsNumbers = new ArrayList<>(); //[позиция_слова:{слово:номер_группы}] List> linesGroups = new ArrayList<>(); //[номер_группы:[строки_группы]] Map mergedGroupNumberToFinalGroupNumber = new HashMap<>(); //{номер_слитой_группы:номер_группы_в_которую_слили} for (String line : lines) { String[] words = line.split(";"); TreeSet foundInGroups = new TreeSet<>(); List newWords = new ArrayList<>(); for (int i = 0; i < words.length; i++) { String word = words[i]; if (wordsToGroupsNumbers.size() == i) wordsToGroupsNumbers.add(new HashMap<>()); if (word.equals("")) continue; Map wordToGroupNumber = wordsToGroupsNumbers.get(i); Integer wordGroupNumber = wordToGroupNumber.get(word); if (wordGroupNumber != null) { while (mergedGroupNumberToFinalGroupNumber.containsKey(wordGroupNumber)) wordGroupNumber = mergedGroupNumberToFinalGroupNumber.get(wordGroupNumber); foundInGroups.add(wordGroupNumber); } else { newWords.add(new NewWord(word, i)); } } int groupNumber; if (foundInGroups.isEmpty()) { groupNumber = linesGroups.size(); linesGroups.add(new ArrayList<>()); } else { groupNumber = foundInGroups.first(); } for (NewWord newWord : newWords) { wordsToGroupsNumbers.get(newWord.position).put(newWord.value, groupNumber); } for (int mergeGroupNumber : foundInGroups) { if (mergeGroupNumber != groupNumber) { mergedGroupNumberToFinalGroupNumber.put(mergeGroupNumber, groupNumber); linesGroups.get(groupNumber).addAll(linesGroups.get(mergeGroupNumber)); linesGroups.set(mergeGroupNumber, null); } } linesGroups.get(groupNumber).add(line); } linesGroups.removeAll(Collections.singleton(null)); return linesGroups; } Возможно, у меня слишком быстрый для тестирования скорости работы "как им надо" компьютер, но для миллиона строк, каждая из которых состоит из 5 слов, каждое из которых, в свою очередь, состоит из 5 строчных букв английского алфавита, время выполнения составляет примерно 4 секунды.

Ответ 2



Есть такая структура данных - лес непересекающихся множеств (disjoint sets union). Устроен он очень просто, и позволяет быстро объединять связанные элементы. В данном случае, если игнорировать утверждение про необходимость совпадения номера колонки, каждое множество (группа) должно содержать в себе хэш-таблицу (map) элементов типа 123 и т.д. Каждая новая строка делится на элементы и проверяется наличие этих элементов в существующих группах. - Если ни один элемент не имеет совпадений, заводится новая группа. - Если совпадение одно, строка добавляется в эту группу. - Если два и более - строка служит линком, связывающим группы - они объединяются, и строка добавляется к объединенной группе. При учёте номера колонки заводится количество map, соответствующее максимальному количеству колонок, и поиск проводится в каждом из map.

вторник, 26 ноября 2019 г.

Как убрать ошибки измерений?




Есть вот такой набор точек, каждая точка представляет собой gps координату автобус
(x,y), у каждой точки есть timestamp.  На построенном графике  видны явные ошибки измерения
Как их можно убрать? Решение должно быть простым, так как всего координат около 100 тысяч. Интересует идея, но желательно, чтобы ее можно было без особых проблем реализовать средствами Java

Пример исходных данных:

  1447037729    3054.619968    2409.828279    570d8


Первое поле - UNIX-время, второе и третье - (x,y) соответственно, четвертое - идентификатор автобуса (автобусов около 50ти) . 
Исходные данные : https://drive.google.com/file/d/0B4bA9d5B_O_BcVpPUXpYTmZBUFE/view
    


Ответы

Ответ 1



Приведённый набор точек - это две зависимости: x(t) и y(t), и по каждой идёт импульсны шум. К таким данным идеально подходит алгоритм медианной обработки в скользящем окне на 7-9 элементов, когда i-тый во времени элемент заменяется на медианное значение элементов с номерами (i-h,i+h) при h=3...4. Обработку для x(t) и y(t) следует проводить независимо, после чего подменить ими исходные массивы. Обработка эффективна при высоком уровне импульсной помехи (в тестовом примере искажен третья часть данных). Дополнительный плюс - что сохраняется формат исходных данных. Обработка краёв ведётся на окнах меньшего размера. Минусы обработки в скользящем окне проявляются при разворотах последовательности, поскольку выступы и провалы шириной меньше h выполаживаются. В демо-программе представлена рекуррентная сортировка массива в окне. Для этого точки лежащие между старым (удаляемым) и новым (добавляемым) элементами, сдвигаются в сторону старого элемента, после чего на место крайнего из возникших дубликатов записывается новый элемент. Это резко снижает вычислительные затраты. Демо-программа (PHP): function print_a($a, $name){ print("$name: "); foreach($a as $item){ printf("%2d, ",$item); } } function slide_median($h, $a){ $size = count($a); $result = []; $slide = []; array_push($slide, reset($a)); array_push($result,$slide[0]); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); for($i=1; $i<=$h; $i++){ array_push($slide, next($a), next($a)); sort($slide); array_push($result, $slide[$i]); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); } for($i=0; $i < $size-2*$h-1; $i++){ $old = $a[$i]; $new = $a[$i+2*$h+1]; if($old < $new){ for($key = 0; $key <= 2*$h; $key++){ if($new < $slide[$key]){ break; } if(($old <= $slide[$key])&&($slide[$key] < $new)) $slide[$key] $slide[$key+1]; } $slide[$key-1] = $new; } if($old > $new){ for($key = 2*$h; $key >= 0; $key--){ if($new > $slide[$key]){ break; } if(($old >= $slide[$key])&&($slide[$key] > $new)) $slide[$key] $slide[$key-1]; } $slide[$key+1] = $new; } array_push($result, $slide[$h]); print(" old = $old, new =$new"); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); } for($i = $h-1; $i > 0; $i--){ $slide = array_slice($a, $size-2*$i-1, 2*$i+1); sort($slide); array_push($result, $slide[$i]); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); } $slide = [$a[$size-1]]; array_push($result, $slide[0]); print_a([end($a)], " Сортировка в окне"); print_a($a, "

Исходный массив: "); print_a($result, "
Массив результата"); return $result; }; $a = range(20, 40); foreach($a as &$item){ $item += 5*mt_rand(-1,1)*(int)(mt_rand(0,199)/100); } print_a($a, "Исходный массив: "); slide_median(3, $a); Результаты (импульсный шум, амплитуда 5): Исходный массив: : 20, 21, 22, 23, 19, 20, 21, 27, 28, 29, 30, 31, 32, 28, 29, 35, 36, 42, 43, 39, 35,  Сортировка в окне: 20, Массив результата: 20,  Сортировка в окне: 20, 21, 22, Массив результата: 20, 21,  Сортировка в окне: 19, 20, 21, 22, 23, Массив результата: 20, 21, 21,  Сортировка в окне: 19, 20, 20, 21, 21, 22, 23, Массив результата: 20, 21, 21, 21,  old = 20, new =27 Сортировка в окне: 19, 20 21, 21, 22, 23, 27, Массив результата: 20, 21, 21, 21, 21,  old = 21, new =28 Сортировка в окне: 19 20, 21, 22, 23, 27, 28, Массив результата: 20, 21, 21, 21, 21, 22,  old = 22, new =29 Сортировка в окне 19, 20, 21, 23, 27, 28, 29, Массив результата: 20, 21, 21, 21, 21, 22, 23,  old = 23, new =30 Сортировка в окне: 19, 20, 21, 27, 28, 29, 30, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27,  old = 19, new =31 Сортировк в окне: 20, 21, 27, 28, 29, 30, 31, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28,  old = 20, new =32 Сортировка в окне: 21, 27, 28, 29, 30, 31, 32, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29,  old = 21, new =28 Сортировка в окне: 27, 28, 28, 29, 30, 31, 32, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29,  old = 27, new =29 Сортировка в окне: 28, 28, 29, 29, 30, 31, 32, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29,  old = 28, new =35 Сортировка в окне: 28, 29, 29, 30, 31, 32, 35, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30,  old = 29, new =36 Сортировка в окне: 28, 29, 30, 31, 32, 35, 36, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31,  old = 30, new =42 Сортировка в окне: 28, 29, 31, 32, 35, 36, 42, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32,  old = 31, new =43 Сортировка в окне: 28, 29, 32, 35, 36, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35,  old = 32, new =39 Сортировка в окне: 28, 29, 35, 36, 39, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36,  old = 28, new =35 Сортировка в окне: 29, 35, 35, 36, 39, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36,  Сортировка в окне: 35, 36, 39, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36, 39,  Сортировка в окне: 35, 39, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36, 39, 39,  Сортировка в окне: 35, Исходный массив: : 20, 21, 22, 23, 19, 20, 21, 27, 28, 29, 30, 31, 32, 28, 29, 35, 36, 42, 43, 39, 35, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36, 39, 39, 35, Сравнение скользящей медианы и скользящего среднего на интенсивной импульсной помехе проведено с помощью следующей программы: function print_a($a, $name){ print("$name: "); foreach($a as $item){ printf("%3d, ",$item); } } function slide_median($h, $a){ $size = count($a); $result = []; $slide = []; array_push($slide, reset($a)); array_push($result,$slide[0]); for($i=1; $i<=$h; $i++){ array_push($slide, next($a), next($a)); sort($slide); array_push($result, $slide[$i]); } for($i=0; $i < $size-2*$h-1; $i++){ $old = $a[$i]; $new = $a[$i+2*$h+1]; if($old < $new){ for($key = 0; $key <= 2*$h; $key++){ if($new < $slide[$key]){ break; } if(($old <= $slide[$key])&&($slide[$key] < $new)) $slide[$key] $slide[$key+1]; } $slide[$key-1] = $new; } if($old > $new){ for($key = 2*$h; $key >= 0; $key--){ if($new > $slide[$key]){ break; } if(($old >= $slide[$key])&&($slide[$key] > $new)) $slide[$key] $slide[$key-1]; } $slide[$key+1] = $new; } array_push($result, $slide[$h]); } for($i = $h-1; $i > 0; $i--){ $slide = array_slice($a, $size-2*$i-1, 2*$i+1); sort($slide); array_push($result, $slide[$i]); } $slide = [$a[$size-1]]; array_push($result, $slide[0]); print_a($a, "

Исходный массив "); print_a($result, "
Массив медиан  "); return $result; }; function slide_average($h, $a){ $size = count($a); $b = array_merge([0], $a); $sum = reset($a); $result = [$sum]; for($i=1; $i<=$h; $i++){ $sum += next($a)+next($a); $average = (int)($sum/(2*$i+1)+.5); array_push($result, $average); } reset($b); for($i=0; $i < $size-2*$h-1; $i++){ $sum += next($a) - next($b); $average = (int)($sum/(2*$h+1)+.5); array_push($result, $average); } for($i = $h-1; $i >=0; $i--){ $sum -= (next($b) + next($b)); $average = (int)($sum/(2*$i+1)+.5); array_push($result, $average); } print_a($a, "

Исходный массив "); print_a($result, "
Массив средних  "); return $result; }; $a = range(200, 240); foreach($a as &$item){ $item += 50*mt_rand(-1,1)*(int)(mt_rand(0,149)/100); } slide_median(3, $a); slide_average(3, $a); Результаты: Исходный массив : 200, 201, 202, 203, 204, 155, 206, 207, 208, 209, 210, 211, 212 213, 264, 265, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 183, 234, 235, 236, 287, 238, 239, 240, Массив медиан  : 200, 201, 202, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212 213, 216, 217, 218, 219, 219, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 229, 230, 231, 232, 234, 235, 236, 238, 239, 239, 240, Исходный массив : 200, 201, 202, 203, 204, 155, 206, 207, 208, 209, 210, 211, 212 213, 264, 265, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 183, 234, 235, 236, 287, 238, 239, 240, Массив средних  : 200, 201, 202, 196, 197, 198, 199, 200, 201, 209, 210, 218, 226 227, 228, 229, 230, 231, 225, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 223, 224, 225, 226, 234, 235, 236, 244, 248, 239, 240, Видно, что скользящая медиана лучше сглаживает интенсивные случайные выбросы данных. Для данных из реального массива (x,y округлялись до целых): Обработка массива x80c48 Исходный массив : 10790, 10728, 10565, 10228, 10148, 9911, 9861, 9880, 9887, 9894 9907, 9910, 9917, 9932, 9937, 9925, 9900, 9684, 9579, 9446, 9040, 8912, 8703, 8457 8350, 8338, 8129, 8040, 7900, 7836, 7731, 7490, 7271, 7250, 7165, 7013, 6912, 6848 6823, 6857, 6868, 6894, 6902, 6903, 6904, 7114, 7067, 7047, 6994, 6883, 6848, 6787, 6722, 6412, 6236, 6136, 6012, 5991, 5659, 5648, 5595, 5327, 5079, 5015, 4678, 4639, 4569, 4294, 4241, 4164, 4137, 3948, 3905, 3771, 3731, 3664, 3577, 3422, 3304, 3086, 3053, 2977, 2967, 3248, 3257, 3202, 2954, 2834, 2594, 2574, 2611, 2730, 2766, 2514, 2387, 2368, 2365, 2344, 2312, 2098, 1905, 1722, 1579, 1233, 1206, 963, 815, 825, Массив медиан  : 10790, 10728, 10565, 10228, 10148, 9911, 9894, 9894, 9894, 9894 9907, 9910, 9917, 9917, 9917, 9917, 9900, 9684, 9579, 9446, 9040, 8912, 8703, 8457 8350, 8338, 8129, 8040, 7900, 7836, 7731, 7490, 7271, 7250, 7165, 7013, 6912, 6868 6868, 6868, 6868, 6894, 6902, 6903, 6904, 6994, 6994, 6994, 6994, 6883, 6848, 6787, 6722, 6412, 6236, 6136, 6012, 5991, 5659, 5648, 5595, 5327, 5079, 5015, 4678, 4639, 4569, 4294, 4241, 4164, 4137, 3948, 3905, 3771, 3731, 3664, 3577, 3422, 3304, 3086, 3086, 3086, 3086, 3053, 2977, 2967, 2954, 2834, 2730, 2730, 2611, 2594, 2574, 2514, 2387, 2368, 2365, 2344, 2312, 2098, 1905, 1722, 1579, 1233, 1206, 963, 825, 825, Исходный массив : 10790, 10728, 10565, 10228, 10148, 9911, 9861, 9880, 9887, 9894 9907, 9910, 9917, 9932, 9937, 9925, 9900, 9684, 9579, 9446, 9040, 8912, 8703, 8457 8350, 8338, 8129, 8040, 7900, 7836, 7731, 7490, 7271, 7250, 7165, 7013, 6912, 6848 6823, 6857, 6868, 6894, 6902, 6903, 6904, 7114, 7067, 7047, 6994, 6883, 6848, 6787, 6722, 6412, 6236, 6136, 6012, 5991, 5659, 5648, 5595, 5327, 5079, 5015, 4678, 4639, 4569, 4294, 4241, 4164, 4137, 3948, 3905, 3771, 3731, 3664, 3577, 3422, 3304, 3086, 3053, 2977, 2967, 3248, 3257, 3202, 2954, 2834, 2594, 2574, 2611, 2730, 2766, 2514, 2387, 2368, 2365, 2344, 2312, 2098, 1905, 1722, 1579, 1233, 1206, 963, 815, 825, Массив средних  : 10790, 10694, 10492, 10319, 10189, 10069, 9973, 9927, 9893, 9894 9904, 9912, 9917, 9918, 9886, 9839, 9772, 9644, 9498, 9323, 9117, 8927, 8749, 8561 8418, 8274, 8150, 8046, 7923, 7771, 7645, 7520, 7394, 7262, 7136, 7040, 6981, 6927 6888, 6872, 6871, 6879, 6920, 6950, 6976, 6990, 6987, 6980, 6963, 6907, 6813, 6697, 6575, 6450, 6328, 6167, 6013, 5897, 5767, 5616, 5473, 5286, 5140, 4986, 4800, 4645, 4514, 4389, 4285, 4180, 4066, 3985, 3903, 3819, 3717, 3625, 3508, 3405, 3298, 3198, 3151, 3127, 3113, 3094, 3063, 3008, 2952, 2861, 2786, 2723, 2660, 2597, 2564, 2534, 2496, 2437, 2341, 2254, 2159, 2046, 1885, 1722, 1529, 1346, 1192, 1008, 868, 825, Обработка массива y80c48 Исходный массив : 7289, 7275, 7243, 7178, 7163, 7119, 7109, 6903, 6785, 6680, 6448 6379, 6264, 5869, 5709, 5448, 5353, 5083, 5081, 5083, 5063, 5062, 5042, 5166, 5186 5172, 4916, 4925, 4982, 5009, 5025, 5007, 4993, 4991, 4986, 4976, 4968, 4964, 4962 4636, 4488, 4148, 4038, 4029, 4010, 3960, 3798, 3709, 3462, 3475, 3480, 3489, 3498, 3542, 3567, 3581, 3598, 3586, 3647, 3649, 3656, 3693, 3727, 3736, 3782, 3788, 3797, 3831, 3837, 3743, 3710, 3550, 3511, 3406, 3379, 3435, 3520, 3455, 3367, 3204, 3179, 3123, 3115, 2534, 2519, 2472, 2351, 2281, 2141, 2129, 2066, 1862, 1801, 1522, 1302, 1218, 1200, 1038, 851, 812, 778, 746, 721, 621, 529, 420, 386, 279, Массив медиан  : 7289, 7275, 7243, 7178, 7163, 7119, 7109, 6903, 6785, 6680, 6448 6379, 6264, 5869, 5709, 5448, 5353, 5083, 5083, 5081, 5081, 5081, 5083, 5063, 5062 5042, 5009, 5009, 5007, 4993, 4993, 4993, 4993, 4991, 4986, 4976, 4968, 4964, 4962 4636, 4488, 4148, 4038, 4029, 4010, 3960, 3798, 3709, 3489, 3489, 3489, 3489, 3498, 3542, 3567, 3581, 3586, 3598, 3647, 3649, 3656, 3693, 3727, 3736, 3782, 3788, 3788, 3788, 3788, 3743, 3710, 3550, 3511, 3511, 3455, 3435, 3406, 3379, 3367, 3204, 3179, 3123, 3115, 2534, 2519, 2472, 2351, 2281, 2141, 2129, 2066, 1862, 1801, 1522, 1302, 1218, 1200, 1038, 851, 812, 778, 746, 721, 621, 529, 420, 386, 279, Исходный массив : 7289, 7275, 7243, 7178, 7163, 7119, 7109, 6903, 6785, 6680, 6448 6379, 6264, 5869, 5709, 5448, 5353, 5083, 5081, 5083, 5063, 5062, 5042, 5166, 5186 5172, 4916, 4925, 4982, 5009, 5025, 5007, 4993, 4991, 4986, 4976, 4968, 4964, 4962 4636, 4488, 4148, 4038, 4029, 4010, 3960, 3798, 3709, 3462, 3475, 3480, 3489, 3498, 3542, 3567, 3581, 3598, 3586, 3647, 3649, 3656, 3693, 3727, 3736, 3782, 3788, 3797, 3831, 3837, 3743, 3710, 3550, 3511, 3406, 3379, 3435, 3520, 3455, 3367, 3204, 3179, 3123, 3115, 2534, 2519, 2472, 2351, 2281, 2141, 2129, 2066, 1862, 1801, 1522, 1302, 1218, 1200, 1038, 851, 812, 778, 746, 721, 621, 529, 420, 386, 279, Массив средних  : 7289, 7269, 7230, 7197, 7141, 7071, 6991, 6887, 6775, 6653, 6475 6305, 6114, 5924, 5729, 5544, 5375, 5260, 5168, 5110, 5083, 5098, 5111, 5087, 5067 5056, 5051, 5031, 5005, 4980, 4990, 4999, 4998, 4992, 4984, 4977, 4926, 4854, 4735 4601, 4466, 4330, 4187, 4067, 3956, 3858, 3778, 3699, 3625, 3559, 3522, 3502, 3519, 3536, 3552, 3574, 3596, 3612, 3630, 3651, 3671, 3699, 3719, 3740, 3765, 3785, 3788, 3784, 3751, 3711, 3655, 3591, 3533, 3502, 3465, 3439, 3395, 3363, 3326, 3280, 3140, 3006, 2878, 2756, 2628, 2488, 2347, 2280, 2186, 2090, 1972, 1832, 1700, 1567, 1420, 1276, 1135, 1028, 949, 878, 795, 723, 661, 600, 529, 447, 362, 279, По сравнению с алгоритмом скользящего среднего, скользящая медиана намного аккуратнее обращается с данными. Есть перевод статьи на английский язык.

Ответ 2



Для обработки зашумленных данных стоит подумать о фильтре Калмана. Так как у вас тут автобус - сразу вопрос - этот автобус движется по заранее известному маршруту?

Ответ 3



Если нужно сгладить график - есть алгоритмы фильтрации и аппроксимации. Во первы это метод скользящего среднего, которым можно немного подкорректировать входные данны для их большей гладкости. Далее, можно разбить график на небольшие куски и аппроксимировать их, скажем, параболами с помощью метода наименьших квадратов (о его идее я рассказывал здесь). Ещё можно использовать Фурье преобразование входных данных, убрать оттуда высокие частоты (что и будет шумом), после чего использовать обратное преобразование.

Ответ 4



На графике видны как явные, так и неявные ошибки. Имеются повороты, плюс на каждо повороте есть сомнительные точки, по которым сразу и не скажешь, ошибка это, или автобус такой загогулиной ехал. То есть простые варианты типа отбрасывания точек для "невероятных" перемещений на участках поворотов вряд ли помогут. Могу предложить использовать сглаживание методом локальной регрессии, затем построит доверительный интервал (вернее, доверительную область), но не для регрессии, а для значений (см. здесь), и отбрасывать точки за его пределами. 95% интервал может оказаться очень широким, возможно, потребуется взять 90%, 80% и т.д. Что касается 100 тыс. наблюдений, то никакой бигдаты тут нет, на вполне заурядном компе можно пробовать самые разные методы.

Ответ 5



Предлагаю два критерия отбрасывания плохих точек. Зигзаги - два поворота в разные стороны больше 90 градусов подряд. Это на самом дел возможно при откате и пробуксовывании, но если это происходит на одной линии, то н повредит внешний вид графика, а если со смещением, то скорее всего это результат ошибки. Однако при редких точках возможно, что автобус действительно так ехал, здесь критерий мне не ясен. Ещё часто наблюдается ошибочный дрейф во время стоянки. При обнаружении зигзага н маленькой скорости вместо удаления можно скопировать соседнюю точку. Возможно так же считать критерием стоянки постоянную очень маленькую скорость длительное время, но это может быть и движение в пробке. Слишком большое нормальное ускорение с чередованием знака (автобус не дрова везёт) - тут предлагаю порог 0,4 м/c^2 относительно предыдущего значения. Программа #include #include #include #define ZZMAXLEN 40 #define MAXDA 0.4 int main(int argn, char **argv) { double x[4], y[4]; // кольцевые буферы данных int t[4]={0,0,0,0}, n=0, h=0; char id[9]; if(argn!=2) { fprintf(stderr, "Usage:\n\t%s bus-ID\n", argv[0]); return 1; } while(scanf("%d %lf %lf %8s", t+h, x+h, y+h, id)==4) { if(strcmp(id, argv[1])) continue; h= h+1 & 3; if(++n < 4) continue; int dt1= t[h+1&3]-t[h]; int dt2= t[h+2&3]-t[h+1&3]; int dt3= t[h+3&3]-t[h+2&3]; double dx1= x[h+1&3]-x[h]; double dy1= y[h+1&3]-y[h]; double dx2= x[h+2&3]-x[h+1&3]; double dy2= y[h+2&3]-y[h+1&3]; double dx3= x[h+3&3]-x[h+2&3]; double dy3= y[h+3&3]-y[h+2&3]; int filter=0; if(dx1*dx2+dy1*dy2 < 0 && dx2*dx3+dy2*dy3 < 0 && dx1*dx3+dy1*dy3 > 0 && dx2*dx2+dy2*dy2 < ZZMAXLEN*ZZMAXLEN) { // разворот на угол более 90 градусов 2 раза подряд if((dx2*dx2+dy2*dy2)>0 && (dx2*dx2+dy2*dy2)/(dt2*dt2)<0.05) { fprintf(stderr, "D %d\n", n-1); // дрейф filter=2; } else { fprintf(stderr, "Z %d\n", n-2); filter=1; } } // скорости dx1/=dt1; dx2/=dt2; dx3/=dt3; dy1/=dt1; dy2/=dt2; dy3/=dt3; double vx12= (dx1+dx2)/(dt1+dt2); double vy12= (dy1+dy2)/(dt1+dt2); double vx23= (dx2+dx3)/(dt2+dt3); double vy23= (dy2+dy3)/(dt2+dt3); double v12= sqrt(vx12*vx12+vy12*vy12); double v23= sqrt(vx23*vx23+vy23*vy23); // ускорения double ax1= (dx2-dx1)*2/(dt1+dt2); double ay1= (dy2-dy1)*2/(dt1+dt2); double ax2= (dx3-dx2)*2/(dt2+dt3); double ay2= (dy3-dy2)*2/(dt2+dt3); // нормальные ускорения double an1= v12? (vx12*ay1-vy12*ax1)/v12 : 0; double an2= v23? (vx23*ay2-vy23*ax2)/v23 : 0; if(fabs(an1-an2) > MAXDA && an1*an2<0) { fprintf(stderr, "N %d %f %f\n", n-2, an1, an2); // два резких поворота filter=1; } if(filter) { // определяем какую точку удалить if(fabs(an1) > fabs(an2)) if(filter==2) { x[h+1&3]=x[h+2&3]; y[h+1&3]=y[h+2&3]; } else { x[h+1&3]=x[h+2&3]; y[h+1&3]=y[h+2&3]; t[h+1&3]=t[h+2&3]; } else if(filter==2) { x[h+2&3]=x[h+1&3]; y[h+2&3]=y[h+1&3]; } else { x[h+2&3]=x[h+1&3]; y[h+2&3]=y[h+1&3]; t[h+2&3]=t[h+1&3]; } } // тут можно пропустить точки с поворяющимся временем, но я оставил для построения графиков printf("%d\t%lf\t%lf\t%s\n", t[h], x[h], y[h], id); } t[h]=0; do { h=h-1&3; } while (t[h-1&3]0); do { printf("%d\t%lf\t%lf\t%s\n", t[h], x[h], y[h], argv[1]); h= h+1&3; } while (t[h-1&3]0); return 0; } Результаты работы получаем на стандартном выводе, а в стандартном выводе ошибок список удалённых точек и причина (N - нормальное ускорение, Z - зигзаг, D - зигзаг в дрейфе): $ ./a.out 80c48 < data.txt > 80c48.txt N 154 -0.163844 0.490944 D 249 D 252 Z 303 Z 444 Z 567 N 631 0.283293 -0.136238 Z 636 N 727 0.321036 -0.103197 N 984 -0.366378 0.309231 Z 991 N 1082 0.414078 -0.203378 N 1199 -0.020139 0.572870 D 1213 Z 1414 D 1507 D 1515 D 1517 D 1538 И иллюстрация удаления точки 154: Видимо исходные данные уже обработаны каким-то фильтром, так как отсчёты времен неравномерны. Данный метод вырезает одиночные ошибки, но данные GPS часто могут накапливать ошибк (и фильтр Калмана тоже) , и для борьбы с этим можно предложить притягивать слишком отклонённые точки к сетке дорог, которую можно получить усредняя много траекторий.

вторник, 9 июля 2019 г.

Большая MYSQL таблица (более 200 000 000 записей). Как расставить индексы?

Есть большая (на мой взгляд) MYSQL таблица (19 полей). Даже запрос SELECT COUNT(*) FROM table_name; выполняется 6 минут, что меня не очень устраивает. Нужно расставить индексы так, чтобы не испортить дела окончательно и быстро производить выборки по нескольким определенным полям (примерно 5-7 из 19). Можно ли добавлять индексы к уже заполненной таблице, и сколько по времени это будет происходить?


Ответ

Добавление индекса на большой табличке
Добавить индекс можно всегда. Вопрос в том, что при этом произойдёт постороннего. В зависимости от storage engine и версии СУБД.
mysql/innodb до 5.6 при добавлении индекса заблокируют таблицу на запись. Т.е. все insert, update, delete запросы будут ждать окончание создания индекса. select работать сможет. Начиная с 5.6 - создание индекса возможно конкурентное.
Если вы на старой версии - то классический трюк был поднять репликацию, затем на слейве создать индекс, подождать, пока слейв догонит мастер, переключить слейв в новый мастер. Этот же трюк должен получиться с любым storage engine.
Сколько времени займёт создание индекса - в зависимости от объёма данных и железки, на которой крутится mysql
count
Просто не используйте count на больших наборах строк. Для аналитики - считайте отдельно. Для тяжёлой аналитики нормальное решение - отдельная железка с репликацией базы только для запросов расчёта аналитики.
Или считать count'ы отдельно, редисом, или колоночными субд. Или триггерами, если читать количество надо гораздо чаще, чем писать.
как расставить индексы
Нужно изучать конкретное приложение. Универсального ничего сказать нельзя.
Что вообще делать с большой табличкой
У mysql из коробки есть поддержка партицирования. Некоторые ограничения есть по использованию, например не поддерживаются foreign keys. Как правило, почти все запросы от приложения к большим табличкам имеют под собой какой-то общий фильтр. Например, 90% запросов хотят данные только за последний месяц, или почти всегда привязаны к id пользователя. Возможно, данные в вашей табличке можно порезать на партиции.

вторник, 25 июня 2019 г.

Эффективная работа с большими объемами данных

Изначальная архитектура приложения была построена ошибочно - все данные сливались в один файл, размер которого перевалил теперь за отметку 950ГБ.
Есть ли какой-нибудь эффективный метод (из области big-data) выделения из этого массива данных групп сущностей, состоящих из одних и тех же символов (так называемых анаграмм)?


Ответ

Стандартная идея вот какая:
К каждой сущности дописать её номер в исходном списке. У каждой сущности отсортировать символы. Отсортировать сущности. Теперь анаграммы будут находиться рядом. Теперь для нахождения анаграмм нужен лишь один пробег по данным.
Все эти операции хорошо «параллелятся», Кроме, пожалуй сортировки.
С учётом этого можно изменить немного алгоритм:
Распартиционировать данные как угодно. Пронумеровать сущности и отсортировать их символы на каждом хосте по отдельности (номеру назначать уникальный префикс, чтобы не смешивать) Отсортировать данные каждого хоста. Смёржить все данные. Сначала заливать результат на первый хост, когда 1/n всех данных зальётся — на второй хост и т. д. (Это по сути перепартиционирование.) Далее пробег по данным на каждом хосте.
Вместо пункта 4, возможно, более эффективно будет не сливать данные вместе, а делать многохостовый пробег:
string currentValue = null; int anagramCount = 0; string[] nextByHost = new string[N]; for (i = 0..n-1) { currentByHost[i] = ""; nextByHost = fetch from host[i]; }
while (any of hosts has data) { if any of nextByHost[i] equals to currentValue, anagramCount++ fetch next from host[i] to nextByHost[i] else store currentValue and anagramCount currentValue = min(nextByHost) }

четверг, 20 июня 2019 г.

Где я ошибаюсь в алгоритме классификации текстов?

Здравствуйте. У меня такая проблема. В связи скудностью информации по классификации текстовых сообщений на русском языке возникли некоторые вопросы и не до конца понятен четкий алгоритм действий.
Дано - csv файл с запросами (10000), которые надо распределить на категории. Я так понял такой алгоритм:
Берём файл и проводим нормализацию - удаляем стоп-слова и знаки препинания, приводим все слова к единой форме (или правильно сказать в начальную форму, т.е. выполняется стемминг). Потом делим всю выборку на тестовую и обучающую (30 к 70). Получается вручную размечаем корпус по категориям? Или можно использовать TF-IDF для выделения часто встречающихся слов? Переводим слова в векторную форму. Тут тоже вопрос - как лучше? Использовать Bag of Words? Получается для каждого запроса строить отдельно вектор с встречающимся в них словах или делать сразу для всей категории (или возможно для всей выборки)? То есть на выходе мы должны получить несколько векторов или один большой вектор (с часто входящими словами?) для целой категории? Подаем полученный/ые вектор/а на вход какому-нибудь из алгоритмов классификации. Обучаем его. Берем запрос из тестовой выборки, так же приводим его в нормальную форму и подаем на вход алгоритма и смотрим ответ.
Вроде как-то так. И сразу последний вопрос - чтобы по 10 раз не обучать классификатор и не хранить все в памяти можно как-нибудь (например, если мы возьмем нейронную сеть) записывать веса и при загрузке просто распределять уже их и готово или каждый раз придется проходить обучение? Заранее всем спасибо


Ответ

1) Классификация звучит как-то размыто. Какие конкретно классы нужны? Вариантов масса - спам\неспам, извлечение тематики и т.д. Можно даже предоставить пару примеров исходных текстов.
2) Нормализация и стемминг алгоритмы похожие, но все же разные. Нормализация - приведение слова к единственному числу, именительному падежу, инфинитив для глагола, настоящее время и т.д. Стемминг - крайне грубая операция, которая просто отсекает суффиксы и окончания. Зато быстрая. Ну и как правильно было отмечено, перед нормализацией удаляются стоп-слова - предлоги, междометия всякие. Может даже имена собственные - даже на этом шаге есть над чем поразмышлять.
3) Если изначально нет обучающей выборки с размеченными классами, то задача резко усложняется. TF-IDF - всего лишь способ представить слова в виде векторов - он не сможет автоматически извлечь какие-то слова, которые характеризовали бы предложение. У TF-IDF нет такого параметра, как "важность", "вес" слова. (Кстати, выходом частотной модели векторизации будет множество векторов - по одному вектору на каждое уникальное слово). Есть только частота, а я бы поостерегся утверждать, что какое-то одно слово характеризует весь текст или предложение, основываясь только на частоте. Таким образом, если нет обучающей выборки, то это тупик. И нужно смотреть на другие алгоритмы - LDA, LSI - они способны разбить множество входных текстов на категории, на "темы", основываясь на содержании этих текстов. Неизвестен патентный статус. Меня результаты работы этих алгоритмов не впечатлили.
4) Если есть обучающая выборка, то все также не очень просто. Нельзя просто так взять и обучить классификатор на входных данных переменной длины - в разных документах или предложениях разное количество векторов. А все классичиеские классификаторы работают с входным вектором одинаковой длины. Выходов здесь можно придумать также массу - интерполировать недостающие значения, заполнять пустышками, попытаться отсеивать незначащие признаки.
Таким образом, готового алгоритма в виде "скормили" массив документов и все сделалось хорошо не существует. Наиболее близки LDA, LSI. А строить гипотезы можно очень долго, осбенно, если задача толком неясна.
На чем реализовывать? Так как метка языка не была указана осммелюсь посоветовать писать это все на python. Уже написаны прекрасные библиотеки для всего, чего угодно. gensim, NLTK - для работы с текстом; skikit-learn, numpy, scipy, FANN, PyBrain, Theano - для работы с числами; pymorhy2 - для нормализации русского текста, но нормализация проводится без контекста (то есть не получится отличить "сталь" от "стать" в слове "стали").

понедельник, 3 июня 2019 г.

Big data, оптимизация запросов

Есть следующая простая структура данных
Id; fk_Security_Id; DateTime; Price
Строка хранит данные по инструменту(активу), дату и время, цену(котировку). Строк в БД на данный момент ~ 1 млрд. 200 млн. (10 инструментов с историей за прошлые 10 лет)
Задача - выборка данных по указанному fk_Security_Id и промежутку DateTime(например, июль 2000г.) за адекватный промежуток времени (в идеале меньше минуты).
Сначала, я использовал знакомый мне MSSQL и навесил в лоб clustered index на эти 2 поля. В результате поиск по этим 2 полям занимает в районе 35 минут и сожранные 6.5Gb RAM. Не совсем то, что конечно хотелось бы. Какие варианты решения вижу пока я:
Не менять выбранную бд, а изменить саму структуру хранения данных. Например разнести в разные таблицы данные по разным инструментам. В этом случае конечно будут абсолютно идентичные таблицы с точки зрения структуры, но можно будет выиграть некоторое время на поиске и дальнейшее добавление новых инструментов не будет влиять на то самое время поиска. И тогда вместо композитного кластерного индекса, индекс будет состоять из одного поля - datetime. Также возможно здесь имеет смысл вместо поля datetime в качестве индекса брать некий timestamp или преобразованный Id. Но не уверен что это даст существенный прирост в поиске, хотя стоит попробовать думаю. Использовать какую-нибудь более легковесную бд, например postgres (дружит с необходимым мне EF, что очень хотелось бы) + есть нативная поддержка Sphinx-а например. Использовать какое-нибудь NoSql решение. С данными бд дел не имел, но допускаю,что в моем случае данные укладываются в простую структуру key-value. Правда, наверное те NoSql которые держат данные в RAM мне не подойдут потому что у меня просто столько памяти нету. Хотя, если я не ошибась есть и достаточно шустрые дисковые NoSql , Aerospike например. Но опять же поскольку я с ними не работал я не могу оценить насколько они дадут выигрыш по времени по сравнению с обыными реляционными бд.
База не распределенная, ресурсы машины - 8 потоков и 8Gb RAM. Буду рад любому совету.


Ответ

Пара мыслей (eсли вы всё же остановитесь на MSSQL).
На мой взгляд big-data подразумевает щепетильное отношение к структурам хранения данных и типам хранимых данных.
Сравните, к примеру, размеры различных типов данных для хранения дат и чисел:
declare @dt datetime = getdate(), @dt2 datetime2(0) = getdate(), @sdt smalldatetime = getdate(), @m money = 1.0, @f float = 1.0, @dec_15_5 decimal(15,5) = 1.0, @r real = 1.0
select [datetime] = datalength(@dt), [datetime2(0)] = datalength(@dt2), [smalldatetime] = datalength(@sdt), [money] = datalength(@m), [float] = datalength(@f), [decimal(15,5)] = datalength(@dec_15_5), [real] = datalength(@r)
datetime datetime2(0) smalldatetime money float decimal(15,5) real --------- ------------- -------------- ------ ------ -------------- ----- 8 6 4 8 8 5 4
Если тип столбца DateTime у вас datetime, рассмотрите возможность использования, например, типа smalldatetime (диапазон значений от 1900-01-01 до 2079-06-06 с точностью 1 минута). Если, тип стоблца Price, к примеру, float - рассмотрите возможность использования типов decimal (numeric) или real. Чем меньше размер строки данных, тем больше строк помещается в одну страницу памяти, соответственно легче оперировать ими в запросах. В таблицах с большим числом строк нелишним будет избегать NULL-able столбцов (это также сэкономит немного места). Правда следствием компактного хранения может быть некоторое неудобство в написании запросов, когда, например, при вычислении среднего для сохранения точности приходится делать кастинг в тип с большей точностью, а потом обратно. Да и сам кастинг несколько повысит стоимость запроса.
Ваша идея разнести инструменты по таблицам имеет рациональное зерно. Нужно ли их держать в одной таблице, и в самом ли деле нужен Id в таблице, если, к примеру, на неё нет ссылок - решать вам. Однако если разнести данные по таблицам вида
create table SomeInstrument ( DateTime smalldatetime not NULL primary key, Rate real not NULL )
то общий объём хранимых данных явно уменьшится, т.к. не будет столбцов Id и fk_Security_Id.
Если всё же оставите всё в одной таблице, то fk_Security_Id (вместе с primary key таблицы, на которую он ссылается) имеет смысл перевести на тип tinyint, раз уж инструментов всего около десятка.

понедельник, 24 декабря 2018 г.

Как эффективнее всего распарсить огромный файл логов на слабой машине?

Есть сервер с 1gb RAM. Есть лог файл nginx (любой другой веб-сервер) на 70gb. Как максимально быстро собрать статистику по user agent пользователей сайта, учитывая описанные ограничения по ресурсам.


Ответ

можно воспользоваться StringTokenizer в языке Java, который позволяет считывать файл построчно и не тратить память на хранение всех строк файла.
StringTokenizer tok = new StringTokenizer("/path/to/file"); while (tok.hasMoreTokens()) { String line = tok.nextToken(); // работаешь со строкой. }
Также можно указывать разделитель в конструкторе, по умолчанию стоит \t

\f

понедельник, 29 октября 2018 г.

Как найти наибольшую биклику, допуская неполное совпадение?

Дан двумерный массив булевых значений. Строки это аккаунты, столбцы – различные свойства каждого аккаунта: «состоит в группе А», «совершал покупки» и т.п.
Надо найти максимальную биклику (полный двудольный граф) – т.е. тот наибольший набор аккаунтов×свойств, где свойства true
Задача NP-полная, и с её реализацией я кое-как в лоб разобрался. Но теперь два усложнения.
хочется сравнивать «нечётко»: допустить вхождение в биклику аккаунтов, у которых недостаёт 1-2 (задаётся параметром нечеткости) свойств. Лучше посчитаем, как будто они у них true, если это позволит включить их в наибольший набор. Неужели для каждой найденной клики нужно зановой перебрать все данные, проверяя гипотезы о каждом свойства, которое можно бы добавить? Подскажите алгоритм. размер исследуемых наборов растёт. Может, вы прошли курс Machine Learning или прочие BigData – посоветуйте методики поиска, альтернативные полному перебору? Я краем уха слышал, есть такие, эффективно работающие на больших объёмах, пусть, дающие приблизительный результат – объясните, пожалуйста, «на пальцах», как их можно применить к задаче?
Иллюстрации проблемы

Синие ячейки true, белые false. Нечёткая максимальная биклика обведена красным – в каждой строке добавили по три «фантомных» ячейки (серые). Порядок строк/столбцов не имеет значения. Максимальная биклика вовсе не обязательно будет сплошным прямоугольником, это только для иллюстрации.

Например, здесь светло-синие ячейки были отброшены на этапе оптимизации, а оставшиеся формируют две би-клики: строки (3,5) × столбцы (1,3,4,6) и (3,4,5)×(1,3). Однако, если включить fuzziness на 2, то можно добавив в 4-й строке столбцы (4,6), а в строках (3,5) столбец 5 – получить ещё большую биклику (3,4,5)×(1,3,4,5,6).


Ответ

В случае нечеткого определения понятия "полнота" возможны серьезные отклонения в определении "максимальной" биклики в зависимости от метрики, по которой оценивается максимум, так как наличие ненулевого числа false в строках и столбцах биклики позволяет выбирать метрику так, что "максимальными" для каждой метрики будут разные биклики.
Например, в случае метрики "превышение true над false на всей биклике" (количество заполненных против количества незаполненных клеток) и нечеткости 3 на примере с прямоугольником максимумом окажется биклика из столбцов 1,2,6,7 прямоугольника и всех его строк, с метрикой +8. При использовании метрики "всего истин" (число заполненных прямоугольников) выделенный прямоугольник является максимальным. Таким образом, для задачи с нечеткостью метрику нужно жестко задать, причем в зависимости от метрики может варьироваться сам алгоритм выбора биклики.
В общм случае (если метрика линейно зависит от количества строк и столбцов, участвующих в биклике) подойдет жадный алгоритм захвата столбцов. Вначале сортируем столбцы по количеству строк с данным атрибутом, установленным в true, потом включаем в множество столбцов по очереди и смотрим изменение метрики. Для параметра нечеткости начальное рассмотрение должно включать минимум (параметр нечеткости)+1 столбцов, иначе биклика с нечеткостью захватит весь массив строк - этого нам явно не надо. После появления набора столбцов добавляем по одному так, чтобы промежуточное значение метрики возрастало. Количество строк в биклике будет убывать, так как не всегда у имеющихся строк будет true в новом столбце, и какие-то из них выпадут по причине превышения false предела нечеткости, в итоге либо добавим все, либо на каком-то этапе ни одной не получится добавить. Проверяем, удастся ли выкинуть хоть один столбец чтобы метрика полезла вверх, если да, выкидываем и продолжаем, пока изменение включения одного столбца не приведет к увеличению метрики. Выдаем её как локальный максимум. Для очистки совести можно рассмотреть наибольшую по метрике стартовую комбинацию из столбцов, не вошедших в найденную биклику и точно так же её обработать. Если сойдется к этому же набору, его выдаем, иначе проверяем, какая из найденных биклик больше, и если новая, проверяем ещё какую-нибудь комбинацию на случай нескольких локальных максимумов, пока либо не придем к уже найденному локальному максимуму, либо к меньшему, тогда возвращаем текущий локальный максимум как ответ.

пятница, 5 октября 2018 г.

Как эффективно группировать строки?

Нужное решить такую задачу:
Разбить множество уникальных строк на непересекающиеся группы по следующему критерию: Если две строчки имеют совпадения непустых значений в одной или более колонках, они принадлежат одной группе. Например, строчки
111;123;222 200;123;100 300;;100 все принадлежат одной группе, так как первые две строчки имеют одинаковое значение 123 во второй колонке, а две последние одинаковое значение 100 в третьей колонке
Также есть ограничение на время работы программы (30 секунд). Также могу добавить количество строк - около миллиона. Вот мой код:
private static Set> findLineGroups(List lines) { Set> resultSet = new TreeSet<>((Comparator>) (trSet1, trSet2) -> { int diff = trSet2.size() - trSet1.size(); if (diff != 0) return diff;
Iterator iterator1 = trSet1.iterator(); Iterator iterator2 = trSet2.iterator(); while (iterator1.hasNext()) { diff = iterator1.next() - iterator2.next(); if (diff != 0) return diff; }
return 0; });
Map termLineGroupsPairs = new HashMap<>(); List> lineNumGroups = new ArrayList<>();
for (int lineNum = 0; lineNum < lines.size(); lineNum++) { String line = lines.get(lineNum); String[] lineElements = line.replaceAll("\"", "").replaceAll(" ", "").split(";"); Set termSet = new HashSet<>(Arrays.asList(lineElements)); termSet.remove("");
Integer groupNum = null; TreeSet tempSet = new TreeSet<>(termLineGroupsPairs.keySet()); tempSet.retainAll(termSet); //оставляем только общие элементы if (!tempSet.isEmpty()) { String term = tempSet.first(); groupNum = termLineGroupsPairs.get(term); lineNumGroups.get(groupNum).add(lineNum); }
if (groupNum == null) { TreeSet group = new TreeSet<>(); group.add(lineNum); lineNumGroups.add(group); groupNum = lineNumGroups.size() - 1; } for (String term : termSet) { termLineGroupsPairs.put(term, groupNum); } if (lineNumGroups.size() % 1000 == 0) System.out.println(lineNumGroups.size()); }
resultSet.addAll(lineNumGroups); return resultSet; }
И все мои решения работают слишком долго (а пробовал по-разному решать эту задачу). Правда, если строк меньше тысячи, то работает быстро (укладываюсь в указанное ограничение), и практически с любым моим алгоритмом.
Прошу подсказать, как можно решить эту задачку (или что изменить в моём решении, чтобы работало быстро).


Ответ

К какому решению "почти в лоб" я пришел:
храним результат в виде списка списков: [номер_группы -> [строки_группы]] используем вспомогательный список хэш-таблиц: [позиция_слова -> { слово -> номер_группы }] и вспомогательную хэш-таблицу для хранения какая группа в какую была влита каждую строку разбиваем на слова каждое слово строки ищем в соответствующей (позиции слова в строке) хэш-таблице
если слово есть, запоминаем номер группы (значение из хэш-таблицы), в которой оно найдено если слова нет, то добавляем его в список новых слов если строка (а точнее её слова) найдена в группах, то берём первую из "живых" (объяснение этого позже) групп, иначе создаём новую группу добавляем новые слова в соответствующие хэш-таблицы с номером найденной/созданной группы объединяем найденные группы в одну, выбранную ранее. Так как группы хранятся в виде списка строк, то просто объединяем списки строк в один у выбранной группы, а более ненужные группы отмечаем как "мёртвые" (присваиваем null, дабы не перемещать элементы внутри списка) добавляем строку в список строк группы
Кода выходит ещё больше, чем слов:
//вспомогательный класс для добавления новых слов private static class NewWord { public String value; public int position;
public NewWord(String value, int position) { this.value = value; this.position = position; } }
private static List> findGroups(List lines) { List> wordsToGroupsNumbers = new ArrayList<>(); //[позиция_слова:{слово:номер_группы}] List> linesGroups = new ArrayList<>(); //[номер_группы:[строки_группы]] Map mergedGroupNumberToFinalGroupNumber = new HashMap<>(); //{номер_слитой_группы:номер_группы_в_которую_слили} for (String line : lines) { String[] words = line.split(";"); TreeSet foundInGroups = new TreeSet<>(); List newWords = new ArrayList<>(); for (int i = 0; i < words.length; i++) { String word = words[i];
if (wordsToGroupsNumbers.size() == i) wordsToGroupsNumbers.add(new HashMap<>());
if (word.equals("")) continue;
Map wordToGroupNumber = wordsToGroupsNumbers.get(i); Integer wordGroupNumber = wordToGroupNumber.get(word); if (wordGroupNumber != null) { while (mergedGroupNumberToFinalGroupNumber.containsKey(wordGroupNumber)) wordGroupNumber = mergedGroupNumberToFinalGroupNumber.get(wordGroupNumber); foundInGroups.add(wordGroupNumber); } else { newWords.add(new NewWord(word, i)); } } int groupNumber; if (foundInGroups.isEmpty()) { groupNumber = linesGroups.size(); linesGroups.add(new ArrayList<>()); } else { groupNumber = foundInGroups.first(); } for (NewWord newWord : newWords) { wordsToGroupsNumbers.get(newWord.position).put(newWord.value, groupNumber); } for (int mergeGroupNumber : foundInGroups) { if (mergeGroupNumber != groupNumber) { mergedGroupNumberToFinalGroupNumber.put(mergeGroupNumber, groupNumber); linesGroups.get(groupNumber).addAll(linesGroups.get(mergeGroupNumber)); linesGroups.set(mergeGroupNumber, null); } } linesGroups.get(groupNumber).add(line); } linesGroups.removeAll(Collections.singleton(null)); return linesGroups; }

Возможно, у меня слишком быстрый для тестирования скорости работы "как им надо" компьютер, но для миллиона строк, каждая из которых состоит из 5 слов, каждое из которых, в свою очередь, состоит из 5 строчных букв английского алфавита, время выполнения составляет примерно 4 секунды.

вторник, 2 октября 2018 г.

Как убрать ошибки измерений?


Есть вот такой набор точек, каждая точка представляет собой gps координату автобуса (x,y), у каждой точки есть timestamp. На построенном графике видны явные ошибки измерения. Как их можно убрать? Решение должно быть простым, так как всего координат около 100 тысяч. Интересует идея, но желательно, чтобы ее можно было без особых проблем реализовать средствами Java
Пример исходных данных:
1447037729 3054.619968 2409.828279 570d8
Первое поле - UNIX-время, второе и третье - (x,y) соответственно, четвертое - идентификатор автобуса (автобусов около 50ти) . Исходные данные : https://drive.google.com/file/d/0B4bA9d5B_O_BcVpPUXpYTmZBUFE/view


Ответ

Приведённый набор точек - это две зависимости: x(t) и y(t), и по каждой идёт импульсный шум. К таким данным идеально подходит алгоритм медианной обработки в скользящем окне на 7-9 элементов, когда i-тый во времени элемент заменяется на медианное значение элементов с номерами (i-h,i+h) при h=3...4. Обработку для x(t) и y(t) следует проводить независимо, после чего подменить ими исходные массивы.
Обработка эффективна при высоком уровне импульсной помехи (в тестовом примере искажена третья часть данных). Дополнительный плюс - что сохраняется формат исходных данных. Обработка краёв ведётся на окнах меньшего размера. Минусы обработки в скользящем окне проявляются при разворотах последовательности, поскольку выступы и провалы шириной меньше h выполаживаются.
В демо-программе представлена рекуррентная сортировка массива в окне. Для этого точки, лежащие между старым (удаляемым) и новым (добавляемым) элементами, сдвигаются в сторону старого элемента, после чего на место крайнего из возникших дубликатов записывается новый элемент. Это резко снижает вычислительные затраты.
Демо-программа (PHP):
function print_a($a, $name){ print("$name: "); foreach($a as $item){ printf("%2d, ",$item); } }
function slide_median($h, $a){ $size = count($a); $result = []; $slide = []; array_push($slide, reset($a)); array_push($result,$slide[0]); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата");
for($i=1; $i<=$h; $i++){ array_push($slide, next($a), next($a)); sort($slide); array_push($result, $slide[$i]); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); }
for($i=0; $i < $size-2*$h-1; $i++){ $old = $a[$i]; $new = $a[$i+2*$h+1]; if($old < $new){ for($key = 0; $key <= 2*$h; $key++){ if($new < $slide[$key]){ break; } if(($old <= $slide[$key])&&($slide[$key] < $new)) $slide[$key] = $slide[$key+1]; } $slide[$key-1] = $new;
} if($old > $new){ for($key = 2*$h; $key >= 0; $key--){ if($new > $slide[$key]){ break; } if(($old >= $slide[$key])&&($slide[$key] > $new)) $slide[$key] = $slide[$key-1]; } $slide[$key+1] = $new; } array_push($result, $slide[$h]); print(" old = $old, new =$new"); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); }
for($i = $h-1; $i > 0; $i--){ $slide = array_slice($a, $size-2*$i-1, 2*$i+1); sort($slide); array_push($result, $slide[$i]); print_a($slide, " Сортировка в окне"); print_a($result, "
Массив результата"); } $slide = [$a[$size-1]]; array_push($result, $slide[0]); print_a([end($a)], " Сортировка в окне"); print_a($a, "

Исходный массив: "); print_a($result, "
Массив результата");
return $result; };
$a = range(20, 40); foreach($a as &$item){ $item += 5*mt_rand(-1,1)*(int)(mt_rand(0,199)/100); } print_a($a, "Исходный массив: "); slide_median(3, $a);
Результаты (импульсный шум, амплитуда 5):
Исходный массив: : 20, 21, 22, 23, 19, 20, 21, 27, 28, 29, 30, 31, 32, 28, 29, 35, 36, 42, 43, 39, 35,  Сортировка в окне: 20, Массив результата: 20,  Сортировка в окне: 20, 21, 22, Массив результата: 20, 21,  Сортировка в окне: 19, 20, 21, 22, 23, Массив результата: 20, 21, 21,  Сортировка в окне: 19, 20, 20, 21, 21, 22, 23, Массив результата: 20, 21, 21, 21,  old = 20, new =27 Сортировка в окне: 19, 20, 21, 21, 22, 23, 27, Массив результата: 20, 21, 21, 21, 21,  old = 21, new =28 Сортировка в окне: 19, 20, 21, 22, 23, 27, 28, Массив результата: 20, 21, 21, 21, 21, 22,  old = 22, new =29 Сортировка в окне: 19, 20, 21, 23, 27, 28, 29, Массив результата: 20, 21, 21, 21, 21, 22, 23,  old = 23, new =30 Сортировка в окне: 19, 20, 21, 27, 28, 29, 30, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27,  old = 19, new =31 Сортировка в окне: 20, 21, 27, 28, 29, 30, 31, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28,  old = 20, new =32 Сортировка в окне: 21, 27, 28, 29, 30, 31, 32, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29,  old = 21, new =28 Сортировка в окне: 27, 28, 28, 29, 30, 31, 32, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29,  old = 27, new =29 Сортировка в окне: 28, 28, 29, 29, 30, 31, 32, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29,  old = 28, new =35 Сортировка в окне: 28, 29, 29, 30, 31, 32, 35, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30,  old = 29, new =36 Сортировка в окне: 28, 29, 30, 31, 32, 35, 36, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31,  old = 30, new =42 Сортировка в окне: 28, 29, 31, 32, 35, 36, 42, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32,  old = 31, new =43 Сортировка в окне: 28, 29, 32, 35, 36, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35,  old = 32, new =39 Сортировка в окне: 28, 29, 35, 36, 39, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36,  old = 28, new =35 Сортировка в окне: 29, 35, 35, 36, 39, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36,  Сортировка в окне: 35, 36, 39, 42, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36, 39,  Сортировка в окне: 35, 39, 43, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36, 39, 39,  Сортировка в окне: 35,
Исходный массив: : 20, 21, 22, 23, 19, 20, 21, 27, 28, 29, 30, 31, 32, 28, 29, 35, 36, 42, 43, 39, 35, Массив результата: 20, 21, 21, 21, 21, 22, 23, 27, 28, 29, 29, 29, 30, 31, 32, 35, 36, 36, 39, 39, 35,
Сравнение скользящей медианы и скользящего среднего на интенсивной импульсной помехе проведено с помощью следующей программы:
function print_a($a, $name){ print("$name: "); foreach($a as $item){ printf("%3d, ",$item); } }
function slide_median($h, $a){ $size = count($a); $result = []; $slide = []; array_push($slide, reset($a)); array_push($result,$slide[0]);
for($i=1; $i<=$h; $i++){ array_push($slide, next($a), next($a)); sort($slide); array_push($result, $slide[$i]); }
for($i=0; $i < $size-2*$h-1; $i++){ $old = $a[$i]; $new = $a[$i+2*$h+1]; if($old < $new){ for($key = 0; $key <= 2*$h; $key++){ if($new < $slide[$key]){ break; } if(($old <= $slide[$key])&&($slide[$key] < $new)) $slide[$key] = $slide[$key+1]; } $slide[$key-1] = $new;
} if($old > $new){ for($key = 2*$h; $key >= 0; $key--){ if($new > $slide[$key]){ break; } if(($old >= $slide[$key])&&($slide[$key] > $new)) $slide[$key] = $slide[$key-1]; } $slide[$key+1] = $new; } array_push($result, $slide[$h]); }
for($i = $h-1; $i > 0; $i--){ $slide = array_slice($a, $size-2*$i-1, 2*$i+1); sort($slide); array_push($result, $slide[$i]); } $slide = [$a[$size-1]]; array_push($result, $slide[0]); print_a($a, "

Исходный массив "); print_a($result, "
Массив медиан  ");
return $result; };
function slide_average($h, $a){ $size = count($a); $b = array_merge([0], $a); $sum = reset($a); $result = [$sum];
for($i=1; $i<=$h; $i++){ $sum += next($a)+next($a); $average = (int)($sum/(2*$i+1)+.5); array_push($result, $average); }
reset($b); for($i=0; $i < $size-2*$h-1; $i++){ $sum += next($a) - next($b); $average = (int)($sum/(2*$h+1)+.5); array_push($result, $average); }
for($i = $h-1; $i >=0; $i--){ $sum -= (next($b) + next($b)); $average = (int)($sum/(2*$i+1)+.5); array_push($result, $average); } print_a($a, "

Исходный массив "); print_a($result, "
Массив средних  "); return $result; };
$a = range(200, 240); foreach($a as &$item){ $item += 50*mt_rand(-1,1)*(int)(mt_rand(0,149)/100); } slide_median(3, $a); slide_average(3, $a);
Результаты:
Исходный массив : 200, 201, 202, 203, 204, 155, 206, 207, 208, 209, 210, 211, 212, 213, 264, 265, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 183, 234, 235, 236, 287, 238, 239, 240, Массив медиан  : 200, 201, 202, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 216, 217, 218, 219, 219, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 229, 230, 231, 232, 234, 235, 236, 238, 239, 239, 240,
Исходный массив : 200, 201, 202, 203, 204, 155, 206, 207, 208, 209, 210, 211, 212, 213, 264, 265, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 183, 234, 235, 236, 287, 238, 239, 240, Массив средних  : 200, 201, 202, 196, 197, 198, 199, 200, 201, 209, 210, 218, 226, 227, 228, 229, 230, 231, 225, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 223, 224, 225, 226, 234, 235, 236, 244, 248, 239, 240,
Видно, что скользящая медиана лучше сглаживает интенсивные случайные выбросы данных.
Для данных из реального массива (x,y округлялись до целых):

Обработка массива x80c48
Исходный массив : 10790, 10728, 10565, 10228, 10148, 9911, 9861, 9880, 9887, 9894, 9907, 9910, 9917, 9932, 9937, 9925, 9900, 9684, 9579, 9446, 9040, 8912, 8703, 8457, 8350, 8338, 8129, 8040, 7900, 7836, 7731, 7490, 7271, 7250, 7165, 7013, 6912, 6848, 6823, 6857, 6868, 6894, 6902, 6903, 6904, 7114, 7067, 7047, 6994, 6883, 6848, 6787, 6722, 6412, 6236, 6136, 6012, 5991, 5659, 5648, 5595, 5327, 5079, 5015, 4678, 4639, 4569, 4294, 4241, 4164, 4137, 3948, 3905, 3771, 3731, 3664, 3577, 3422, 3304, 3086, 3053, 2977, 2967, 3248, 3257, 3202, 2954, 2834, 2594, 2574, 2611, 2730, 2766, 2514, 2387, 2368, 2365, 2344, 2312, 2098, 1905, 1722, 1579, 1233, 1206, 963, 815, 825, Массив медиан  : 10790, 10728, 10565, 10228, 10148, 9911, 9894, 9894, 9894, 9894, 9907, 9910, 9917, 9917, 9917, 9917, 9900, 9684, 9579, 9446, 9040, 8912, 8703, 8457, 8350, 8338, 8129, 8040, 7900, 7836, 7731, 7490, 7271, 7250, 7165, 7013, 6912, 6868, 6868, 6868, 6868, 6894, 6902, 6903, 6904, 6994, 6994, 6994, 6994, 6883, 6848, 6787, 6722, 6412, 6236, 6136, 6012, 5991, 5659, 5648, 5595, 5327, 5079, 5015, 4678, 4639, 4569, 4294, 4241, 4164, 4137, 3948, 3905, 3771, 3731, 3664, 3577, 3422, 3304, 3086, 3086, 3086, 3086, 3053, 2977, 2967, 2954, 2834, 2730, 2730, 2611, 2594, 2574, 2514, 2387, 2368, 2365, 2344, 2312, 2098, 1905, 1722, 1579, 1233, 1206, 963, 825, 825,
Исходный массив : 10790, 10728, 10565, 10228, 10148, 9911, 9861, 9880, 9887, 9894, 9907, 9910, 9917, 9932, 9937, 9925, 9900, 9684, 9579, 9446, 9040, 8912, 8703, 8457, 8350, 8338, 8129, 8040, 7900, 7836, 7731, 7490, 7271, 7250, 7165, 7013, 6912, 6848, 6823, 6857, 6868, 6894, 6902, 6903, 6904, 7114, 7067, 7047, 6994, 6883, 6848, 6787, 6722, 6412, 6236, 6136, 6012, 5991, 5659, 5648, 5595, 5327, 5079, 5015, 4678, 4639, 4569, 4294, 4241, 4164, 4137, 3948, 3905, 3771, 3731, 3664, 3577, 3422, 3304, 3086, 3053, 2977, 2967, 3248, 3257, 3202, 2954, 2834, 2594, 2574, 2611, 2730, 2766, 2514, 2387, 2368, 2365, 2344, 2312, 2098, 1905, 1722, 1579, 1233, 1206, 963, 815, 825, Массив средних  : 10790, 10694, 10492, 10319, 10189, 10069, 9973, 9927, 9893, 9894, 9904, 9912, 9917, 9918, 9886, 9839, 9772, 9644, 9498, 9323, 9117, 8927, 8749, 8561, 8418, 8274, 8150, 8046, 7923, 7771, 7645, 7520, 7394, 7262, 7136, 7040, 6981, 6927, 6888, 6872, 6871, 6879, 6920, 6950, 6976, 6990, 6987, 6980, 6963, 6907, 6813, 6697, 6575, 6450, 6328, 6167, 6013, 5897, 5767, 5616, 5473, 5286, 5140, 4986, 4800, 4645, 4514, 4389, 4285, 4180, 4066, 3985, 3903, 3819, 3717, 3625, 3508, 3405, 3298, 3198, 3151, 3127, 3113, 3094, 3063, 3008, 2952, 2861, 2786, 2723, 2660, 2597, 2564, 2534, 2496, 2437, 2341, 2254, 2159, 2046, 1885, 1722, 1529, 1346, 1192, 1008, 868, 825,
Обработка массива y80c48
Исходный массив : 7289, 7275, 7243, 7178, 7163, 7119, 7109, 6903, 6785, 6680, 6448, 6379, 6264, 5869, 5709, 5448, 5353, 5083, 5081, 5083, 5063, 5062, 5042, 5166, 5186, 5172, 4916, 4925, 4982, 5009, 5025, 5007, 4993, 4991, 4986, 4976, 4968, 4964, 4962, 4636, 4488, 4148, 4038, 4029, 4010, 3960, 3798, 3709, 3462, 3475, 3480, 3489, 3498, 3542, 3567, 3581, 3598, 3586, 3647, 3649, 3656, 3693, 3727, 3736, 3782, 3788, 3797, 3831, 3837, 3743, 3710, 3550, 3511, 3406, 3379, 3435, 3520, 3455, 3367, 3204, 3179, 3123, 3115, 2534, 2519, 2472, 2351, 2281, 2141, 2129, 2066, 1862, 1801, 1522, 1302, 1218, 1200, 1038, 851, 812, 778, 746, 721, 621, 529, 420, 386, 279, Массив медиан  : 7289, 7275, 7243, 7178, 7163, 7119, 7109, 6903, 6785, 6680, 6448, 6379, 6264, 5869, 5709, 5448, 5353, 5083, 5083, 5081, 5081, 5081, 5083, 5063, 5062, 5042, 5009, 5009, 5007, 4993, 4993, 4993, 4993, 4991, 4986, 4976, 4968, 4964, 4962, 4636, 4488, 4148, 4038, 4029, 4010, 3960, 3798, 3709, 3489, 3489, 3489, 3489, 3498, 3542, 3567, 3581, 3586, 3598, 3647, 3649, 3656, 3693, 3727, 3736, 3782, 3788, 3788, 3788, 3788, 3743, 3710, 3550, 3511, 3511, 3455, 3435, 3406, 3379, 3367, 3204, 3179, 3123, 3115, 2534, 2519, 2472, 2351, 2281, 2141, 2129, 2066, 1862, 1801, 1522, 1302, 1218, 1200, 1038, 851, 812, 778, 746, 721, 621, 529, 420, 386, 279,
Исходный массив : 7289, 7275, 7243, 7178, 7163, 7119, 7109, 6903, 6785, 6680, 6448, 6379, 6264, 5869, 5709, 5448, 5353, 5083, 5081, 5083, 5063, 5062, 5042, 5166, 5186, 5172, 4916, 4925, 4982, 5009, 5025, 5007, 4993, 4991, 4986, 4976, 4968, 4964, 4962, 4636, 4488, 4148, 4038, 4029, 4010, 3960, 3798, 3709, 3462, 3475, 3480, 3489, 3498, 3542, 3567, 3581, 3598, 3586, 3647, 3649, 3656, 3693, 3727, 3736, 3782, 3788, 3797, 3831, 3837, 3743, 3710, 3550, 3511, 3406, 3379, 3435, 3520, 3455, 3367, 3204, 3179, 3123, 3115, 2534, 2519, 2472, 2351, 2281, 2141, 2129, 2066, 1862, 1801, 1522, 1302, 1218, 1200, 1038, 851, 812, 778, 746, 721, 621, 529, 420, 386, 279, Массив средних  : 7289, 7269, 7230, 7197, 7141, 7071, 6991, 6887, 6775, 6653, 6475, 6305, 6114, 5924, 5729, 5544, 5375, 5260, 5168, 5110, 5083, 5098, 5111, 5087, 5067, 5056, 5051, 5031, 5005, 4980, 4990, 4999, 4998, 4992, 4984, 4977, 4926, 4854, 4735, 4601, 4466, 4330, 4187, 4067, 3956, 3858, 3778, 3699, 3625, 3559, 3522, 3502, 3519, 3536, 3552, 3574, 3596, 3612, 3630, 3651, 3671, 3699, 3719, 3740, 3765, 3785, 3788, 3784, 3751, 3711, 3655, 3591, 3533, 3502, 3465, 3439, 3395, 3363, 3326, 3280, 3140, 3006, 2878, 2756, 2628, 2488, 2347, 2280, 2186, 2090, 1972, 1832, 1700, 1567, 1420, 1276, 1135, 1028, 949, 878, 795, 723, 661, 600, 529, 447, 362, 279,
По сравнению с алгоритмом скользящего среднего, скользящая медиана намного аккуратнее обращается с данными.
Есть перевод статьи на английский язык