でどうしても預金残高を見たい - seabornで現金のストック・フローを可視化する


はじめに

貯金をしたいと考えても、そもそも今現金が増えているのか、減っているのかは分からないがち。
ZaimやMoneyforwardを使えば可視化できるかもしれないが、分析にはプレミアム会員になる必要があったり、いちいち入力するのが面倒だったりで管理する気が削がれる。
現金の入出金状況だけでもよいからさっくり見える化したいということで、PythonのSeabornを使って預金口座のお金の増減を可視化するスクリプトを実装した。

方針

大体のネットバンキングは1年分ぐらいの入出金履歴をcsvでエクスポートすることができる。このcsvをインプットに使用し、現金の入出金履歴を可視化する。
ひとえにお金の流れの可視化と言っても、見るべき切り口はいろいろある。ここではざっくり「ストック」と「フロー」の2つを可視化することとする。預金口座を浴槽、現金を水に例えると、ストックとフローは以下のように説明できる。

(1) ストック
ある時点で浴槽に溜まっている水量。すなわちある月の預金残高を表す。異なる月のストックを比較すれば、預金残高の増減傾向がわかる。

(2) フロー
一定期間に流れた水量。更に(2a)流入量と(2b)流出量に分けて考えることができ、ある月の収入と支出を表す。同月における流入と流出を比較すれば、ある月における損益額がわかる。

実装

各節はJupyter Notebookなどのノートブックで書くことを想定している。一度ノートブックに書いてしまえば、後から預金に更新があってもcsvファイルを差し替えて逐次実行すればいいので楽。VersionはPython3さえ使っていればだいたい動くはず。

尚、データはランダムにそれっぽい値を生成したものを使用している。自分の手取りより多めである

パッケージインポート

import numpy as np
import pandas as pd
import codecs
import seaborn as sns
sns.set(font='Hiragino Sans')
import matplotlib.pyplot as plt

anacondaを使用しているならすべて問題なくインポートできるはず。
macの場合そのままではseabornが日本語を認識しないので、sns.set()で日本語対応のフォントを指定する。

データインポート・前処理

コード

# 入出金履歴ファイルの読み込み
with codecs.open("../input/20200517212253.csv", "r", "Shift-JIS", "ignore") as file:
    df = pd.read_table(file, delimiter=",")

# 前処理
"""
読み込んだdfの前処理を実装する。
dfは「取引年月日」をindexとし、他に以下4つのカラムをもたせる。
 1. 取引名:「支払」か「入金」の2値を持つ。
 2. 金額:取引金額(フロー)
 3. 取引後残高:その時の預金残高(ストック)
"""

csv読み込み時には、文字化け回避のため文字コードを指定する。
前処理のコードは使用しているネットバンキングが出力する項目によって異なるため省略する。コード記載の通り、indexに取引年月日をもたせ、他に「取引名」「金額(=フロー)」「取引後残高(=ストック)」を持たせればよい。

dfサンプル

ちなみにネットバンキングあるあるで出力結果の列が「年」「月」「日」に分かれていることがあるが、以下のようにすれば1つのカラムにdatetime型で結合できる。

df["取引年月日"] = [pd.Timestamp(year=int(row["取扱日付 年"]), 
                            month=int(row["取扱日付 月"]), 
                            day=int(row["取扱日付 日"])) for i, row in df.iterrows()]

①ストックの可視化 - 預金残高の増減傾向をplotする

異なる月の預金残高(ストック)を比較し、残高が増加傾向・減少傾向のどちらにあるのかを確認する。

元データのcsvは入出金履歴、すなわち取引発生ベースで挿入されているため、「○月1日を基点として月次推移をグラフ化」のようなことはできない。そこで、dfのカラム「取引後残高」をすべてプロットした折れ線グラフを描画する。

コード

# ストック描画用のデータフレームを生成
df4stock = df.copy()

# 可視化
fig, ax = plt.subplots(figsize=(20, 10))
sns.lineplot(x=df4stock.index, y="取引後残高", data=df4stock, estimator=None);
fig.gca().ticklabel_format(style='plain', axis='y')

出力結果

青丸のマーカーは取引の発生と、その時点における残高を示す。
給与の収入とカード支払による支出は月次の周期性を構成している。全体の増減傾向は、毎月の最高残高(給料日)から推測する。

時系列分析においては、全体の増減傾向をトレンド成分、月次周期などサイクルを伴う増減を季節成分と定義する。トレンド成分を抽出したい場合は、取引が発生しなかった日のダミーデータを挿入し、statsmodeltsa.seasonal_decomposeを使用することでトレンド成分と季節成分を分離させることができる。本記事では実装を省略する。
(参考:さくっとトレンド抽出: Pythonのstatsmodelsで時系列分析入門

②フローの可視化 - 月ごとの入出金額をplotする

ある月における収入・支出額(フロー)を比較し、残高がどれぐらい増えたのか・減ったのかを確認する。

元データを年月でグループ化し、各月の支出・収入額を持つデータフレームを新たに生成する。作成したデータフレームを元に各月の収支と差額をプロットした棒グラフを描画する。

コード

#フロー描画用のデータフレームを生成
df4flow = df.groupby([df.index.year, df.index.month, "取引名"]).sum().reset_index("取引名")
df4flow

#可視化
fig, ax = plt.subplots(figsize=(20, 10))
ax = sns.barplot(x=df4flow.index, y="金額", hue="取引名", data=df4flow);

# 値ラベルで表示する各棒の金額をリストに格納
height = [0 if np.isnan(i.get_height()) else i.get_height() for i in ax.patches]


for i, patch in enumerate(ax.patches):

    diff = height[i//2] - height[i//2 + len(height)//2]
    maxh = max(height[i//2], height[i//2 + len(height)//2])

    # 値ラベルの表示
    if i % 2 == 0:
        colidx = i//2
        ax.text(i//2 - patch.get_width()/2.0, height[colidx]/2.0, int(height[colidx]), color='black', ha="center")
        ax.text(i//2, maxh+10000, int(diff), color='black', ha="center")

    else:
        colidx = i//2 + len(height)//2
        ax.text(i//2 + patch.get_width()/2.0, height[colidx]/2.0, int(height[colidx]), color='black', ha="center")

出力結果

グラフ用に新しく生成したdf4flowデータフレームは、年、月をMultiIndexに持った収入/支出別の取引額の合計を列に持つ。
描画では棒グラフの他、各収入・支出、及びその差額の値ラベルをヘルパーメソッドax.text()で描画している。出力する値は、各棒グラフの長方形としての情報を持つRectangleオブジェクトax.patchesから取得する。長方形の高さがそのまま金額となるので、同オブジェクトのメソッドget_height()で高さを取得する。値ラベルの位置は各棒の真ん中になるように計算して配置する。

※参考:ax.patches.get_height()の出力

コード

for i in ax.patches:
    print(i.get_height())

出力結果

ax.pathces[0:4]は収入(青色)、ax.pathces[5:9]は支出(朱色)の棒の情報を持つことがわかる。出力順はデータフレームと異なることに留意する。

終わりに

現金のストック・フローを可視化し、今お金が増えているのか減っているのかがわかるようになった(減っていたのでPCを買うのは諦めた)。
前述の通り、Notebookで上記コードをまとめておけば後はinputデータを更新すればよいだけなので管理が楽になった。

Excelでやるようなグラフ出力をSeabornMatplotlibでやろうとすると意外と大変。グラフが生成するArtistオブジェクトの構造等をもっとよく知る必要がありそうだ。

参考:早く知っておきたかったmatplotlibの基礎知識、あるいは見た目の調整が捗るArtistの話