【Python】pandasを使って「売上ダッシュボードアプリ」を作ってみた
おはようございます。タカヒデです。
本日はPythonで「pandasを使った売上ダッシュボードアプリ」を作ってみました。
STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。
- プログラミング初心者
- 何から始めればよいか分からない
- pandasを活用したデータ集計アプリを作ってみたい
ぜひ実際にコードを打ちながら作成してみてください。
「売上ダッシュボードアプリ」完成イメージ
まずは、「売上ダッシュボードアプリ」完成後の最終的なコードと完成イメージです。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from datetime import datetime
from pathlib import Path
import pandas as pd
import matplotlib
from matplotlib import font_manager as fm
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
def set_japanese_font() -> None:
preferred = [
"Noto Sans CJK JP",
"Noto Sans JP",
"IPAexGothic",
"IPAGothic",
"TakaoGothic",
"Yu Gothic",
"Meiryo",
"Hiragino Sans",
"MS Gothic",
]
installed = {f.name for f in fm.fontManager.ttflist}
for name in preferred:
if name in installed:
matplotlib.rcParams["font.family"] = name
break
matplotlib.rcParams["axes.unicode_minus"] = False
def parse_date(s: str) -> datetime | None:
s = (s or "").strip()
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
return None
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
class App(tk.Tk):
def __init__(self):
super().__init__()
set_japanese_font()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df: pd.DataFrame | None = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
kpi = ttk.Frame(right)
kpi.grid(row=0, column=0, columnspan=2, sticky="ew")
for i in range(4):
kpi.columnconfigure(i, weight=1)
self.kpi_sales = self._kpi(kpi, 0, "総売上", "— 円")
self.kpi_orders = self._kpi(kpi, 1, "注文数", "— 件")
self.kpi_qty = self._kpi(kpi, 2, "販売数量", "— 個")
self.kpi_aov = self._kpi(kpi, 3, "平均注文単価", "— 円")
lf1 = ttk.LabelFrame(right, text="月別売上", padding=6)
lf1.grid(row=1, column=0, sticky="nsew", padx=(0, 6), pady=(10, 0))
lf2 = ttk.LabelFrame(right, text="カテゴリ別売上", padding=6)
lf2.grid(row=1, column=1, sticky="nsew", padx=(6, 0), pady=(10, 0))
self.fig_month = Figure(figsize=(5, 3), dpi=100)
self.ax_month = self.fig_month.add_subplot(111)
self.canvas_month = FigureCanvasTkAgg(self.fig_month, master=lf1)
self.canvas_month.get_tk_widget().pack(fill="both", expand=True)
self.fig_cat = Figure(figsize=(5, 3), dpi=100)
self.ax_cat = self.fig_cat.add_subplot(111)
self.canvas_cat = FigureCanvasTkAgg(self.fig_cat, master=lf2)
self.canvas_cat.get_tk_widget().pack(fill="both", expand=True)
self._draw_empty()
def _kpi(self, parent: ttk.Frame, col: int, title: str, value: str) -> ttk.Label:
box = ttk.LabelFrame(parent, text=title, padding=8)
box.grid(row=0, column=col, sticky="ew", padx=6)
lbl = ttk.Label(box, text=value, font=("", 14, "bold"))
lbl.pack(anchor="w")
return lbl
def _draw_empty(self) -> None:
self.ax_month.clear()
self.ax_month.set_title("データ未読み込み")
self.canvas_month.draw()
self.ax_cat.clear()
self.ax_cat.set_title("データ未読み込み")
self.canvas_cat.draw()
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
try:
df = read_csv_safely(path)
except Exception as e:
messagebox.showerror("読み込みエラー", f"{e}")
return
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
df = df.copy()
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
self.file_label.config(text=Path(path).name)
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
def reset_filters(self) -> None:
if self.df is None:
return
dmin = self.df["order_date"].min().strftime("%Y-%m-%d")
dmax = self.df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
def apply_filters(self) -> None:
if self.df is None:
messagebox.showinfo("未読み込み", "先にCSVを読み込んでください")
return
start_dt = parse_date(self.start_entry.get())
end_dt = parse_date(self.end_entry.get())
if start_dt is None or end_dt is None:
messagebox.showerror("日付エラー", "YYYY-MM-DD 形式で入力してください")
return
if start_dt > end_dt:
messagebox.showerror("日付エラー", "開始日が終了日より後です")
return
idx = self.lb_pref.curselection()
prefs = [self.lb_pref.get(i) for i in idx] if idx else []
if not prefs:
messagebox.showerror("選択エラー", "都道府県を1つ以上選択してください")
return
df = self.df
mask = (
(df["order_date"] >= pd.Timestamp(start_dt))
& (df["order_date"] <= pd.Timestamp(end_dt))
& (df["prefecture"].isin(prefs))
)
f = df.loc[mask].copy()
total_sales = float(f["sales_amount"].sum()) if len(f) else 0.0
total_orders = int(f["order_id"].nunique()) if len(f) else 0
total_qty = float(f["quantity"].sum()) if len(f) else 0.0
aov = (total_sales / total_orders) if total_orders else 0.0
self.kpi_sales.config(text=f"{total_sales:,.0f} 円")
self.kpi_orders.config(text=f"{total_orders:,} 件")
self.kpi_qty.config(text=f"{total_qty:,.0f} 個")
self.kpi_aov.config(text=f"{aov:,.0f} 円")
self._draw_charts(f)
def _draw_charts(self, f: pd.DataFrame) -> None:
self.ax_month.clear()
if len(f) == 0:
self.ax_month.set_title("該当データなし")
else:
tmp = f.copy()
tmp["year_month"] = tmp["order_date"].dt.to_period("M").astype(str)
monthly = tmp.groupby("year_month", as_index=False)["sales_amount"].sum().sort_values("year_month")
self.ax_month.plot(monthly["year_month"], monthly["sales_amount"], marker="o")
self.ax_month.set_title("月別売上")
self.ax_month.tick_params(axis="x", rotation=45)
self.ax_month.ticklabel_format(style="plain", axis="y")
self.fig_month.tight_layout()
self.canvas_month.draw()
self.ax_cat.clear()
if len(f) == 0:
self.ax_cat.set_title("該当データなし")
else:
cat = f.groupby("category", as_index=False)["sales_amount"].sum().sort_values("sales_amount", ascending=False)
self.ax_cat.bar(cat["category"], cat["sales_amount"])
self.ax_cat.set_title("カテゴリ別売上")
self.ax_cat.tick_params(axis="x", rotation=45)
self.ax_cat.ticklabel_format(style="plain", axis="y")
self.fig_cat.tight_layout()
self.canvas_cat.draw()
if __name__ == "__main__":
App().mainloop()
売上データのCSVを読み込み、期間と都道府県で絞り込んだうえで「KPI」と「グラフ」を表示するデスクトップアプリを作ります。

