Карты, деньги, ELK. Или как айтишник деньги считал

Привет, Хабр! Вот уже 7 с половиной лет я веду учет своих личных финансов в одном из многочисленных приложений. Всё это время оно неплохо закрывало базовые потребности в моменте, но с годами захотелось глубже проанализировать свою накопленную микро-бигдату и просмотреть на картину в целом. Желательно, в буквальном смысле: воспринимать информацию в виде визуализаций, диаграмм и дашбордов мне проще.

Поэксперементировав, я за несколько вечеров собрал себе решение на довольно нестандартной для таких целей платформе — Kibana. Как по мне, получилось неплохо. По горячим следам я описал этот кейс в своем англоязычном Твиттере и поделился им же в одном русскоязычном сообществе. Угадайте, откуда какой первый комментарий:

— А [зачем], собственно?
— Интересное решение! Я евангелист из Эластик — не хочешь на митапе выступить?

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

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

Предыстория

Шел 2016 год. Я работал сетевым инженером в крупной страховой компании в Москве на позиции Главного специалиста, уже имел за плечами 5 лет опыта в ИТ и параллельно с основной работой иногда успевал в свободное время консультировать клиентов на Upwork в географии от Австралии до Северной Америки. В общем, был на пути к успеху и джентльменским айтишным 300к/нс.

Привычка делать сбережения у меня была еще с детства (привет, 90-е), и деньги на счетах в разных банках и валютах вроде бы копились. Но и траты по субъективным ощущениям тоже неизменно росли. А вот насколько — было непонятно. И в какой-то момент, ударно поработав и вернувшись из небольшого Евротура с друзьями, я решил навести порядок и внести больше ясности в своих личных финансах (должен заметить, обменный курс в Братиславе уже не тот). Так я пришел к необходимости записывать траты и доходы.

Мой подход к учету

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

  • Я стремлюсь записывать все отдельно взятые траты, но с выборочной детализацией.

  • Категорий и подкатегорий не должно быть слишком много. Погоня за чрезмерной детализацией — самый быстрый способ забросить. Разбивать чеки имеет смысл в двух случаях:

    • Если какая-то общая категория начинает занимать большой процент в тратах.

    • Если просто интересно узнать, сколько денег уходит на что-то конкретное.

  • Чем меньше прошло времени с момента траты, тем меньше времени и ментальных усилий потребуется на ее запись.

  • Учет финансов должен снижать поводы для беспокойства, а не создавать дополнительное давление на себя.

  • На долгом интервале времени систематичность важнее точности: какую-то часть трат можно списывать, сравнивая остатки по счетам и распределяя навскидку по категориям. Или вовсе, списывая на дефолтную категорию под такой случай (у меня она зовется «Забыто»).

  • Пропуски в учете случаются. И их можно наверстать: помогает предыдущий пункт.

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

Заткнись и считай мои деньги

Приложение на iOS ровно с таким названием я и выбрал для ведения бюджета, перепробовав полдесятка аналогов. Правда, в тогда оно называлось просто «Мои деньги», а нынешнее «Shut up and count my money» в оригинале звучит менее токсично и отсылает к известному мему (именование продуктов и никнеймы в русскоязычном интернете вообще заслуживают отдельного исследования).

При не самом современном даже по меркам 2016 года UI «Мои деньги» практически полностью покрывали нужный мне функционал:

  • Поддержка нескольких счетов.

  • Поддержка мультивалютности.

  • Поддержка категорий/тэгов.

  • Поддержка переводов между своими счетами.

  • Поддержка базовой статистики по доходам и тратам.

  • Локальная база данных и возможность работы оффлайн.

  • Никаких «дайте доступ к вашим банковским счетам».

  • Экспорт данных в XLS.

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

Скриншот из AppStore. Дизайн приложения, к слову, остался неизменным и по сей день.

Скриншот из AppStore. Дизайн приложения, к слову, остался неизменным и по сей день.

При всем при этом до последнего времени приложение было полностью бесплатным (уже в момент написания статьи прилетел апдейт, вводящий ограничение на количество счетов без подписки (~$2/мес) до двух).

