ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

2026-09-04

QQuickWidget でトレンドチャート 〜 PySide6

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。

QChart を含む QtCharts モジュールは、Qt 6.10 / PySide6 6.10 以降、公式に非推奨となりました[1] [2]

従来 QChart で作成していたプロットを(Matplotlib や PyQtGraph を利用せずに)新しいやり方で作成できたので、前回に引き続き、備忘録的にサンプルを紹介します。

今回は、折れ線グラフです。時間の経過に伴うデータの変化や傾向(トレンド)を表すことを意識して、トレンドチャートと呼んでいます。

下記の環境で動作確認をしています。

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.7
PySide6 6.11.2
NumPy 2.5.2

トレンドチャートのサンプル

実行例

まずは、実行例を示します。乱数を生成してつなげてプロットしただけのサンプルです。

qt_quickwidget_trend.py の実行例

Python のコード

ひとつのファイルにまとめたコードを示しました。

import sys

import numpy as np
from PySide6.QtCore import Qt
from PySide6.QtGraphs import (
    QGraphsTheme,
    QLineSeries,
    QValueAxis,
)
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QStyle,
)


class MyTrend(QQuickWidget):
    def __init__(self):
        super().__init__()
        self.setResizeMode(
            QQuickWidget.ResizeMode.SizeRootObjectToView
        )
        self.setFixedSize(600, 300)

        # --------------------------------
        # Theme
        # --------------------------------
        self.theme = theme = QGraphsTheme()
        grid = theme.grid()
        grid.setMainWidth(1.0)
        grid.setSubWidth(0.5)
        theme.setGrid(grid)

        # --------------------------------
        # Axes
        # --------------------------------
        self.axis_x = axis_x = QValueAxis()
        axis_x.setRange(0, 100)

        self.axis_y = axis_y = QValueAxis()
        axis_y.setRange(0, 1)
        axis_y.setTitleText("Value")

        # --------------------------------
        # Line series
        # --------------------------------
        self.series = series = QLineSeries()
        series.setAxisX(axis_x)
        series.setAxisY(axis_y)

        # 線の太さと色
        series.setWidth(1.0)
        series.setColor(Qt.GlobalColor.green)

        # GraphsView に渡すプロパティ
        self.setInitialProperties({
            "theme": theme,
            "axisX": axis_x,
            "axisY": axis_y,
            "seriesList": [series],
        })

        # QtGraphs の GraphsView をロード
        self.loadFromModule(
            "QtGraphs",
            "GraphsView",
        )

    def append_data(self, x: float, y: float):
        self.series.append(x, y)


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMenuButton
        )
        self.setWindowIcon(icon)
        self.setWindowTitle("Trend Chart")

        # --------------------------------
        # QQuickWidget
        # --------------------------------
        trend = MyTrend()
        self.setCentralWidget(trend)

        arr_y = np.random.random(100)
        for i, y in enumerate(arr_y):
            trend.append_data(i, y)


def main():
    app = QApplication(sys.argv)
    win = Example()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

プログラムの流れ

このプログラムでは、QQuickWidget を継承した MyTrend クラスを作成し、Qt GraphsQLineSeries を使って折れ線グラフを表示しています。MyTrend の初期化時に表示サイズを 600×300 に固定し、グラフのテーマ、グリッド線、X軸とY軸、そして折れ線グラフ用の系列を順番に設定します。

テーマには QGraphsTheme を使用し、グリッド線についてはメインの線を 1 ピクセル、サブの線を 0.5 ピクセルに設定しています。X軸とY軸には QValueAxis を使用し、X軸を 0 ~ 100、Y軸を 0 ~ 1 の範囲に設定しています。また、Y軸には Valueというタイトルを設定しています。

グラフのデータを保持する QLineSeries を作成し、先ほど作成した X軸 と Y軸 を割り当てます。折れ線の太さを 1 ピクセル、色を緑色に設定した後、テーマ、軸、系列を GraphsView に渡すための初期プロパティとして設定します。その後、QtGraphs モジュールの GraphsView をロードすることで、グラフが表示される状態になります。

