streamlitを使ったお手軽Webアプリ開発

streamlitを使えばWebアプリケーションが簡単に作成できます。
この記事では、streamlitでよく使う機能をサンプルコードと共に紹介し、それらを組み合わせたWebアプリケーションの作成方法を紹介していこうと思います。

## 目次
[1. はじめに](#1-はじめに)
[2. streamlitの基本的な使い方](#2-streamlitの基本的な使い方)
[3. streamlitの応用的な使い方](#3-streamlitの応用的な使い方)
[4. テキストマイニングアプリを開発してみる](#4-テキストマイニングアプリを開発してみる)
[5. streamlitの注意点](#5-streamlitの注意点)
[6. まとめ](#6-まとめ)

## 1. はじめに

streamlitはPythonで簡単にWebアプリケーションを作成できるオープンソースのライブラリです。streamlitを使うことで、データの可視化や機械学習モデルのデバッグ、プロトタイプの作成など、様々な用途でWebアプリケーションを作成することができます。

また、streamlitは簡単なコードで高度なUIを作成することができ、ウェブフレームワークのような複雑な設定やコーディングは必要ありません。そのため、Pythonの初心者や、Webアプリケーション作成に慣れていない人でも簡単に使いこなすことができます。

## 2. streamlitの基本的な使い方

### 2.1 streamlitのインストール方法

まずはstreamlitをインストールしましょう。以下のコマンドを実行することでstreamlitをインストールすることができます。(Python3.7以上必須)

```
$ pip install streamlit
```

### 2.2 サンプルコードの実行方法

streamlitをインストールしたらサンプルコードとして下記の内容で`app.py`というファイルを作成してみましょう。

```python
import streamlit as st

input_num = st.number_input('Input a number', value=0)

result = input_num ** 2
st.write('Result: ', result)
```

ファイルを作成したら、以下のコマンドを実行して、サンプルコードを実行してみましょう。

```
$ streamlit run app.py
```

上記のコマンドを実行することで、streamlitが立ち上がり、Webアプリケーションが表示されます。アプリケーションには`8501`番ポートがデフォルトで使用されます。ブラウザから`8501`番ポートにアクセスすると実行されているアプリケーションを確認することができます。例えば`8080`番にポートを変更したい場合は以下のようにコマンドを変更します。

```
$ streamlit run app.py --server.port 8080
```

サンプルコードでは、ユーザが入力した値を2乗して表示するだけの簡単なアプリケーションが作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/c15907b0-cf68-b138-9928-a1e8eadf6e7f.png)

### 2.3 基本的なコンポーネントの使い方

streamlitでは様々なコンポーネントを使って、簡単にWebアプリケーションを作成することができます。以下では、streamlitでよく使われる基本的なコンポーネントの使い方を紹介します。

- タイトルの表示

    ```python
    import streamlit as st
    
    st.title('streamlit Tutorial')
    ```

- ヘッダーの表示

    ```python
    import streamlit as st
    
    st.header('This is a header')
    ```

- サブヘッダーの表示

    ```python
    import streamlit as st
    
    st.subheader('This is a subheader')
    ```

- テキストの表示
    
    ```python
    import streamlit as st
    
    st.text('Hello World!')
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/eb0d6231-fe3b-cf23-e3cc-ce15a7867d57.png)


- Merkdown表記でのテキストの表示

    ```python
    import streamlit as st
    
    # st.write()はMarkdown表記対応
    st.write('# headline1')
    # 以下のように明示的に書くことも可能
    st.markdown('# headline2')
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/efb8e079-851c-ba2e-45a8-74e285fc0064.png)

- リストの表示

    ```python
    import streamlit as st
    
    st.write(['apple', 'orange', 'banana'])
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/5df88776-5b2d-5a8b-c0ed-cea4a7a0b629.png)

- DataFrameの表示

    ```python
    import streamlit as st
    import pandas as pd
    
    # ダミーデータの作成
    df = pd.DataFrame({
        'name': ['Alice', 'Bob'],
        'age': [25, 30],
        'gender': ['female', 'male']
    })
    
    # DataFrameを表示
    st.write(df)
    # st.dataframe()でも表示可能
    st.dataframe(df)
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/4d94e1af-edd4-761e-e2db-068dc166b0f0.png)

- ファイルのアップロード

    ```python
    import streamlit as st
    
    uploaded_file = st.file_uploader("Choose a file")
    if uploaded_file is not None:
        st.write(uploaded_file)
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/0b253219-3e18-4b86-edb7-d26fb1716a50.png)

- ボタンの表示

    ```python
    import streamlit as st
    
    if st.button('Say hello'):
        st.write('Hello World!')
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/433d461c-5800-e059-d339-6e56ba92e82c.png)

- チェックボックスの表示

    ```python
    import streamlit as st
    
    if st.checkbox('Show/Hide'):
        st.write('Some text')
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/caafef3b-11d2-739f-187c-6e3e76ac77ef.png)

- ラジオボタンの表示

    ```python
    import streamlit as st
    
    option = st.radio(
        'Which number do you like best?', 
        ['1', '2', '3']
    )
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/7b91a193-4de5-17a0-c578-0eca4779b54a.png)

- セレクトボックスの表示
    
    ```python
    import streamlit as st
    
    option = st.selectbox(
        'Which number do you like best?', 
        ['1', '2', '3']
    )
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/08ad1fbd-df7b-78ed-0183-619a5d0a8da9.png)

- マルチセレクトボックスの表示

    ```python
    import streamlit as st
    
    options = st.multiselect(
        'What are your favorite colors',
        ['Green', 'Yellow', 'Red', 'Blue'],
        default=['Yellow', 'Red'] # デフォルトの設定
    )
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/79dc9926-98b2-0b09-04c6-71110b0d9c94.png)

