StreamlitとFastAPIでwebアプリを作ってみた（フロントエンド編）

## はじめに
今回PythonのライブラリであるStreamlitとWebフレームワークであるFastAPIを用いてwebアプリを作ってみたので記事にまとめました。

前回以下の記事でバックエンドの実装方法をまとめました。今回はStreamlitによるフロントエンドの実装についてまとめていこうと思います。

**前回までの記事はこちら**

https://qiita.com/kizataka/items/061624fdcb59c6437b84

https://qiita.com/kizataka/items/7e293b5814c5acdbb5f0

### 記事の目次
1. アプリの概要(再掲)
1. ディレクトリ構成
1. フロントエンドの実装
1. ローカル環境でアプリを起動する
1. まとめ

## 1. アプリの概要(再掲)
一言で言うと、誹謗中傷のような批判的なツイートを排除するアプリです。

具体的には、アプリ上で好きな人物名を入力して検索をかけると、Twitterからその人物名に関するツイートを取得してAIが自動でツイート内容を解析し、肯定的なツイートのみを表示させるようにするというものです。

と、理想はこのように考えていたのですが、途中でTwitterAPIの規約変更によりツイート取得ができないことに気づきました。
アプリ自体のテーマを変更するのは面倒なので、アプリの概要はそのままでKaggleにある文章データを利用してそれをツイート取得とみなし、擬似的に動かせるようにしました。

AIについては自然言語処理による感情分析という技術を用います。