Что с этим делать и как с этим жить

Тут замечу, что учет трат — в общем-то бесконечно скучный процесс, поддерживать который может только выработанная привычка, любопытство и какое-то положительное подкрепление. Всё почти как с регулярным занятием спортом: главное начать и не бросить, а дальше мозг сам найдет удобное ему когнитивное искажение, чтобы продолжать.

Для меня положительным подкреплением стала возможность в любой момент времени понимать мое финансовое положение и оценивать близость к каким-то финансовым целям.

Для этого я обращаю внимание на следующее:

  1. Общая сумма денег на всех моих счетах. В мобильном приложении она удобно выведена на главный экран.

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

  3. Процент обязательных и базовых трат. Жизненный опыт подсказывает, что кризисы и черные лебеди — это «когда», а не «если». Нужно понимать нужный размер запасов на этот случай.

  4. Суммарный и среднемесячный доход от года к году.

  5. Норма сбережений от года к году.

Последние три пункта я обычно сводил и выписывал вручную в начале каждого последующего года. Внимательный читатель может задаться вопросом, а почему бы не работать с выгрузкой из приложения в XLS? И будет прав.

Как известно, люди делятся на тех, кто не делает бэкапы, кто уже делает бэкапы, и кто делает бэкапы и проверяет возможность восстановления. Восстановление из бинарного бэкапа (это отдельная функция) я делал на ранних этапах. А вот посчитать статистику по XLS-выгрузке мне захотелось спустя несколько лет, и тут обнаружился неприятный баг: приложение выдавало файл с битой структурой, который не хотел открываться ни одной доступной мне версией Excel и его опенсорсных аналогов.

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

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

Как погасить технический долг без регистрации и смс

В том же 2016 году случились еще две вещи, примечательные для этой истории и моей последующей карьеры: я решил начать изучать Python и внедрил на работе ELK для сбора и анализа статистики из системы корпоративной телефонии (дашборды Kibana дали как сервис самообслуживания для отдельных групп пользователей).

XLS-выгрузка из приложения при более детальном изучении все же открывалась текстовыми редакторами и внутри содержала XLM структуру данных. А значит появилась надежда на то, что блоки с самими данными сгенерированы корректно, и их можно извлечь.

На помощь пришел Python и пара популярных библиотек:

  • BeautifulSoup4. Вообще это библиотека для веб-скраппинга, но она содержит в себе удобные XML-парсеры, которые и помогли извлечь данные.

  • Pandas. Золотой стандарт для анализа данных в Python, для многих в представлении не нуждается. На первой итерации помог записать извлеченные данные в XLSX-файл корректного формата.

import pandas as pd
from bs4 import BeautifulSoup

SOURCE_FILE = 'mymoney.xls' # default export file name
OUT_FILE = 'transactions.xlsx'

with open(SOURCE_FILE) as broken_file:
    bs = BeautifulSoup(broken_file.read(), 'xml')
    with pd.ExcelWriter(OUT_FILE) as writer:
        for sheet in bs.findAll('Worksheet'):
            sheet_as_list = []
            for row in sheet.findAll('Row'):
                sheet_as_list.append(
                    [cell.Data.text if cell.Data else '' for cell in row.findAll('Cell')]
                )
            pd.DataFrame(sheet_as_list).to_excel(
                writer,
                sheet_name=sheet.attrs['ss:Name'],
                index=False,
                header=False
            )

Код выше дал на выходе открывающийся в Excel и читающийся Pandas’ом файл. Но радоваться было пока еще рано — нужно было убедиться в корректности содержимого. А для этого нужно было воспроизвести на основе этих данных статистику из приложения и сравнить получившиеся цифры. Что я и сделал.

# Load saved file back into Pandas dataframe
my_money_df = pd.read_excel(OUT_FILE, sheet_name='MyMoney')
my_money_df.info()

