Как быстро собрать кубик для простых задач: играемся с TinyOLAP

50b1584724a70e0f3f2869cd2fa268d8.jpg

Привет, Хабр. Сегодняшняя статья будет интересна тем, кто хочет собрать простой OLAP‑куб для анализа данных, чтобы понять, как он устроен и работает. Экспериментировать будем с помощью TinyOLAP, одного из немногих OpenSource движков на Python.

Меня зовут Дмитрий Богданов, я старший бэкенд‑разработчик Polymatica в компании SL Soft. Некоторое время назад я изучал возможности различных существующих фреймворков для создания OLAP‑кубов, в том числе, и TinyOLAP.

Терминология

OLAP (On‑Line Analytical Processing, оперативная аналитическая обработка данных) — метод анализа, основанный на многомерных структурах данных и объединении информации из различных источников. Сегодня это один из основных подходов в BI‑системах, поскольку позволяет быстро обрабатывать большие массивы информации.

OLAP‑куб (OLAP cube) — многомерная структура данных, которая помогает их лучше понимать и анализировать. Можно представить себе игрушечный кубик, при вращении которого вы видите его разные стороны и таким образом изучаете, что происходит внутри бизнеса. Работая с OLAP‑кубами можно принимать более осознанные решения на основе накопленных статистических данных.

Измерения (dimension) — специальная структура данных, которая позволяет описать их различные аспекты. Продолжим описание на примере кубика. Каждая из его сторон является измерением = аспектом: продукты, даты, регионы и иные характеристики характеристики. Каждый аспект заранее создан и известен, это как большой справочник, в который уже внесены записи.

Показатели (measures) — количественные или качественные свойства данных. По ним мы можем произвести группировку или агрегацию. Вернемся к нашему образу — в каждое пересечение измерений мы можем внести свойство этих данных, например, количество продаж за дату, сумму продаж по товарам.

Строим свой куб в TinyOLAP

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

Нам потребуются библиотеки TinyOLAP и Pandas. Установим их:

pip install tinyolap 
pip install pandas 

Начнем с создания объекта Database, то есть условной базы данных, которая будет хранить все сущности. В нашем случае будем использовать параметр in_memory=True — все данные хранятся в памяти. Если указать параметр False, то будет использована SQLite3 и по умолчанию создается новая БД.

from tinyolap.database import Database 
db = Database("forms", in_memory=True)  
db.caching = False

Далее создадим измерения (dimension). Они включают в себя только уникальные элементы измерений. Тестовые данные и код можно взять у меня на GitHub.

# названия файлов с измерениями 
dimension_names = ["dim_company", "dim_assets", "dim_date"] 
cwd = os.getcwd() 
 
dimensions = [] 
 
for name in dimension_names: 
	# строим путь до файла 
	file_name = os.path.join(cwd, "data", f"{name}.csv") 
     
	# создаем новое измерение в TinyOLAP и открываем его на редактирование 
	dim = db.add_dimension(name).edit() 
	df = pd.read_csv(file_name) 
     
	# делаем список уникальных элементов измерения 
	unique_dim = df[df.columns[0]].unique().astype(str) 
     
	# добавляем в наше созданное измерение 
	dim.add_many(unique_dim) 
     
	# добавляем агрегирующий элемент, который включает все остальные 
	if len(unique_dim) > 1: 
    		dim.add_many("All", tuple(unique_dim)) 

	# "закрепляем" изменения измерения 
	dim.commit() 

Далее создадим показатели. В реализации TinyOLAP показатели создаются как измерения, но со своими особенностями: данные показателей могут быть только числовыми (целочисленные и с плавающей точкой). Создадим общее измерение и поместим названия измерений, которые есть в данных.

data_file_name = os.path.join(cwd, "data", "data.csv") 
 
df = pd.read_csv(data_file_name) 
 
measures_target_names = ["price", "quantity", "amount"] 
 
# создаем общий показатель "measures" 
measures = db.add_dimension("measures").edit() 
 
# добавляем в него элементы измерений из исходных данных  
measures.add_many(measures_target_names) 
 
# "закрепляем" изменения измерения 
measures.commit()  
dimension_names.append('measures') 

Переходим к созданию самого куба и заполнения его данными.

# создаем OLAP куб. В качестве измерений передадим все созданные измерения и показатели 
cube = db.add_cube(name="data", dimensions=dimension_names) 
     
dim_count = 3 
 
for i in range(dim_count): 
	df[df.columns[i]] = df[df.columns[i]].astype(str) 

# заполняем данные из файлы. Путь для каждого значения имеет вид 
# (измерение1,...,измерениеn,показатель1) 
# (измерение1,...,измерениеn,показательn) 
for row in df.itertuples(index=False): 
	for cur_measure_index, cur_measure in enumerate(measures_target_names): 
    	address = tuple(row[:dim_count]) + (cur_measure, ) 
    	value = row.__getattribute__(cur_measure) 
    	cube.set(address, value) 

У нас создан куб, но пока он бесполезен и является просто табличкой с данными.

Далее будем разбираться с «магией» OLAP кубов. Создадим срез данных в нужном нам ракурсе:

view = View(cube, definition={ 
    	"filters": {"dimensions": ["dim_assets", "dim_date"]}, 
    	"columns": {"dimensions": ["measures"]}, 
    	"rows": {"dimensions": ["dim_company"]} 
	}) 
 
print(view.to_console_output()) 

698a906c47aa8a743366effacccfe43f.png

Проблемы и особенности TinyOLAP

Явные — неявные фильтры

Видим, что значений нет. Так произошло, поскольку мы не указали в фильтре явные значения — TinyOLAP взял значения по умолчанию, то есть первые внесенные. Пофиксим это! Для этого используем ранее созданные агрегирующие элементы All.

