Медведь бухгалтер или эксперимент над личными финансами на python и polars

33490cf019024fc94dfe4760dffbe870.png

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

Но если вы знаете python и хотя бы раз сталкивались с pandas, для вас финансовая аналитика может показаться куда более интересной.

Подготовим рабочую поверхность

Для анализа данных нам потребуются данные, python и polars. Чтобы получить данные о финансах, можно воспользоваться банковским сайтом. У Т-Банка это можно сделать во вкладке операции, выбрать «за весь период» и нажать кнопку «скачать в формате csv», хотя я все-таки рекомендую формат экселя, потому что csv в polars у меня почему-то не завелся, хотя это вроде бы самый простой формат данных.

Также нам понадобится python и polars, чтобы скачать python достаточно зайти на python.org и проследовать инструкции по загрузке или найти гайд. Чтобы скачать polars, достаточно создать папку с проектом, добавить туда наш датасет, создать виртуальное окружение командой python -m venv .venv и написать pip install polars, чтобы установить библиотеку polars.

Немнго про нашего медведя

Polars это новая библиотека для обработки данных на python, она созвучна с pandas и pyspark по области применения. Polars написан на rust, что в сравнении с pandas дает ощутимый прирост скорости. Об этом писали тут.

Начнем с простого

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

import polars as pl
load_data():
    return pl.read_excel('money.xls')

Здесь я работаю именно с экселем, потому что в моем случае csv файл неккоректно читался.

Дальше я использовал функции, чтобы выбрать нужные колонки, в polars для этого используется DataFrame.select(), обратите внимание, название колонки нужно передавать, используя pl.col('Название колонки'). Также я добавил фильтр, чтобы получить только отрицательные значения, так как Тинькофф хранит расходы именно со знаком минус. Итоговая функция выглядит вот так:

def get_valid_columns():
    return load_data() \
	    .select(pl.col('Дата операции', ... , "Описание")) \
	    .filter(pl.col('Сумма операции') < 0)

Дальше мы пишем функцию для агрегации суммы расходов по категориям. Для этого нужно выполнить несколько шагов:

  1. Сгруппируем данные по категориям data.group_by('Категория'), здесь можно использовать название колонки без pl.col()

  2. Суммируем по категориям и сумме операции. data.agg(pl.sum('Сумма операции'))

  3. Сортируем данные data.sort() Итоговая функция выглядит вот так:

def get_categories_agg():
    return get_valid_columns() \
	    .group_by('Категория') \
	    .agg(pl.sum('Сумма операции')) \
	    .sort('Сумма операции')

Дорогой день

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

Чтобы разбить средние траты по дням недели нам нужно выполнить несколько шагов:

  1. Получить нужные колонки с уже написанной функцией get_valid_columns()

  2. Написать функцию для конвертации строки в день недели с помощью модуля datetime

from datetime import datetime

def date_converter(date: str):
    format = "%d.%m.%Y %H:%M:%S"
    return datetime.strptime(date, format).strftime("%A")
  1. Создать новую колонку с днями недели используя функцию with_columns: df = get_valid_columns().with_columns(pl.col('Дата операции').map_elements(lambda x: date_converter(x), return_dtype=pl.String).alias('День недели'))

  2. Тут мы с помощью маппинга применяем к каждой строчке функцию date_converter(), указываем возвращаемое значение и элиас

  3. Группируем по дню недели и выводим медиану для суммы операции: df.group_by('День недели').agg(pl.col('Сумма операции').median())

Итоговые преобразования выглядят так:

def date_converter(date: str):
    format = "%d.%m.%Y %H:%M:%S"
    return datetime.strptime(date, format).strftime("%A")

def extract_week():
    return get_valid_columns() \
        .with_columns(pl.col('Дата операции') \
        .map_elements(lambda x: date_converter(x), return_dtype=pl.String) \
        .alias('День недели'))

def get_median_week_exp():
    return extract_week()\
        .group_by('День недели') \
        .agg(pl.col('Сумма операции') \
        .median()) \
        .sort('Сумма операции')

get_median_week_exp()

Медианные расходы по годам

  1. Теперь посмотрим на сколько изменились медианные траты в месяц от года к году. Для этого сначала создадим отдельные колонки с указанием месяца и года.

format = "%d.%m.%Y %H:%M:%S"

def create_month_and_year_col():
    return get_valid_columns() \
        .with_columns(
            pl.col('Дата операции').map_elements(lambda x: datetime.strptime(x,format).month, return_dtype=pl.Int64).alias('Месяц'),
            pl.col('Дата операции').map_elements(lambda x: datetime.strptime(x,format).year, return_dtype=pl.Int64).alias('Год')
        )
  1. Потом сгруппируем данные по Месяцу и Году

def group_by_month_and_year():
    return create_month_and_year_col() \
        .group_by('Месяц','Год') \
        .agg(pl.all().sum()) \
        .sort(pl.col('Год'), pl.col('Месяц')) \
        .select(pl.col('Месяц'), pl.col('Год'), pl.col('Сумма операции'))
  1. И посчитаем все вместе. Добавим сортировку по году

def aggregate_by_year():
    return group_by_month_and_year() \
        .select(pl.col('Год'),pl.col('Сумма операции')) \
        .group_by('Год') \
        .agg(pl.all() \
        .median()) \
        .sort('Год')

И вот как в итоге у нас получилось:

format = "%d.%m.%Y %H:%M:%S"

def create_month_and_year_col():
    return get_valid_columns() \
        .with_columns(
            pl.col('Дата операции').map_elements(lambda x: datetime.strptime(x,format).month, return_dtype=pl.Int64).alias('Месяц'),
            pl.col('Дата операции').map_elements(lambda x: datetime.strptime(x,format).year, return_dtype=pl.Int64).alias('Год')
        )

def group_by_month_and_year():
    return create_month_and_year_col() \
        .group_by('Месяц','Год') \
        .agg(pl.all().sum()) \
        .sort(pl.col('Год'), pl.col('Месяц')) \
        .select(pl.col('Месяц'), pl.col('Год'), pl.col('Сумма операции'))

def aggregate_by_year():
    return group_by_month_and_year() \
        .select(pl.col('Год'),pl.col('Сумма операции')) \
        .group_by('Год') \
        .agg(pl.all() \
        .median()) \
        .sort('Год')

aggregate_by_year()

Выводы

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

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

  2. В какой день выходить из дома без карточки.

  3. Насколько растет мое потребление от года к году. Составить прогноз.

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

© Habrahabr.ru