Работа с древовидными структурами в Loginom

18 января 2024
0 комментариев

Дерево — одна из популярных структур хранения и передачи данных. Универсальные форматы обмена, такие как JSON и XML, используют именно иерархическое представление информации. Однако большинство алгоритмов рассчитаны на то, что анализируемые данные хранятся в виде плоских таблиц. В Loginom имеются специальные компоненты для работы с древовидными структурами.

Самыми популярными структурами для хранения и передачи данных являются плоская таблица и дерево. В плоских таблицах информация представлена в виде простой конструкции, где строка является одним объектом или событием, а столбец — атрибутом, который описывает каждую запись. Дерево устроено сложнее — данные в нем хранятся в виде иерархии произвольной глубины вложенности.

Наиболее популярными форматами представления древовидных данных являются JSON и XML. Они хранят информацию в виде пар «ключ-значение», упорядоченных списков и других объектов, могут включать вложенные структуры. Эти форматы независимы от какой-либо платформы, операционной системы или базы данных.

Деревья обеспечивают большую гибкость, позволяют описывать сложные объекты, не имеют ограничений на количество атрибутов. Однако большинство алгоритмов анализа данных ориентированы на обработку более простых структур — плоских таблиц.

Пример структуры JSON файла

Файлы в формате JSON используются для обмена данными между приложениями, включая базы данных, аналитические платформы. Например, кредитные бюро предоставляют банкам доступ к кредитной истории заемщиков через API, основанном на JSON.

Кредитная история содержит большой объем сложно структурированной информации: данные о человеке, сведения о месте работы и ранее выданных кредитах, история платежей и многое другое. В формате JSON это может выглядеть так:

{
  "applicant": {
    "name": "Job Pou",
    "birthdate": "1980-05-15",
    "address": "123 Main St, City, State",
    "employment": "ТТТ",
    "annual_income": 600000
  },
  "credit_scores": {
    "equifax": 750,
    "transunion": 720,
    "experian": 730
  },
  "credit_history": [
    {
      "account_type": "Credit Card",
      "account_number": "123456789",
      "current_balance": 50000,
      "credit_limit": 100000,
      "payment_status": "Current"
    },
    {
      "account_type": "Auto Loan",
      "account_number": "987654321",
      "current_balance": 15000,
      "original_balance": 20000,
      "payment_status": "Late"
    }
  ]
}

В этом примере содержится следующая информация:

  • applicant — заявитель: имя, дата рождения, адрес, место работы и годовой доход;
  • credit_scores — кредитные оценки от разных бюро;
  • credit_history — данные о кредитных счетах: тип счета, номер счета, текущий баланс, предельный кредитный лимит и статус платежа.

Данная структура небольшая, но даже в ней имеется массив кредитной истории: Credit Card и Auto Loan. В реальности подобный файл обычно хранит намного больше информации и может содержать тысячи строк.

Заявка в виде дерева подается на вход системе принятия решений — кредитному конвейеру, который выполняет сложные проверки и формирует окончательный ответ. В процессе обработки иерархия может обогащаться новыми данными, такими как условия договора, процентная ставка, сумма кредита и т.п. Обычное для аналитических алгоритмов представление в виде плоской таблицы не способно отразить подобную сложную «древовидную» структуру.

Loginom предоставляет механизмы, которые позволяют обрабатывать древовидные структуры, преобразовывать их в таблицы и обратно.

Деревья в Loginom

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

Большинство компонентов платформы работают с плоскими таблицами, а при необходимости обрабатывать иерархические данные можно воспользоваться двумя специальными компонентами, позволяющими преобразовать дерево в таблицу и обратно.

Древовидная структура данных

Таким образом, деревья трансформируются в данные, которые можно обработать при помощи стандартных механизмов, встроенных в Loginom. Либо провести обратную операцию — преобразовать итоги расчетов в иерархическую структуру для обмена информацией со сторонними системами.

В статье на конкретном примере рассматривается преобразование таблицы в дерево и обратно. Вся работа происходит в аналитической платформе Loginom (скачать бесплатную версию Loginom Community).

Для успешного усвоения материала предлагаем скачать сценарий и выполнить все шаги.

Таблица в дерево и обратно

При импорте XML-файла данные автоматически трансформируются в дерево. Написания кода для этого не требуется, т.к. структура описывается в WSDL-файле. Файл с описанием структуры должен обязательно присутствовать.