- スライダーの表示

    ```python
    import streamlit as st
    
    value = st.slider('Select a value', 0, 100, 50) # min, max, default
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/5a327ce4-485d-2dfd-9c2d-1ab33c4ebb9d.png)

- テキスト入力の表示
    
    ```python
    import streamlit as st
    
    # テキスト入力ボックス
    text_input = st.text_input('Input', 'Input some text here.')
    # テキストエリア
    text_area = st.text_area('Text Area', 'Input some text here.')
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/89333043-6e22-968e-9a9e-32c755e55f9c.png)

- ぐるぐるマークの表示
    ```python
    import streamlit as st
    import time
    
    # ボタンを押したら3秒間出力を待つ
    if st.button('start'):
        with st.spinner('processiong...'):
            time.sleep(3)
            st.write('end!')
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/7eeebf54-2340-3a20-b3da-2c0eee37e2bc.png)

- サイドバーの表示

    ```python
    import streamlit as st
    
    # Sidebarの選択肢を定義する
    options = ["Option 1", "Option 2", "Option 3"]
    choice = st.sidebar.selectbox("Select an option", options)
    
    # Mainコンテンツの表示を変える
    if choice == "Option 1":
        st.write("You selected Option 1")
    elif choice == "Option 2":
        st.write("You selected Option 2")
    else:
        st.write("You selected Option 3")
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/4b18f487-a813-81f3-47b2-a19d17e39e9e.png)

- カラムの分割
    
    ```python
    import streamlit as st
    import pandas as pd
    
    # ダミーデータの作成
    df = pd.DataFrame({
        'name': ['Alice', 'Bob'],
        'age': [25, 30],
        'gender': ['female', 'male']
    })
    
    # 2列のカラムを作成
    col1, col2 = st.columns(2)
    
    # col1にテキストを表示
    with col1:
        st.header("Column 1")
        st.write("This is column 1.")
    
    # col2にDataFrameを表示
    with col2:
        st.header("Column 2")
        # DataFrameを表示
        st.write(df)
    ```
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/afd51aa0-3e6f-efd0-4c5c-db43af514e28.png)


以上が基本的なコンポーネントの使い方です。これらのコンポーネントを組み合わせることで、簡単なWebアプリケーションを作成することができます。

## 3. streamlitの応用的な使い方

### 3.1 グラフの描画

streamlitでは簡単にグラフを描画することができます。以下はMatplotlibを使って簡単なグラフを描画するサンプルコードです。

```python
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt

# Warningの非表示
st.set_option('deprecation.showPyplotGlobalUse', False)

# グラフを描画する
def plot_graph():
    x = np.linspace(0, 10, 100)
    y = np.sin(x)
    plt.plot(x, y)
    st.pyplot()

# グラフを表示するボタンを表示する
if st.button('Plot graph'):
    plot_graph()
```

