chatGPTとLangchainが導く！最先端のノーコード自然言語データ分析ツール

## 概要
この記事では、対話型データセット自然言語クエリと分析ツールについて紹介します。
自然言語処理を活用し、これまで、 __プログラミングを扱ったことがないような人__ でも、データ分析を行えるような環境を実現します。自然言語でのクエリ入力により、直感的かつ簡単にデータ加工や前処理が可能です。対話型のインターフェースで、効果的なEDAを行い、洞察を得ることができます。また、モデル学習用のデータ生成も行えるため、モデル構築までの手前まで実行可能です。このツールを活用して、あなたもデータ分析のエキスパートへと一歩近づきましょう。手軽に始められるこの分析ツールで、データ分析をぜひ体験してください。
と、大げさに表現していますが、ご指摘・ご要望いただけると幸いです。



## はじめに

#### 以前の問題点
[前記事：ChatGPTとLangChainでデータ分析が楽々！自然言語で簡単にデータ探索ツール](https://qiita.com/mquc/items/e3373a7ff6486323152c)

1. 大きな出力結果の表示問題: 
以前のツールでは、クエリの出力結果が大きい場合、正しく表示されず、ユーザーが結果を確認するのが難しい状況でした。
2. データ前処理機能の不足: 
データの前処理に関するクエリに対応できる範囲が限定的であり、ユーザーが求めるデータ前処理を行えない場合がありました。

#### 改善・改良の目的

1. 出力結果の表示問題の解決: 
大きな出力結果でも正しく表示されるように改善し、ユーザーが結果を確認しやすくなることを目指します。

2. データ前処理機能の拡充: 
データ前処理に関するクエリを拡充し、より多くのデータ前処理要求（欠損値補完、除去、重複処置）等に対応できるように改良します。

## デモンストレーション

改善・改良されたツールを使った実例の紹介

百聞は一見にしかずということで、一人のデータ分析官になったつもりで、デモをしてみます。

```
あるデータ分析者は、タイタニック号の乗客データを用いて、生存者の特徴とその要因を調査することにしました。  
彼はnatural-language-analysis-toolツールを使って、以下のような手順で分析を行いました。

データのアップロード: 
彼はまず、CSV形式のタイタニック号の乗客データをツールにアップロードしました。データの情報や要約統計がすぐに表示され、データの概要を確認することができました。

データ前処理: 
年齢に欠損値が多数含まれていることがわかったため、「年齢の欠損値を中央値で補完」というクエリを入力しました。ツールはそのクエリに応じて適切なPythonコードを生成し、データを前処理しました。さらに、deckの欠損が多いので「'deck'をNoDataで埋める」というクエリを使って、データの前処理をしました。

EDAクエリ: 
彼は「生存者の情報は？」というクエリを入力し、その結果を確認しました。さらに、ツールの自動生成されたEDAプロットを用いて、乗客の性別、年齢、客室クラス、運賃などのデータの分布や相関を視覚的に調査しました。

より深い分析のためのデータ準備: 
最後に、彼は「モデル学習用データ生成」ボタンをクリックして、データを学習用とテスト用に分割し、それぞれのCSVファイルをダウンロードしました。これにより、彼は機械学習モデルの構築や評価を行う準備が整いました。
```

![qiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638253/b30fdf2c-4e9d-f7ea-50c7-534dd57819f3.gif)




この実例では、このツールが __「一般的なデータ分析のデータマート作成をプログラミングを使うことなしに汎用的に実施する可能」__ であることが示されています。


## 機能

- CSVファイルのアップロード: 
ユーザーはアプリケーションにCSVファイルをアップロードできます。アップロードされたデータは、データフレームの情報、要約統計、最初の10行が表示されます。

- データ前処理: 
ユーザーは自然言語で前処理クエリを入力し、そのクエリに対応するデータ操作をPandasデータフレームに適用できます。

- 探索的データ分析（EDA）: 
ユーザーは自然言語でEDAクエリを入力し、そのクエリに対応するデータ操作をPandasデータフレームに適用できます。また、欠損値、数値データの分布、カテゴリデータの分布、数値データの相関を表示するEDAプロットも生成されます。

- モデル学習用データ生成: 
ユーザーはデータフレームをトレーニングセットとテストセットに分割し、CSVファイルとしてダウンロードできます。

### GPTとLangchainを用いた実装の詳細
GPTとLangchainを組み合わせて、自然言語で入力されたデータ操作クエリを受け取り、そのクエリに対応するデータ操作をPandasデータフレームに適用する機能を実装しています。具体的な実装は以下の通りです。

```python
from langchain.agents import create_pandas_dataframe_agent
from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate


def translate_to_english(text: str) -> str:
    llm = OpenAI(temperature=0)
    prompt = PromptTemplate(
        input_variables=["text"],
        template="Translate the following Japanese text to English: {text}",
    )
    chain = LLMChain(llm=llm, prompt=prompt)
    return chain.run(text)


def get_agent(df):
    return create_pandas_dataframe_agent(
        OpenAI(temperature=0),
        df,
        verbose=True,
        max_iterations=5,
        early_stopping_method="generate",
    )


def chat_tool_with_pandas_df(df, query):
    translated_query = translate_to_english(query)
    translated_query+="It applies operations directly to the original data without creating a new copy."
    agent = get_agent(df)
    result = agent.run(translated_query)
    print(result)
    if "inplace" in result:
        exec(result)
    else:
        df = eval(result)
    
    return df



```

translate_to_english関数: 
この関数は、入力のテキストを受け取り、GPTを使用して英語に翻訳します。LLMChainを使用してOpenAIのLLM（Large Language Model）にプロンプトを生成し、実行して結果を返します。

get_agent関数: 
この関数は、Pandasデータフレームを受け取り、Langchainのエージェントを生成します。このエージェントは、入力されたクエリを解釈し、データフレームに対する操作を実行します。

chat_tool_with_pandas_df関数: 
この関数は、Pandasデータフレームと入力のクエリを受け取り、翻訳して英語のクエリに変換し、Langchainエージェントを使ってデータフレームに適用します。この関数では、クエリを翻訳した後に、元のデータに直接操作を適用しています。

この実装により、ユーザーは入力でPandasデータフレームに対するクエリを入力し、そのクエリをGPTを使って英語に翻訳し、Langchainを使ってデータフレームに適用することができます。これにより、話者が自然な形式でデータ操作を行うことが可能になります。


## ツール詳細

#### データのアップロード
1. ファイルアップロードUIの作成: dcc.Uploadを使用して、ファイルアップロードUIを作成し、アップロードエリアに「Drag and Drop or Select a CSV File」と表示させます。このUIは、一度に1つのCSVファイルをアップロードできます。
1. データの保存と表示: dcc.Storeを使って、アップロードされたデータフレームとEDA用のデータフレームを保存します。また、アップロードされたデータフレームの情報が表示されるhtml.Div要素も作成しています。
1. アップロードされたデータの処理: update_uploaded_dataframeというコールバック関数を定義し、アップロードされたCSVファイルの内容をデコードし、データフレームに変換します。データフレームが作成されると、情報、要約統計、最初の10行を表示するためのHTML要素が返されます。

このコードによって、ユーザーがCSVファイルをアップロードでき、アップロードされたデータがデータフレームに変換され、その情報と要約統計が表示される機能が実現されています。

```python
# Define the layout for the Dash application
app.layout = html.Div(
    [
    >>>
            dcc.Upload(
            id="upload-data",
            children=html.Div(["Drag and Drop or ", html.A("Select a CSV File")]),
            style={
                "width": "100%",
                "height": "60px",
                "lineHeight": "60px",
                "borderWidth": "1px",
                "borderStyle": "dashed",
                "borderRadius": "5px",
                "textAlign": "center",
                "margin": "10px",
            },
            multiple=False,
        ),
        # Data store and info section
        dcc.Store(id="stored-dataframe"),
        dcc.Store(id="stored-dataframe-eda"),
        html.Div(id="dataframe-info"),
    <<<
]


# Callback to store the uploaded dataframe and display its information
@app.callback(
    Output("stored-dataframe", "data", allow_duplicate=True),
    Output("dataframe-info", "children"),
    Input("upload-data", "contents"),
    State("upload-data", "filename"),
    prevent_initial_call=True,
)
def update_uploaded_dataframe(contents, filename):
    layout = []
    if contents:
        content_type, content_string = contents.split(",")
        decoded = base64.b64decode(content_string)
        try:
            if "csv" in filename:
                # Assume that the user uploaded a CSV file
                uploaded_df = pd.read_csv(
                    io.StringIO(decoded.decode("utf-8")), index_col=0
                )
                info_buffer = StringIO()
                uploaded_df.info(buf=info_buffer)
                info_str = info_buffer.getvalue()
                layout = [
                    html.Label("データフレームの情報:"),
                    html.Pre(info_str),
                    html.Label("データフレームの要約統計:"),
                    dash_table.DataTable(
                        id="describe-table",
                        columns=[
                            {"name": i, "id": i}
                            for i in uploaded_df.reset_index().describe().columns
                        ],
                        data=uploaded_df.describe().reset_index().to_dict("records"),
                    ),
                    html.Label("データフレームの最初の10行:"),
                    dash_table.DataTable(
                        id="head-table",
                        columns=[
                            {"name": i, "id": i} for i in uploaded_df.head(10).columns
                        ],
                        data=uploaded_df.head(10).to_dict("records"),
                    ),
                ]
            else:
                return html.Div(["Invalid file type. Please upload a CSV file."])
        except Exception as e:
            return html.Div([f"Error processing file: {str(e)}"])
        return uploaded_df.to_dict("records"), layout

    return None, layout
```

#### データ前処理 
1. データ前処理クエリ入力UIの作成: 
dcc.Inputを使用して、データ前処理クエリの入力UIを作成し、プレースホルダーに「前処理クエリを入力してください（例：'Age'の欠損値を平均値で補完）」と表示させます。また、html.Buttonでサブミットボタンを作成します。
1. 前処理クエリ結果の表示: 
dcc.Loadingとhtml.Divを使って、前処理クエリ結果が表示される場所を作成します。
1. 前処理クエリの実行と結果の更新: 
update_preprocessing_resultsというコールバック関数を定義し、サブミットボタンがクリックされたときに、入力された前処理クエリを実行し、結果を表示するHTML要素を返します。このとき、chat_tool_with_pandas_df関数を使って、入力されたクエリに対応するPythonコードを実行し、結果を取得します。また、結果のデータフレームを保存します。

このコードによって、ユーザーがデータ前処理クエリを入力し、その結果をデータフレームとして表示できる機能が実現されています。

```python
app.layout = html.Div(
    [
    >>>
        html.Div(
            [
                html.Li("データ前処理クエリ入力"),
                dcc.Input(
                    id="query-input-preprocessing",
                    placeholder="前処理クエリを入力してください（例：'Age'の欠損値を平均値で補完）",
                    style={"width": "100%", "height": "50px"},
                ),
                html.Button("Submit", id="submit-button-preprocessing"),
            ],
            id="preprocessing-input-container",
        ),
        # Preprocessing query result section
        dcc.Loading(html.Div(id="query-result-preprocessing")),
    <<<
]


# Callback to update preprocessing query results and store the result dataframe
@app.callback(
    Output("query-result-preprocessing", "children"),
    Output("stored-dataframe", "data", allow_duplicate=True),
    Input("submit-button-preprocessing", "n_clicks"),
    State("query-input-preprocessing", "value"),
    State("stored-dataframe", "data"),
    prevent_initial_call=True,
)
def update_preprocessing_results(n_clicks, query, df):
    if n_clicks is None or df is None:
        return "", None

    if query:
        df = pd.DataFrame(df)
        query += "そのpythoncodeは？"
        result_df= chat_tool_with_pandas_df(df, query)
        store_data = result_df.to_dict("records")
        info_buffer = StringIO()
        result_df.info(buf=info_buffer)
        info_str = info_buffer.getvalue()
        layout = [
            html.Li("前処理クエリ結果"),
            html.Label("データフレームの情報:"),
            html.Pre(info_str),
            html.Label("データフレームの要約統計:"),
            dash_table.DataTable(
                id="describe-table",
                columns=[
                    {"name": i, "id": i}
                    for i in result_df.reset_index().describe().columns
                ],
                data=result_df.describe().reset_index().to_dict("records"),
            ),
            html.Label("データフレームの最初の10行:"),
            dash_table.DataTable(
                id="head-table",
                columns=[{"name": i, "id": i} for i in result_df.head(10).columns],
                data=result_df.head(10).to_dict("records"),
            ),
        ]
        return layout, store_data

    return "クエリが入力されていません。", None


```


#### EDA
1. EDAクエリ入力UIの作成: 
dcc.Inputを使用して、EDAクエリの入力UIを作成し、プレースホルダーに「クエリを入力してください（例：男性で３０歳以上４０歳未満で生き残った人は？）」と表示させます。また、html.Buttonでサブミットボタンを作成します。
1. EDAクエリ結果の表示: 
dcc.Loadingとhtml.Divを使って、EDAクエリ結果が表示される場所を作成します。
1. EDAクエリの実行と結果の更新: 
update_eda_query_resultsというコールバック関数を定義し、サブミットボタンがクリックされたときに、入力されたEDAクエリを実行し、結果を表示するHTML要素を返します。このとき、chat_tool_with_pandas_df関数を使って、入力されたクエリに対応するPythonコードを実行し、結果を取得します。また、結果のデータフレームを保存します。
1. EDAプロットの生成: 
generate_eda_plotsというコールバック関数を定義し、保存されたデータフレームから以下のEDAプロットを生成します。

- 欠損値の確認: 欠損値のプロットを生成します。
- 数値データの分布: 数値データの分布プロットを生成します。
- カテゴリデータの分布: カテゴリデータの分布プロットを生成します。
- 数値データの相関: 数値データの相関プロットを生成します。


このコードによって、ユーザーがEDAクエリを入力し、その結果をデータフレームとして表示できる機能が実現されています。また、EDAプロットを自動的に生成して表示する機能も提供されています。

```python
app.layout = html.Div(
    [
    >>>
        # EDA query input section
        html.H2("EDA"),
        html.Li("EDAクエリ入力"),
        html.Div(
            [
                dcc.Input(
                    id="query-input-eda",
                    placeholder="クエリを入力してください（例：男性で３０歳以上４０歳未満で生き残った人は？）",
                    style={"width": "100%", "height": "50px"},
                ),
                html.Button("Submit", id="submit-button-eda"),
            ]
        ),
        # EDA query result section
        dcc.Loading(html.Div(id="query-result-eda")),
        # EDA plots section
        dcc.Loading(html.Div(id="eda-plots")),
    <<<
]

# Callback to update EDA query results and store the result dataframe
@app.callback(
    Output("query-result-eda", "children"),
    Output("stored-dataframe-eda", "data", allow_duplicate=True),
    Input("submit-button-eda", "n_clicks"),
    State("query-input-eda", "value"),
    State("stored-dataframe", "data"),
    prevent_initial_call=True,
)
def update_eda_query_results(n_clicks, query, df):
    if n_clicks is None or df is None:
        return "", None

    if query:
        df = pd.DataFrame(df)
        query+="そのpandas codeは？"
        result_df = chat_tool_with_pandas_df(df, query)
        store_data = result_df.to_dict("records")
        return [
            html.Li("EDAクエリ結果"),
            dash_table.DataTable(
                columns=[{"name": i, "id": i} for i in result_df.columns],
                data=result_df.to_dict("records"),
            ),
        ], store_data

    return "クエリが入力されていません。", None

# Callback to generate EDA plots from DataFrame
@app.callback(
    Output("eda-plots", "children"),
    Input("stored-dataframe", "data"),
    Input("stored-dataframe-eda", "data"),
)
def generate_eda_plots(data_default, data):
    if not data_default:
        raise PreventUpdate
    if not data:
        data=data_default

    df = pd.DataFrame(data)

    # Generate missing value plot
    missing_plot = generate_missing_value_plot(df)
    # Generate numerical distribution plot
    numerical_plot = generate_numerical_distribution_plot(df)
    # Generate categorical distribution plot
    categorical_plot = generate_categorical_distribution_plot(df)
    # Generate correlation plot
    correlation_plot = generate_correlation_plot(df)

    layout = [
        html.Li("EDAプロット"),
        html.Label("欠損値の確認"),
        missing_plot,
        html.Label("数値データの分布"),
        dcc.Graph(figure=numerical_plot),
        html.Label("カテゴリデータの分布"),
        dcc.Graph(figure=categorical_plot),
        html.Label("数値データの相関"),
        dcc.Graph(figure=correlation_plot),
    ]

    return layout


```


#### 学習用データの生成とダウンロード
1. ボタンの作成: 
html.Buttonを使って、「Split Dataset and Download」ボタンを作成します。
1. 学習用データとテスト用データのCSVファイルリンク表示エリアの作成: 
html.Divを使用して、学習用データとテスト用データのCSVファイルリンクが表示される場所を作成します。
1. データセットの分割とCSVファイルリンクの生成: 
split_dataset_into_train_and_testというコールバック関数を定義し、ボタンがクリックされたときにデータセットを学習用データとテスト用データに分割し、それぞれのデータセットをCSV形式に変換し、base64エンコードしたデータをリンクとして表示します。ここでtrain_test_split関数を使ってデータセットを分割し、df_to_csv_data関数を使ってデータフレームをCSV形式のデータに変換しています。

このコードによって、ユーザーはボタンをクリックすることで、データセットを学習用データとテスト用データに分割し、それぞれのデータセットをCSVファイルとしてダウンロードすることができます。

```python
app.layout = html.Div(
    [
    >>>
        html.H2("モデル学習用データ生成"),
        html.Button("Split Dataset and Download", id="split-dataset"),
        html.Div(id="train-test-csv-files"),
    <<<
]


# Create a callback function to split the dataset into train and test sets
@app.callback(
    Output("train-test-csv-files", "children"),
    Input("split-dataset", "n_clicks"),
    State("stored-dataframe", "data"),
    prevent_initial_call=True,
)
def split_dataset_into_train_and_test(n_clicks, data):
    if not data:
        return [html.P("データがありません")]

    if n_clicks:
        df = pd.DataFrame(data)
        train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)

        train_csv_data = df_to_csv_data(train_df)
        test_csv_data = df_to_csv_data(test_df)

        train_href = f"data:text/csv;charset=utf-8;base64,{train_csv_data}"
        test_href = f"data:text/csv;charset=utf-8;base64,{test_csv_data}"

        layout = [
            html.Div(
                [
                    html.P("Download Train Set:"),
                    html.A(
                        "Download train.csv",
                        id="download-train-link",
                        download="train.csv",
                        href=train_href,
                        target="_blank",
                    ),
                ]
            ),
            html.Div(
                [
                    html.P("Download Test Set:"),
                    html.A(
                        "Download test.csv",
                        id="download-test-link",
                        download="test.csv",
                        href=test_href,
                        target="_blank",
                    ),
                ]
            ),
        ]

    else:
        layout = []

    return layout



```

## 課題
複雑な処理には対応できていない。例：特徴量エンジニアなど
それ以外にも多くの課題があると思いますが、ご指摘いただけると幸いです。

## まとめ
本ツールでは、GPTとLangchainを組み合わせた対話型自然言語クエリ分析ツールを提供し、プログラミング経験がないユーザーでもデータ分析を行うことができる環境を実現しました。データのアップロード、前処理、EDA、学習用データの生成とダウンロードなど、データ分析に必要な機能をとりあえずは網羅していると思います。これにより、ユーザーは自然言語でクエリを入力するだけで、直感的かつ簡単にデータ加工や前処理を行うことが可能かと思いますがいかがでしょうか。

## 今後の展望
今後の開発では、以下のような機能や改善点を取り入れることで、さらに使いやすく効果的なツールを提供できることを目指します。

- より多様なデータ形式への対応: 現在CSVファイルのみ対応していますが、ExcelやJSONなどのデータ形式にも対応
- より高度なデータ前処理機能の実装: より複雑なデータ前処理や特徴エンジニアリングをサポートする機能を追加
- モデル構築・評価機能の実装: 学習用データを利用して機械学習モデルの構築と評価が行えるように機能を拡張
- より詳細なEDA結果の表示: EDA結果をより詳細に表示し、ユーザーがデータの特徴や構造をより深く理解できるように改良します。

これらの改善を行うことで、プログラミング経験がないユーザーでも、簡単かつ効果的にデータ分析を行えるツールを提供していくことが目標です。