Zenn
Closed13

Pythonでデータ&AIを活用したWebアプリを構築できるライブラリ「Taipy」を試す

kun432kun432

GitHubレポジトリ

https://github.com/Avaiga/taipy

Taipy

Pythonデータ&AIウェブアプリケーションの構築

シンプルなパイロットプロジェクトから、あっという間に本番環境対応のウェブアプリケーションへ。
パフォーマンス、カスタマイズ性、拡張性において一切の妥協はありません。

既存のライブラリを超えて

⭐ 何のために?

Taipyは、データサイエンティストや機械学習エンジニアがデータとAIを活用したウェブアプリケーションを作成するために設計されています。

⭐️ 本番環境に対応したウェブアプリケーションの構築を可能にします。
⭐️ 新たな言語を学ぶ必要はなく、Pythonのみで十分です。
⭐️ データとAIアルゴリズムに専念でき、開発の複雑さはTaipyに委ねられます。
⭐️ 運用(ホスティング、デプロイメント、保守など)を簡素化します。

✨ TaipyとTaipyエコシステム

Taipyには、開発者がエンドユーザーに容易に力を与えるためのTaipy Pythonライブラリが含まれており、次の機能を提供します:

- ユーザーインターフェイスの生成
- データ統合
- パイプラインのオーケストレーション
- ワットイフ分析およびシナリオ管理
- 認証、ロール、ユーザー管理
- Cronジョブおよびスケジューリング

Taipyライブラリに加えて、Taipyエコシステムには次が含まれます:

- Taipy Designer
- Taipy Studio
- 事前定義されたテンプレート
- データプラットフォームとの統合

Taipyには、運用および保守を容易にするための各種資料が付属しています。

- コマンドラインインターフェイス
- デプロイスクリプト
- バージョン管理
- データマイグレーション
- テレメトリーとモニタリング

競合としてはStreamlitあたりになるのかも

ライセンスはApache-2.0

kun432kun432

Getting Started

https://docs.taipy.io/en/latest/tutorials/getting_started/

公式ドキュメントのGetting Startedに従って進める。Colabだとngrokが必要になりそうなので、今回はローカルのMacで。

作業ディレクトリ+Python仮想環境を作成。最近はuvを使ってる。

mkdir taipy-work && cd taipy-work
uv venv -p 3.12.8

パッケージインストール

uv pip install taipy
出力
Installed 84 packages in 434ms
(snip)
 + taipy==4.0.2
 + taipy-common==4.0.2
 + taipy-core==4.0.2
 + taipy-gui==4.0.2
 + taipy-rest==4.0.2
 + taipy-templates==4.0.2
(snip)

サンプルコードを試してみる。シンプルなスライダーでパラメータを調整するとグラフが動的に動くというもの(変数名valueはわかりにくいのでvalに変えた)。

sample_markdown.py
from taipy.gui import Gui
from math import cos, exp

val = 10

page = """
# **Taipy** *を始めてみよう*

値: <|{val}|text|>

<|{val}|slider|on_change=slider_moved|>

<|{data}|chart|>
"""

def slider_moved(state):
    state.data = compute_data(state.val)

def compute_data(decay:int)->list:
    return [cos(i/6) * exp(-i*decay/600) for i in range(100)]

data = compute_data(val)

if __name__ == "__main__":
    Gui(page).run(title="動的なグラフ")

実行

uv run sample_markdown.py
出力
[2025-02-21 06:11:00.854][Taipy][INFO]  * Server starting on http://127.0.0.1:5000

ブラウザが起動して以下のような出力になる。

スライダーを動かすと動的にグラフが変化する。

サンプルコードを少し見ていく。

ビジュアル要素

Taipyはさまざまなビジュアル要素が用意され、Pythonの変数や環境とインタラクションできる。このビジュアル要素はMarkdownだと以下のように定義される。

<|{変数名}|ビジュアル要素のタイプ|...|>

例えば、上のサンプルコードだと以下の部分

(snip)

page = """
# **Taipy** *を始めてみよう*

値: <|{val}|text|>

<|{val}|slider|on_change=slider_moved|>

<|{data}|chart|>
"""

(snip)