なお、このアプリでは以下のダミー売上データを活用しているので、事前にダウンロードしておいてください。
STEP0:使用するライブラリと事前準備
この売上ダッシュボードアプリでは、主に以下のライブラリを使用します。
- tkinter
Python標準のGUIライブラリ。
ウィンドウやボタン、入力欄などの画面を作るために使用します。 - pandas
CSVデータを読み込み、集計やフィルタ処理を行うためのライブラリです。
売上データの分析処理の中心になります。 - matplotlib
データをグラフとして可視化するためのライブラリです。
月別売上やカテゴリ別売上のグラフ表示に使用します。
※tkinterは Python に標準で含まれているため、通常は追加インストール不要です。
pip install pandas matplotlib事前にターミナルで上記コマンドを実行し、インポートしておきましょう。
STEP1:アプリのウィンドウを作る
このSTEPでは、アプリのウィンドウを作り、左右に配置するためのフレームを用意します。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
if __name__ == "__main__":
App().mainloop()
手順①:Appクラスで「ウィンドウ本体」を作る
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
まず、アプリ全体を表すクラスとして「App(tk.Tk)」を用意しています。
「super().__init__()」を呼ぶことで、Tkinterのウィンドウが作られます。
- title
- タイトルバーの文字
- geometry
- 初期サイズ
- minsize
- 最小サイズ
手順②:CSVデータを入れる変数を用意する
self.df = None
この段階ではまだCSVを読み込みませんが、後でデータを持てるように、変数を先に用意しています。
「df」はこの先で「読み込んだ売上データ」が入る予定です。
今は未読み込みなので「None」にしています。
手順③:レイアウトの左右フレームを作る
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
画面を「左=操作」「右=表示」に分けるため、2つのフレームを作っています。
- grid
- 表計算のように行・列で配置する配置方法
- sticky
- どの方向にくっつけるか
手順④:アプリを起動してウィンドウを表示する
if __name__ == "__main__":
App().mainloop()
この2行があることでアプリが起動し、ウィンドウが表示されます。
「App()」でウィンドウを作り、「mainloop()」で「ウィンドウを表示し続ける状態」に入ります。
STEP1が終わった時点では、売上ダッシュボードのウインドウのみが表示されています。

STEP2:CSV選択と期間入力のUIを追加する
このSTEPでは、左側パネルに「CSVを開く」ボタンと、選択したファイル名の表示欄を追加します。
あわせて、期間フィルタ用に「開始」「終了」の入力欄も用意します。
from __future__ import annotations
import tkinter as tk
# STEP2変更
from tkinter import ttk, filedialog
# STEP2追加
from datetime import datetime
from pathlib import Path
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
# STEP2追加
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
# STEP2追加
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
# STEP2追加
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
self.file_label.config(text=Path(path).name)
if __name__ == "__main__":
App().mainloop()
手順①:ファイル選択ダイアログを使えるようにする
from tkinter import ttk, filedialog
ファイル選択ダイアログを出したいので、「filedialog」を読み込みます。
これを使うと、OS標準のファイル選択画面が表示されます。
手順②:「CSVを開く」ボタンを作る
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
左側パネルの一番上に、ボタンと表示欄を配置しています。
「command=self.open_csv」により、ボタンを押すと「open_csvメソッド」が呼ばれます。
「self.file_label」は選んだファイル名を表示するためのラベルで、最初は「未選択」と表示しています。
手順③:期間入力欄を作る
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))期間を入力できるように、ラベルフレームと入力欄を用意します。
ここではフレームを作っている段階です。
次のSTEP以降で、この入力値を使ってデータを絞り込めるようにしていきます。
手順④:open_csvでファイル名を表示する
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
self.file_label.config(text=Path(path).name)CSVの中身はまだ読み込みません。まずは「選んだファイル名を画面に出す」までを確認します。
「askopenfilename」は、ファイルのパスを文字列で返します。
キャンセルされた場合は空になるので、「if not path:」で止めています。
「Path(path).name」で「ファイル名だけ」を取り出し、ラベルに表示します。
この時点では、以下の通り、CSVを開くことができるか確認してみてください。

