Python未導入環境においてPandasGUIとpandas-profilingを使用可能なEDAツール『Pandas Anywhere』を作ってみた

# はじめに
この度、PythonライブラリであるPandasGUIとpandas-profilingを、Pythonをインストールしていない環境においても使用できるEDAツール**『Pandas Anywhere』**を作成したので公開します。本ツールを使用することで、**誰でもどこでも簡易にビッグデータ※の分析が可能となります。**

※本記事でいうビッグデータとはMicrosoft ExcelやAccessで扱うのが困難な大容量データを指します。

![pandas_anywhere.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/6c99dfe2-3567-307d-e9b8-8235acfa04a1.png)


# 作成の動機
前回書いた記事[「Python初学者のためのPandas100本ノック」](https://qiita.com/kunishou/items/bd5fad9a334f4f5be51c)では、知り合いにPython・機械学習を始める人が増えてきていることから、そのような人たちの学習を支援するコンテンツを作成しました。私の周りでは、このコンテンツをきっかけにPython、pandasを用いたデータ分析ができるようになった人もおり、目的通りの効果があったと思います。

しかし、中にはPythonの習得に時間をかけられない人もいると思います。そのような人がビッグデータを分析する必要が生じた場合、読み込めるファイル容量に制限があるMicrosoft ExcelやAccessでは分析は困難です。また、Tableauのようなデータ分析ツールが使えればいいですが、ライセンスを購入するにもお金がかかります。そこで、このような**非Pythonユーザーでもビッグデータを簡易に分析できるツールがあると良いと考え、今回のツールを作成しました。**

非Pythonユーザー向けのツールですが、**Pythonユーザーにおいても、手元のデータをいちいちコードを書いてEDA（探索的データ分析）することが面倒に感じるときがあるかと思います。そのような人にとっても、今回のツールは大いに役立つと考えています。**

# Pandas Anywhereの概要
* ツールには、PandasGUIとpandas-profilingの機能が搭載
* exeファイルからツールを起動
* GUI上から分析したいデータファイル（csv、txtファイル）を読み込み、GUI上の「PandasGUI」、「pandas-profiling」ボタンをクリックすることで各機能が呼び出される

※ GUIはPython Tkinterフレームワークで実装、exe化はPyinstallerを使用


![joinesgifimage-diet.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/8ad65774-205f-b508-deff-3388a41d9b3c.gif)


# PandasGUIとは
PandasGUIはPandasデータフレームをGUI上で分析するためのライブラリです。エクセルライクなインタフェース上でデータ操作が可能です。主な機能としては以下の5つがあります。

* カラムの昇順・降順の並べ替え
* クエリ式を用いたカラムのフィルタリング
* 統計要約の表示
* データのチャート表示（グラフ表示）
* ピボット

詳細については下記URLの解説が分かりやすいです。

[PandasGUI：グラフィカルユーザーインターフェイスを使用したPandasデータフレームの分析](https://ichi.pro/pandasgui-gurafyikaruyu-za-inta-fyeisu-o-shiyoshita-pandas-de-ta-fure-mu-no-bunseki-60429136395037)

以下ではクエリ式を用いたフィルタリングのクエリ例を紹介します。

## カラムのフィルタリングのクエリ例
フィルタリングのクエリ例を紹介しますが、まずFiltersタブを以下のようにドラッグし右側に配置しておくと操作がしやすいです（私個人の好みなので任意で実施して下さい）。
![joinesgifimage-9184044.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/ac43c8b2-ce8f-4dfb-3071-66e7f4c28358.gif)
**※ クエリ例はTitanicの乗客データを例に説明します**

Filetersタブ内の「Enter query expression」欄に以下のクエリを入力し、「Add Filter」ボタンをクリックすることでクエリに従いデータのフィルタリングが実施されます。

### 乗客の性別を女性のみに絞る
```python
sex == 'female'
```
文字列の抽出時には文字を一重引用符' 'もしくは二重引用符" "で囲む
### 乗客の性別を女性以外に絞る
```python
sex != 'female'
```
### 乗客の年齢を20歳以上に絞る
```python
age >= 20
```
### 乗客の年齢を20歳以上かつ40歳以下に絞る
```python
age >= 20 & age <= 40
or
age.between(20,40)
```
AND条件のときは「&」を使用する
### 乗客の年齢が20歳以上または性別が男性のレコードに絞る
```python
age <= 20 | sex == 'male'
```
OR条件のときは「|」を使用する
### 乗客の年齢が空白のレコードに絞る
```python
age.isnull()
```
### 乗客の年齢が空白でないレコードに絞る
```python
~age.isnull()
```
頭に「~」を付加するとnotの意味になる
### 乗客のチケットクラスを「1」、「3」に絞る
```python
pclass.isin([1,3])
```
頭に「~」を付与すれば「1」、「3」を含まないレコードに絞り込める
### 乗客の名前に「Mr.」を含むレコードに絞る
```python
name.str.contains('Mr.')
```

ここまで、フィルタリングする際のクエリの一例を紹介しました。
他にもフィルタリングするためのクエリ表現はありますので必要時に調べてみて下さい。

なお、フィルタリングした状態でファイル出力したい場合は、PandasGUIの画面左上の「Edit」→「Export」にて出力が可能です。

## チャートによるデータ可視化
PnadasGUIにはPlotlyベースのインタラクティブチャートが12種類用意されています。チャートを用いることで読み込んだデータを簡易に可視化できます。以下の動画ではHistgram、Box、Scatter 3D、Word Cloudを表示しています。

![joinesgifimage-2889575.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/4ff0c351-8343-b51c-0bf9-8608d3a6f9ed.gif)

# pandas-profilingとは
pandas-profilingとは、pandasデータフレームのプロファイリング結果をまとめて出力してくれるライブラリです。通常、与えられたデータをEDA（基本統計量、カラムの欠損値有無やヒストグラム、カラム間の相関分布などの確認）するためにはpandasのメソッドを用いて何行にも渡るコードを書かなくてはいけません。このライブラリを使用することで1～2行程度のコードで大まかなEDAを実行してくれます。

Kaggle、Signateなどの機械学習コンペにて初手の大まかなEDAでpandas-profilingを使っている人も多いのではないでしょうか。

![profiling.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/692165/4846cb83-e294-bfa9-7993-fec3ed8992a2.gif)


pandas-profilingの出力結果項目の詳細については下記URLの解説を参考にして下さい。

[【便利！】pandas-profiling（Python）による簡易データ解析](https://qiita.com/ryo111/items/705347799a984acd5d08#:~:text=pandas%2Dprofiling%20%E3%81%AF%E3%80%81pandas%20%E3%81%AE,%E3%81%A7%E3%81%8D%E9%9D%9E%E5%B8%B8%E3%81%AB%E4%BE%BF%E5%88%A9%E3%81%A7%E3%81%99%E3%80%82)

#作成において苦労した点
後半に本ツールの実装コードを掲載していますが、見ていただけると分かる通り、コード自体はシンプルなものになっています。となると、誰でも簡単にこのツールを作成できるのでは？と思われますが、今回、**コードをexe化するために使用したPyinstallerというライブラリが非常に癖があり苦労しました。**十数行のコードのexe化であればPyinstallerもエラーは出しませんが、今回のPandasGUIやpandas-profilingのような作りの複雑なライブラリを同封しようとすると山のように多くのエラーをはき出します。

### **トータルで100個以上はエラーがあったと思います（爆）**

コンソール画面やJupyter NotebookなどからPythonコードを実行する場合と、exe化されたコードを実行するのとでは内部の挙動が異なることが原因です。

ここでは細かく書きませんが、今回のツール作成でPyinstallerのエラー対処法もノウハウとして多く得られたので、そのうちPyinstallerの処方箋的な記事も書いてみたいと思います。

# 使用方法
**※ 本ツールはWindows10環境のみで動作可能です**

* [Github](https://github.com/kunishou/Pandas_Anywhere/releases/)よりダウンロードしたexeファイルをダブルクリック（起動に30秒ほどかかります）
* GUI上の「Load File」ボタンをクリックし、読み込ませたいファイルを選択します。なお、読み込み可能なファイル形式は、csvもしくはtxt（カンマ区切り）のみになります。
* ファイル読み込みの設定としてEncoding format（utf-8もしくはcp932）、Line number to start reading（読み込み開始行数）の指定があります。読み込み開始行数については、例えば、最初の2行が空白で3行目からデータが始まっているファイルの場合は「3」を指定して下さい。
* ファイルの読み込み完了後に、GUI上の「PandasGUI」もしくは「pandas-profiling」のボタンをクリックすることで各機能を呼び出します。
* pandas-profilingでは、プロファイリングが完了するとexeファイルと同じディレクトリにプロファイリング結果がHTMLファイルで出力されます。このファイルをクリックするとブラウザ上で内容を確認することができます。

**※Windowsセキュリティにより、exeファイル実行時に「PCはWindowsによって保護されました」という青いポップアップが表示される場合は、ポップアップ内の「詳細情報」をクリックすると「実行する」ボタンが表示→クリックでツールが起動されます。**

#ツールの留意点
本ツールには以下の留意点があります。

* ファイル容量が大きい（およそ280MB)
ツール機能に必要なPythonの各種ライブラリが同封されており、それらのライブラリ容量がツール全体の容量に影響しています。**Anaconda（約5GB）をインストールするよりかは小さいほうだと思って許してください。**

* ツールの起動が遅い（およそ30秒）
exe化に使用したPyinstallerの影響ではありますが、exeファイルの起動が遅く、起動に30秒程度かかります（スペックの低いPCだとさらにもう少しかかると思います）。

* pandas-profiling後にツールが閉じる
現状版だとpandas-profilingでreportファイルの書き出しが完了するとツール自体が終了してしまいます。今後改善します。

# ダウンロード
ツールはGitHubよりダウンロードできます。Assets内の「pandas_anywhere_xxx.exe」をクリックすることでダウンロードが開始します（テスト読み込み用にTitanicの乗客データも置いておきます）。

https://github.com/kunishou/Pandas_Anywhere/releases/

# 使用範囲・免責事項
* 使用範囲
個人・法人を問わず誰でも無償で使用可能

* ライセンス
GNU GENERAL PUBLIC LICENSE Version 3
（ツールに同封しているPandasGUI、pandas-profiling等のサードパーティライブラリの著作権については各ライブラリの作成者に帰属します。また、ライセンスに関しても各ライブラリのそれに従います。）

* 免責事項
本ツールの使用により発生したいかなる損害についても作成者は一切の責任を負いません。

# 参考：実装コード
```python
import pandas as pd
import tkinter as tk
import tkinter.ttk as ttk
import tkinter.filedialog as filedialog
from tkinter import messagebox
import os
import re
import datetime

import pandas_profiling as pdp
from multiprocessing import Process, freeze_support
from pandasgui import show

now = datetime.datetime.now()

f = 0 #ファイルが読み込まれたかのフラグ(0:未読み込み 1:読み込み済み)

#アプリケーション本体
class SearchWindow(tk.Frame):
    
    def __init__(self, master=None, parent=None):
        super().__init__(master)
        self.master = master
        self.master.geometry("500x240")
        #self.master.resizable(width=0, height=0) #windowサイズを固定
        
        self.master.title("Pandas Anywhere Ver1.0.0")
        self.pack()
        self.filePath = tk.StringVar()
        self.create_widgets()

    def create_widgets(self):
        self.pw_main = ttk.PanedWindow(self.master, orient="vertical")
        self.pw_main.pack(expand=True, fill=tk.BOTH, side="left")
        
        self.pw_top = ttk.PanedWindow(self.pw_main, orient="vertical", height=40)
        self.pw_main.add(self.pw_top)
        
        self.pw_middle1 = ttk.PanedWindow(self.pw_main, orient="vertical", height=50)
        self.pw_main.add(self.pw_middle1)
        
        self.pw_middle2 = ttk.PanedWindow(self.pw_main, orient="vertical", height=50)
        self.pw_main.add(self.pw_middle2)
        
        self.pw_bottom = ttk.PanedWindow(self.pw_main, orient="vertical", height=50)
        self.pw_main.add(self.pw_bottom)

        self.create_input_frame(self.pw_top)
        self.create_input_frame2(self.pw_middle1)
        self.create_input_frame3(self.pw_middle2)
        self.create_input_frame4(self.pw_bottom)
        
    def create_input_frame(self, parent):        
        fm_input = ttk.Frame(parent, )
        parent.add(fm_input)
        
        self.rb_var = tk.StringVar()
        self.rb_var.set('cp932')
        
        lbl_keyword = ttk.Label(fm_input, text="■ Select encoding format :", width=25)
        lbl_keyword.grid(row=0,column=0, padx=5, pady=10)
        
        self.rb1 = ttk.Radiobutton(fm_input, text="cp932",variable=self.rb_var,value='cp932')
        self.rb1.grid(row=0,column=1, padx=5, pady=10,sticky=tk.W)
        
        self.rb2 = ttk.Radiobutton(fm_input, text="utf-8",variable=self.rb_var,value='utf-8')
        self.rb2.grid(row=0,column=2, padx=5, pady=10,sticky=tk.W)

    def create_input_frame2(self, parent):        
        fm_input = ttk.Frame(parent, )
        parent.add(fm_input)
        
        vcmd1 = (self.master.register(self.validate_text), '%P','%S','%W') #入力値検証の変数
        
        self.entry = tk.IntVar()
        self.entry.set(1)
        
        lbl_keyword2 = ttk.Label(fm_input, text="■ Line number to start reading :", width=25)
        lbl_keyword2.grid(row=0,column=0, padx=5, pady=10,sticky=tk.W)
        
        self.ent_keyword = ttk.Entry(fm_input, justify="left", textvariable=self.entry,width=10, validate='key', validatecommand=vcmd1)
        self.ent_keyword.grid(row=0, column=1, padx=5, pady=10,sticky=tk.W)        

    def create_input_frame3(self, parent):
        fm_input = ttk.Frame(parent, )
        parent.add(fm_input)
        filepathEntry = ttk.Entry(fm_input,textvariable=self.filePath,width=40)
        filepathEntry.grid(row=0,column=0, padx=10, pady=10) 
        filepathButton = ttk.Button(fm_input,text=" Load File ",command=self.openFileDialog)
        filepathButton.grid(row=0,column=1, padx=5, pady=10)     
 
    def create_input_frame4(self, parent):
        fm_input = ttk.Frame(parent, )
        parent.add(fm_input)
        
        style = ttk.Style()
        style.configure("blue.TButton",background='SteelBlue1')
        
        outputButton = ttk.Button(fm_input,text="PandasGUI",style="blue.TButton",command=self.show_gui)
        outputButton.grid(row=1,column=1, padx=10, pady=10) 
        outputButton2 = ttk.Button(fm_input,text=" Pandas-Profilng ",style="blue.TButton",command=self.profiling)
        outputButton2.grid(row=1,column=2, padx=5, pady=10)  

    def set_data(self):
        self.enc = self.rb_var.get()
        self.skiprows = int(self.ent_keyword.get()) - 1
        print('Encoding:{}'.format(self.enc))
        print('Line number to start reading:{}'.format(self.skiprows+1))
        
    def openFileDialog(self): #ファイルを読み込む
        """
        ファイルダイアログを開く

        """
        
        self.set_data() #エンコード形式、読み込み開始行を呼び出し
        
        file  = filedialog.askopenfilename(filetypes=[("csv", "*.csv"),("txt","*.txt")]);
        
        self.file = file
        self.filePath.set(file)
         
        if file is None:         
            messagebox.showerror("load_file","File loading failure")
        else:
            if self.file[-3:] == 'csv':
                df = pd.read_csv(file, encoding=self.enc,skiprows=self.skiprows)
                #df = pd.read_csv(file, encoding='cp932')
            else:
                df = pd.read_table(file, encoding=self.enc,skiprows=self.skiprows)
                #df = pd.read_table(file, encoding='cp932')
            
            self.data = df
            
            global f
        
            f = 1 #ファイル読み込みフラグ            
            
            messagebox.showinfo("Load_File","File is loaded")

    #PnadasGUI
    def show_gui(self):
        global f
        
        if f == 0: #ファイルが読み込まれていない場合      
            messagebox.showerror("Warning","File is not loading")
        else:
            gui = show(self.data)

    #pandas-profiling
    def profiling(self):
        global f
        
        if f == 0: #ファイルが読み込まれていない場合      
            messagebox.showerror("Warning","File is not loading")
        else:
            profile = pdp.ProfileReport(self.data)
            profile.to_file("profile_report" + now.strftime('_%Y%m%d_%H%M') + ".html")
            messagebox.showinfo("Pandas-Profilng","End of profiling")
            
    def validate_text(self, after, newtext, widget): #入力バリデーションの設定
        # 正規表現で入力された値が半角数字であるか判定
        if re.match(re.compile('[0-9]+'), newtext):
            # 入力された数字が1以上の場合にのみTrueを返し、数字が入力出来る
            if len(after) <= 2: 
                if after != '0':
                    return True
            return False
        # 該当しなかった場合にはFalseが返され、値はentryに反映されない
        else:
            return False
            
def main():
    root = tk.Tk()
    app = SearchWindow(master=root)
    app.mainloop()

if __name__ == "__main__":
    freeze_support()
    
    main()
```

# 最後に
本ツールに関してご質問・ご要望があればご連絡下さい（バグもあればご報告いただけると助かります）。

機会があれば本ツールにPyCaret等も組み込んで、EDAから機械学習までをスタンドアロンでできる自作AutoML的なアプリケーションも作ってみたいです（また、Pyinstallerのエラー処理が半端なさそうですが）。

**（追記）**
デスクトップアプリケーションではないですが、PyCaretを組み込んだWEBアプリケーションをStreamlitにて作成しました。興味があれがあればこちらの記事も参考にして下さい。

https://qiita.com/kunishou/items/68d676410f4878c568aa


# 更新履歴
 * 2021/6/4：V1.0.1に更新  
・読み込み可能なテキストファイルをtxt（タブ区切り）からtxt（カンマ区切り）へ変更