この部分は

  • 変数valが「テキスト」と「スライダー」に紐づいている。
  • 変数dataが「グラフ(chart)」に紐づいている。
  • それぞれの変数の値に応じて、各ビジュアル要素が動的に変更される。
  • ビジュアル要素がユーザのアクションにより可変な場合はコールバック関数を紐づけることで、それぞれの変数の値が動的に変更される。
    • 「スライダー」が変更された場合(on_changeslider_movedが実行される。

という感じ。

で、変数・ビジュアル要素・アクションの紐づけは、Markdownだけでなく、Pythonコードでも定義できる。

sample_python.py
from taipy.gui import Gui
import taipy.gui.builder as tgb
from math import cos, exp

val = 10

def slider_moved(state):
    state.data = compute_data(state.val)

def compute_data(decay:int)->list:
    return [cos(i/6) * exp(-i*decay/600) for i in range(100)]

# Pythonコードで定義する場合
with tgb.Page() as page:
    tgb.text(value="# **Taipy** *を始めてみよう*", mode="md")
    tgb.text(value="値: {val}")
    tgb.slider(value="{val}", on_change=slider_moved)
    tgb.chart(data="{data}")

data = compute_data(val)

if __name__ == "__main__":
    Gui(page).run(title="動的なグラフ")

アクションによるインタラクティブ

上にコードにあるように、ビジュアル要素の中にはユーザからのアクションが可能なものがある。これらの場合は、コールバック関数を指定することができる。

<|{value}|slider|on_change=slider_moved|>

上記の例だと、スライダーを動かすたびにコールバック関数slider_moved()が呼ばれ、Stateオブジェクトが最初のパラメータとして渡される。

def slider_moved(state):
    state.data = compute_data(state.val)

Stateは個々のユーザを表すオブジェクト(つまりユーザごとに別に管理される)であり、ビジュアル要素とコードはこのStateを介して変数などのやり取りをする。上の例だと、スライダーには変数valが紐づけられているため、渡されてきたState内のvalにはスライダー操作後の値が入っており、これを取り出してcompute_dateに渡して処理、その結果をState内のdataに入れて、以下で表示するということになる。

<|{data}|chart|>

最後に以下でGUIサーバを起動する。

if __name__ == "__main__":
    Gui(page=page).run(title="Dynamic chart")
kun432kun432

ところで、公式ドキュメントのチュートリアルのメニューを見ていると、非常に内容が豊富に見える。

https://docs.taipy.io/en/latest/tutorials/

上でやった"Getting Started"は、"Fundamentals"、つまり基礎編の一番最初ということで、基礎編のチュートリアルは他にもある様子。

"Getting Started"で雰囲気はわかったが、もう少しどういったことができるのかを押さえておきたい。とりあえず"Fundamentals"を進めてみる。

kun432kun432

Tutorials - Fundamentals -Creating a Sales Dashboard

https://docs.taipy.io/en/latest/tutorials/articles/sales_dashboard/

このチュートリアルではシンプルなセールスダッシュボードアプリを作る。こういうもの。

以下の4つのステップで作っていく。

  1. Visual Elements
  2. Styling
  3. Charts
  4. Multipage

このチュートリアルではTaipy以外にplotlyも使うようなので、インストールしておく。

uv pip install plotly

データとして以下のデータセットを使用する

https://www.kaggle.com/datasets/rohitsahoo/sales-forecasting

上記のデータセットをダウンロードしておく。

wget https://raw.githubusercontent.com/Avaiga/taipy-course-gui/refs/heads/develop/data.csv
kun432kun432

1. Visual Elements

https://docs.taipy.io/en/latest/tutorials/articles/sales_dashboard/step_01/step_01/

今回はPythonコードでビジュアル要素を定義していく。まず最初にカテゴリのセレクタを作る。

sales_dashboard.py
from taipy.gui import Gui
import taipy.gui.builder as tgb
import pandas as pd

# データをpandasのデータフレームとして読み込む
data = pd.read_csv("data.csv")

# カテゴリの初期値
selected_category = "Furniture"

# カテゴリを選択するセレクタのカテゴリリストを作成
categories = list(data["Category"].unique())

# カテゴリを選択したら実行されるコールバック関数
def change_category(state):
    # 今は何もしないコードになっているが、このあと実装していく
    print("selected_category:", state.selected_category)
    return None

with tgb.Page() as page:
    # カテゴリを選択するセレクタ
    tgb.selector(
        value="{selected_category}",
        lov=categories,
        on_change=change_category
    )

Gui(page=page).run(
    title="売上",
    dark_mode=False,
    debug=True,        # デバッグモードを有効化(エラー発生時に詳細を表示)
    use_reloader=True, # ファイルを変更したら自動でリロード
    port=8080          # Macの場合は5000番は都合が悪いようなので変更した
)

ビジュアル要素の定義部分はここ。

with tgb.Page() as page:
    # カテゴリを選択するセレクタ
    tgb.selector(
        value="{selected_category}",
        lov=categories,
        on_c

セレクタはtgb.selector()で作成できる。変数は "{〜}"のように「カッコ+クォート」することによってStateで受け渡される変数として認識される。よってここでは{selected_category}がState変数となる。なお、lovはセレクタの全値のリストを指定するのだが、こちらは元のデータが変わらない限り、受け渡す必要はない。その場合にはカッコ+クォートなしで指定すると、State変数と認識されないため、受け渡されなくなる。

これらの値の初期値は以下でCSVを読み込んで作成している。

# データをpandasのデータフレームとして読み込む
data = pd.read_csv("data.csv")

# カテゴリの初期値
selected_category = "Furniture"

# カテゴリを選択するセレクタのカテゴリリストを作成
categories = list(data["Category"].unique())

さらに、このセレクタは変更時のコールバック変数が指定してある。

# カテゴリを選択したら実行されるコールバック関数
def change_category(state):
    # 今は何もしないコードになっているが、このあと実装していく
    print("selected_category:", state.selected_category)
    return None

現時点では何も行わないが、このあとここに処理を追加していく。とりあえずまずはセレクタが機能しているかを確認するためにprint文を追加してある。

アプリケーションとして実行するのが最後のところ。

Gui(page=page).run(
    title="売上",
    dark_mode=False,
    debug=True,        # デバッグモードを有効化(エラー発生時に詳細を表示)
    use_reloader=True, # ファイルを変更したら自動でリロード
    port=8080          # Macの場合は5000番は都合が悪いようなので変更
)

debug=Trueを指定することで、起動したコンソールでエラー発生時に詳細を表示することができたり、use_reloader=Trueでコード修正時のホットリロードが有効になるので、開発中は指定しておくと良さそう。なお、Macの場合だけかもしれないが、

  • use_reloader=Trueでサーバ側は自動でリロードされるが、ブラウザのリロードは必要
  • デフォルトだと5000番ポート(TaipyはFlaskを使用しているため)で起動するが、このポートはAirplayが使用するので、色々都合が悪い。別のポートを指定したほうが良さそう。

ということを補足しておく。

ここまでの状態で一旦起動してみる。

uv run sales_dashboard.py

カテゴリを選択してみる。

起動したコンソールを見ると以下のように選択したカテゴリが受け渡されているのがわかる。

出力
[2025-02-21 09:23:44.651][Taipy][INFO] 'allow_unsafe_werkzeug' has been set to True
[2025-02-21 09:23:44.651][Taipy][INFO] 'async_mode' parameter has been overridden to 'threading'. Using Flask built-in development server with debug mode
[2025-02-21 09:23:44.671][Taipy][INFO]  * Server starting on http://127.0.0.1:8080
 * Serving Flask app 'Taipy'
 * Debug mode: on
[2025-02-21 09:23:45.569][Taipy][INFO]  * Server reloaded on http://127.0.0.1:8080
selected_category: Technology

次にグラフを追加する。グラフはtgb.chart()を使う。

(snip)

with tgb.Page() as page:
    # カテゴリを選択するセレクタ
    tgb.selector(
        value="{selected_category}",
        lov=categories,
        on_change=change_category
    )

    # ***** 以下を追加 *****
    # グラフを表示
    tgb.chart(
        data="{chart_data}",
        x="State",
        y="Sales",
        type="bar",
        layout="{layout}",
    )

(snip)

chart_datalayoutというState変数が定義されている。これの初期値を作成する。

(snip)

# データをpandasのデータフレームとして読み込む
data = pd.read_csv("data.csv")

# ***** 以下を追加 *****
# 読み込んだデータを州別に集計して、グラフ用のデータを作成
chart_data = (
    data.groupby("State")["Sales"]
    .sum()
    .sort_values(ascending=False)
    .head(10)
    .reset_index()
)
# グラフのレイアウト(タイトル等)の設定
layout = {
    "xaxis": {"title": "州"},
    "yaxis": {"title": "収益(ドル)"},
    "title": "州別売上"
}
(snip)

以下のようにグラフが表示された。ただし、セレクタのコールバックは何もしていないので変化はしない。

そして、テーブルを追加する。

(snip)

with tgb.Page() as page:
    # カテゴリを選択するセレクタ
    tgb.selector(
        value="{selected_category}",
        lov=categories,
        on_change=change_category
    )

    # データをグラフで表示
    tgb.chart(
        data="{chart_data}",
        x="State",
        y="Sales",
        type="bar",
        layout="{layout}",
    )

    # ***** 以下を追加 *****
    # データをテーブルで表示
    tgb.table(data="{data}")

(snip)

テーブルはtgb.table()を使う。データは一番最初にCSVから読み込んだものをそのまま指定している。

これで以下のようになる。

ちょっとグラフとテーブルの間が詰まってしまっている。ビジュアル要素の中にはHTMLで要素を含めるtgb.html()があるのでこれを使って改行を入れる。

(snip)

    # データをグラフで表示
    tgb.chart(
        data="{chart_data}",
        x="State",
        y="Sales",
        type="bar",
        layout="{layout}",
    )

    # ***** 以下を追加 *****
    # 空行を入れる
    tgb.html("br")

    # データをテーブルで表示
    tgb.table(data="{data}")

(snip)

こんな感じでうまくスペースができている。

ではセレクタの変更により、グラフとテーブルを動的に変更するように、change_categoryを修正する。基本的に、変更はstate.変数 = 新しい値とするように書けばよい。

(snip)

# カテゴリを選択したら実行されるコールバック関数
def change_category(state):
    # テーブル用に、選択されたカテゴリのデータを取得
    state.data = data[data["Category"] == state.selected_category]

    # グラフ用に、選択されたカテゴリの州別集計データを作成
    state.chart_data = (
        state.data.groupby("State")["Sales"]
        .sum()
        .sort_values(ascending=False)
        .head(10)
        .reset_index()
    )

    # 選択されたカテゴリに合わせて、グラフのレイアウト(タイトル等)の設定
    state.layout = {
        "xaxis": {"title": "州"},
        "yaxis": {"title": "収益(ドル)"},
        "title": f"{state.selected_category}の州別売上"
    }

(snip)

では確認してみる。

選択したカテゴリごとにグラフとテーブルが更新されていればOK。

ここまでの全体コードを以下においておく。

sales_dashboard.pyの全体コード
sales_dashboad.py
from taipy.gui import Gui
import taipy.gui.builder as tgb
import pandas as pd

# データをpandasのデータフレームとして読み込む
data = pd.read_csv("data.csv")

# グラフのデータを作成
chart_data = (
    data.groupby("State")["Sales"]
    .sum()
    .sort_values(ascending=False)
    .head(10)
    .reset_index()
)

# グラフのレイアウト(タイトル等)の設定
layout = {
    "xaxis": {"title": "州"},
    "yaxis": {"title": "収益(ドル)"},
    "title": "州別売上"
}

# カテゴリの初期値  ※公式のコードに合わせているが、空のほうがいいかも
selected_category = "Furniture"

# カテゴリを選択するセレクタのカテゴリリストを作成
categories = list(data["Category"].unique())

# カテゴリを選択したら実行されるコールバック関数
def change_category(state):
    # テーブル用に、選択されたカテゴリのデータを取得
    state.data = data[data["Category"] == state.selected_category]

    # グラフ用に、選択されたカテゴリの州別集計データを作成
    state.chart_data = (
        state.data.groupby("State")["Sales"]
        .sum()
        .sort_values(ascending=False)
        .head(10)
        .reset_index()
    )

    # 選択されたカテゴリに合わせて、グラフのレイアウト(タイトル等)の設定
    state.layout = {
        "xaxis": {"title": "州"},
        "yaxis": {"title": "収益(ドル)"},
        "title": f"{state.selected_category}の州別売上"
    }

with tgb.Page() as page:
    # カテゴリを選択するセレクタ
    tgb.selector(
        value="{selected_category}",
        lov=categories,
        on_change=change_category
    )

    # グラフを表示
    tgb.chart(
        data="{chart_data}",
        x="State",
        y="Sales",
        type="bar",
        layout="{layout}",
    )

    # 空行を入れる
    tgb.html("br")

    # テーブルを表示
    tgb.table(data="{data}")


Gui(page=page).run(
    title="売上",
    dark_mode=False,
    debug=True,        # デバッグモードを有効化(エラー発生時に詳細を表示)
    use_reloader=True, # ファイルを変更したら自動でリロード
    port=8080          # Macの場合は5000番は都合が悪いようなので変更
)
kun432kun432

2. Styling

https://docs.taipy.io/en/latest/tutorials/articles/sales_dashboard/step_02/step_02/

Taipyでは、デフォルトのデザインからカスタマイズもできる。この章では、セレクタを増やして、かつ、レイアウトをカスタマイズする。前の章のコードを流用することもできるのだが、あえてイチからやるみたい。

ということで最初のコード。

sales_dashboard_2.py
from taipy.gui import Gui
import taipy.gui.builder as tgb
import pandas as pd

with tgb.Page() as page:
    with tgb.part(class_name="container"):
        tgb.text("# **州別** の売上", mode="md")

Gui(page=page).run(
    title="売上",
    dark_mode=False,
    debug=True,
    use_reloader=True,
    port=8080
)

Partを使うと、「クラス」を指定して、ビジュアル要素のスタイルを設定したりグルーピングを行う「コンテナ」を作成することができる。ここではcontainerというクラスを指定されているが、containerクラスはあらかじめ事前定義されたスタイルで、このPartに属するビジュアル要素の横幅を制限するものになる。

そしてまずテキストを設定するビジュアル要素textを配置して、Markdownでテキストを設定。

以下のような表示になる。

この文字列にCSSでスタイリングを追加する。Pythonファイルと同じ名前のCSSファイルを用意して、以下の内容を記述する。

sales_dashboard_2.py
strong,
b {
    font-weight: bold;
    color: var(--color-primary);
}

すると表示は以下のように変わる。

さらに新しいコンテナを追加する。

(snip)

with tgb.Page() as page:
    with tgb.part(class_name="container"):
        tgb.text("# **州別** の売上", mode="md")
    
    with tgb.part(class_name="card"):
        with tgb.layout(columns="1 2 1"):
            with tgb.part():
                tgb.text("日付フィルタ **開始日**", mode="md")
            with tgb.part():
                tgb.text("製品フィルタ **カテゴリ**", mode="md")
            with tgb.part(class_name="text-center"):
                tgb.button(
                    "適用",
                )

(snip)

以下のようになる。

cardクラスも事前定義されたクラスで、ビジュアル要素を白い矩形の中に配置するクラス。そしてtgb.layout()は列を構成でき、ここでは3つの列を"1:2:1"の比率で並べて、その配下にセレクタのラベルとなるテキストx2、選択したセレクタを適用するためのボタンを配置している。

これに日付セレクタとカテゴリセレクタを追加する。以下のように修正する。

(snip)
    with tgb.part(class_name="card"):
        with tgb.layout(columns="1 2 1"):
            with tgb.part():
                tgb.text("日付フィルタ **開始日**", mode="md")
                tgb.date("{start_date}")
                tgb.text("**終了日**", mode="md")
                tgb.date("{end_date}")
            with tgb.part():
                tgb.text("製品フィルタ **カテゴリ**", mode="md")
                tgb.selector(
                    value="{selected_category}",
                    lov="{categories}",
                    on_change=change_category,
                    dropdown=True,
                )
                tgb.text("製品フィルタ **サブカテゴリ**", mode="md")
                tgb.selector(
                    value="{selected_subcategory}",
                    lov="{subcategories}",
                    dropdown=True,
                )
            with tgb.part(class_name="text-center"):
                tgb.button(
                    "適用",
                    on_action=apply_changes,

(snip)

上記のままだと、カテゴリの変更時、フィルタ適用ボタン押下時のコールバックが存在せず、エラーでプロセスが落ちてしまう。とりあえず以下のように追加しておく。

def change_category(state):
    return None

def apply_changes(state):
    return None

以下のように、日付をカレンダーで指定でき、カテゴリをドロップダウンで指定できる、セレクタが追加される。ただしカテゴリのセレクタは値をまだ設定していないので機能しない。

ボタンにもスタイルを設定する。

(snip)
            with tgb.part(class_name="text-center"):
                tgb.button(
                    "適用",
                    class_name="plain apply_button",    # 追加
                    on_action=apply_changes,

(snip)

ボタンにはplainapply_buttonの2つのクラスを割り当てている。plainクラスはこれも事前定義されたクラスで、これによりボタンがオレンジ色になる。apply_buttonクラスはカスタムに定義したクラスで、このあと使う。

ボタンがオレンジ色になった。ただ、ボタンの位置がちょっと上すぎるので、apply_buttonクラスをCSSでスタイリングする。

.apply_button {
    margin-top: 158px;
}

またCSSにも事前定義されたクラスがあり、.taipy_buttonクラスは、Taipyで使用可能な全てのボタンに対して適用されるくらすとなっている。ここではボタンの横幅を少し広げる。

.taipy-button {
    width: 60%
}

あとはこれまでと同じように、データを読み出してState変数に割り当て、コールバック内でそれぞれアクションに応じて書き換えるようにすれば良い。

ということで全体コード。

sales_dashboard_2.py
from taipy.gui import Gui
import taipy.gui.builder as tgb
import pandas as pd

data = pd.read_csv("data.csv")
chart_data = (
    data.groupby("State")["Sales"]
    .sum()
    .sort_values(ascending=False)
    .head(10)
    .reset_index()
)

start_date = "2015-01-01"
start_date = pd.to_datetime(start_date)
end_date = "2018-12-31"
end_date = pd.to_datetime(end_date)

categories = list(data["Category"].unique())
selected_category = "Furniture"

selected_subcategory = "Bookcases"
subcategories = list(
    data[data["Category"] == selected_category]["Sub-Category"].unique()
)

layout = {
    "xaxis": {"title": "州"},
    "yaxis": {"title": "収益(ドル)"},
    "title": "州別売上"
}


def change_category(state):
    state.subcategories = list(
        data[data["Category"] == state.selected_category]["Sub-Category"].unique()
    )
    state.selected_subcategory = state.subcategories[0]


def apply_changes(state):
    new_data = data[
        (
            pd.to_datetime(data["Order Date"], format="%d/%m/%Y")
            >= pd.to_datetime(state.start_date)
        )
        & (
            pd.to_datetime(data["Order Date"], format="%d/%m/%Y")
            <= pd.to_datetime(state.end_date)
        )
    ]
    new_data = new_data[new_data["Category"] == state.selected_category]
    new_data = new_data[new_data["Sub-Category"] == state.selected_subcategory]
    state.data = new_data
    state.chart_data = (
        state.data.groupby("State")["Sales"]
        .sum()
        .sort_values(ascending=False)
        .head(10)
        .reset_index()
    )
    state.layout = {
        "xaxis": {"title": "州"},
        "yaxis": {"title": "収益(ドル)"},
        "title": f"{state.selected_category}の州別売上"
    }


with tgb.Page() as page:
    with tgb.part(class_name="container"):
        tgb.text("# **州別** の売上", mode="md")
    
    with tgb.part(class_name="card"):
        with tgb.layout(columns="1 2 1"):
            with tgb.part():
                tgb.text("日付フィルタ **開始日**", mode="md")
                tgb.date("{start_date}")
                tgb.text("**終了日**", mode="md")
                tgb.date("{end_date}")
            with tgb.part():
                tgb.text("製品フィルタ **カテゴリ**", mode="md")
                tgb.selector(
                    value="{selected_category}",
                    lov="{categories}",
                    on_change=change_category,
                    dropdown=True,
                )
                tgb.text("製品フィルタ **サブカテゴリ**", mode="md")
                tgb.selector(
                    value="{selected_subcategory}",
                    lov="{subcategories}",
                    dropdown=True,
                )
            with tgb.part(class_name="text-center"):
                tgb.button(
                    "適用",
                    class_name="plain apply_button",
                    on_action=apply_changes,
                )

    tgb.html("br")

    tgb.chart(
        data="{chart_data}",
        x="State",
        y="Sales",
        type="bar",
        layout="{layout}",
    )

    tgb.html("br")

    tgb.table(data="{data}")

Gui(page=page).run(
    title="売上",
    dark_mode=False,
    debug=True,
    use_reloader=True,
    port=8080
)

こんな感じで動作する。

kun432kun432

3. Charts

https://docs.taipy.io/en/latest/tutorials/articles/sales_dashboard/step_03/step_03/

ここではPlotlyを使って地図を使ったデータの可視化を追加する。可視化のコードは以下にすでに作成済みとなっている。

https://github.com/Avaiga/taipy-course-gui/blob/develop/4_charts/chart.py

このコードをダウンロードしておく。

wget https://raw.githubusercontent.com/Avaiga/taipy-course-gui/refs/heads/develop/4_charts/chart.py

1つ前の章のコードをそのまま使う。上のコードからgenerate_map関数をインポートし、データから地図プロットを生成する。

from taipy.gui import Gui
import taipy.gui.builder as tgb
import pandas as pd

# chart.pyからgenerate_map関数をインポート
from chart import generate_map

data = pd.read_csv("data.csv")

# generate_map関数にデータフレームを渡して地図プロットを取得
map_fig = generate_map(data)
(snip)

グラフ部分を少し修正。tgb.layoutでラップして、2列のカラムに分割し、左にこれまでのグラフ、右に生成した地図プロットを表示するようにする。

(snip)
    tgb.html("br")

    # 2列のカラムに分割する
    with tgb.layout(columns="2 3"):
        # 最初(左)のカラムに州別売上グラフを表示
        tgb.chart(
            data="{chart_data}",
            x="State",
            y="Sales",
            type="bar",
            layout="{layout}",
        )
        # 2番目(右)のカラムに地図プロットを表示
        tgb.chart(figure="{map_fig}")

    tgb.html("br")
(snip)

そしてフィルタ適用ボタンのコールバックでもデータを更新

def apply_changes(state):
    (snip)
    state.map_fig = generate_map(state.data)

(snip)

こういう感じになった。

kun432kun432

4. Multipage

https://docs.taipy.io/en/latest/tutorials/articles/sales_dashboard/step_04/step_04/

Taipyでは、Streamlitにあるようなマルチページを作ることもできる。1つ前で使用したコードを使う。

まず、ページで使用するアイコンデータをダウンロードする。

mkdir images
wget https://raw.githubusercontent.com/Avaiga/taipy-course-gui/refs/heads/develop/5_multipage/images/map.png -P images
wget https://raw.githubusercontent.com/Avaiga/taipy-course-gui/refs/heads/develop/5_multipage/images/person.png -P images

最初に起点となるルートページを定義する。ルートページのtgb.menuに紐づけられているコールバックもとりあえず空で定義しておく(じゃないとプロセスが落ちる)

(snip)
from taipy.gui import Gui, Icon, navigate    # 修正
import taipy.gui.builder as tgb
import pandas as pd
from chart import generate_map
(snip)

# メニュークリック時のコールバック関数
def menu_option_selected(state, action, info):
    return None

# ルートページの定義
with tgb.Page() as root_page:
    tgb.menu(
        label="Menu",
        lov=[
            ("page1", Icon("images/map.png", "売上")),
            ("page2", Icon("images/person.png", "顧客")),
        ],
        on_action=menu_option_selected,
    )

Gui(page=page).run(
(snip)

tgb.Page()を使ってページの定義をするのはこれまでと同じで、root_pageとして定義しているのが違うだけ。で、tgb.menu()を使うといわゆる「サイドメニュー」が作成できる。

サイドメニューに表示される個々のメニューとページの紐づけは以下のように指定する。

(page_url, Icon(icon_image_path, page_name))

今回のコードだと

  • 1ページ目
    • URL: page1
    • アイコン画像パス: images/map.png
    • ページ名: 売上
  • 2ページ目
    • URL: page2
    • アイコン画像パス: images/person.png
    • ページ名: 顧客

となる。

そして、on_action=menu_option_selectedで、個々のメニューがクリックされたときのコールバック関数を定義する。

1番目のページはすでに作成済みの州別売上ページを使うこととして、2番目のページを仮で作る。

# ルートページの定義
with tgb.Page() as root_page:
    tgb.menu(
        label="Menu",
        lov=[
            ("page1", Icon("images/map.png", "売上")),
            ("page2", Icon("images/person.png", "顧客")),
        ],
        on_action=menu_option_selected,
    )

# 2番目のページの定義
with tgb.Page() as page_2:
    tgb.text("# 顧客 **管理**", mode="md")
    tgb.button("ログアウト", class_name="plain")

Gui(page=page).run(
(snip)

そして、コールバックを修正して、アプリ起動部分を以下のように書き換える。

def menu_option_selected(state, action, info):
    page = info["args"][0]
    navigate(state, to=page)

(snip)

pages = {
    "/": root_page,
    "page1": page,
    "page2": page_2
}

Gui(pages=pages).run(    # page->pagesに書き換える
(snip)

ここちょっとややこしいので整理しておく。

まずTaipyでの各ページの定義は以下のように、pageroot_pagepage_2となっている。

with tgb.Page() as page:
    (snip)

with tgb.Page() as root_page:
    (snip)

with tgb.Page() as page_2:
    (snip)

で、ルートページで定義されているサブメニューは、「売上」というメニューには/page1、「顧客」というメニューには/page2というURLパスがリンク先になっている。

with tgb.Page() as root_page:
    tgb.menu(
        label="Menu",
        lov=[
            ("page1", Icon("images/map.png", "売上")),
            ("page2", Icon("images/person.png", "顧客")),
        ],
        on_action=menu_option_selected,
    )

/page1をクリックしたらpageが表示され、/page2をクリックしたらpage_2が表示され、デフォルトは/root_pageを表示されるように、ルーティングのマップを指定する。

pages = {
    "/": root_page,
    "page1": page,
    "page2": page_2
}

これをアプリが起動時に読み出してマルチページに対応させる。

Gui(pages=pages).run(
(snip)

実際にメニューをクリックした際にはmenu_option_selectedが呼ばれるが、infoにはpage1とかpage2といったURLパスが入っていて、これを拾ってnavigateでページを遷移させるという挙動になる。

def menu_option_selected(state, action, info):
    page = info["args"][0]
    navigate(state, to=page)

全体のコードを以下に記載する。

sales_dashboard_2.py
from taipy.gui import Gui, Icon, navigate    # 修正
import taipy.gui.builder as tgb
import pandas as pd
from chart import generate_map

data = pd.read_csv("data.csv")

map_fig = generate_map(data)

chart_data = (
    data.groupby("State")["Sales"]
    .sum()
    .sort_values(ascending=False)
    .head(10)
    .reset_index()
)

start_date = "2015-01-01"
start_date = pd.to_datetime(start_date)
end_date = "2018-12-31"
end_date = pd.to_datetime(end_date)

categories = list(data["Category"].unique())
selected_category = "Furniture"

selected_subcategory = "Bookcases"
subcategories = list(
    data[data["Category"] == selected_category]["Sub-Category"].unique()
)

layout = {
    "xaxis": {"title": "州"},
    "yaxis": {"title": "収益(ドル)"},
    "title": "州別売上"
}


def change_category(state):
    state.subcategories = list(
        data[data["Category"] == state.selected_category]["Sub-Category"].unique()
    )
    state.selected_subcategory = state.subcategories[0]


def apply_changes(state):
    new_data = data[
        (
            pd.to_datetime(data["Order Date"], format="%d/%m/%Y")
            >= pd.to_datetime(state.start_date)
        )
        & (
            pd.to_datetime(data["Order Date"], format="%d/%m/%Y")
            <= pd.to_datetime(state.end_date)
        )
    ]
    new_data = new_data[new_data["Category"] == state.selected_category]
    new_data = new_data[new_data["Sub-Category"] == state.selected_subcategory]
    state.data = new_data
    state.chart_data = (
        state.data.groupby("State")["Sales"]
        .sum()
        .sort_values(ascending=False)
        .head(10)
        .reset_index()
    )
    state.layout = {
        "xaxis": {"title": "州"},
        "yaxis": {"title": "収益(ドル)"},
        "title": f"{state.selected_category}の州別売上"
    }
    state.map_fig = generate_map(state.data)


with tgb.Page() as page:
    with tgb.part(class_name="container"):
        tgb.text("# **州別** の売上", mode="md")
    
    with tgb.part(class_name="card"):
        with tgb.layout(columns="1 2 1"):
            with tgb.part():
                tgb.text("日付フィルタ **開始日**", mode="md")
                tgb.date("{start_date}")
                tgb.text("**終了日**", mode="md")
                tgb.date("{end_date}")
            with tgb.part():
                tgb.text("製品フィルタ **カテゴリ**", mode="md")
                tgb.selector(
                    value="{selected_category}",
                    lov="{categories}",
                    on_change=change_category,
                    dropdown=True,
                )
                tgb.text("製品フィルタ **サブカテゴリ**", mode="md")
                tgb.selector(
                    value="{selected_subcategory}",
                    lov="{subcategories}",
                    dropdown=True,
                )
            with tgb.part(class_name="text-center"):
                tgb.button(
                    "適用",
                    class_name="plain apply_button",
                    on_action=apply_changes,
                )

    tgb.html("br")

    with tgb.layout(columns="2 3"):
        tgb.chart(
            data="{chart_data}",
            x="State",
            y="Sales",
            type="bar",
            layout="{layout}",
        )
        tgb.chart(figure="{map_fig}")

    tgb.html("br")

    tgb.table(data="{data}")


def menu_option_selected(state, action, info):
    page = info["args"][0]
    navigate(state, to=page)


with tgb.Page() as root_page:
    tgb.menu(
        label="Menu",
        lov=[
            ("page1", Icon("images/map.png", "Sales")),
            ("page2", Icon("images/person.png", "Account")),
        ],
        on_action=menu_option_selected,
    )

with tgb.Page() as page_2:
    tgb.text("# 顧客 **管理**", mode="md")
    tgb.button("ログアウト", class_name="plain")


pages = {
    "/": root_page,
    "page1": page,
    "page2": page_2
}


Gui(pages=pages).run(
    title="売上",
    dark_mode=False,
    debug=True,
    use_reloader=True,
    port=8080
)

動かしてみるとこんな感じでメニューが表示され、それぞれのメニューをクリックするとそれぞれのページが表示される。

また一番上の"Menu"をクリックすると、サイドメニューの幅を切り替えることができる。

kun432kun432

ここまでの所感

Streamlitに慣れていれば、基本的な考え方は流用できると思うので、それほど難しいものではないと思うし、レイアウトやデザインなんかはStreamlitよりもカスタマイズが効きそうな期待感がある。

ただ、個人的にちょっとイマイチだなと感じたのは

  • Streamlitのオートリロードだと、コードを修正するとサーバ側+ブラウザ側の両方をリロードしてくれるが、Taipyはサーバ側だけ。
  • Streamlitのオートリロードだと、エラーが起きてもプロセスが落ちるようなことはなく、画面にエラーが表示されるだけで、修正すれば元に戻る。Taipyの場合、全てではなさそうだが、一部のエラーはプロセスが落ちるのであらためて起動し直す必要がある。

あたりのところで、前者はイマイチなんだけどまあFlask自体そうなのだと思えばしょうがないと思えるが、後者は結構な面倒さを感じるところ。ここは逆にStreamlitでの開発のスムーズさをあらためて感じる部分でもあった。

あと、ドキュメントはしっかりしてると思うのだけど、サンプルコードで使用している変数名とかオブジェクト名の付け方がちょっとイマイチかなぁ・・・結構曖昧というか混同しそう・・・このあたりは自分で書く場合には少し意識したほうがいいかもしれない。

kun432kun432

Tutorials - Fundamentals - Scenario management overview

https://docs.taipy.io/en/latest/tutorials/articles/scenario_management_overview/

Taipyには「Scenario management 」と呼ばれる、データ処理パイプライン機能があり、以下のようなメリットがあるらしい。

  • 各パイプラインの実行記録により、KPIの時系列監視や異なる実行結果の比較、what-ifシナリオの評価が可能
  • パイプライン操作用のUIコンポーネントが提供され、入力・パラメータ選択、実行・追跡、結果の視覚化をサポート
  • 変更がないデータに対して不要な計算を回避し、効率的に処理を管理
  • 多くの人気データソースとの簡単な統合が可能
  • 並列計算をサポートし、処理速度とスケーラビリティを向上

サンプルとして挙げられているコードを実行すると以下のようなアプリになっている

過去の気温データから指定された日付の気温を推論する(といっても予測モデル等を使うわけではなく、単に同じ月日の平均値を出しているだけ)というようなもので、実際に動かしてみたのだが、

  • なんとなくジョブ管理っぽいことをしている風な雰囲気はある
  • シナリオ設定して実行すると保存され後から読み出せるような雰囲気はある

という感はあるので、いわゆる実験管理っぽいものを作れる、ということなのかな?と推測したのだが、とりあえず読んで始めてみたものの、

  • GUIでパイプラインのフローが可視化されているのだが、編集もできないし、各ノードの詳細を見たりもできない。単に表示されているだけ、というふうに思える。
  • アプリができることが少なすぎるので、メリット感がよくわからない

あたりが気になって、個人的には全くピンとこず、離脱・・・

この次のチュートリアルはこれらを踏まえたものになっているように見えるので、それをやればもっとしっくり来るのかな?と思いつつも、いまいちやる気になれないのでここはスルーすることにした。

https://docs.taipy.io/en/latest/tutorials/articles/complete_application/

kun432kun432

Tutorials - Fundamentals - Creating applications

ここはLLMを使ったチャットアプリのチュートリアルになっていて、シンプルなチャットアプリとRAGを使ったチャットアプリの2つが紹介されている。

https://docs.taipy.io/en/latest/tutorials/articles/chatbot/

https://docs.taipy.io/en/latest/tutorials/articles/rag_chatbot/

後者のRAGの方はちょっと面倒そうなので、前者のチャットアプリをやってみようかと思ったのだが、以下にデモがあるのでまず確認してみた。

https://demo-llm-chat.taipy.cloud/

んー、とりあえずよくあるチャットアプリを作るには、以下をクリアする必要がありそう。

  • IMEのENTER問題があるように思える(お約束)
  • ストリーミングには非対応っぽい

一旦やってみる。OpenAIを使う。

パッケージを追加

uv pip install openai

OpenAIのAPIキーを環境変数にセットしておく

export OPENAI_API_KEY="XXXXXXXXXX"

ではまずベースのコード。最低限のページを表示するようにしてある。

chat_sample.py
from taipy.gui import Gui, State, notify
import taipy.gui.builder as tgb

with tgb.Page() as page:
    tgb.text("# チャットのサンプル", mode="md")

if __name__ == "__main__":
    Gui(page=page).run(
        title="チャットのサンプル",
        dark_mode=False,
        debug=True,
        use_reloader=True,
        port=8080
    )

ここに処理を追加していく。

まずOpenAIに接続するためのクライアントを初期化。

(snip)
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

(snip)

チャットを構成するビジュアル要素として、会話履歴を表示するテーブルとメッセージを入力する入力フィールドを追加する。入力内容を送信するコールバック関数も空で一旦指定する。

(snip)
# メッセージを送信するコールバック関数
def send_message(state: State) -> None:
    return None

with tgb.Page() as page:
    tgb.text("# チャットのサンプル", mode="md")
    # 会話履歴を表示するテーブル
    tgb.table(
        "{conversation}",
        show_all=True
    )
    # ユーザーのメッセージを入力する入力フィールド
    tgb.input(
        "{current_user_message}",
        on_action=send_message,
        change_delay=-1,
        label="メッセージを入力",
        class_name="fullwidth"
    )

with tgb.Page() as page:
  (snip)

2つのState変数がある。conversationは会話の履歴、current_user_messageはユーザの入力テキストになる。これらの初期値を設定する。

(snip)
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# 会話履歴の初期値
conversation = {
    "会話": [
        "こんにちは。あなたの名前は?",
        "私はAIアシスタントです。今日はどんなことをお手伝いしましょうか?"
    ]
}
# ユーザーのメッセージの初期値
current_user_message = ""

(snip)

以下のように表示される。少しチャットアプリらしくなってきた。

次にメッセージの送信周り。

# コンテキストの初期値(システムプロンプトのような位置づけ)
context = """\
あなたは、日本語のAIアシスタントです。親切で、創造性があり、賢く、とてもフレンドリーです。
以下はあなたとユーザの会話履歴です。
Human: こんにちは。あなたの名前は?
AI: 私はAIアシスタントです。今日はどんなことをお手伝いしましょうか?
"""

# OpenAI APIを使用してレスポンスを取得する関数
def request(prompt: str) -> str:
    response = client.chat.completions.create(
        messages=[
            {
                "role": "user",
                "content": f"{prompt}",
            }
        ],
        model="gpt-4o-mini",
    )
    return response.choices[0].message.content


# メッセージを送信するコールバック関数
def send_message(state: State) -> None:
    # コンテキストの初期化
    if state.context == "":
        state.context = context
    # ユーザーのメッセージをAPIに送信してレスポンスを取得
    state.context += f"Human: \n {state.current_user_message}\n\n AI:"
    answer = request(state.context).replace("\n", "")
    # レスポンスをコンテキストに追加
    state.context += answer
    # 会話履歴を更新
    conv = state.conversation._dict.copy()
    conv["会話"] += [state.current_user_message, answer]
    state.conversation = conv
    # 入力フィールドをクリア
    state.current_user_message = ""

この部分は少しわかりにくいのだが、チャット履歴を個々のメッセージオブジェクトのリストとして扱う一般的な形式ではなくて、ユーザプロンプトに、指示・過去の会話履歴・ユーザの今回のメッセージをテキストとして投げるという仕組みになっている様子。

この段階で動かしてみるとこんな感じで会話ができていることを確認できる。

ただしこの状態だとユーザとLLMの区別がしにくいので、スタイリングを設定する。まず以下のCSSを作成して、ユーザとLLMのメッセージそれぞれ別のスタイルを指定。

chat_sample.css
.gpt_message td {
    margin-left: 30px;
    margin-bottom: 20px;
    margin-top: 20px;
    position: relative;
    display: inline-block;
    padding: 20px;
    background-color: #ff462b;
    border-radius: 20px;
    max-width: 80%;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
    font-size: large;
    color: white;
}

.user_message td {
    margin-right: 30px;
    margin-bottom: 20px;
    margin-top: 20px;
    position: relative;
    display: inline-block;
    padding: 20px;
    background-color: #140a1e;
    border-radius: 20px;
    max-width: 80%;
    float: right;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
    font-size: large;
    color: white;
}

テーブルにCSSを定義する関数を用意。

def style_conv(state: State, idx: int, row: int) -> str:
    if idx is None:
        return None
    elif idx % 2 == 0:
        return "user_message"
    else:
        return "gpt_message"

この関数をテーブルに割り当てる

(snip)
    # 会話履歴を表示するテーブル
    tgb.table(
        "{conversation}",
        show_all=True,
        row_class_name=style_conv  # ここを追加
    )
(snip)

こんな感じになる

ここまでの全体コード

from taipy.gui import Gui, State, notify
import taipy.gui.builder as tgb
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# 会話履歴の初期値
conversation = {
    "会話": [
        "あなたの名前は?",
        "私はAIのアシスタントです。今日はどんなことをお手伝いしましょうか?"
    ]
}
# ユーザーのメッセージの初期値
current_user_message = ""

# コンテキストの初期値
context = """\
あなたは、日本語のAIアシスタントです。親切で、創造性があり、賢く、とてもフレンドリーです。
以下はあなたとユーザの会話履歴です。
Human: こんにちは。あなたの名前は?
AI: 私はAIアシスタントです。今日はどんなことをお手伝いしましょうか?
"""

# OpenAI APIを使用してレスポンスを取得する関数
def request(prompt: str) -> str:
    response = client.chat.completions.create(
        messages=[
            {
                "role": "user",
                "content": f"{prompt}",
            }
        ],
        model="gpt-4o-mini",
    )
    return response.choices[0].message.content


# メッセージを送信するコールバック関数
def send_message(state: State) -> None:
    # ユーザーのメッセージをコンテキストに追加
    if state.context == "":
        state.context = context
    state.context += f"Human: \n {state.current_user_message}\n\n AI:"
    # ユーザーのメッセージをAPIに送信してレスポンスを取得
    answer = request(state.context).replace("\n", "")
    # レスポンスをコンテキストに追加
    state.context += answer
    # 会話履歴を更新
    conv = state.conversation._dict.copy()
    conv["会話"] += [state.current_user_message, answer]
    state.conversation = conv
    # 入力フィールドをクリア
    state.current_user_message = ""


def style_conv(state: State, idx: int, row: int) -> str:
    if idx is None:
        return None
    elif idx % 2 == 0:
        return "user_message"
    else:
        return "gpt_message"


with tgb.Page() as page:
    tgb.text("# チャットのサンプル", mode="md")
    # 会話履歴を表示するテーブル
    tgb.table(
        "{conversation}",
        show_all=True,
        row_class_name=style_conv
    )
    # ユーザーのメッセージを入力する入力フィールド
    tgb.input(
        "{current_user_message}",
        on_action=send_message,
        change_delay=-1,
        label="メッセージを入力",
        class_name="fullwidth"
    )

if __name__ == "__main__":
    Gui(page=page).run(
        title="チャットのサンプル",
        dark_mode=False,
        debug=True,
        use_reloader=True,
        port=8080
    )

公式のサンプルコードは、上記の内容にいろいろな機能追加を施したより複雑なものとなっているので、興味があれば。


この章はいろいろ不満を感じる内容だった

  • チュートリアルとサンプルコードが乖離してるのは正直不親切
  • チャット部分の実装が直感的ではない
    • システムプロンプトを使ったメッセージ履歴にしていない
    • 会話履歴の表示をテーブルで実装していて、ユーザ・LLMのそれぞれのメッセージを自前でスタイリングしないといけない
    • メッセージ送信部分はブラッシュアップの余地があるが、メッセージ表示はテーブルで頑張るしかなさそう

このあたりはチャット専用のコンポーネントがあるStreamlitに比べるとかなり物足りなさがある。

kun432kun432

Tutorials - Fundamentals - User Roles and Permissions

https://docs.taipy.io/en/latest/tutorials/articles/user_management/

ユーザの権限や認証などのユーザ管理について。以下に認証認可についてのドキュメントもあるのだが、このあたりはTaipy Enterpriseエディションのみとなっている模様・・・・

https://docs.taipy.io/en/latest/userman/advanced_features/auth/

簡易なものであれば自前で実装できそうな気はするけど、面倒ではある。

kun432kun432

まとめ

一通りFundamentalsのチュートリアルを終わらせたが、後半に行くにつれて少し不満が募る内容だった。データアプリ作成ツールとしての触りは悪くなかったのだが、

  • シナリオマネージメントはやや難易度が高く、イマイチメリット感を感じなかった。全部Taipyで書かなくても、なにかしらのツールと連携するやり方があるのでは?(W&Bとか)
  • チャットアプリ作成ツールとしては、正直プリミティブな実装でちょっと面倒。

このあたりを踏まえると、Taipyが合うのはやはりデータアプリなんだろうなというふうに個人的には感じた。以下のようなケースであればいいかもしれない。

  • Streamlitの見た目やカスタマイズ性に不満がある
  • 目的はチャットアプリではなく、データアプリの場合

あくまでも個人&チュートリアルだけの感想なのであしからず。

このスクラップは1ヶ月前にクローズされました
ログインするとコメントできます