STEP3:都道府県リストと「集計・リセット」ボタンを追加する
このSTEPでは、左側パネルに「都道府県(複数選択)」のリストと、「集計」「リセット」ボタンを追加します。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, filedialog
from datetime import datetime
from pathlib import Path
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
# STEP3追加
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
# STEP3追加
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
self.file_label.config(text=Path(path).name)
# STEP3追加
def apply_filters(self) -> None:
return
# STEP3追加
def reset_filters(self) -> None:
return
if __name__ == "__main__":
App().mainloop()
手順①:都道府県の入力方法を「リスト選択」にする
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
都道府県はリストから選択する項目です。
文字入力ではなく「Listbox」を使い、選択できる形にします。
「selectmode=”extended”」にすることで、ShiftやCtrlで複数選択できるようにしています。
手順②:Listboxを配置して画面に表示する
self.lb_pref.pack(fill="both", expand=True)
作った「Listbox」を「pack」で枠いっぱいに広げて配置します。
- fill=”both”
- 縦横に広げる指定
- expand=True
- 余ったスペースも使って広がる指定
手順③:「集計」「リセット」ボタンを配置する
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
次に、ユーザーが押すボタンを用意します。
ボタンは「command=」でメソッドにつなぎます。
この時点ではまだ処理がないので、次の手順で「空のメソッド」を用意します。
手順④:「apply_fillters」「reset_fillters」メソッドを用意する
def apply_fillters(self)->None:
return
def reset_fillters(self)->None:
returnボタンに「command=self.apply_filters」と書いた場合、対応するメソッドが無いと実行時にエラーになります。
そのため、いったん中身は何もしない return だけのメソッドを置きます。
これで都道府県リストと「集計・リセット」ボタンが追加されました。

STEP4:CSVを読み込み、期間と都道府県リストを埋める
このSTEPでは、CSVを実際に読み込み、アプリ内にデータとして保持できる状態にします。あわせて、CSV内の日付の最小・最大を使って期間欄を自動入力し、都道府県リストもCSVから作って自動表示します。
from __future__ import annotations
import tkinter as tk
# STEP4変更
from tkinter import ttk, filedialog, messagebox
from datetime import datetime
from pathlib import Path
# STEP4追加
import pandas as pd
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
# STEP4追加
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
# STEP4変更
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
try:
df = read_csv_safely(path)
except Exception as e:
messagebox.showerror("読み込みエラー", f"{e}")
return
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
df = df.copy()
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
self.file_label.config(text=Path(path).name)
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
def apply_filters(self) -> None:
return
def reset_filters(self) -> None:
return
if __name__ == "__main__":
App().mainloop()
手順①:pandasでCSVを読み込めるようにする
import pandas as pd
pandasは「表形式のデータ」を扱うためのライブラリです。
ここでは、「pd.read_csv」や「pd.to_datetime」を使うために読み込みます。
手順②:文字コードを意識してCSVを読みこむ
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
CSVは作られ方によって文字コードが違うことがあります。
Excelで作ったCSVは「cp932」になることがあり、UTF-8で読むと文字化けやエラーの原因になります。
ここでは、よくある候補を順番に試す作りにしています。
手順③:CSVの列をチェックする
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
このアプリは「sales_sample_records.csv」のデータを前提に動きます。
もしCSVに必要な列が無いまま進むと、後の処理でエラーになります。
そこで先に列を調べ、足りなければダイアログで知らせて止めます。
手順④:日付・数値を扱いやすい形に直して保存する
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
CSVの中身は「文字列」として読み込まれることが多いです。
そこで、日付は日付として、数値は数値として扱えるように変換します。
- pd.to_datetime
- 日付への変換
- pd.to_numeric
- 数字への変換
- errors=”coerce”
- 変換できない値を「不正な値」として扱い、後で取り除けるようにする
最後に、日付や売上が欠けた行を「dropna」で落とします。
そして「self.df」に保存することで、他の処理でも同じデータを使えるようになります。
手順⑤:期間と都道府県をCSVから自動で埋める
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")期間はCSV内の日付の最小・最大を取得して、開始・終了に自動入力します。
都道府県はCSVの都道府県列から「重複なしの一覧」を作り、リストに表示します。
最後に「select_set(0, “end”) 」で「全部選択」にしておき、CSV読み込み時は全体集計ができる状態になります。
これで、CSVを読み込むことで期間と都道府県のデータが反映されるようになりました。