### データセットについて
Kaggleにある[Women's E-Commerce Clothing Reviews](https://www.kaggle.com/datasets/nicapotato/womens-ecommerce-clothing-reviews?resource=download)を例として利用しました。
ECサイトでの婦人服の売上データとなっており、商品分類名や商品に関するレビュー文が格納されています。
レビュー文をツイートに置き換えて考え、ファイルをアップロードすることでツイートを取得できたと仮定しています。

### 使い方
![web_app_movie](https://github.com/kizataka/web_app_sentiment_analyzer/assets/112063667/c514762d-908b-4e8a-b31a-8f00877578e2) 

1. ファイル（Kaggleから取得してきた文章データ）をアップロードします。
2. 文章データが含まれるカラムを選択します。
2. 取得したいレビューに関するキーワードを入力します(本来ならここで人物名を入力し、その人物に関するツイートを取得します)。デモでは`shirt`に関するレビューを取得しています。
2. 取得したいデータ数を指定します。
2. 今回は日付指定は関係ないです。（本来ならどれくらいの期間のツイートを取得するのかを指定します。）
2. 表示するレビューをポジティブにするのかネガティブにするのかを指定します。
2. 分析開始ボタンをクリックします。クリックすると感情分析が始まります。
2. `shirt`に関する肯定的なレビューのみが表示されます。（本来なら検索した人物名に関するポジティブなツイートが表示されます。）

## 2. ディレクトリ構成
```
.
├── app.py                       Streamlitでフロントエンド実装
└── sql_app
	├── __init__.py
	├── main.py                  FastAPIのバックエンド実装
	├── schemas.py               Pydanticによる型定義
	└── sentiment_analysis.py    感情分析モデルの設定
```
今回はapp.pyについてまとめていきたいと思います。

## 3. フロントエンドの実装
フロントエンドにはPythonのフレームワークである`Streamlit`を使っていきたいと思います。

https://streamlit.io/

`Streamlit`はPyhonのみで簡易的にアプリケーションを作成できるのが特徴です。なのでデータ分析などの領域でデータの可視化ツールなどGUIアプリケーションの作成によく使われているそうです。

とにかくアプリを完成させることが目標だったので、慣れているPythonで全て完結できるStreamlitを選びました。

今回は見やすくなるように、メイン画面にファイルのアップロードや分析後のレビュー一覧（本来ならツイート一覧）を表示させ、サイドバーにカラムの選択やキーワード検索ができるようにしたいと思います。

```app.py
import streamlit as st
import pandas as pd
import chardet
import requests

# タイトルの設定
st.title('ツイート感情分析アプリ')
st.write('')
st.write(
    """
    このアプリはTwitterから検索ワードに関するツイートを取得してその内容を感情分析し、
    ポジティブまたはネガティブなツイートのみを表示させるアプリです。
    """
)
st.write('')

# ファイルアップロードの設定
uploaded_file = st.file_uploader('csvファイルをアップロードしてください', type='csv')

if uploaded_file is not None:
    # エンコーディングの設定
    rawdata = uploaded_file.read()
    result = chardet.detect(rawdata)
    encoding = result['encoding']

    uploaded_file.seek(0)
    data = pd.read_csv(uploaded_file, encoding=encoding)

    st.write('')
    st.write('データの先頭5件を表示しています')
    st.write(data.head())
    st.write('')

    # サイドバー（検索条件）の設定
    st.sidebar.header('検索設定')
    column_to_process = st.sidebar.selectbox('ツイートが含まれるカラムの選択:', data.columns)
    st.sidebar.write('')
    keyword = st.sidebar.text_input('検索ワード:', value='キーワードを入力してください')
    st.sidebar.write('')
    num_comments = st.sidebar.slider('取得するツイート数:', min_value=1, max_value=len(data), value=10, step=10)
    st.sidebar.write('')
    st.sidebar.write('取得するツイートの期間指定:')
    date_from = st.sidebar.date_input('何日から')
    date_to = st.sidebar.date_input('何日まで')
    st.sidebar.write('')
    sentiment_filter = st.sidebar.radio('感情選択:', options=['Positive', 'Negative'])

    st.write('検索設定が完了したら下の分析開始ボタンを押してください')
    st.write('')

    if st.button('分析開始'):
        # データの前処理
        filtered_data = data[data[column_to_process].str.contains(keyword, case=False, na=False)]
        filtered_data = filtered_data.head(num_comments)

        # 感情分析の結果を格納するためのカラムの追加
        filtered_data['sentiment'] = ''
        filtered_data['sentiment_score'] = 0.0
        filtered_data = filtered_data[[column_to_process, 'sentiment', 'sentiment_score']]

        # 感情分析の実行
        for index, row in filtered_data.iterrows():
            url = 'https://127.0.0.1:8000/analyze_sentiment/'
            response = requests.post(url, json={'text': row[column_to_process]})

            if response.status_code == 200:
                sentiment_data = response.json()
                filtered_data.at[index, 'sentiment'] = sentiment_data['sentiment']
                filtered_data.at[index, 'sentiment_score'] = sentiment_data['sentiment_score']
                
            else:
                st.error(f'感情分析エラー:{response.text}')  # APIエラーの表示

        # 選択した感情のみのツイートを表示するように設定
        if sentiment_filter == 'Positive':
            filtered_data = filtered_data[filtered_data['sentiment'] == 'POSITIVE']
        elif sentiment_filter == 'Negative':
            filtered_data = filtered_data[filtered_data['sentiment'] == 'NEGATIVE']

        st.success('分析が完了しました')
        st.markdown('## ツイート一覧')
        st.write(f'{sentiment_filter}なツイートのみ表示しています')
        st.write(f'表示件数:{num_comments}')

        # sentiment_scoreの大きい順に並び替え
        filtered_data = filtered_data.sort_values(by='sentiment_score', ascending=False)

        # テキストの表示
        for index, row in filtered_data.iterrows():
            st.markdown(f'#### Tweet {index + 1}')
            st.write(row[column_to_process])
            st.write(f'{row["sentiment"]}: {row["sentiment_score"]}')
            st.write('---')
```
ここでのポイントは以下の3つです。

### サイドバーに関して
`st.sidebar`をつけるようにします。

### エンコーディングの設定について
アップロードするファイルによってはエンコーディングが`UTF-8`でない場合があります。

`pd.read_csv()`はデフォルトで`UTF-8`を想定しているらしいので、異なるエンコーディングのファイルがアップロードされるとエラーが生じてしまいました。

そこで、`chardet`ライブラリを利用して自動的にファイルのエンコーディングを推測し、エンコーディングエラーを回避するように設定しました。こうすることで、異なるエンコーディングを持つファイルでも正常に読み込むことができるようになります。

### 感情分析の手順
1. 感情分析を行いたいテキストを選択します。ここでは`row[column_to_process]`のようにデータフレームのカラムを選択して指定しています。
1. バックエンドの`main.py`で指定した感情分析を実行するエンドポイントを`url`として指定し、そのエンドポイントに選択したテキストを`POSTリクエスト`として送信します。
1. バックエンド側で感情分析が実行されます。
1. 結果が`response`に格納されます。
1. `response.status_code`が200の時は上手く行った時なので、200以外の数値が出た場合にエラーが出るように条件分岐を作成しておきます。

HTTPメソッドには`GET`, `POST`, `PUT`, `DELETE`などようなメソッドがありますが、ここではバックエンドの`main.py`で設定した`FastAPI`にテキストを送信して感情分析を実行するように要求したかったので`request.post`になっています。

## 4. ローカル環境でアプリを起動する
以上でバックエンドとフロントエンドの実装が完了しました。あとはwebアプリとしてきちんと動くかどうかを確認していきます。

webアプリを動かすには`Streamlit`と`FastAPI`の両方を起動させる必要があります。

### Streamlitの立ち上げ方法
```
$ streamlit run app.py
```

### FastAPIの立ち上げ方法
```
$ uvicorn main:app --reload
```
vscodeのターミナルなどで次のようにターミナルを二つにしてそれぞれ実行します。
![スクリーンショット 2023-10-07 12.53.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/e6e979e9-23ce-64a5-6b0c-f3929c8943f4.png)
それぞれのファイルがあるディレクトリ上かどうかに注意してください。

上手く起動してブラウザ上に以下のページが出現して感情分析まで実行できれば、ローカルでの今回のアプリ開発は成功です。

![スクリーンショット 2023-10-07 12.59.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2741553/cbfa0c46-03af-8373-7756-086eba9bde05.png)

実際のKaggleのデータは少し大きすぎるので私は100行に縮小したCSVファイル[test.csv](https://drive.google.com/file/d/1cAJ2OdLswGHbR9fdrt8YFQYxP2c1SnKL/view?usp=drive_link)を作成して処理を軽くしていました。

## 5.まとめ
以上でようやくアプリが完成しました。

pythonでフロントエンドが実装できるというのは想像以上に便利でした。またStreamlitも簡単なのでいじりやすかったです。他にもたくさんのメソッドがあるようなので試していこうと思います。

次回は完成したアプリをデプロイする方法についてまとめていきたいと思います。

**次回の記事はこちら**

https://qiita.com/kizataka/items/e7273b2399f692be4440