MyTrend には append_data() メソッドを用意しており、X座標 と Y座標を受け取って QLineSeries に追加します。これによって、グラフそのものを管理する MyTrend と、そこへデータを供給する側を分離しています。


Example クラスでは MyTrend をメインウィンドウの中央ウィジェットとして配置し、NumPyrandom.random() を使って 100個 のランダムな値を生成します。生成した値を enumerate() で順番に取り出し、インデックスを X座標、ランダムな値を Y座標 として append_data() に渡します。

その結果、0 ~ 99 の X座標 に対して 0 ~ 1 の範囲でランダムに生成された 100個 のデータが QLineSeries に追加され、Qt Graphs によって折れ線グラフとして描画されます。

最後に main()QApplication を生成し、メインウィンドウを表示してイベントループを開始します。

参考サイト

  1. Qt Charts C++ Classes | Qt Charts
  2. Obsolete Members for QChart | Qt Charts
  3. PySide6.QtGraphs.QGraphsTheme - Qt for Python
  4. PySide6.QtGraphs.QLineSeries - Qt for Python
  5. PySide6.QtGraphs.QValueAxis - Qt for Python
  6. PySide6.QtQuickWidgets.QQuickWidget - Qt for Python

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-09-03

QQuickWidget で散布図 〜 PySide6

PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。

QChart を含む QtCharts モジュールは、Qt 6.10 / PySide6 6.10 以降、公式に非推奨となりました[1] [2]

従来 QChart で作成していたプロットを(Matplotlib や PyQtGraph を利用せずに)新しいやり方で作成できたので、備忘録的にサンプルを紹介します。現時点では自由に使いこなせるほど理解ができているとは言えないので、より良い方法が判れば、その都度、投稿していく予定です。

下記の環境で動作確認をしています。

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.7
PySide6 6.11.2
NumPy 2.5.2

散布図のサンプル

実行例

まずは、実行例を示します。乱数を生成して散布図にプロットしただけのサンプルです。

qt_quickwidget_scatter.py の実行例

Python のコード

ひとつのファイルにまとめたコードを示しました。

import sys

import numpy as np
from PySide6.QtCore import QUrl
from PySide6.QtGraphs import (
    QGraphsTheme,
    QScatterSeries,
    QValueAxis,
)
from PySide6.QtQml import QQmlComponent, QQmlEngine
from PySide6.QtQuickWidgets import QQuickWidget
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QStyle,
)


class MyScatter(QQuickWidget):
    def __init__(self):
        super().__init__()
        self.setResizeMode(
            QQuickWidget.ResizeMode.SizeRootObjectToView
        )
        self.setFixedSize(600, 600)

        # --------------------------------
        # Theme
        # --------------------------------
        self.theme = theme = QGraphsTheme()
        grid = theme.grid()
        grid.setMainWidth(1.0)
        grid.setSubWidth(0.5)
        theme.setGrid(grid)

        # --------------------------------
        # Axes
        # --------------------------------
        self.axis_x = axis_x = QValueAxis()
        axis_x.setRange(0, 1)

        self.axis_y = axis_y = QValueAxis()
        axis_y.setRange(0, 1)

        # --------------------------------
        # Scatter series
        # --------------------------------
        self.series = series = QScatterSeries()
        series.setAxisX(axis_x)
        series.setAxisY(axis_y)

        # ----------------------
        # QMLで描画ポイントを修飾
        # ----------------------
        qml = """
        import QtQuick

        Rectangle {
            width: 5
            height: 5
            radius: width / 2
        }
        """
        self.engine = QQmlEngine()
        self.point_delegate = QQmlComponent(self.engine)
        self.point_delegate.setData(qml.encode(), QUrl())

        if self.point_delegate.isError():
            print(self.point_delegate.errors())
        else:
            series.setPointDelegate(self.point_delegate)

        # GraphsView に渡すプロパティ
        self.setInitialProperties({
            "theme": theme,
            "axisX": axis_x,
            "axisY": axis_y,
            "seriesList": [series],
        })

        # QtGraphs の GraphsView をロード
        self.loadFromModule(
            "QtGraphs",
            "GraphsView",
        )

    def append_data(self, x: float, y: float):
        self.series.append(x, y)


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMenuButton
        )
        self.setWindowIcon(icon)
        self.setWindowTitle("Scatter Chart")

        # --------------------------------
        # QQuickWidget
        # --------------------------------
        scatter = MyScatter()
        self.setCentralWidget(scatter)

        arr_xy = np.random.random(size=(200, 2))
        for x, y in arr_xy:
            scatter.append_data(x, y)