STEP5:期間と都道府県でデータを絞り込む
このSTEPでは、入力した「開始日・終了日」と、選択した「都道府県」を使って、CSVデータを絞り込みます。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from datetime import datetime
from pathlib import Path
import pandas as pd
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
# STEP5追加
def parse_date(s: str) -> datetime | None:
s = (s or "").strip()
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
return None
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
try:
df = read_csv_safely(path)
except Exception as e:
messagebox.showerror("読み込みエラー", f"{e}")
return
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
df = df.copy()
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
self.file_label.config(text=Path(path).name)
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
# STEP5変更
def apply_filters(self) -> None:
if self.df is None:
messagebox.showinfo("未読み込み", "先にCSVを読み込んでください")
return
start_dt = parse_date(self.start_entry.get())
end_dt = parse_date(self.end_entry.get())
if start_dt is None or end_dt is None:
messagebox.showerror("日付エラー", "YYYY-MM-DD 形式で入力してください")
return
if start_dt > end_dt:
messagebox.showerror("日付エラー", "開始日が終了日より後です")
return
idx = self.lb_pref.curselection()
prefs = [self.lb_pref.get(i) for i in idx] if idx else []
if not prefs:
messagebox.showerror("選択エラー", "都道府県を1つ以上選択してください")
return
df = self.df
mask = (
(df["order_date"] >= pd.Timestamp(start_dt))
& (df["order_date"] <= pd.Timestamp(end_dt))
& (df["prefecture"].isin(prefs))
)
f = df.loc[mask].copy()
messagebox.showinfo("フィルタ結果", f"該当件数: {len(f)} 件")
def reset_filters(self) -> None:
return
if __name__ == "__main__":
App().mainloop()
手順①:日付文字列を「日付」とする関数を用意する
def parse_date(s: str) -> datetime | None:
s = (s or "").strip()
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
return None
期間入力欄は「文字列」のため、そのままだと日付の大小比較ができません。
そこで「datetime.strptime」を使い「YYYY-MM-DD」の形なら日付に変換し、違う形なら 「None」を返すようにします。
手順②:CSV未読み込みや日付入力ミスを弾く
def apply_filters(self) -> None:
if self.df is None:
messagebox.showinfo("未読み込み", "先にCSVを読み込んでください")
return
start_dt = parse_date(self.start_entry.get())
end_dt = parse_date(self.end_entry.get())
if start_dt is None or end_dt is None:
messagebox.showerror("日付エラー", "YYYY-MM-DD 形式で入力してください")
return
if start_dt > end_dt:
messagebox.showerror("日付エラー", "開始日が終了日より後です")
return
想定外のエラーは先にチェックして、メッセージで止めています。
手順③:都道府県の選択値をListboxから取り出す
idx = self.lb_pref.curselection()
prefs = [self.lb_pref.get(i) for i in idx] if idx else []
「curselection()」は、選択されている行番号の一覧を返します。
それを使って「get(i)」で都道府県名を取り出し、都道府県のリストを作っています。
手順④:pandasで「期間+都道府県」を絞り込む
mask = (
(df["order_date"] >= pd.Timestamp(start_dt))
& (df["order_date"] <= pd.Timestamp(end_dt))
& (df["prefecture"].isin(prefs))
)
f = df.loc[mask].copy()
ここでフィルタの処理をします。
- mask
- 条件を満たす行だけTrueになる真偽の一覧
- df.loc[mask]
- Trueの行だけを取り出す
- isin(prefs)
- 「都道府県が選択リストのどれかに含まれるか」を判定
手順⑤:現時点での動作確認
messagebox.showinfo("フィルタ結果", f"該当件数: {len(f)} 件")
この段階で、フィルタ結果が正しく作れているかを確認します。
該当件数や、エラー時の所作が正しく動いているか確認しましょう。

STEP6:KPIを計算して表示する
このSTEPでは、データから「総売上」「注文数」「販売数量」「平均注文単価」などのKPIを計算し、右側パネルに表示します。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from datetime import datetime
from pathlib import Path
import pandas as pd
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
def parse_date(s: str) -> datetime | None:
s = (s or "").strip()
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
return None
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
# STEP6追加
kpi = ttk.Frame(right)
kpi.grid(row=0, column=0, columnspan=2, sticky="ew")
for i in range(4):
kpi.columnconfigure(i, weight=1)
self.kpi_sales = self._kpi(kpi, 0, "総売上", "— 円")
self.kpi_orders = self._kpi(kpi, 1, "注文数", "— 件")
self.kpi_qty = self._kpi(kpi, 2, "販売数量", "— 個")
self.kpi_aov = self._kpi(kpi, 3, "平均注文単価", "— 円")
# STEP6追加
def _kpi(self, parent: ttk.Frame, col: int, title: str, value: str) -> ttk.Label:
box = ttk.LabelFrame(parent, text=title, padding=8)
box.grid(row=0, column=col, sticky="ew", padx=6)
lbl = ttk.Label(box, text=value, font=("", 14, "bold"))
lbl.pack(anchor="w")
return lbl
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
try:
df = read_csv_safely(path)
except Exception as e:
messagebox.showerror("読み込みエラー", f"{e}")
return
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
df = df.copy()
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
self.file_label.config(text=Path(path).name)
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
def apply_filters(self) -> None:
if self.df is None:
messagebox.showinfo("未読み込み", "先にCSVを読み込んでください")
return
start_dt = parse_date(self.start_entry.get())
end_dt = parse_date(self.end_entry.get())
if start_dt is None or end_dt is None:
messagebox.showerror("日付エラー", "YYYY-MM-DD 形式で入力してください")
return
if start_dt > end_dt:
messagebox.showerror("日付エラー", "開始日が終了日より後です")
return
idx = self.lb_pref.curselection()
prefs = [self.lb_pref.get(i) for i in idx] if idx else []
if not prefs:
messagebox.showerror("選択エラー", "都道府県を1つ以上選択してください")
return
df = self.df
mask = (
(df["order_date"] >= pd.Timestamp(start_dt))
& (df["order_date"] <= pd.Timestamp(end_dt))
& (df["prefecture"].isin(prefs))
)
f = df.loc[mask].copy()
# STEP6変更
total_sales = float(f["sales_amount"].sum()) if len(f) else 0.0
total_orders = int(f["order_id"].nunique()) if len(f) else 0
total_qty = float(f["quantity"].sum()) if len(f) else 0.0
aov = (total_sales / total_orders) if total_orders else 0.0
self.kpi_sales.config(text=f"{total_sales:,.0f} 円")
self.kpi_orders.config(text=f"{total_orders:,} 件")
self.kpi_qty.config(text=f"{total_qty:,.0f} 個")
self.kpi_aov.config(text=f"{aov:,.0f} 円")
def reset_filters(self) -> None:
return
if __name__ == "__main__":
App().mainloop()
手順①:KPIを置くフレームを作る
kpi = ttk.Frame(right)
kpi.grid(row=0, column=0, columnspan=2, sticky="ew")
for i in range(4):
kpi.columnconfigure(i, weight=1)
右側パネルの上部にKPI用のフレームを作ります。
- kpi
- KPIを4つ並べるためのフレーム
- columnspan=2
- 右側の2列を使用
- columnconfigure(i, weight=1)
- 4つのKPIを均等に横に広げる
手順②:KPI表示を作るメソッドを用意する
def _kpi(self, parent: ttk.Frame, col: int, title: str, value: str) -> ttk.Label:
box = ttk.LabelFrame(parent, text=title, padding=8)
box.grid(row=0, column=col, sticky="ew", padx=6)
lbl = ttk.Label(box, text=value, font=("", 14, "bold"))
lbl.pack(anchor="w")
return lbl
KPIは4つとも「見出し+値」という同じ形なので、同じ処理を関数にして使い回します。
- LabelFrame
- 枠にタイトルが付いたフレーム
- lbl
- フレーム内の値の表示
最後にlblを返しておくことで「config(text=…)」で値を更新できます。
手順③:フィルタ後データからKPIを計算する
total_sales = float(f["sales_amount"].sum()) if len(f) else 0.0
total_orders = int(f["order_id"].nunique()) if len(f) else 0
total_qty = float(f["quantity"].sum()) if len(f) else 0.0
aov = (total_sales / total_orders) if total_orders else 0.0
フィルタ後の「f」に対してpandasで集計します。
- sum()
- 合計
- nunique()
- 重複を除いた件数
- if total_orders else 0.0
- データが0件のときにゼロ割りが起きないようにする
手順④:計算したKPIを画面に反映する
self.kpi_sales.config(text=f"{total_sales:,.0f} 円")
self.kpi_orders.config(text=f"{total_orders:,} 件")
self.kpi_qty.config(text=f"{total_qty:,.0f} 個")
self.kpi_aov.config(text=f"{aov:,.0f} 円")
ラベルの文字を書き換えて画面の表示を更新します。{:,}はカンマで区切り見やすくする指定です。
ダッシュボード右側にKPIの数値が表示されたことが分かります。