C файлами JSON другая ситуация. Наличие файла с описанием структуры не обязательно. В этом случае автоматический разбор невозможен. Но эту проблему можно решить с помощью компонента JavaScript.

Входной JSON выглядит следующим образом:

Исходный JSON

Работа с такими данными неудобна, информацию можно привести к табличному виду с помощью компонента JavaScript. Для этого нужно написать небольшой скрипт, в котором преобразуется вложенная структура JSON в плоский формат таблицы, где каждая строка представляет один элемент данных, а столбцы таблицы соответствуют полям данных.

import { InputTable, InputTables, InputVariables, OutputTable, DataType, DataKind, UsageType } from "builtIn/Data";

const json = JSON.parse(InputTable.Get(0,'RawJSON'))
const fields = JSON.parse(InputVariables.Items.fields.Value)

let fieldsMap = {}
let globalIndexesCounters = {}
OutputTable.ClearColumns;
fields.forEach(field => {
 OutputTable.AddColumn(field)
    fieldsMap[field.DisplayName] = field.Name
    if(field.Name.includes('_global_index')){
     globalIndexesCounters[field.Name] = 0;
    }
})

function AddRow(row){
    OutputTable.Append()
 Object.keys(row).forEach(key=>{
     OutputTable.Set(key, row[key])
    })
}

let rows = []

function getObjType(obj){
    if(!obj || typeof obj !== 'object'){
     return 'key'
    } else {
  if(Array.isArray(obj)){
         return 'array'
        } else {
         return 'object'   
       }
    }
}

function objWalk(obj, objKeyName, parentKeyPath, currentRow){
    let objKeyPath = parentKeyPath ? parentKeyPath + '.' + objKeyName : objKeyName

    if(Array.isArray(obj)){
        let rowOfArr = Object.assign({},currentRow);

        if(obj.length > 0){
            const itemType = getObjType(obj[0])
            switch(itemType){
                case 'key':
                                       let fieldIndex = fieldsMap[objKeyPath + '.' + objKeyName + '_global_index']
                    let field = fieldsMap[objKeyPath + '.' + objKeyName]
                    if(fieldIndex && field){
                        obj.forEach((item,idx) => {
                            rowOfArr[fieldIndex] = globalIndexesCounters[fieldIndex]++;
                            rowOfArr[field] = item
                            rows.push(Object.assign({},rowOfArr))
                            rowOfArr = Object.assign({},currentRow)
                        });
                    }
                    break;
                case 'object':
                    obj.forEach((item,idx) => {
                        let fieldIndex = fieldsMap[objKeyPath + '.' + objKeyName + '_global_index']
                        if(fieldIndex){
                            rowOfArr[fieldIndex] = globalIndexesCounters[fieldIndex]++;
                            objWalk(item, objKeyName, parentKeyPath, rowOfArr)
                            rows.push(Object.assign({},rowOfArr))
                            rowOfArr = Object.assign({},currentRow);
                        }
                    });
                    break;

                case 'array':
                    let fieldIndexA = fieldsMap[objKeyPath + '.' + objKeyName + '_global_index']
                    if(fieldIndexA){
                        obj.forEach((item,idx) => {
       rowOfArr[fieldIndexA] = globalIndexesCounters[fieldIndexA]++;
                            objWalk(item, objKeyName+ '_SUBARR', objKeyPath, rowOfArr)
                            rows.push(Object.assign({},rowOfArr))
                            rowOfArr = Object.assign({},currentRow);

                    }); 
                    }
                    break;
            }
        }
    } 
else {
        let keysByType = {
         'key': [],
            'object': [],
            'array': []
        }
        Object.keys(obj).forEach(key => {
         keysByType[getObjType(obj[key])].push(key)
        })

        keysByType.key.forEach(key => {
         let keyPath = objKeyPath ? objKeyPath + '.' + key : key
            let fieldName = fieldsMap[keyPath]
            if(fieldName){
                currentRow[fieldName] = obj[key]
            }
        })

        keysByType.object.forEach(key => {
            objWalk(obj[key], key, objKeyPath, currentRow)
        })

        keysByType.array.forEach(key => {
            objWalk(obj[key], key, objKeyPath, currentRow)
        })
    }
}