def main():
    app = QApplication(sys.argv)
    win = Example()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

プログラムの流れ

まず main() が呼び出され、Qt アプリケーションの実行環境が作られます。その後、Example クラスのウィンドウが生成されます。

Example の初期化では、まずメインウィンドウのアイコンとタイトルを設定します。続いて MyScatter のインスタンスを生成し、メインウィンドウの中央ウィジェットに設定します。

MyScatterQQuickWidget を継承しています。MyScatter の初期化では、

  1. 最初にウィジェットのリサイズ方法とサイズを設定します。
  2. 次に、グラフ全体の外観を管理する QGraphsTheme を生成します。このテーマからグリッド線の設定を取得し、主グリッド線と補助グリッド線の線幅を変更してから、テーマへ戻します。
  3. 続いて X 軸と Y 軸をそれぞれ QValueAxis として生成し、両方とも 0 から 1 の範囲に設定します。
  4. その後、散布図のデータを保持する QScatterSeries を生成します。ここで X 軸と Y 軸をこのシリーズに関連付けます。
  5. 次に、散布図の各データポイントをどのように描画するかを設定します。今回は QML を外部ファイルにせず、Python の文字列として Rectangle を定義しています。この Rectangle は幅と高さを 5 にし、角を丸めることで円形のマーカーになります。
  6. この QML を実際に利用するために QQmlEngineQQmlComponent を生成し、QML の文字列を QQmlComponent に読み込ませます。QML にエラーがなければ、そのコンポーネントを QScatterSeriespointDelegate に設定します。
  7. グラフを構成するオブジェクトの準備が終わったら、テーマ、X 軸、Y 軸、散布図シリーズを GraphsView の初期プロパティとして登録します。
  8. 最後に loadFromModule() を呼び出し、Qt GraphsGraphsView をロードします。この順序によって、先ほど準備したテーマ、軸、シリーズが GraphsView に初期状態として渡されます。

ここまでで MyScatter の準備が完了します。

Example に戻ると、MyScatter が中央ウィジェットとして配置された後、NumPy を使って 200 個のランダムな X・Y 座標を生成します。それぞれの座標について MyScatterappend_data() を呼び出し、QScatterSeries にデータポイントを追加します。

データが追加されると、Qt Graphs がそれぞれのポイントを描画します。その際、先ほど設定した pointDelegate が使われるため、通常の散布点ではなく、QML の Rectangle によって定義された直径 5 程度の円として表示されます。

最後に Example のウィンドウを表示し、Qt のイベントループを開始します。以降は Qt が画面の描画やイベント処理を担当し、生成された 200 個のデータポイントが散布図として表示されます。

参考サイト

  1. Qt Charts C++ Classes | Qt Charts
  2. Obsolete Members for QChart | Qt Charts
  3. PySide6.QtGraphs.QGraphsTheme - Qt for Python
  4. PySide6.QtGraphs.QScatterSeries - Qt for Python
  5. PySide6.QtGraphs.QValueAxis - Qt for Python
  6. PySide6.QtQml.QQmlComponent - Qt for Python
  7. PySide6.QtQml.QQmlEngine - Qt for Python
  8. PySide6.QtQuickWidgets.QQuickWidget - Qt for Python

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-08-04