STEP7:グラフを描画する
このSTEPでは、右側パネルに「月別売上」と「カテゴリ別売上」のグラフを追加します。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from datetime import datetime
from pathlib import Path
import pandas as pd
# STEP7追加
import matplotlib
from matplotlib import font_manager as fm
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
# STEP7追加
def set_japanese_font() -> None:
preferred = [
"Noto Sans CJK JP",
"Noto Sans JP",
"IPAexGothic",
"IPAGothic",
"TakaoGothic",
"Yu Gothic",
"Meiryo",
"Hiragino Sans",
"MS Gothic",
]
installed = {f.name for f in fm.fontManager.ttflist}
for name in preferred:
if name in installed:
matplotlib.rcParams["font.family"] = name
break
matplotlib.rcParams["axes.unicode_minus"] = False
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
def parse_date(s: str) -> datetime | None:
s = (s or "").strip()
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
return None
class App(tk.Tk):
def __init__(self):
super().__init__()
set_japanese_font() # STEP7追加
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
self.df = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
kpi = ttk.Frame(right)
kpi.grid(row=0, column=0, columnspan=2, sticky="ew")
for i in range(4):
kpi.columnconfigure(i, weight=1)
self.kpi_sales = self._kpi(kpi, 0, "総売上", "— 円")
self.kpi_orders = self._kpi(kpi, 1, "注文数", "— 件")
self.kpi_qty = self._kpi(kpi, 2, "販売数量", "— 個")
self.kpi_aov = self._kpi(kpi, 3, "平均注文単価", "— 円")
# STEP7追加
lf1 = ttk.LabelFrame(right, text="月別売上", padding=6)
lf1.grid(row=1, column=0, sticky="nsew", padx=(0, 6), pady=(10, 0))
lf2 = ttk.LabelFrame(right, text="カテゴリ別売上", padding=6)
lf2.grid(row=1, column=1, sticky="nsew", padx=(6, 0), pady=(10, 0))
self.fig_month = Figure(figsize=(5, 3), dpi=100)
self.ax_month = self.fig_month.add_subplot(111)
self.canvas_month = FigureCanvasTkAgg(self.fig_month, master=lf1)
self.canvas_month.get_tk_widget().pack(fill="both", expand=True)
self.fig_cat = Figure(figsize=(5, 3), dpi=100)
self.ax_cat = self.fig_cat.add_subplot(111)
self.canvas_cat = FigureCanvasTkAgg(self.fig_cat, master=lf2)
self.canvas_cat.get_tk_widget().pack(fill="both", expand=True)
self._draw_empty()
def _kpi(self, parent: ttk.Frame, col: int, title: str, value: str) -> ttk.Label:
box = ttk.LabelFrame(parent, text=title, padding=8)
box.grid(row=0, column=col, sticky="ew", padx=6)
lbl = ttk.Label(box, text=value, font=("", 14, "bold"))
lbl.pack(anchor="w")
return lbl
# STEP7追加
def _draw_empty(self) -> None:
self.ax_month.clear()
self.ax_month.set_title("データ未読み込み")
self.canvas_month.draw()
self.ax_cat.clear()
self.ax_cat.set_title("データ未読み込み")
self.canvas_cat.draw()
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
try:
df = read_csv_safely(path)
except Exception as e:
messagebox.showerror("読み込みエラー", f"{e}")
return
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
df = df.copy()
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
self.file_label.config(text=Path(path).name)
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
def apply_filters(self) -> None:
if self.df is None:
messagebox.showinfo("未読み込み", "先にCSVを読み込んでください")
return
start_dt = parse_date(self.start_entry.get())
end_dt = parse_date(self.end_entry.get())
if start_dt is None or end_dt is None:
messagebox.showerror("日付エラー", "YYYY-MM-DD 形式で入力してください")
return
if start_dt > end_dt:
messagebox.showerror("日付エラー", "開始日が終了日より後です")
return
idx = self.lb_pref.curselection()
prefs = [self.lb_pref.get(i) for i in idx] if idx else []
if not prefs:
messagebox.showerror("選択エラー", "都道府県を1つ以上選択してください")
return
df = self.df
mask = (
(df["order_date"] >= pd.Timestamp(start_dt))
& (df["order_date"] <= pd.Timestamp(end_dt))
& (df["prefecture"].isin(prefs))
)
f = df.loc[mask].copy()
total_sales = float(f["sales_amount"].sum()) if len(f) else 0.0
total_orders = int(f["order_id"].nunique()) if len(f) else 0
total_qty = float(f["quantity"].sum()) if len(f) else 0.0
aov = (total_sales / total_orders) if total_orders else 0.0
self.kpi_sales.config(text=f"{total_sales:,.0f} 円")
self.kpi_orders.config(text=f"{total_orders:,} 件")
self.kpi_qty.config(text=f"{total_qty:,.0f} 個")
self.kpi_aov.config(text=f"{aov:,.0f} 円")
# STEP7追加
self._draw_charts(f)
# STEP7追加
def _draw_charts(self, f: pd.DataFrame) -> None:
self.ax_month.clear()
if len(f) == 0:
self.ax_month.set_title("該当データなし")
else:
tmp = f.copy()
tmp["year_month"] = tmp["order_date"].dt.to_period("M").astype(str)
monthly = tmp.groupby("year_month", as_index=False)["sales_amount"].sum().sort_values("year_month")
self.ax_month.plot(monthly["year_month"], monthly["sales_amount"], marker="o")
self.ax_month.set_title("月別売上")
self.ax_month.tick_params(axis="x", rotation=45)
self.ax_month.ticklabel_format(style="plain", axis="y")
self.fig_month.tight_layout()
self.canvas_month.draw()
self.ax_cat.clear()
if len(f) == 0:
self.ax_cat.set_title("該当データなし")
else:
cat = f.groupby("category", as_index=False)["sales_amount"].sum().sort_values("sales_amount", ascending=False)
self.ax_cat.bar(cat["category"], cat["sales_amount"])
self.ax_cat.set_title("カテゴリ別売上")
self.ax_cat.tick_params(axis="x", rotation=45)
self.ax_cat.ticklabel_format(style="plain", axis="y")
self.fig_cat.tight_layout()
self.canvas_cat.draw()
def reset_filters(self) -> None:
return
if __name__ == "__main__":
App().mainloop()
手順①:matplotlibをインポートする
import matplotlib
from matplotlib import font_manager as fm
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
「matplotlib」はグラフ描画のライブラリです。
「FigureCanvasTkAgg」を使うと、「matplotlib」の図をTkinterのパーツとして表示できます。
手順②:日本語フォント設定を追加する
def set_japanese_font() -> None:
...
グラフの日本語が文字化けしやすいため、フォント設定を行います。
この関数は「使える日本語フォントを探して設定する」役割です。
「__init__」の最初で「set_japanese_font()」を呼ぶことで、アプリ起動時に設定が反映され、文字化けしにくくなります。
手順③:パネルにグラフ枠とキャンバスを用意する
lf1 = ttk.LabelFrame(right, text="月別売上", padding=6)
lf2 = ttk.LabelFrame(right, text="カテゴリ別売上", padding=6)
self.fig_month = Figure(...)
self.ax_month = self.fig_month.add_subplot(111)
self.canvas_month = FigureCanvasTkAgg(self.fig_month, master=lf1)
- LabelFrame
- グラフの枠
- Figure
- グラフ全体の入れ物
- ax
- 線や棒を描く座標
- canvas
- FigureをTkinterに表示するためのパーツ
手順④:フィルタ結果から「月別」「カテゴリ別」を集計して描画する
tmp = f.copy()
tmp["year_month"] = tmp["order_date"].dt.to_period("M").astype(str)
monthly = tmp.groupby("year_month", as_index=False)["sales_amount"].sum().sort_values("year_month")
self.ax_month.plot(monthly["year_month"], monthly["sales_amount"], marker="o")
self.ax_month.set_title("月別売上")
self.ax_month.tick_params(axis="x", rotation=45)
self.ax_month.ticklabel_format(style="plain", axis="y")月別は、日付から「年月」を作り「groupby」で合計を出します。
カテゴリ別は「category」で「groupby」により合計を出し、棒グラフにします。
「ticklabel_format(style=”plain”, axis=”y”)」により、1e6のような表示を抑え、通常の数値にしています。
手順⑤:データ未読み込み時の表示を整える
def _draw_empty(self) -> None:
self.ax_month.set_title("データ未読み込み")
最初はCSVが無いので、グラフが空になります。
そこで「データ未読み込み」と表示しておくと、画面が真っ白にならず、状態が分かりやすくなります。
これで実行するとグラフが表示されるようになりました。
見た目はこれで整いましたね。

