家計簿ダッシュボード
収支データをpandasで集計しmatplotlibで月別・カテゴリ別グラフを表示するビジュアル家計簿。
1. アプリ概要
収支データをpandasで集計しmatplotlibで月別・カテゴリ別グラフを表示するビジュアル家計簿。
このアプリはdataカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)・pandas・matplotlib、難易度は ★★★ です。
このアプリは「データ処理」カテゴリです。データ分析・処理アプリは Python の真骨頂と言える領域で、生データから意味ある可視化・要約までを一通り体験できます。tkinter(標準ライブラリ)・pandas・matplotlib を活かして実装するこの構造は、他のアプリにも応用が効きます。
コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。
応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。
2. 機能一覧
- 「◀」ボタン(
_prev_month())・「▶」ボタン(_next_month())・「今月」ボタン(_set_month())・「▶ 読み込み」ボタン(_load_month())・「+ 追加」ボタン(_add())・「🗑 削除」ボタン(_delete())で操作 ttk.Treeviewによる表形式の一覧表示sqlite3.connect()による SQLite データベース接続と SQL 実行messagebox.showerror()によるダイアログ通知- ウィンドウはタイトル「家計簿ダッシュボード」・サイズ 1060x660 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
インストールが必要なライブラリ
pip install matplotlib pandas
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3
import os
from datetime import date
import pandas as pd
try:
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import font_manager
MATPLOTLIB_AVAILABLE = True
except ImportError:
MATPLOTLIB_AVAILABLE = False
if MATPLOTLIB_AVAILABLE:
# グラフ内の日本語が豆腐(□)にならないよう、入っている和文フォントを1つ選ぶ
_installed = {f.name for f in font_manager.fontManager.ttflist}
for _name in ("Meiryo", "Yu Gothic", "Noto Sans CJK JP", "Noto Sans JP",
"Hiragino Sans", "IPAexGothic", "MS Gothic", "DejaVu Sans"):
if _name in _installed:
matplotlib.rcParams["font.family"] = _name
break
matplotlib.rcParams["axes.unicode_minus"] = False
DB_PATH = os.path.join(os.path.dirname(__file__), "kakeibo.db")
class App081:
"""家計簿ダッシュボード"""
CATEGORIES = ["食費", "交通費", "光熱費", "通信費", "娯楽", "医療",
"日用品", "衣服", "外食", "住居費", "教育", "貯蓄", "その他"]
INCOME_CATS = ["給与", "副業", "ボーナス", "その他収入"]
MONTHS_BACK = 12 # 月別グラフに表示する月数
def __init__(self, root):
self.root = root
self.root.title("家計簿ダッシュボード")
self.root.geometry("1060x660")
self.root.configure(bg="#1e1e1e")
self._init_db()
self._build_ui()
self._load_month()
def _init_db(self):
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
type TEXT NOT NULL,
category TEXT NOT NULL,
amount INTEGER NOT NULL,
memo TEXT DEFAULT ''
)
""")
conn.commit()
conn.close()
def _build_ui(self):
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="💰 家計簿ダッシュボード",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
# 月選択
nav_f = tk.Frame(self.root, bg="#2d2d2d", pady=4)
nav_f.pack(fill=tk.X)
ttk.Button(nav_f, text="◀", command=self._prev_month).pack(side=tk.LEFT, padx=4)
self.month_var = tk.StringVar(value=date.today().strftime("%Y-%m"))
ttk.Entry(nav_f, textvariable=self.month_var, width=8).pack(side=tk.LEFT)
ttk.Button(nav_f, text="▶", command=self._next_month).pack(side=tk.LEFT, padx=2)
ttk.Button(nav_f, text="今月",
command=lambda: self._set_month(date.today().strftime("%Y-%m"))
).pack(side=tk.LEFT, padx=4)
ttk.Button(nav_f, text="▶ 読み込み",
command=self._load_month).pack(side=tk.LEFT, padx=4)
# メイン PanedWindow
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 入力フォーム + 一覧
left = tk.Frame(paned, bg="#1e1e1e", width=380)
paned.add(left, weight=0)
# 入力フォーム
form = tk.LabelFrame(left, text="収支入力", bg="#252526",
fg="#ccc", font=("Arial", 9), padx=6, pady=6)
form.pack(fill=tk.X, padx=4, pady=4)
tk.Label(form, text="日付:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=0, column=0, sticky="w")
self.date_var = tk.StringVar(value=date.today().isoformat())
ttk.Entry(form, textvariable=self.date_var, width=12).grid(row=0, column=1, padx=4)
tk.Label(form, text="種別:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=0, column=2, sticky="w")
self.type_var = tk.StringVar(value="支出")
ttk.Combobox(form, textvariable=self.type_var,
values=["支出", "収入"],
state="readonly", width=6).grid(row=0, column=3, padx=4)
self.type_var.trace_add("write", self._on_type_change)
tk.Label(form, text="カテゴリ:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=1, column=0, sticky="w", pady=4)
self.cat_var = tk.StringVar(value="食費")
self.cat_cb = ttk.Combobox(form, textvariable=self.cat_var,
values=self.CATEGORIES,
state="readonly", width=10)
self.cat_cb.grid(row=1, column=1, padx=4)
tk.Label(form, text="金額:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=1, column=2, sticky="w")
self.amount_var = tk.IntVar(value=0)
ttk.Entry(form, textvariable=self.amount_var, width=10).grid(row=1, column=3, padx=4)
tk.Label(form, text="メモ:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=2, column=0, sticky="w")
self.memo_var = tk.StringVar()
ttk.Entry(form, textvariable=self.memo_var, width=28).grid(
row=2, column=1, columnspan=3, padx=4, sticky="ew", pady=4)
btn_row = tk.Frame(form, bg="#252526")
btn_row.grid(row=3, column=0, columnspan=4, pady=2)
tk.Button(btn_row, text="+ 追加", command=self._add,
bg="#2e7d32", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=14, pady=4,
activebackground="#1b5e20", bd=0).pack(side=tk.LEFT, padx=4)
tk.Button(btn_row, text="🗑 削除", command=self._delete,
bg="#c62828", fg="white", relief=tk.FLAT,
font=("Arial", 10), padx=14, pady=4,
activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=4)
# 収支一覧
tk.Label(left, text="明細一覧", bg="#1e1e1e", fg="#888",
font=("Arial", 9)).pack(anchor="w", padx=4)
cols = ("date", "type", "category", "amount", "memo")
self.tree = ttk.Treeview(left, columns=cols, show="headings", height=14)
for col, text, width in [
("date", "日付", 80),
("type", "種別", 50),
("category", "カテゴリ", 70),
("amount", "金額", 70),
("memo", "メモ", 110),
]:
self.tree.heading(col, text=text)
self.tree.column(col, width=width, anchor="w")
tsb = ttk.Scrollbar(left, command=self.tree.yview)
self.tree.configure(yscrollcommand=tsb.set)
tsb.pack(side=tk.RIGHT, fill=tk.Y)
self.tree.pack(fill=tk.BOTH, expand=True, padx=(4, 0))
self.tree.tag_configure("income", foreground="#26a69a")
self.tree.tag_configure("expense", foreground="#ef5350")
# 右: サマリー + グラフ
right = tk.Frame(paned, bg="#1e1e1e")
paned.add(right, weight=1)
# サマリーカード
summary_f = tk.Frame(right, bg="#1e1e1e")
summary_f.pack(fill=tk.X, padx=4, pady=4)
self._summary_labels = {}
for key, label, color in [
("income", "収入合計", "#26a69a"),
("expense", "支出合計", "#ef5350"),
("balance", "収支差額", "#4fc3f7"),
]:
card = tk.Frame(summary_f, bg="#252526", padx=10, pady=8)
card.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4)
tk.Label(card, text=label, bg="#252526", fg="#888",
font=("Arial", 9)).pack()
lbl = tk.Label(card, text="¥0", bg="#252526", fg=color,
font=("Arial", 14, "bold"))
lbl.pack()
self._summary_labels[key] = lbl
# グラフ(上段: カテゴリ別の円グラフ・日別の棒グラフ / 下段: 月別推移の棒グラフ)
if MATPLOTLIB_AVAILABLE:
self.fig = Figure(figsize=(5, 4.4), facecolor="#1e1e1e")
self.ax_pie = self.fig.add_subplot(221)
self.ax_bar = self.fig.add_subplot(222)
self.ax_month = self.fig.add_subplot(212)
self.mpl_canvas = FigureCanvasTkAgg(self.fig, master=right)
self.mpl_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=4)
else:
tk.Label(right, text="pip install matplotlib でグラフが表示されます",
bg="#1e1e1e", fg="#555").pack(expand=True)
self.status_var = tk.StringVar(value="準備完了")
tk.Label(self.root, textvariable=self.status_var,
bg="#252526", fg="#858585", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)
def _on_type_change(self, *_):
if self.type_var.get() == "収入":
self.cat_cb["values"] = self.INCOME_CATS
self.cat_var.set(self.INCOME_CATS[0])
else:
self.cat_cb["values"] = self.CATEGORIES
self.cat_var.set(self.CATEGORIES[0])
# ── DB操作 ───────────────────────────────────────────────
def _add(self):
try:
amount = self.amount_var.get()
except tk.TclError:
messagebox.showerror("エラー", "金額を整数で入力してください")
return
if amount <= 0:
messagebox.showerror("エラー", "金額は正の整数で入力してください")
return
conn = sqlite3.connect(DB_PATH)
conn.execute(
"INSERT INTO transactions (date, type, category, amount, memo) VALUES (?,?,?,?,?)",
(self.date_var.get(), self.type_var.get(),
self.cat_var.get(), amount, self.memo_var.get()))
conn.commit()
conn.close()
self.amount_var.set(0)
self.memo_var.set("")
self._load_month()
def _delete(self):
sel = self.tree.selection()
if not sel:
return
row_id = self.tree.item(sel[0], "text")
conn = sqlite3.connect(DB_PATH)
conn.execute("DELETE FROM transactions WHERE id=?", (row_id,))
conn.commit()
conn.close()
self._load_month()
# ── 集計(pandas)─────────────────────────────────────────
def _read_month(self, start, end):
"""指定期間の明細を pandas の DataFrame として読み込む"""
conn = sqlite3.connect(DB_PATH)
try:
cur = conn.execute(
"SELECT id, date, type, category, amount, memo "
"FROM transactions WHERE date >= ? AND date < ? ORDER BY date",
(start, end))
# execute() の戻り値(カーソル)から行と列名を取り出して DataFrame にする
return pd.DataFrame(cur.fetchall(),
columns=[c[0] for c in cur.description])
finally:
conn.close()
def _monthly_expense(self):
"""全期間の支出を月別に集計し、直近 MONTHS_BACK か月ぶんを返す"""
conn = sqlite3.connect(DB_PATH)
try:
cur = conn.execute("SELECT date, type, amount FROM transactions")
df = pd.DataFrame(cur.fetchall(),
columns=[c[0] for c in cur.description])
finally:
conn.close()
df = df[df["type"] != "収入"].copy()
if df.empty:
return {}
df["month"] = df["date"].str[:7] # "2026-09" の形に切り出す
monthly = df.groupby("month")["amount"].sum().sort_index()
return monthly.tail(self.MONTHS_BACK).astype(int).to_dict()
def _load_month(self):
month = self.month_var.get().strip()
try:
y, m = month.split("-")
start = f"{y}-{int(m):02d}-01"
# 翌月1日
nm = int(m) + 1
ny = int(y)
if nm > 12:
nm = 1
ny += 1
end = f"{ny}-{nm:02d}-01"
except Exception:
messagebox.showerror("エラー", "月は YYYY-MM 形式で入力してください")
return
df = self._read_month(start, end)
self.tree.delete(*self.tree.get_children())
for row in df.itertuples(index=False):
tag = "income" if row.type == "収入" else "expense"
self.tree.insert("", tk.END, text=int(row.id),
values=(row.date, row.type, row.category,
f"¥{int(row.amount):,}", row.memo),
tags=(tag,))
# 収入と支出に分け、groupby でカテゴリ別・日別の合計を作る
expense_df = df[df["type"] != "収入"]
income = int(df.loc[df["type"] == "収入", "amount"].sum())
expense = int(expense_df["amount"].sum())
cat_expense = (expense_df.groupby("category")["amount"].sum()
.sort_values(ascending=False).astype(int).to_dict())
daily_expense = (expense_df.groupby("date")["amount"].sum()
.sort_index().astype(int).to_dict())
balance = income - expense
self._summary_labels["income"].config(text=f"¥{income:,}")
self._summary_labels["expense"].config(text=f"¥{expense:,}")
color = "#26a69a" if balance >= 0 else "#ef5350"
self._summary_labels["balance"].config(text=f"¥{balance:,}", fg=color)
if MATPLOTLIB_AVAILABLE:
self._draw_charts(cat_expense, daily_expense, self._monthly_expense())
self.status_var.set(f"{month}: 収入 ¥{income:,} 支出 ¥{expense:,} 差額 ¥{balance:,}")
def _draw_charts(self, cat_expense, daily_expense, monthly_expense):
self.ax_pie.clear()
self.ax_bar.clear()
self.ax_month.clear()
for ax in (self.ax_pie, self.ax_bar, self.ax_month):
ax.set_facecolor("#1e1e1e")
if cat_expense:
labels = list(cat_expense.keys())
vals = list(cat_expense.values())
self.ax_pie.pie(vals, labels=labels, autopct="%1.0f%%",
startangle=90, textprops={"fontsize": 6, "color": "#c9d1d9"},
colors=["#4fc3f7", "#ef5350", "#26a69a", "#ffa726",
"#ab47bc", "#66bb6a", "#ec407a", "#ff7043"])
self.ax_pie.set_title("支出カテゴリ", color="#c9d1d9", fontsize=8)
if daily_expense:
dates = sorted(daily_expense.keys())
amounts = [daily_expense[d] for d in dates]
day_labels = [d[5:] for d in dates] # MM-DD
self.ax_bar.bar(range(len(dates)), amounts, color="#4fc3f7")
step = max(1, len(dates) // 6)
self.ax_bar.set_xticks(range(0, len(dates), step))
self.ax_bar.set_xticklabels(day_labels[::step], fontsize=6,
color="#8b949e", rotation=30)
self.ax_bar.set_title("日別支出", color="#c9d1d9", fontsize=8)
self.ax_bar.tick_params(colors="#8b949e", labelsize=6)
if monthly_expense:
months = list(monthly_expense.keys())
m_vals = list(monthly_expense.values())
self.ax_month.bar(range(len(months)), m_vals, color="#26a69a")
self.ax_month.set_xticks(range(len(months)))
self.ax_month.set_xticklabels([mm[2:] for mm in months], fontsize=6,
color="#8b949e", rotation=30)
self.ax_month.set_title(f"月別支出(直近{len(months)}ヶ月)",
color="#c9d1d9", fontsize=8)
self.ax_month.tick_params(colors="#8b949e", labelsize=6)
for ax in (self.ax_pie, self.ax_bar, self.ax_month):
for spine in ax.spines.values():
spine.set_edgecolor("#30363d")
self.fig.tight_layout(pad=1.5)
self.mpl_canvas.draw()
def _set_month(self, month_str):
self.month_var.set(month_str)
self._load_month()
def _prev_month(self):
try:
y, m = map(int, self.month_var.get().split("-"))
m -= 1
if m < 1:
m = 12
y -= 1
self._set_month(f"{y}-{m:02d}")
except Exception:
pass
def _next_month(self):
try:
y, m = map(int, self.month_var.get().split("-"))
m += 1
if m > 12:
m = 1
y += 1
self._set_month(f"{y}-{m:02d}")
except Exception:
pass
if __name__ == "__main__":
root = tk.Tk()
app = App081(root)
root.mainloop()
5. コード解説
家計簿ダッシュボードのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App081 クラスにアプリの全機能をまとめています(メソッド13個・全391行)。__init__ ではタイトル「家計簿ダッシュボード」とウィンドウサイズ 1060x660 を設定します。そのあと _init_db() → _build_ui() → _load_month() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×6(ほかにループでも生成)・tk.Label×9(ほかにループでも生成)・ttk.Button×4・ttk.Entry×4・ttk.PanedWindow・tk.LabelFrame・ttk.Combobox×2・tk.Button×2・ttk.Treeview・ttk.Scrollbar で構成しています。見出し付きの枠(LabelFrame)は「収支入力」のラベルでエリアを区切っています。PanedWindow を使っているので、ペインの境界をマウスでドラッグして幅を変えられます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「◀」ボタン(_prev_month())・「▶」ボタン(_next_month())・「今月」ボタン(_set_month())・「▶ 読み込み」ボタン(_load_month())・「+ 追加」ボタン(_add())・「🗑 削除」ボタン(_delete())を command= で接続しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
SQLite データベースの接続と実行
sqlite3.connect() でデータベースファイルに接続し、SQL の実行は execute()、確定は commit() で行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:INCOME_CATS = ['給与', '副業', 'ボーナス', 'その他収入']、MONTHS_BACK = 12。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError・tk.TclError を捕捉しています。あわせて messagebox.showerror() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app081.py と保存します。外部ライブラリ
matplotlib・pandasを使います。先にpip install matplotlib pandasを実行してください。 -
2クラスの骨格を作る
App081クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("家計簿ダッシュボード")・geometry("1060x660")・configure(bg="#1e1e1e")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×6(ほかにループでも生成)・tk.Label×9(ほかにループでも生成)・ttk.Button×4・ttk.Entry×4・ttk.PanedWindow・tk.LabelFrame・ttk.Combobox×2・tk.Button×2・ttk.Treeview・ttk.Scrollbarを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_prev_month()・_next_month()・_set_month()・_load_month()・_add()・_delete())につなぎます。 -
6中心になるメソッドを実装する
アプリの本体である
_load_month()(43行)・_add()(19行)・_delete()(10行)など を実装します。SQL の実行結果はexecute()の戻り値から取り出します。 -
7動作確認する
python app081.pyで起動し、各ボタンが反応すること・データベースファイルを開いて内容が表示されることを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 クラス定数を書き換えて動きを変える
データはクラス定数(CATEGORIES・INCOME_CATS)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
💡 カテゴリで絞り込んで一覧とグラフを切り替える
「食費だけ」「交通費だけ」のようにカテゴリを選ぶと、明細一覧とグラフをそのカテゴリだけに絞り込めるようにしましょう。ttk.Combobox でカテゴリを選ばせ、<<ComboboxSelected>> イベントから _load_month() を呼び直す方針です。_read_month() が返す DataFrame を df[df["category"] == selected] で絞り込めば、SQL 側を変えずに実装できます。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ 「database is locked」というエラーが出る
原因:同じデータベースファイルを別のツール(DB Browser など)で開いたまま書き込むと、SQLite がファイルをロックしてこのエラーになります。
解決法:他のツールでの接続を閉じてから実行してください。書き込みは commit() で確定するまで反映されない点にも注意しましょう。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("1060x660") の固定サイズで起動するためです。
解決法:この数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。
❌ 起動直後にグラフが真っ白・何も出ない
原因:transactions テーブルは空の状態で作成されます。収支をまだ1件も登録していない月は集計結果が空になり、円グラフ・棒グラフとも描画するデータがありません。
解決法:「収支入力」から数件登録してから「▶ 読み込み」または月を切り替えて確認してください。データが無い月を開いてしまったときは「今月」ボタンで登録済みの月へ戻れます。サンプルデータで試したい場合は、_init_db() のテーブル作成直後に SELECT COUNT(*) FROM transactions で件数を数え、0件のときだけ INSERT を実行するようにすると、初回起動時だけサンプルが入ります(条件を付けずに書くと起動のたびに同じデータが増えていきます)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。
-
課題1:1日あたりの平均支出を出す
サマリーカードに「1日平均支出」を足します。分母は「その月に支出があった日数」にしてください。
期待結果:2026-09 に 9/1 で 3,000円、9/2 で 1,500円と500円を登録すると、支出合計 ¥5,000・1日平均支出 ¥2,500(支出があったのは2日)と表示される。支出が1件もない月は ¥0。
合格条件
- 支出0件の月でも
ZeroDivisionErrorが出ない - 収入は平均に含めない(上の例で給与を登録しても ¥2,500 のまま)
- 「◀」「▶」で月を切り替えると平均も切り替わる
ヒント①(どこを触るか): 日別の集計は
_load_month()がすでにdaily_expenseとして作っています。カードは_build_ui()のfor key, label, color in [...]のリストに1つ足すだけで増えます。
ヒント②(使うもの): 平均はsum(daily_expense.values()) / len(daily_expense)、表示はf"¥{avg:,.0f}"(カンマ区切り・小数なし)です。
つまずきやすい点: 支出が1件もない月はdaily_expenseが空でlen()が 0 になります。グラフ側はif daily_expense:で守られていますが、サマリー側にも同じガードが必要です。 - 支出0件の月でも
-
課題2:カテゴリ別の予算と超過額を出す
カテゴリごとの月予算を登録できるようにし、その月の支出が予算を超えたカテゴリを明細一覧の上に赤で表示します。
期待結果:「食費 20,000円」を登録した月の食費が 23,500円なら「⚠ 食費 23,500 / 20,000(超過 3,500)」と赤で出る。予算内のカテゴリは表示されない。
合格条件
- アプリを再起動しても予算が残っている(
kakeibo.dbに保存されている) - 予算を登録していないカテゴリは表示されない
- 同じカテゴリに予算を登録し直すと、行が増えずに金額だけ変わる
ヒント①(どこを触るか): 保存先は既存の
kakeibo.dbです。_init_db()に2つ目のテーブルを足し、_load_month()が作っているcat_expense(カテゴリ別の支出合計)と突き合わせます。
ヒント②(使うもの): テーブルはCREATE TABLE IF NOT EXISTS budgets (category TEXT PRIMARY KEY, amount INTEGER)、登録し直しはINSERT OR REPLACE INTO budgets VALUES (?,?)。文字色はlabel.config(fg="#ef5350")で後から変えられます。
つまずきやすい点:CREATE TABLE IF NOT EXISTSで後から追加できるのは「テーブル」だけです。既存のtransactionsに列を足したくなった場合はこの書き方では反映されず、ALTER TABLE transactions ADD COLUMN …が必要になります。 - アプリを再起動しても予算が残っている(
-
課題3:キー操作に対応させる
ボタンから呼んでいる
_prev_month()を、キーボードのキーからも呼べるようにしてみましょう。tkinter では bind を使ってキーイベントとメソッドを結び付けられます。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。