【備忘録】Python の @dataclass

Python の @dataclass は、データを保持するクラスを簡潔に記述するためのデコレータです [1]。通常であれば自分で実装する必要がある __init__()__repr__()__eq__() などの定型コードを自動生成してくれるため、コード量を大幅に減らすことができます。

@dataclass デコレータの利用例

通常のクラスであれば、次のようにコンストラクタを実装する必要があります。

class Config:
    def __init__(self):
        self.host = "localhost"
        self.port = 8080
        self.timeout = 30

一方、@dataclass を利用すると、次のようにメンバー変数(ここでは「フィールド」と呼びます)を宣言するだけで同じようなクラスを作成できます。

from dataclasses import dataclass

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    timeout: int = 30

@dataclass は、フィールドの宣言から __init__() を自動生成してくれるため、自分でコンストラクタを書く必要がありません。また、オブジェクトの内容を分かりやすく表示する __repr__() や、オブジェクト同士を比較する __eq__() なども自動的に用意されます。

このサンプルでは 3 つのフィールドだけなので、ありがたみが分かりにくいのですが、何十ものフィールドを扱うときには管理が楽なので、@dataclass デコレータを利用しています。

データクラス という名前から、メソッドを持たない単純なクラスを想像しがちですが、実際には通常のクラスとほぼ同じようにメソッドを追加できます。そのため、データだけでなく、そのデータに関連する処理も自然に記述できます。

今回は、@dataclass を利用する際によく悩む「他のクラスのインスタンスをどこで生成するか」について、__post_init__() を使った方法を備忘録としてまとめました。

他のインスタンスを初期化する方法

@dataclass デコレータを付けたクラスでは、__init__() が自動生成されるため、他のクラスのインスタンスを生成したい場合には __post_init__() を利用します。

from dataclasses import dataclass, field

class Processor:
    def __init__(self, size: int):
        self.size = size

@dataclass
class Config:
    buffer_size: int = 1024

    processor: Processor = field(init=False)

    def __post_init__(self):
        self.processor = Processor(size=self.buffer_size)

ポイント

  • field(init=False) を指定すると、そのフィールドは @dataclass が生成する __init__() の引数に含まれません。
  • __post_init__()@dataclass が生成した __init__() の直後に呼び出されます。
  • そのため、この例のように buffer_size の値を引数にして Processor のインスタンスを生成することができます。

このパターンを覚えておくと、@dataclass を利用しながら、通常のクラスと同じように他のオブジェクトを保持・初期化できるようになります。

参考サイト

  1. dataclasses --- データクラス — Python ドキュメント

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-06-20

モジュールを動的にインポート 〜 Python 〜

Python アプリを作っていると、動的にクラスをインポートしたいニーズがときどきあります。

覚えた機能について今後も使う可能性があれば、その時になってあれこれ調べ直さなくて済むように、簡単なサンプルを作成して本ブログに記事にまとめています。動的にクラスをインポートについても、何年か前に記事を書いていました [1]。しかし、読み直してみると、今ひとつよく解らなかったので、一般的な用途に合うようにサンプルを作り直しました。

下記の環境で動作確認をしています。

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.5
PySide6 6.11.1

プラグイン用サンプル

まず、動作確認用にプラグイン用サンプルを用意します。プロジェクト内に plugins というディレクトリを作り、テンプレート用の抽象クラスを記述した abstract.pyPluginTemplate クラス)と、このクラスを継承して実装した simple_1.pysimple_2.py を用意します。

plugins/
├ abstract.py
├ simple_1.py
└ simple_2.py
from abc import ABC, abstractmethod


class PluginTemplate(ABC):
    NAME = "template"

    @abstractmethod
    def run(self) -> None: ...
from plugins.abstract import PluginTemplate


class Plugin(PluginTemplate):
    NAME = "simple 1"

    def run(self) -> None:
        print(self.NAME)
from plugins.abstract import PluginTemplate