view = View(cube, definition={ 
    	"filters": {"dimensions": [{"dimension": "dim_assets", "members": "All"}, {"dimension": "dim_date", "members": "All"}]}, 
    	"columns": {"dimensions": ["measures"]}, 
    	"rows": {"dimensions": ["dim_company"]} 
	}) 
 
print(view.to_console_output()) 

331a3698981b4ef0c7305cd8698fa130.png

Ура, мы получили первые наши результаты. Но… данные не соответствуют реальности. Мы столкнулись со следующей особенностью TinyOLAP: по умолчанию функция агрегации — суммирование, но для цены данный метод не подходит. Для таких случаев есть правила (rules).

Правила

Правила (rules) — понятие, относящееся к TinyOLAP. Они нужны для определения пользовательских вычислений или бизнес‑логики, которая должна быть назначена кубу. По умолчанию, как мы уже выяснили ранее, существует только агрегация для вложенных сущностей. Поэтому нам нужно создать свои правила для цены и суммы.

@rule("data", trigger=["measures:amount"], scope=RuleScope.BASE_LEVEL) 
def rule_cost(c: Cell): 
	quantity = c["quantity"] 
	price = c["price", c.BYPASS_RULES] 
	if quantity and price: 
    	return float(price) * float(quantity) 
	return c.CONTINUE 
 
@rule("data", trigger=["measures:price"], scope=RuleScope.AGGREGATION_LEVEL) 
def rule_price(c: Cell): 
	quantity = c["quantity"] 
	amount = c["amount"] 
	if quantity != 0: 
    	return float(amount / quantity) 
	return c.CONTINUE 
 
cube.register_rule(rule_cost) 
cube.register_rule(rule_price) 

2d2c1b073887b304d54bcb2e5e5a0fa8.png

Видим, что сумма стала верной, но цена по‑прежнему некорректна. Тут проблема в форматировании вывода в консоль.

Варианты вывода данных

Ранее для визуализации данных использовали метод to_console_log. Однако в TinyOLAP существуют и другие варианты представления отчетов: to_dict, to_json, to_html.

Воспользуемся функцией to_dict к объекту view.

print(view.to_dict()) 
 
{'contentType': 'TinyOlap.View', 'version': '0.8.16', 'uid': '0bab13b6-b8ef-49c8-9585-0e4aaadbc085', 'database': 'forms', 'cube': 'data', 'name': '7f4b46c5', 'title': '', 'description': '', 'zeroSuppressionOnRows': False, 'zeroSuppressionOnColumns': False, 'statistics': {'lastRefresh': '2024-03-06T00:55:19.211856', 'refreshDuration': 0.001097, 'cellsCount': 3, 'rowsCount': 1, 'columnsCount': 3, 'rowDimensionsCount': 1, 'columnDimensionsCount': 1, 'executedRules': 22, 'executedCellRequests': 49, 'executedCellAggregations': 88}, 'axes': {'filters': {'dimensions': [{'dimension': 'dim_assets', 'ordinal': 0, 'members': ['All']}, {'dimension': 'dim_date', 'ordinal': 1, 'members': ['All']}], 'positions': [{'row': 0, 'members': [{'name': 'All', 'level': 1, 'root': True}, {'name': 'All', 'level': 1, 'root': True}]}]}, 'rows': {'dimensions': [{'dimension': 'dim_company', 'ordinal': 0, 'members': ['2000']}], 'positions': [{'row': 0, 'members': [{'name': '2000', 'level': 0, 'root': True}]}]}, 'columns': {'dimensions': [{'dimension': 'measures', 'ordinal': 0, 'members': ['price', 'quantity', 'amount']}], 'positions': [{'row': 0, 'members': [{'name': 'price', 'level': 0, 'root': True}]}, {'row': 1, 'members': [{'name': 'quantity', 'level': 0, 'root': True}]}, {'row': 2, 'members': [{'name': 'amount', 'level': 0, 'root': True}]}]}}, 'cells': [{'row': 0, 'cells': [{'row': 0, 'col': 0, 'value': 160.24096385542168, 'caption': '160.24'}, {'row': 0, 'col': 1, 'value': 83.0, 'caption': '83.00'}, {'row': 0, 'col': 2, 'value': 13300.0, 'caption': '13,300.00'}]}]} 

Как видим, все значения верны.

Используем OLAP по истинному предназначению

Основное предназначение OLAP — вывод данных не просто в виде таблички, а в разрезе, необходимом для анализа. Построим срез, в котором будет отображено количество продаж каждого товара по датам.

 view = View(cube, definition={
        "filters": {"dimensions": [{"dimension": "dim_company", "members": "2000"}]},
        "columns": {"dimensions": [{"dimension": "measures", "members": "quantity"}, "dim_date"]},
        "rows": {"dimensions": ["dim_assets"]}
    })

print(view.to_console_output())

3eb3287193698869c6dca633fcd6d5ab.png

Немного о TinyOLAP

Библиотека TinyOLAP позволяет создать все структуры данных и сам кубик, загрузить в него данные и построить различные срезы. По типу хранения данных относится к MOLAP (многомерная оперативная аналитическая обработка).

У TinyOLAP есть как свои плюсы, так и минусы.

Плюсы:

Минусы:

  • нет обновлений и поддержки,

  • слабая документация (некоторые моменты описаны, но не реализованы),

  • ограниченность типов данных для измерений и показателей,

  • скорость работы и ресурсы для тяжелых датасетов.

Мы работали с небольшим демонстрационным датасетом на OpenSourse‑версии, описаны лишь некоторые проблемы. В текущей версии TinyOLAP часть функциональных возможностей заявлена, но не имплементирована, поэтому при попытке их использования возникают ошибки. При внесении модификаций большинство проблем «лечится».

© Habrahabr.ru