let currentRow = {}
if(getObjType(json) == 'array'){
 json.forEach((item,idx) => {
     currentRow = { _global_index: idx}
        objWalk(item, '', null,currentRow)
    })
} else {
 objWalk(json, '', null,currentRow)
}
if(rows.length == 0){
 rows.push(currentRow)
}

rows.forEach(row => {
 AddRow(row)
})

Получим следующую таблицу:

Отображение данных в табличном виде

Таблица состоит из 4 строк и 30 столбцов. Имеются дубли и пропуски.

Т.к. присутствует адрес регистрации и фактический, в табличном виде добавляется столбец с индексом, появились строки «null», фактический адрес, регистрационный адрес.

Столбец с индексом

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

Можно выделить уровни, которые представляют ветви дерева:

  • личная информация;
  • документ;
  • адрес.

После этого соединить в одну таблицу, тогда ее отображение будет без пустых значений, дублей.

Существует несколько вариантов решения, включая комбинированное использование компонентов: ГруппировкаФильтр строкПараметры полей.

Этот подход вызывает определенные сложности:

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

Альтернативное решение — это применение компонентов Таблица в дерево и Дерево в таблицу.

Переведем таблицу в древовидную структуру с помощью компонента «Таблица в дерево».

Есть два варианта проставления связей:

  • создание узлов вручную: oдин корень дерева, от него добавляются дочерние узлы, от которых, в свою очередь, можно добавлять дочерние и соседние.
  • чтение структуры из готовой схемы (файла xsd).

При ручной настройке нужно создать главный узел «Person». При этом указать, что узел является «Контейнером». Это дает возможность создавать от него дочерние узлы.

Создания узлов

От «Person» надо создать узлы с личной информацией, добавить узел «Document» с пометкой контейнер, чтобы добавлять в него узлы.

При создании узла «Addresses» нужно добавить пометку «Массив», т.к. в него вложен массив с объектами.

При ручном соединении необходимо проставить связь «Addresses_global_index» с «#Глобальный индекс».

Ручное соединение

Далее следует устанавливать связи. Их можно связать автоматически или проставить вручную.

Настройка узла «Таблица в дерево»

В результате получится следующая древовидная структура данных.

Древовидная структура данных

Такая структура корректно конвертируется в JSON и XML файлы.

Далее с помощью компонента «Дерево в таблицу» разделим данные на 3 таблицы, к которым можно применить различные алгоритмы.

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

Настройка компонента «Дерево в таблицу»

Таблица с паспортными данными, извлеченными из дерева, выглядит следующим образом.

Таблица с паспортными данными клиента

Для корректного отображения адреса при помощи Калькулятора можно собрать нужную строку concat( "Российская федерация, ",Region + ", ", City + ", ", Street + ", ", "д. " + House + ", ","к. " + Korpus + ", ","кв. " + Building)

Таблица с адресом

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

Сценарий на платформе Loginom

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

Заключение

Древовидная структура данных, содержащая вложенные структуры, корректно конвертируется в JSON и XML. Данные правильно отображают сложные иерархические отношения.

Плюсы использования древовидной структуры данных на платформе Loginom:

  • подходит для организации данных, которые имеют иерархическую форму;
  • при правильном устройстве позволяет быстро находить и группировать элементы;
  • облегчает навигацию и фильтрацию по данным на разных уровнях;
  • корректно конвертируется в JSON и XML файлы;
  • отображает отношения и зависимости между элементами данных.

Минусы:

  • при большом объеме данных изменение структуры дерева может быть сложным и затратным процессом;
  • полученные данные в большинстве случаев невозможно подавать на вход других узлов. Приходится пользоваться компонентом «Дерево в таблицу» и после этого проводить дальнейший анализ.

Большинство алгоритмов анализа не приспособлены для обработки иерархических данных. Наличие компонентов, позволяющих преобразовать дерево в таблицу и обратно, позволяет, с одной стороны, принимать и отдавать сторонним системам иерархическую информацию, а с другой — применять в полном объеме все алгоритмы анализа, встроенные в Loginom.

Чтобы оптимизировать работу с JSON-файлами в Loginom пройдите курс «Эффективная работа с JSON».

Другие материалы по теме:

Библиотеки компонентов Loginom Kits: кому и для чего

Loginom: что под «капотом»

Смотрите также