class Plugin(PluginTemplate):
    NAME = "simple 2"

    def run(self) -> None:
        print(self.NAME)

PluginTemplate クラスを継承して実装した simple_1.pysimple_2.py のクラス名は、ひとまず Plugin に統一しています。

クラス一覧をインポートする関数

指定したパス、パッケージ名、継承したクラス名に合致するモジュールをロードする関数 load_plugins です。

import importlib
import inspect
import pkgutil


def load_plugins(
        path_plugin: str,
        package_name: str,
        plugin_base_class: type
) -> dict[str, type]:
    # NOTE:
    # 現在はプロジェクト配下のパッケージのみを対象としているため、
    # importlib.import_module() を利用している。
    #
    # 将来的にユーザーが任意のディレクトリへ配置した外部プラグインを
    # 読み込む場合は、sys.path の追加、または
    # importlib.util.spec_from_file_location() を利用した
    # ファイルパスベースのロード方式を検討すること。
    dict_plugin = {}
    for _, module_name, _ in pkgutil.iter_modules([path_plugin]):
        module = importlib.import_module(f"{package_name}.{module_name}")
        for _, cls in inspect.getmembers(module, inspect.isclass):
            if issubclass(cls, plugin_base_class) and cls is not plugin_base_class:
                dict_plugin[cls.NAME] = cls
    return dict_plugin

引数 plugin_base_class に指定した抽象クラス(この例では PluginTemplate)を継承したクラス cls のみを辞書に cls.NAME をキーに登録して返す関数です。

テスト用 GUI サンプル

動的にクラスをインポートするプラグインの機能を確認する PySide6 の GUI サンプルです。

import sys

from PySide6.QtGui import QAction
from PySide6.QtWidgets import (
    QApplication,
    QComboBox,
    QMainWindow,
    QStyle,
    QToolBar,
)

from load_plugins import load_plugins
from plugins.abstract import PluginTemplate


class Example(QMainWindow):
    def __init__(self):
        super().__init__()

        # プラグインの一覧を取得
        plugins: dict[str, type] = load_plugins(
            path_plugin="./plugins",
            package_name="plugins",
            plugin_base_class=PluginTemplate
        )

        self.setWindowTitle("Plugin Sample")

        toolbar = QToolBar()
        self.addToolBar(toolbar)

        self.combo = combo = QComboBox()
        for key in sorted(plugins.keys()):
            cls = plugins[key]
            combo.addItem(key, cls)
        toolbar.addWidget(combo)

        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_MediaPlay
        )
        action_play = QAction(self)
        action_play.setIcon(icon)
        action_play.triggered.connect(self.on_play)
        toolbar.addAction(action_play)

    def on_play(self):
        cls = self.combo.currentData()
        obj = cls()
        obj.run()


def main():
    app = QApplication(sys.argv)
    ex = Example()
    ex.show()
    sys.exit(app.exec())


if __name__ == '__main__':
    main()

サンプルはコンボボックスに動的に読み込んだクラスの NAME の一覧を表示しています。隣の ▶ ボタンをクリックすると、コンボボックスに表示されている NAME に対応するクラスのインスタンスを生成して、run メソッドを実行します。

qt_plugin_sample.py の実行例
simple 1
simple 2

参考サイト

  1. bitWalk's: プラグイン・ウィジェットを扱う ~ PySide6 ~ [2023-02-07]
  2. importlib --- import の実装 — Python ドキュメント
  3. inspect --- 活動中のオブジェクトを調査する — Python ドキュメント
  4. pkgutil --- パッケージ拡張ユーティリティ — Python ドキュメント

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-06-16

NavigationToolbar2QT のカスタマイズ 〜 Matplotlib 〜

Matplotlib は、Python と NumPy のためのプロットライブラリです。Tkinter、wxPython、Qt、GTK のような汎用 GUI ツールキットを使ったアプリケーションにプロットを埋め込むためのオブジェクト指向 API を提供しています。

Wikipedia より引用、翻訳

