データ分析の作業をしていると、データを数字で追うのとは別にグラフ等で視覚的にデータの特徴を確認することも多々あると思います。
そういった作業が多く、データの組みあわせを自由に変えながらグラフをすぐに表示できる「Glance」というツールを作ってみました。
GUI作成にはPythonのtkinter
というライブラリを使用しました。
ファイルはこちらにあります。
※Windows環境で使う用に作ったのでMac環境だと多分別途修正が必要です...
概要
読み込んだデータから選択してグラフ表示するだけのシンプルなツールです。
実装した動作は大きく3つあります。
ハマった部分の簡単な紹介
tkinter内での値の受け渡しについて
今回初めてtkinterを使いましたが、詰まったところのひとつとして「ウィジェットに割り当てた関数の返り値を受け取り方がわからない」というところです。
そのためにデータを持たせるためのクラスを定義して、オブジェクトに逐一データを持たせることで受け渡しする必要がありそうでした。
例えば今回のツールの場合、ファイルから読み取るfileselect
という関数がありますが、読み取ったデータフレームはreturnで返すわけではなく、処理の中でオブジェクトに持たせています。
def fileselect():
root = tkinter.Tk()
root.withdraw()
fTyp = [("", "*")]
iDir = os.path.abspath(os.path.dirname(__file__))
model.path = tkinter.filedialog.askopenfilename(filetypes=fTyp,
initialdir=iDir)
suffix = model.path.rsplit('.', 1)[1]
if suffix == 'csv' or suffix == 'pkl':
model.set_data(suffix) #ここでデータを持たせている。
graph.config(state='active')
Entry1.insert(tk.END, model.path)
line_size = columns_list.size()
columns_list.delete(0, line_size)
for column in enumerate(model.data.columns):
columns_list.insert(tk.END, column)
else:
tkinter.messagebox.showinfo(model.path, 'csvかpickleファイルを選択してください。')
グラフを動的に更新する方法
tkinterのウィジェットを関数で操作する場合、
変数のスコープ上関数の中に操作用関数を定義することになります。
今回はグラフを別窓で開くnew_window
も定義していますので、
グラフを更新するredraw
関数の構造は以下のようになります。
def main():
# 処理
def new_window():
def redraw():
# 処理
scale = tk.Scale(command=redraw) # スライドバー
こうすることで、scaleを操作するたびにredraw関数が実行されるようになります。
redraw関数の中は自由でよいですが、matplotlibで表現する内容を変更した後
canvas.draw()
が最後に無いとGUI上に反映されないので気をつけて下さい。
コードを分けられない
これは解決していない問題なのですが、自分の書き方的にはmain()の中に
他の関数を全て定義してしまっているので関数や別ファイルとして外出しすることができませんでした。
(でもこうするしか関数でウィジェットを操作する方法がわからない...)
結果的にmain()が長くなって読みづらくなってしまうのですがどうしたものか...
コード全体
import os
import tkinter as tk
import tkinter.filedialog
import tkinter.messagebox
import japanize_matplotlib
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure
import numpy as np
import pandas as pd
def main():
class model_data:
def __init__(self):
self.data = pd.DataFrame()
self.path = ''
self.columns = []
def set_path(self, file_path):
self.path = file_path
def set_data(self, suffix):
if suffix == 'csv':
self.data = pd.read_csv(self.path)
elif suffix == 'pkl':
self.data = pd.read_pickle(self.path)
def new_window(event):
if any(model.data):
def _quit_sub():
sub_win.destroy()
def _redraw(event):
min_lim = min_scale.get()
max_lim = max_scale.get()
val = []
if min_lim >= max_lim:
return
else:
t = target.iloc[min_lim:max_lim, :]
for index in selection:
label = target.columns[index]
val.append(t[label].max())
val.append(t[label].min())
max_val = max(val)
min_val = min(val)
ax.set_xlim(min_lim, max_lim)
ax.set_ylim(min_val, max_val)
canvas.draw()
selection = columns_list.curselection()
sub_win = tk.Toplevel(root)
sub_win.configure(bg='white')
sub_win.wm_title("Embedding in Tk")
target = model.data.copy()
target.reset_index(drop=True, inplace=True)
fig = Figure(figsize=(10, 4), dpi=100)
ax = fig.add_subplot(111)
if norm_bln.get():
for index in selection:
label = target.columns[index]
max_num = target[label].max()
min_num = target[label].min()
target[label] = (target[label] - min_num) / (max_num - min_num)
ax.plot(target.index,
target[label],
label=label)
else:
for index in selection:
label = target.columns[index]
ax.plot(target.index,
target[label],
label=label)
canvas_frame = tk.Frame(master=sub_win, bg='white')
canvas_frame.pack(side=tk.LEFT)
canvas = FigureCanvasTkAgg(fig,
master=canvas_frame)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP,
fill=tk.BOTH,
expand=1)
control_frame = tk.Frame(master=sub_win)
control_frame.pack(side=tk.RIGHT)
toolbar = NavigationToolbar2Tk(canvas,
canvas_frame)
toolbar.update()
canvas.get_tk_widget().pack(side=tk.TOP,
fill=tk.BOTH,
expand=True)
min_scale = tk.Scale(control_frame,
label='Min',
orient='h',
from_=0,
bg='light blue',
resolution=1,
to=model.data.shape[0]-1,
command=_redraw)
min_scale.pack(anchor=tk.NW)
max_scale = tk.Scale(control_frame,
label='Max',
orient='h',
from_=1,
bg='tan1',
resolution=1,
to=model.data.shape[0],
command=_redraw)
max_scale.set(target.shape[0])
max_scale.pack(anchor=tk.NW)
button = tkinter.Button(master=canvas_frame,
text="Quit",
command=_quit_sub)
button.pack(side=tkinter.BOTTOM)
else:
tkinter.messagebox.showinfo('Glance.py', '先に読み込むデータを選択してください')
def fileselect():
root = tkinter.Tk()
root.withdraw()
fTyp = [("", "*")]
iDir = os.path.abspath(os.path.dirname(__file__))
model.path = tkinter.filedialog.askopenfilename(filetypes=fTyp,
initialdir=iDir)
suffix = model.path.rsplit('.', 1)[1]
if suffix == 'csv' or suffix == 'pkl':
model.set_data(suffix)
graph.config(state='active')
Entry1.insert(tk.END, model.path)
line_size = columns_list.size()
columns_list.delete(0, line_size)
for column in enumerate(model.data.columns):
columns_list.insert(tk.END, column)
else:
tkinter.messagebox.showinfo(model.path, 'csvかpickleファイルを選択してください。')
def _quit(event):
root.quit()
root.destroy()
root = tk.Tk()
model = model_data()
root.title('Glance')
root.geometry('680x300')
frame1 = tk.Frame(bd=3, relief=tk.SUNKEN, pady=5)
frame1.pack(padx=5, pady=5, ipadx=10, ipady=10, side=tk.LEFT)
Static1 = tk.Label(frame1, text='File Path')
Static1.pack()
Entry1 = tk.Entry(frame1, width=80)
Entry1.insert(tk.END, model.path)
Entry1.pack()
Static1 = tk.Label(frame1, text='Column Names')
Static1.pack()
var = tk.StringVar(value=[])
columns_list = tk.Listbox(frame1,
listvariable=var,
width=80,
selectmode='multiple')
scrollbar = tk.Scrollbar(frame1,
command=columns_list.yview)
columns_list.yscrollcommand = scrollbar.set
scrollbar.pack(fill=tk.BOTH,
side=tk.RIGHT)
columns_list.pack()
Button1 = tk.Button(frame1,
text='SelectFile',
width=10,
command=fileselect)
Button1.pack(pady=5)
frame2 = tk.Frame()
frame2.pack(padx=5, pady=5, ipadx=10, ipady=10, fill=tk.BOTH)
norm_bln = tk.BooleanVar()
norm_bln.set('False')
norm = tkinter.Checkbutton(frame2,
variable=norm_bln,
text='Norm')
norm.pack()
graph = tk.Button(frame2,
text='Graph',
width=10,
state='disable')
graph.bind("<Button-1>", new_window)
graph.pack()
frame3 = tk.Frame()
frame3.pack(padx=5, pady=5, ipadx=10, ipady=10, side=tk.BOTTOM)
Button2 = tk.Button(frame3, text='Exit', width=5)
Button2.bind("<Button-1>", _quit)
Button2.pack()
root.mainloop()
if __name__ == "__main__":
main()