RangeIndex: 8365 entries, 0 to 8364
Data columns (total 4 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   Account   8365 non-null   object 
 1   Category  8365 non-null   object 
 2   Sum       8365 non-null   float64
 3   Date      8365 non-null   object 
dtypes: float64(1), object(3)
memory usage: 261.5+ KB

За все время наблюдений накопилось больше 8300 транзакции (в среднем 3 в день). В выгрузке было всего 4 столбца с говорящими названиями: Account, Category, Sum, Date.

Валюты среди них не было, но она сразу же нашлась внутри Account («Зеленый Банк: Счет 1, RUB») вместе с еще одним багом: для аккаунтов, перенесенных в приложении в архивные, валюта к имени в выгрузке не добавлялась («Синий Банк: Счет 1»). Таких счетов у меня было немного, и проблема легко решилась заменой по словарю.

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

accounts = my_money_df['Account'].unique().tolist()
accounts_without_currency = [i for i in accounts if ',' not in i]
accounts_without_currency

['Синий Банк:Счет 1',
 'Синий Банк:Счет 2']
fix_account_names_dict = {
   'Синий Банк:Счет 1': 'Синий Банк:Счет 1, RUB',
   'Синий Банк:Счет 2': 'Синий Банк:Счет 1, RUB',
}

my_money_df.replace({'Account': fix_account_names_dict}, inplace=True)

Попутно я вынес валюту в отдельный столбец.

my_money_df[['Account','Currency']] = my_money_df['Account'].str.split(
    ',', expand=True
)

# Cleanup trailing and leading whitespaces if any
my_money_df['Account'] = my_money_df['Account'].apply(lambda x: x.strip())
my_money_df['Currency'] = my_money_df['Currency'].apply(lambda x: x.strip())

И для удобства дальнейшей фильтрации добавил столбец Transaction_Type со следующими возможными значениями:

  • transfer. Трансферы между своими счетами порождают две транзакции: с отрицательным Sum на счете источника и положительным Sum на счете назначения. Для них есть скрытая категория «Перевод»/«Transfer» (в разное время у меня стоял разный системный язык на iOS. Их классифицируем первыми.

  • initial_balance. Баланс на счете в момент его создания и начала учета.

  • expense. Для трат. Это оставшиеся транзакции с отрицательным Sum.

  • income. Для доходов. Это оставшиеся транзакции с положительным Sum.

import numpy as np

my_money_df['Transaction_Type'] = np.where(
    my_money_df['Category'].str.startswith('Transfer:'), 'transfer', np.where(
        my_money_df['Category'].str.startswith('Перевод:'), 'transfer', np.where(
            my_money_df['Category'].str.startswith('Счет создан'), 'initial_balance', np.where(
                my_money_df['Sum'] < 0, 'expense', 'income'
            )
        )
    )
)

А также выделил из Category основную категорию и подкатегории в разные столбцы. Каждая Category в приложении представляла из себя единую строку с перечислением одного или более названий через двоеточие без ограничений по вложенности. Например, «Путешествия», «Путешествия: Авиабилеты», «Путешествия: Жилье: Airbnb» (это как раз одна из категорий, одновременно занимающих заметную долю в моих тратах и интересных для более детального изучения).

my_money_df['Primary_Category'] = my_money_df['Category'].apply(
    lambda x: x.split(':')[0]
)
my_money_df['Subcategories'] = my_money_df['Category'].apply(
    lambda x: x.split(':')[1:]
)

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

my_money_df.groupby(['Account', 'Currency'])['Sum']\
    .sum()\
    .sort_values(ascending=False)\
    .reset_index()
    Account                 Currency    Sum
0	Зеленый банк:Счет 1    RUB         111111.11
1	Желтый банк:Счет 1     RUB         222222.22

И это уже был первый успех: Pandas выдал балансы счетов, полностью совпадающими с теми, что я видел в приложении! Осталось проверить, верно ли выгружены категории в транзакциях. Какие-то произвольные записи я сверил выборочно там и там, и они сошлись. А затем сделал несколько сводных по разным периодам. К примеру, за 2020:

y_2020 = my_money_df[my_money_df['Date'].dt.year == 2020]

y_2020_income = y_2020[y_2020['Transaction_Type'] == 'income']\
    .groupby(['Primary_Category', 'Currency'])['Sum']\
    .sum().round(2).abs()\
    .sort_values(ascending=False)

y_2020_expenses = y_2020[y_2020['Transaction_Type'] == 'expense']\
    .groupby(['Primary_Category', 'Currency'])['Sum']\
    .sum().round(2).abs()\
    .sort_values(ascending=False)\

print(y_2020_income.head(3))
print(y_2020_expenses.head(3))
Primary_Category        Currency
Зарплата                RUB         1111111.11
Инвестиции              RUB         222222.22

Primary_Category        Currency
Жилье                   RUB         11111.11
Путешествия             RUB         22222.22
Супермаркет             RUB         3333.33

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

Анализ и визуализация данных

На Pandas в общем-то можно было остановиться. Он позволяет делать любую сводную аналитику и со сторонними библиотеками строить по ним визуализации. Но это это неинтерактивный процесс. Хотелось сделать себе дашборд, в котором легко было бы выбирать даты, применять быстрые фильтры, добавлять и перемещать элементы. И тут на ум пришла Kibana — K в уже упомянутой аббревиатуре ELK.

ELK, он же ELK Stack или Elastic Stack состоит из следующих базовых компонентов:

  • Elasticsearch. Мощный движок для индексации данных и поиска по ним (особенно, полнотекстового) с API и своим языком запросов.

  • Logstash. Сервис для парсинга данных в разном формате для отправки в Elasticsearch. На деле чаще заменяется более легковесными и производительными аналогами: Fluentd, Fluentbit, Filebeat. Или же кодом, который напрямую работает с API Elasticsearch.

  • Kibana. Нативный движок для визуализации данных для Elasticsearch. Нередко заменяется на стороннюю альтернативу — Grafana.

Kibana для моего юзкейса — далеко не очевидный выбор. Для дата-аналитики и дашбордов, как минимум, существуют более специализированные PowerBI или Tableau, плюс Python-библиотеки вроде Seaborn. И тем не менее в сторону Kibana меня склонили следующие соображения:

  • Elastic Stack мне хорошо известен, и я уже не раз применял его под не совсем стандартные сценарии. А с более специализированными вариантами пришлось бы разбираться. Так-то я ИТ-менеджер, а такси дата-аналитика — это для души.

  • По заветам Agile лучше здесь и сейчас сделать достаточно хороший MVP на знакомом стеке начать причинять пользу пользователям (себе же), чем долго готовиться и разрабатывать недостежимый идеал.

  • Kibana — гибкий, функциональный и бесплатный (в базовой лицензии) инструмент для визуализации данных и построения дашбордов безотносительно юзкейса.

  • Окружение с ELK легко и быстро поднять в Docker. На официальном сайте Elastic процесс подробно описан с готовыми примерами под Docker Compose. Воспроизводимая Infrastructure-as-Code удобна для экспериментов и моих потребностей: кластер ELK не нужно поддерживать, а всегда можно поднять с нуля и перезалить в него свежие данные.

  • В Python есть библиотека для работы с Elasticsearch, что еще более упрощает загрузку данных в него.

  • Использование привычных инструментов для нестандартных и пограничных случаев помогает узнать их возможности и ограничения еще лучше.

  • Потому что могу. Это проект для себя самого, и в нем можно позволить себе делать что угодно ради интереса.

Сказано — сделано. Подняв локальный кластер (docker-compose up -d), я без проблем загрузил в него все подготовленные в Pandas транзации и набросал себе дашборд за пару чашек кофе (14-я по счету категория трат у меня за всю историю наблюдений).

Статья была бы неполной без скриншотов, но тут уже было не обойтись без тестового датасета по понятным соображениям. Подумав, я сделал ход конем и описал контекст и требования в ChatGPT-4, подписку на который как раз взял недавно для ознакомления и экспериментов. И с какой-то итерации ИИ выдал мне рабочий скрипт на Python для генерации рандомных транзакций виртуального ИИгоря в нужном формате внутри Pandas датафрейма. Во всяком случае, уже написанные на прошлых шагах выборки и аггрегации выглядели на них осмысленно. Мог ли я о таком даже подумать в 2016?

Осторожно, машинный код.

import pandas as pd
from random import randint, choice
from datetime import datetime, timedelta

# Function to create a random date within a specified month and year
def random_date_month(year, month):
    start = datetime(year, month, 1)
    end = datetime(year, month + 1, 1) if month < 12 else datetime(year + 1, 1, 1)
    return start + timedelta(seconds=randint(0, int((end - start).total_seconds())))

# Function to calculate salary based on the year
def calculate_salary(start_year, current_year):
    yearly_increase = (2000 - 1000) / (2022 - start_year)
    return 1000 + (current_year - start_year) * yearly_increase

# Parameters for generating the dataset
num_samples = 2000
start_year = 2019
end_year = 2023
accounts = ["BestBank Current (USD)", "BestBank Saving (USD)", "Cash (USD)"]
spending_categories = ["Groceries", "Rent", "Entertainment", "Utilities", "Travel"]
transfer_category = "Transfer"
annual_interest_rate = 0.05
base_deposit_amount = 10000
transfer_percentage = 0.5

# Generate transactions
transactions = []
for year in range(start_year, end_year + 1):
    for month in range(1, 13):
        monthly_salary = calculate_salary(start_year, year) + randint(10, 300)

        # Salary and Deposit transactions
        transactions.append({
            "Account": "BestBank Current (USD)", 
            "Sum": monthly_salary, 
            "Category": "Salary", 
            "Date": datetime(year, month, 1), 
            "Currency": "USD", 
            "Transaction_Type": "income"
        })
        deposit_interest = base_deposit_amount * (annual_interest_rate / 12)
        transactions.append({
            "Account": "BestBank Saving (USD)", 
            "Sum": deposit_interest, 
            "Category": "Deposit", 
            "Date": datetime(year, month, 1), 
            "Currency": "USD", 
            "Transaction_Type": "income"
        })

        # Monthly transfer to Saving and Cash
        saving_transfer_amount = monthly_salary * 0.1
        cash_transfer_amount = monthly_salary * transfer_percentage
        transactions.append({
            "Account": "BestBank Current (USD)", 
            "Sum": -saving_transfer_amount, 
            "Category": transfer_category, 
            "Date": datetime(year, month, 2), 
            "Currency": "USD", 
            "Transaction_Type": "transfer"
        })
        transactions.append({
            "Account": "BestBank Saving (USD)", 
            "Sum": saving_transfer_amount, 
            "Category": transfer_category, 
            "Date": datetime(year, month, 2), 
            "Currency": "USD", 
            "Transaction_Type": "transfer"
        })
        transactions.append({
            "Account": "BestBank Current (USD)", 
            "Sum": -cash_transfer_amount, 
            "Category": transfer_category, 
            "Date": datetime(year, month, 2), 
            "Currency": "USD", 
            "Transaction_Type": "transfer"
        })
        transactions.append({
            "Account": "Cash (USD)", 
            "Sum": cash_transfer_amount, 
            "Category": transfer_category, 
            "Date": datetime(year, month, 2), 
            "Currency": "USD", 
            "Transaction_Type": "transfer"
        })

        # Spending Transactions from Current and Cash only
        for _ in range(int(num_samples / (12 * (end_year - start_year + 1))) - 6):
            account = choice(["BestBank Current (USD)", "Cash (USD)"])
            category = choice(spending_categories)
            sum_val = -randint(1, int(monthly_salary*0.065))
            date_val = random_date_month(year, month)
            transactions.append({
                "Account": account, 
                "Sum": sum_val, 
                "Category": category, 
                "Date": date_val, 
                "Currency": "USD", 
                "Transaction_Type": "expense"
            })

            if len(transactions) >= num_samples:
                break
        if len(transactions) >= num_samples:
            break

    # Initial balance transactions for the start year
    transactions.extend([
        {
            "Account": "BestBank Saving (USD)", 
            "Sum": 10000, 
            "Category": "Initial Balance", 
            "Date": datetime(start_year, 1, 1), 
            "Currency": "USD", 
            "Transaction_Type": "initial_balance"
        },
        {
            "Account": "BestBank Current (USD)", 
            "Sum": 1000, 
            "Category": "Initial Balance", 
            "Date": datetime(start_year, 1, 1), 
            "Currency": "USD", 
            "Transaction_Type": "initial_balance"
        }
    ])

test_transactions = pd.DataFrame(transactions)

На получившемся тестовом датасете с доходами и расходами виртуального ИИгоря и воспроизведу ключевые моменты. Данные положим в выделенный индекс с названиемtest_dataset_transactions, а каждую транзакцию — в отдельный документ в нем (если грубо сравнить с реляционными БД, то индекс в Elasticsearch — это таблица, документ — строка в ней, с поля документа — колонки).

В современных версиях Elasticsearch и Kibana (я использовал 8.11) типы данных внутри полей определяются автоматически, и дополнительно маппинги в общем случае менять уже не нужно.

from elasticsearch import Elasticsearch
import uuid
import os

ELASTIC_PASSWORD = os.environ.get("ELASTIC_PASSWORD") # make sure to export it

es = Elasticsearch(
    'https://localhost:9200',
    basic_auth=("elastic", ELASTIC_PASSWORD),
    verify_certs=False,
)

for dict_row in test_transactions.to_dict(orient="records"):
    resp = es.index(
        index='test_dataset_transactions',
        id=uuid.uuid1(),
        document=dict_row
    )

Загрузив данные в индекс, можно наконец открыть в браузере Kibana. Docker Compose по ссылке выше по умолчанию поднимает ее на http://localhost:5601/. Залогинившись с логином и паролем, которые были указаны в ENV-file на одном из шагов при запуске проекта, и пройдя в меню в Stack Management > Index Management > Indices, мы увидим созданный индекс.

Последнее подготовительное действие — создать в соседнем разделе меню Stack Management > Data views новый Data View. Index pattern определяет набор индексов, по которым будут строиться визуализации в этом Data View. Поле с временной меткой (Timestamp Field) указывает на поле, которое Kibana должна брать из записей для построения временных диаграмм.

Наконец, в разделе Dashboard создадим новый дашборд и начнем добавлять на него визуализации. В последних версиях Kibana это делается быстро и удобно. Базовый набор визуализаций называется Lens, кнопка Create visualization ведет как раз на него.

Вновь созданный дашборд. Пока еще пустой.

Вновь созданный дашборд. Пока еще пустой.

Kibana Lens позволяет в Drag&Drop конструкторе набрасывать визуализации и легко переключаться между их типами, чтобы подобрать наиболее наглядный и удобный под каждый отдельный случай.

Интерфейс Kibana Lens. Сюда ведет Create visualization.Слева – поля на выбор. Справа настройки визуализации.

Интерфейс Kibana Lens. Сюда ведет Create visualization.
Слева — поля на выбор. Справа настройки визуализации.

Начнем с таблицы с текущими балансами по всем счетам. В Kibana Lens выбираем одноименный тип визуализации (Table) и перетаскиваем поле Account. Затем в разделе Metrics настраиваем, какие значения должны выводиться в таблицу.

Скриншот кликабелен.Посередине – превью того, как будет выглядеть эта визуализация на дашборде.

Скриншот кликабелен.
Посередине — превью того, как будет выглядеть эта визуализация на дашборде.

Для расчета значений можно воспользоваться двумя опциями с говорящими названиями: Quick function и Formula. Quick Function применят одну функцию из фиксированного списка. В Formula можно писать довольно сложносочиненные выражения с возможностью добавления запросов на Kibana Query Language. Там же для вычисляемой метрики можно указать её отображаемое имя.

Для расчета балансов достаточно, как убедились ранее, просуммировать поле Sum. Для этого есть готовая функция Sum. В формулах это же будет выглядеть как sum (Sum). Оба варианта применятся построчно.

Сумма воображаемых сбережений.

Сумма воображаемых сбережений.

А если выбрать тип визуализации Metric, вместо Table, и указать ему в Primary Metric тот же Sum по полю Sum (да, над именованием надо бы поработать), то получится суммарный баланс по всем счетам. Или чистая прибыль за выбранный период времени, если хотите.


Посмотрим, сколько виртуальный ИИгорь зарабатывал денег. В этом поможет предусмотрительно добавленное поле Transaction_Type — Kibana позволяет применять фильтры на специальном Kibana Query Language (KQL) на уровне визуализаций или дашборда (в этом случае фильтр применится для всех визуализаций, вынесенных на него).

Для наглядности сделаем это столбчатой диаграммой (Bar vertical stacked) с разбиением по категориям дохода. По горизонтали — поле Date с агрегацией по годам, по горизонтали — всё тот же sum (Sum).

To the moon.

To the moon.

Уже нагляднее: доходы виртуального ИИгоря планомерно растут, а основная часть приходится на зарплату. Посмотрим, какой процент дохода ему удавалось откладывать от года к году. Это уже менее прямолинейная задача, но все еще решаемая стандартными средствами Kibana Lens. Сделаем это простой столбчатой диаграммой (Bar vertical) со страшной на вид, но тем не менее довольно простой по сути формулой.

Виртуальный ИИгорь стабильно сберегает >10% дохода.» /></p>

<p>Виртуальный ИИгорь стабильно сберегает >10% дохода.</p>

<p>Немного расшифрую формулу. И в числителе, и в знаменателе — всё тот же sum (Sum). Но в первом случае в него добавляется KQL-фильтр, чтобы выбрать транзакции с доходами и расходами, а во втором — только с доходами.</p>

<pre><code class=sum( Sum, kql='Transaction_Type.keyword: "income" or Transaction_Type.keyword: "expense" ' )/sum( Sum, kql='Transaction_Type.keyword: "income"' )

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

Кумулятивная сумма по арифметической сумме поля Sum.

Кумулятивная сумма по арифметической сумме поля Sum.

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

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

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

Траты в процентном соотношении.

Траты в процентном соотношении.

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

Как по мне, довольно наглядно.Основные метрики собраны в одном месте, и прослеживается положительный тренд по графикам.

Как по мне, довольно наглядно.
Основные метрики собраны в одном месте, и прослеживается положительный тренд по графикам.

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

Единожды собранный дашборд можно выгрузить в JSON-формате со всеми настройками и визуализациями и добавить в свой IaC. Через Kibana API можно и импортировать его, и создать Data View. А значит процесс развертывания окружения можно полностью автоматизировать и сделать воспроизводимым.

Послесловие

С момента сборки дашборда прошло более полутора месяцев. Относительно базовой версии из демо он уже успел обогатиться дополнительными интересными мне визуализациями и поддержкой отображения доходов и расходов в разных валютах. Что могу сказать по результатам этого кейса и многолетнего опыта учета:

  • Вести учет можно любыми удобными для себя средствами. А можно и не вести.

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

  • Точные цифры помогают принимать информированные решения, эффективнее распоряжаться своим бюджетом и планировать крупные траты. Например, переезд в другую страну (возможно, как-нибудь напишу и об этом).

  • Из Kibana получился инструмент, которым мне удобно пользоваться. Он представляет информацию в знакомом и настраиваемом под меня виде, позволяя сосредоточиться на содержимом.

  • Основная часть предобработки данных у меня делается в Python еще до загрузки их в Elasticsearch, поэтому переехать на любой другой стек для их визуализации, когда упрусь в ограничения Kibana, будет несложно. По этой же причине легко добавлять и сводить воедино дополнительные источники данных для еще более детального и разностороннего анализа.

  • Я могу продолжить использовать привычное мобильное приложение для ввода данных без боязни потерять накопившуюся историю и периодически анализировать ее в Kibana по выгрузкам, пропущенным через разработанный пайплайн. Да и на другое приложение теперь переехать не проблема, если в нем есть возможность импорта базы данных.

  • Экономя на кофе на вынос, миллионером не стать. 

Спасибо всем, кто дочитал до конца! Как и всегда, рад конструктивной обратной связи и готов ответить на ваши вопросы.

© Habrahabr.ru