PySide6 の GUI で Matplotlib のチャートを扱うときに利用する、ズームや保存などの便利な機能を提供するナビゲーション・ツールバー NavigationToolbar2QT について、ユーザー定義のボタンを追加する方法(+α)をまとめました。

下記の環境で動作確認をしています。

Fedora Linux 44
KDE Plasma Desktop x86_64
Python 3.14.5
matplotlib 3.11.0
pyside6 6.11.1
seaborn 0.13.2

実行例

サンプルとして正規分布の乱数のヒストグラムを表示していますが、今回のテーマは、このチャートの下に表示されているナビゲーション・ツールバーのカスタマイズです。

ナビゲーション・ツールバーのカスタマイズ例として、「保存」ボタンの右側に「Qt」アイコン (SP_TitleBarMenuButton) を表示しています。

qt_matplotlib/toolbar.py の実行例

NavigationToolbar2QT を継承

NavigationToolbar2QT を継承して「Qt」アイコン (SP_TitleBarMenuButton)(ボタン)を追加しています。

from matplotlib.backends.backend_qtagg import (
    NavigationToolbar2QT as NavigationToolbar,
)

class MyNavToolbar(NavigationToolbar):
    def __init__(self, canvas: FigureCanvas):
        super().__init__(canvas)
        user_action = QAction("User", self)
        icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMenuButton
        )
        user_action.setIcon(icon)
        user_action.triggered.connect(self.on_user_action)

        actions = self.actions()

        n = len(actions)
        self.insertAction(actions[n - 1], user_action)

    def on_user_action(self):
        # Zoomモードなら解除
        if self._actions["zoom"].isChecked():
            self._actions["zoom"].trigger()
        # Panモードなら解除
        if self._actions["pan"].isChecked():
            self._actions["pan"].trigger()

        print("User button clicked")

「Qt」アイコン (SP_TitleBarMenuButton)(ボタン)をクリックすると、on_user_action メソッドが実行されて文字列が標準出力されますが、その他の機能として、ズームボタン self._actions["zoom"] あるいはパン(移動)ボタン self._actions["pan"] が選択状態であれば解除するようにしています。

サンプル・コード

サンプルコード qt_matplotlib_toolbar.py を示しました。

qt_matplotlib/toolbar.py

参考サイト

  1. Embedding in Qt — Matplotlib documentation

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加

2026-05-08

【備忘録】product による多重ループ 〜 Python

このところ、利用するプログラミング言語は、すっかり Python だけになってしまいました。しかし、Python の入門書で網羅的に勉強してこなかったツケを感じています。あるデータの集計方法について、生成 AI に教えてもらった内容があまりに素晴らしくて悔しい思いをしました。いまだに Python の標準ライブラリを使いこなせないのは恥ずかしいかぎりです。

生成 AI に教えてもらってその場の対応だけで満足してしまうと、後日、同じような場面で同じような質問を生成 AI に尋ねて、もっと悔しい思いをするのは明白なので、忘れないように事象を単純化して備忘録にしました。

今回の目的
  • 完全実施要因計画 (Full Factorial Design) の実験データを各実験因子毎に集計したい。
    • ただし、実験因子候補のパラメータは多数あり、実用上、実験因子にするのはその一部で、実験の度に実験因子や実験水準を変更。
    • 実験因子をリストで指定するだけで、実験水準を調べて実験因子の水準の組み合わせ毎に集計したい。

下記の OS 環境で動作確認をしています。

Fedora Linux 44
Workstation Edition x86_64
Python 3.14.4
jupyterlab 4.5.7

単純化した問題

問題を単純化して Jupyter Lab 上で集計する例をまとめました。

ライブラリのインポート

まず、使用するライブラリをまとめてインポートします。

import operator
from functools import reduce
from itertools import product

import pandas as pd

実験データの読み込み

実験データ(サンプル)は、下記からダウンロードできます。

sample_doe.zip
# 実験結果を読み込む
file = "sample_doe.zip"
df = pd.read_csv(file)
df