このコードでは`plot_graph()`メソッドを定義し、Matplotlibを使ってグラフを描画しています。その後、ボタンを表示し、ボタンをクリックすると`plot_graph()`メソッドが呼び出されてグラフが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/31c82216-deba-e109-4848-5eff316470d1.png)

### 3.2 データベースとの連携

streamlitでは様々なデータベースと連携をすることができます。以下はSQLiteを使って簡単なデータベースと連携するサンプルコードです。

```python
import streamlit as st
import sqlite3

# データベースに接続する
conn = sqlite3.connect('example.db')
c = conn.cursor()

# データを表示する
def show_data():
    c.execute('SELECT * FROM users')
    data = c.fetchall()
    for d in data:
        st.write(d)

# データを追加する
def add_data(name, age):
    c.execute('INSERT INTO users (name, age) VALUES (?, ?)', (name, age))
    conn.commit()
    st.write('Data added. Please reload page.')

# データベースにテーブルを作成する
c.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)')

# データの表示
show_data()

# データの追加
name = st.text_input('Name')
age = st.number_input('Age')
if st.button('Add data'):
    add_data(name, age)

# データベースをクローズする
conn.close()
```

このコードではSQLiteを使って`users`テーブルを作成し、`show_data()`メソッドでデータを表示しています。また、`add_data()`メソッドを使って、ユーザが入力したデータをデータベースに追加することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/72138150-d747-70e5-8ae0-577cc9f23376.png)

### 3.3 機械学習のモデルの可視化

streamlitでは機械学習のモデルを可視化することができます。以下はscikit-learnを使って簡単な機械学習モデルを作成し、そのモデルを可視化するサンプルコードです。

```python
import streamlit as st
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree

# Warningの非表示
st.set_option('deprecation.showPyplotGlobalUse', False)

# データをロードする
iris = load_iris()
X, y = iris.data, iris.target

# モデルを学習する
model = DecisionTreeClassifier()
model.fit(X, y)

# モデルを可視化する
def plot_model():
    plot_tree(model)
    st.pyplot()

# モデルを表示するボタンを表示する
if st.button('Plot model'):
    plot_model()
```

このコードでは`DecisionTreeClassifier`を使ってIrisデータセットを学習し、`plot_model()`メソッドでモデルを可視化しています。その後、ボタンを表示し、ボタンをクリックすると`plot_model()`メソッドが呼び出されてモデルが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/15cd0459-5f1c-4404-3fde-29118e29f08d.png)

以上が、streamlitを使った応用的な使い方の一例です。様々な用途でstreamlitを使うことができるため、自分のプロジェクトに合わせて使い方を考えることが大切です。

## 4. テキストマイニングアプリを開発してみる

本章では、例としてstreamlitを使ったテキストマイニングのWebアプリケーションの作成方法を紹介します。

テキストマイニングは、大量のテキストデータから有用な情報やパターンを抽出する技術であり、ビジネスや学術分野で重要な役割を果たしています。

筆者自身が自然言語処理関係の業務が多いため、streamlitを利用して簡単に利用できるWebアプリケーションを作成してみました。

このアプリでは、テキストファイルからワードクラウドや品詞別の単語出現頻度表を表示することができます。

:::note info
ワードクラウドは文章やテキストデータの中で頻出する単語を視覚的に表現する手法です。頻出する単語ほど大きく表示され、その単語が文章内で占める重要度が高いことを表現します。ワードクラウドは情報の可視化やデータ分析に使用され、文章の特徴やトピックを把握する上で有用なツールとして利用されています。
:::