STEP8:リセット処理を作る
このSTEPでは、「リセット」ボタンを押したときに、期間をCSVの最小日付・最大日付に戻し、都道府県を全選択に戻したうえで再集計するようにします。
最期のSTEPです!あと少し頑張りましょう。
from __future__ import annotations
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from datetime import datetime
from pathlib import Path
import pandas as pd
import matplotlib
from matplotlib import font_manager as fm
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
REQUIRED_COLUMNS = [
"order_id",
"order_date",
"quantity",
"sales_amount",
"category",
"prefecture",
]
def set_japanese_font() -> None:
preferred = [
"Noto Sans CJK JP",
"Noto Sans JP",
"IPAexGothic",
"IPAGothic",
"TakaoGothic",
"Yu Gothic",
"Meiryo",
"Hiragino Sans",
"MS Gothic",
]
installed = {f.name for f in fm.fontManager.ttflist}
for name in preferred:
if name in installed:
matplotlib.rcParams["font.family"] = name
break
matplotlib.rcParams["axes.unicode_minus"] = False
def parse_date(s: str) -> datetime | None:
s = (s or "").strip()
if not s:
return None
try:
return datetime.strptime(s, "%Y-%m-%d")
except ValueError:
return None
def read_csv_safely(path: str) -> pd.DataFrame:
for enc in ["utf-8-sig", "utf-8", "cp932"]:
try:
return pd.read_csv(path, encoding=enc)
except Exception:
pass
return pd.read_csv(path)
class App(tk.Tk):
def __init__(self):
super().__init__()
set_japanese_font()
self.title("売上ダッシュボード")
self.geometry("1100x650")
self.minsize(1000, 600)
# STEP8変更
self.df: pd.DataFrame | None = None
self._build()
def _build(self) -> None:
self.columnconfigure(0, weight=0)
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)
left = ttk.Frame(self, padding=10)
left.grid(row=0, column=0, sticky="nsw")
right = ttk.Frame(self, padding=10)
right.grid(row=0, column=1, sticky="nsew")
right.columnconfigure(0, weight=1)
right.columnconfigure(1, weight=1)
row = ttk.Frame(left)
row.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(row, text="CSVを開く", command=self.open_csv).pack(side="left")
self.file_label = ttk.Label(row, text="未選択", width=26)
self.file_label.pack(side="left", padx=8)
lf_date = ttk.LabelFrame(left, text="期間(YYYY-MM-DD)", padding=8)
lf_date.grid(row=1, column=0, sticky="ew", pady=(0, 10))
ttk.Label(lf_date, text="開始").grid(row=0, column=0, sticky="w")
self.start_entry = ttk.Entry(lf_date, width=14)
self.start_entry.grid(row=0, column=1, sticky="w", padx=(6, 10))
ttk.Label(lf_date, text="終了").grid(row=0, column=2, sticky="w")
self.end_entry = ttk.Entry(lf_date, width=14)
self.end_entry.grid(row=0, column=3, sticky="w", padx=(6, 0))
lf_pref = ttk.LabelFrame(left, text="都道府県(複数選択)", padding=8)
lf_pref.grid(row=2, column=0, sticky="ew")
self.lb_pref = tk.Listbox(lf_pref, selectmode="extended", height=10, exportselection=False)
self.lb_pref.pack(fill="both", expand=True)
btns = ttk.Frame(left)
btns.grid(row=3, column=0, sticky="ew", pady=(10, 0))
ttk.Button(btns, text="集計", command=self.apply_filters).pack(side="left")
ttk.Button(btns, text="リセット", command=self.reset_filters).pack(side="left", padx=8)
kpi = ttk.Frame(right)
kpi.grid(row=0, column=0, columnspan=2, sticky="ew")
for i in range(4):
kpi.columnconfigure(i, weight=1)
self.kpi_sales = self._kpi(kpi, 0, "総売上", "— 円")
self.kpi_orders = self._kpi(kpi, 1, "注文数", "— 件")
self.kpi_qty = self._kpi(kpi, 2, "販売数量", "— 個")
self.kpi_aov = self._kpi(kpi, 3, "平均注文単価", "— 円")
lf1 = ttk.LabelFrame(right, text="月別売上", padding=6)
lf1.grid(row=1, column=0, sticky="nsew", padx=(0, 6), pady=(10, 0))
lf2 = ttk.LabelFrame(right, text="カテゴリ別売上", padding=6)
lf2.grid(row=1, column=1, sticky="nsew", padx=(6, 0), pady=(10, 0))
self.fig_month = Figure(figsize=(5, 3), dpi=100)
self.ax_month = self.fig_month.add_subplot(111)
self.canvas_month = FigureCanvasTkAgg(self.fig_month, master=lf1)
self.canvas_month.get_tk_widget().pack(fill="both", expand=True)
self.fig_cat = Figure(figsize=(5, 3), dpi=100)
self.ax_cat = self.fig_cat.add_subplot(111)
self.canvas_cat = FigureCanvasTkAgg(self.fig_cat, master=lf2)
self.canvas_cat.get_tk_widget().pack(fill="both", expand=True)
self._draw_empty()
def _kpi(self, parent: ttk.Frame, col: int, title: str, value: str) -> ttk.Label:
box = ttk.LabelFrame(parent, text=title, padding=8)
box.grid(row=0, column=col, sticky="ew", padx=6)
lbl = ttk.Label(box, text=value, font=("", 14, "bold"))
lbl.pack(anchor="w")
return lbl
def _draw_empty(self) -> None:
self.ax_month.clear()
self.ax_month.set_title("データ未読み込み")
self.canvas_month.draw()
self.ax_cat.clear()
self.ax_cat.set_title("データ未読み込み")
self.canvas_cat.draw()
def open_csv(self) -> None:
path = filedialog.askopenfilename(
title="売上CSVを選択",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not path:
return
try:
df = read_csv_safely(path)
except Exception as e:
messagebox.showerror("読み込みエラー", f"{e}")
return
missing = [c for c in REQUIRED_COLUMNS if c not in df.columns]
if missing:
messagebox.showerror("列不足", "不足列:\n" + "\n".join(missing))
return
df = df.copy()
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
df["quantity"] = pd.to_numeric(df["quantity"], errors="coerce")
df["sales_amount"] = pd.to_numeric(df["sales_amount"], errors="coerce")
df = df.dropna(subset=["order_date", "sales_amount"])
self.df = df
self.file_label.config(text=Path(path).name)
dmin = df["order_date"].min().strftime("%Y-%m-%d")
dmax = df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
prefs = sorted(df["prefecture"].dropna().unique().tolist())
self.lb_pref.delete(0, "end")
for p in prefs:
self.lb_pref.insert("end", p)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
# STEP8変更(メソッドの記載箇所ごと変更)
def reset_filters(self) -> None:
if self.df is None:
return
dmin = self.df["order_date"].min().strftime("%Y-%m-%d")
dmax = self.df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
self.apply_filters()
def apply_filters(self) -> None:
if self.df is None:
messagebox.showinfo("未読み込み", "先にCSVを読み込んでください")
return
start_dt = parse_date(self.start_entry.get())
end_dt = parse_date(self.end_entry.get())
if start_dt is None or end_dt is None:
messagebox.showerror("日付エラー", "YYYY-MM-DD 形式で入力してください")
return
if start_dt > end_dt:
messagebox.showerror("日付エラー", "開始日が終了日より後です")
return
idx = self.lb_pref.curselection()
prefs = [self.lb_pref.get(i) for i in idx] if idx else []
if not prefs:
messagebox.showerror("選択エラー", "都道府県を1つ以上選択してください")
return
df = self.df
mask = (
(df["order_date"] >= pd.Timestamp(start_dt))
& (df["order_date"] <= pd.Timestamp(end_dt))
& (df["prefecture"].isin(prefs))
)
f = df.loc[mask].copy()
total_sales = float(f["sales_amount"].sum()) if len(f) else 0.0
total_orders = int(f["order_id"].nunique()) if len(f) else 0
total_qty = float(f["quantity"].sum()) if len(f) else 0.0
aov = (total_sales / total_orders) if total_orders else 0.0
self.kpi_sales.config(text=f"{total_sales:,.0f} 円")
self.kpi_orders.config(text=f"{total_orders:,} 件")
self.kpi_qty.config(text=f"{total_qty:,.0f} 個")
self.kpi_aov.config(text=f"{aov:,.0f} 円")
self._draw_charts(f)
def _draw_charts(self, f: pd.DataFrame) -> None:
self.ax_month.clear()
if len(f) == 0:
self.ax_month.set_title("該当データなし")
else:
tmp = f.copy()
tmp["year_month"] = tmp["order_date"].dt.to_period("M").astype(str)
monthly = tmp.groupby("year_month", as_index=False)["sales_amount"].sum().sort_values("year_month")
self.ax_month.plot(monthly["year_month"], monthly["sales_amount"], marker="o")
self.ax_month.set_title("月別売上")
self.ax_month.tick_params(axis="x", rotation=45)
self.ax_month.ticklabel_format(style="plain", axis="y")
self.fig_month.tight_layout()
self.canvas_month.draw()
self.ax_cat.clear()
if len(f) == 0:
self.ax_cat.set_title("該当データなし")
else:
cat = f.groupby("category", as_index=False)["sales_amount"].sum().sort_values("sales_amount", ascending=False)
self.ax_cat.bar(cat["category"], cat["sales_amount"])
self.ax_cat.set_title("カテゴリ別売上")
self.ax_cat.tick_params(axis="x", rotation=45)
self.ax_cat.ticklabel_format(style="plain", axis="y")
self.fig_cat.tight_layout()
self.canvas_cat.draw()
if __name__ == "__main__":
App().mainloop()
手順①:CSVを読み込んでいるかチェックする
if self.df is None:
return
リセットは「元に戻す」操作なので、そもそもCSVを読んでいないと戻すことができません。
そのため、CSV未読み込みなら何もせず終えるようにしています。
手順②:CSVの最小日付・最大日付に期間を戻す
dmin = self.df["order_date"].min().strftime("%Y-%m-%d")
dmax = self.df["order_date"].max().strftime("%Y-%m-%d")
self.start_entry.delete(0, "end")
self.end_entry.delete(0, "end")
self.start_entry.insert(0, dmin)
self.end_entry.insert(0, dmax)
ここは「open_csv」のときと同じ考え方です。
「期間をCSV内で取れる範囲の全体」に戻します。
- delete(0, “end”)
- 入力欄を空にする
- insert(0, …)
- 先頭に文字列を入れる
手順③:都道府県を全選択に戻す
if self.lb_pref.size() > 0:
self.lb_pref.select_set(0, "end")
「select_set(0, “end”)」は、Listboxの先頭から最後までを選択状態にします。
これにより、最初の状態と同じく「全都道府県が対象」になります。
手順④:リセット後、画面を更新する
self.apply_filters()
リセット後は画面の数字とグラフも元に戻しています。
手順⑤:変数を整える
self.df: pd.DataFrame | None = None
このアプリは、起動直後はCSVを読み込んでいません。そのため「self.dfは最初「None」です。
そしてCSVを開くと「DataFrame」に置き換わります。
この「未読み込み」と「読み込み済み」を同じ変数で管理するために「pd.DataFrame | None」という書き方になります。
実際に実行すると、リセットが有効になっていることが分かります。

完成
以上でPythonで作る「売上ダッシュボード」の完成です。
ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。
都道府県以外の選択項目を増やしたり、他のデータも対応できるようにすると面白いですね。

お疲れさまでした。