実験因子は A と B で、それぞれ2水準です。Y は結果(特性値)です。

BLK は実験の実施単位(ブロック因子)です。仮に日付と見なせば、3日間、完全実施要因計画 (Full Factorial Design) の(ランダマイズ無しに)実験を繰り返したことになります。

実験因子の指定と実験水準の確認

実験因子の指定は仕方がないとしても、実験水準の確認はプログラムで調べます。

# 実験因子
list_factor = ["A", "B"]
# 各実験因子毎の実験水準
levels = [sorted(list(df[factor].unique())) for factor in list_factor]
levels
[[np.int64(1), np.int64(2)], [np.int64(1), np.int64(2)]]

サンプルを単純化しているので触れませんが、現実に使っている実験因子+結果のデータは、実験因子に指定していないパラメータ列も含んでいるので、水準が複数あるパラメータを実験因子として抽出するようにしています。

集計処理

集計ループの部分が、今回の目的になります。itertools.product を利用すると各リストの要素の組み合わせをタプルで取得できます。

# 各因子、各実験水準毎に平均値を集計
rows = []
for combo in product(*levels):
    condition = dict(zip(list_factor, combo))
    mask = reduce(operator.and_, (df[k] == v for k, v in condition.items()))
    avg = df[mask]["Y"].mean()
    row = {**condition, "avg": avg}
    rows.append(row)

rows
[{'A': np.int64(1), 'B': np.int64(1), 'avg': np.float64(0.38033333333333336)},
 {'A': np.int64(1), 'B': np.int64(2), 'avg': np.float64(0.5573333333333333)},
 {'A': np.int64(2), 'B': np.int64(1), 'avg': np.float64(0.701)},
 {'A': np.int64(2), 'B': np.int64(2), 'avg': np.float64(0.7893333333333333)}]

ループを抜けた後ですが、気になる変数の内容を確認します。

combo
(np.int64(2), np.int64(2))
condition
{'A': np.int64(2), 'B': np.int64(2)}
mask
0     False
1     False
2     False
3      True
4     False
5     False
6     False
7      True
8     False
9     False
10    False
11     True
dtype: bool

集計結果をデータフレーム化

最後に、rows をデータフレームにします。

df_summary = pd.DataFrame(rows)
# 行、列が長い時に省略されないように一時的に最大設定を解除
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
    print(df_summary)
   A  B       avg
0  1  1  0.380333
1  1  2  0.557333
2  2  1  0.701000
3  2  2  0.789333

まとめ

今回使用した product は、Python のドキュメント [2] によると、「デカルト積、ネストしたforループと等価」とあります。Python のコーディングではいつもお世話になっているサイト note.nkmk.me にも、profuct についてサンプル付きで紹介されています [4]

しかし、問題意識が無い状態で読んでも心に響きません。心に響かなければ記憶にも残らないので、いざ必要な場面に遭遇しても結びつかず、泥臭いやり方でコーディングをする愚を冒します。

今回は実験因子が A と B の2つだけなので、product のありがたみが薄いのですが、因子数が増えても同じループで処理できるので、とても助かります。あやうく、因子数に応じた多重ループを用意してしまうところでした。

生成 AI に尋ねると、自分が知らなかったこと、記憶にあるけど活用できないことをぐいぐい活用していて驚くことがあります。これは、単に自分がノウハウを持っていないことに他ならないので、コーディングが面倒だと感じたら、迷わず生成 AI に尋ねる習慣がついてしまいました。

参考サイト

  1. functools --- 高階関数と呼び出し可能オブジェクトの操作 — Python ドキュメント
  2. itertools --- 効率的なループ用のイテレータ生成関数群 — Python ドキュメント
  3. operator --- 関数形式の標準演算子 — Python ドキュメント
  4. Pythonで複数のリストの直積(デカルト積)を生成するitertools.product | note.nkmk.me

 

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



このエントリーをはてなブックマークに追加