### Dockerfile
```dockerfile
FROM python:3.9

# mecabのインストール
RUN apt-get update && apt-get install -y \
    mecab \
    libmecab-dev \
    mecab-ipadic-utf8 \
    git \
    make \
    curl \
    xz-utils \
    file \
    sudo \
    wget
  
# neologdのインストール
RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git && \
    cd mecab-ipadic-neologd && \
    ./bin/install-mecab-ipadic-neologd -n -y && \
    echo dicdir = `mecab-config --dicdir`"/mecab-ipadic-neologd">/etc/mecabrc && \
    cp /etc/mecabrc /usr/local/etc

WORKDIR /opt/app-root/src
COPY ./src /opt/app-root/src

# フォントのダウンロード
RUN wget https://moji.or.jp/wp-content/ipafont/IPAexfont/ipaexg00401.zip
RUN unzip ipaexg00401.zip && rm -f ipaexg00401.zip

# ライブラリのインストール
RUN pip install streamlit mecab-python3 wordcloud
```
### docker-compose.yml
```yml
version: '3'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8501:8501"
    command: streamlit run app.py --server.port 8501
```
### app.py
```python
import io
import MeCab
import pandas as pd
import streamlit as st
from collections import Counter
from wordcloud import WordCloud

# ページのレイアウトを設定
st.set_page_config(
    page_title="テキスト可視化",
    layout="wide", # wideにすると横長なレイアウトに
    initial_sidebar_state="expanded"
)

# タイトルの設定
st.title("テキスト可視化")

# サイドバーにアップロードファイルのウィジェットを表示
st.sidebar.markdown("# ファイルアップロード")
uploaded_file = st.sidebar.file_uploader(
    "テキストファイルをアップロードしてください", type="txt"
)

# ワードクラウド、出現頻度表の各処理をサイドバーに表示
st.sidebar.markdown("# 可視化のオプション")
if uploaded_file is not None:
    # 処理の選択
    option = st.sidebar.selectbox(
        "処理の種類を選択してください", ["ワードクラウド", "出現頻度表"]
    )
    # ワードクラウドの表示
    if option == "ワードクラウド":
        pos_options = ["名詞", "形容詞", "動詞", "副詞", "助詞", "助動詞", "接続詞", "感動詞", "連体詞", "記号", "未知語"]
        # マルチセレクトボックス
        selected_pos = st.sidebar.multiselect("品詞選択", pos_options, default=["名詞"])
        if st.sidebar.button("生成"):
            st.markdown("## ワードクラウド")
            with st.spinner("Generating..."):
                io_string = io.StringIO(uploaded_file.getvalue().decode("shift-jis"))
                text = io_string.read()
                tagger = MeCab.Tagger()
                node = tagger.parseToNode(text)
                words = []
                while node:
                    if node.surface.strip() != "":
                        word_type = node.feature.split(",")[0]
                        if word_type in selected_pos: # 対象外の品詞はスキップ
                            words.append(node.surface)
                    node = node.next
                word_count = Counter(words)
                wc = WordCloud(
                    width=800,
                    height=800,
                    background_color="white",
                    font_path="./ipaexg00401/ipaexg.ttf", # Fontを指定
                )
                # ワードクラウドを作成
                wc.generate_from_frequencies(word_count)
                # ワードクラウドを表示
                st.image(wc.to_array())
    
    # 出現頻度表の表示
    elif option == "出現頻度表":
        pos_options = ["名詞", "形容詞", "動詞", "副詞", "助詞", "助動詞", "接続詞", "感動詞", "連体詞", "記号", "未知語"]
        # マルチセレクトボックス
        selected_pos = st.sidebar.multiselect("品詞選択", pos_options, default=pos_options)
        if st.sidebar.button("生成"):
            st.markdown("## 出現頻度表")
            with st.spinner("Generating..."):
                io_string = io.StringIO(uploaded_file.getvalue().decode("shift-jis"))
                text = io_string.read()
                tagger = MeCab.Tagger()
                node = tagger.parseToNode(text)

                # 品詞ごとに出現単語と出現回数をカウント
                pos_word_count_dict = {}
                while node:
                    pos = node.feature.split(",")[0]
                    if pos in selected_pos:
                        if pos not in pos_word_count_dict:
                            pos_word_count_dict[pos] = {}
                        if node.surface.strip() != "":
                            word = node.surface
                            if word not in pos_word_count_dict[pos]:
                                pos_word_count_dict[pos][word] = 1
                            else:
                                pos_word_count_dict[pos][word] += 1
                    node = node.next

                # カウント結果を表にまとめる
                pos_dfs = []
                for pos in selected_pos:
                    if pos in pos_word_count_dict:
                        df = pd.DataFrame.from_dict(pos_word_count_dict[pos], orient="index", columns=["出現回数"])
                        df.index.name = "出現単語"
                        df = df.sort_values("出現回数", ascending=False)
                        pos_dfs.append((pos, df))

                # 表を表示
                for pos, df in pos_dfs:
                    st.write(f"【{pos}】")
                    st.dataframe(df, 400, 400)
else:
    # テキスト未アップロード時の処理
    st.write("テキストファイルをアップロードしてください。")
```

### ディレクトリ構成
```
.
├ src/
│　└ app.py
├ Dockerfile
└ docker-compose.yml
```
### アプリの起動
`docker-compose.yml`のあるディレクトリで以下のコマンドを実行し、コンテナをビルド&起動します。
```sh
$ docker-compose up
```

### ブラウザでアプリを実行
コンテナが起動したら、ブラウザから`localhost:8501`にアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/75accaff-f30c-59d5-1cf9-aec87a3395bf.png)

ためしに、夏目漱石の『こころ』を分析してみます。
(https://www.aozora.gr.jp/cards/000148/card773.html)
1. ワードクラウド
    - 名詞のみ
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/834f766b-e4e2-5ffc-e1fa-cdbd1f4ff592.png)

    - 名詞・動詞・形容詞
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/0f2fa012-3617-d7de-b41f-6181195adf2a.png)
2. 出現頻度表
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/1baa5e4e-3ede-d6ba-319b-d349e2b9448a.png)

    ちなみに「K」は人名としては取れないみたいです(そりゃそうか)。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/7157933f-a076-27bd-f574-5a1df2d7a2ed.png)



## 5. streamlitの注意点

### 5.1 リロード時の挙動

streamlitはブラウザで実行されるアプリケーションのため、リロード時に全てのデータがリセットされます。そのため、ユーザが入力したデータや選択したオプションなどを再度入力する必要があります。データの保持については後述します。

### 5.2 データの保持

streamlitはリロード時に全てのデータがリセットされますが、データを保持する方法もあります。streamlitのバージョン0.85以降、`st.session_state`という機能が追加されました。これを利用すると簡単にデータの保持が可能となります。
- `st.session_state`を利用しない場合

    ```python
    import streamlit as st
    
    # ボタンが押された回数
    count = 0
    
    # ボタンを表示し、クリックされた回数を表示する
    if st.button("Click me"):
        count += 1
    
    st.write(f"You clicked the button {count} times.")
    ```
    何度クリックしても値は1のままでカウントできません。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/6425912e-fde7-9a4f-5d87-7cf591f90f63.png)

- `st.session_state`を利用した場合

    ```python
    import streamlit as st
    
    # ボタンが押された回数を保持する
    if "count" not in st.session_state:
        st.session_state.count = 0
    
    # ボタンを表示し、クリックされた回数を表示する
    if st.button("Click me"):
        st.session_state.count += 1
    
    st.write(f"You clicked the button {st.session_state.count} times.")
    ```
    クリック回数が保持されるのでクリックするごとに値が増えていきます。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3323087/6cd48e0b-c1fd-b385-4af1-696cc1e633c4.png)

参考: [【Streamlit】Session Stateで変数の値を保持する方法](https://qiita.com/kuriyan1204/items/d8342f3a40c6aeb57e5e)

### 5.3 セキュリティ

streamlitはブラウザで実行されるため、セキュリティ上の注意が必要です。例えば、ユーザが入力したデータをそのままデータベースに保存する場合はSQLインジェクション攻撃に注意する必要があります。また、ユーザがアップロードしたファイルについても、ウイルス対策などのセキュリティ対策が必要です。

### 5.4 パフォーマンス

streamlitはブラウザで実行されるため、大量のデータを処理する場合にはパフォーマンスの問題が発生する可能性があります。特にグラフの描画などはデータ量によっては処理に時間がかかることがあります。そのため、大量のデータを処理する場合にはバックエンドでの処理を検討する必要があります。

### 5.5 バージョンアップ

streamlitは比較的新しいライブラリであり、バージョンアップによって大きな変更がされることがあります。そのためアップデート時には注意が必要です。

## 6. まとめ

streamlitはPythonでWebアプリケーションを簡単に作成することができるフレームワークです。

streamlitの主なメリットは、簡単なコーディング、リアルタイム更新、カスタマイズがしやすい点です。また、Pythonのライブラリを利用することで高度なデータ分析や機械学習のモデルの可視化が可能になり、様々な用途に活用することができます。