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技術ブログ オープンソースへ
にほんブログ村

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



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

2026-04-07

【備忘録】SB3 と TensorBoard

Stable Baselines3 (SB3) は、PyTorch をベースに実装した、信頼性の高い強化学習アルゴリズム(PPO, DQN, SAC 等)のオープンソースライブラリです。OpenAI Baselines の改良版で、統一された API により、数行のコードで環境の学習・評価・保存が容易に行えるため、研究やプロトタイピングの効率を大幅に向上できます。

強化学習モデルの学習中に、エピソード毎に解析したい情報を引き出す方法について Microsoft Copilot(以降、単に Copilot)に相談していたところ TensorBoard で可視化する話がでたので、突っ込んで聞いてみました。TensorBoard はその名前から TensorFlow の専用ツールだと勝手に思っていたのですが、全くの誤解でした。

知らない」っていうのは怖いですね。TensorBoard を利用することで RL(強化学習)モデルの学習時の内部状態を(ほぼ)リアルタイムで確認できるようになりました。

TensorBoard について

Copilot が TensorBoard について下記のようにまとめてくれました。

  • 学習中の指標(episode reward, loss など)をグラフでリアルタイム可視化するツール
  • ブラウザで http://localhost:6006 を開いて眺めるスタイル
  • TensorBoard は TensorFlow 専用ではない
  • ログファイルを読み取るだけの汎用可視化サーバー
  • SB3 は標準で TensorBoard 出力に対応している
  • RL の “見えない内部状態” を可視化する最強ツール

TensorBoard の基本的な使い方

備忘録として、以前とりあげた倒立振子問題 (CartPole) [3] の学習を TensorBoard で可視化します。

動作環境は、パッケージのバージョンが更新されていることを除けば、倒立振子問題 (CartPole) の記事 [3] と同じですが、さらに TensorBoard をインストールします。

$ pip install tensorboard

TensorBoard を利用するには、モデル生成時、tensorboard_log に TensorBoard 用ログの出力ディレクトリ(存在していなくとも良い)を指定します。

model = PPO(
    "MlpPolicy",
    env,
    verbose=1,
    tensorboard_log="./ppo_cartpole_tensorboard/",
)

編集したプログラム cartpole_rl.py を起動して学習を始めた後に、別ターミナルで TensorBoard を起動します。その際に --logdir に、モデル生成時に指定したログの出力ディレクトリを指定します。

$ tensorboard --logdir ./ppo_cartpole_tensorboard/

ブラウザで http://localhost:6006 を開きます。

他の PC からアクセスする場合 [AlmaLinux 10]

LAN 内の他の PC のブラウザからアクセスをしたい場合は、TensorBoard のポート番号がファイアウォールを通るようにしておきます。

$ sudo firewall-cmd --add-port=6006/tcp --permanent
$ sudo firewall-cmd --reload

TensorBoard を起動する時に外部からアクセスできるように --bind_all を付けます。

$ tensorboard --logdir ./ppo_cartpole_tensorboard/ --bind_all

実行例

TensorBoard 用のログを出力できるようにしたプログラム cartpole_rl.py を起動します。

cartpole_rl.py の実行例
$ python cartpole_rl.py
Using cpu device
Wrapping the env in a DummyVecEnv.
Logging to ./ppo_cartpole_tensorboard/PPO_1
---------------------------------
| rollout/           |          |
|    ep_len_mean     | 20.3     |
|    ep_rew_mean     | 20.3     |
| time/              |          |
|    fps             | 46       |
|    iterations      | 1        |
|    time_elapsed    | 44       |
|    total_timesteps | 2048     |
---------------------------------
-----------------------------------------
| rollout/                |             |
|    ep_len_mean          | 26.4        |
|    ep_rew_mean          | 26.4        |
| time/                   |             |
|    fps                  | 46          |
|    iterations           | 2           |
|    time_elapsed         | 88          |
|    total_timesteps      | 4096        |
| train/                  |             |
|    approx_kl            | 0.008908875 |
|    clip_fraction        | 0.0802      |
|    clip_range           | 0.2         |
|    entropy_loss         | -0.686      |
|    explained_variance   | 0.000353    |
|    learning_rate        | 0.0003      |
|    loss                 | 6.18        |
|    n_updates            | 10          |
|    policy_gradient_loss | -0.0121     |
|    value_loss           | 45.3        |
-----------------------------------------
...
(以降省略)

別ターミナルで TensorBoard を起動します。

$ tensorboard --logdir ./ppo_cartpole_tensorboard/

Firefox を起動して http://localhost:6006 にアクセスします。

Firefox 上に表示した TensorBoard(例)

参考サイト

  1. Tensorboard Integration — Stable Baselines3 documentation
  2. tensorflow/tensorboard: TensorFlow's Visualization Toolkit
  3. bitWalk's: 倒立振子問題 (CartPole) [2025-10-03]

 

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

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



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

2026-03-12

TA-Lib の Streaming API

2001 年にリリースされた TA-Lib, Technical Analysis Library は、150 以上のテクニカル指標をサポートしていて、長年にわたる検証を経ています。現在の Python 用パッケージは、Cython と Numpy を用いて効率的かつクリーンに TA-Lib をバインドしています。TA-Lib は BSD License (BSD-2-Clause license) の元で配布されているオープンソースのライブラリです。

株価の値動きなどを分析する際に TA-Lib のパッケージを利用していますが、デイトレ用のリアルタイム・システムで利用しようとすると、ちょっと扱いにくいと感じていました。それは、TA-Lib では全てのデータ列に対して計算するからです。そのため、リアルタイムの処理では専用の処理をするクラスを用意して対応しているのですが、計算負荷を少なくするための検討は結構面倒な作業です。

Github の TA-Lib / ta-lib-python プロジェクトサイトにある README をよく読むと、実験段階ながら Streaming API が提供されていることがわかりました。

詳細なベンチマークテストをしていませんが(下に追記しました [2026-03-12])、リアルタイム風に動作するサンプルを作ったので紹介します。

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

Fedora Linux 43 x86_64
Workstation Edition
Python 3.14.3
numpy 2.4.3
pandas 3.0.1
pyqtgraph 0.14.0
pyside6 6.10.2
ta-lib 0.6.8

TA-Lib の Streaming API とは

従来の API は、過去データ全体に対してインジケーターを計算する設計です。

import numpy as np
import talib as ta

# 過去のデータ全体を一度に処理
prices = np.array([100, 101, 102, 103, 104, 105], dtype=float)
ma = ta.SMA(prices, timeperiod=3)
# 結果:
print(ma)
[ nan  nan 101. 102. 103. 104.]

Streaming API は、直近のデータウィンドウに対してインジケーターを計算します。

import numpy as np
from talib import stream

# 新しいデータポイントが到着するたびに計算
buffer = np.array([100, 101, 102], dtype=float)
ma = stream.SMA(buffer, timeperiod=3)
print(ma)

# 次のデータが到着
buffer = np.array([101, 102, 103], dtype=float)
ma = stream.SMA(buffer, timeperiod=3)
print(ma)
101.0
102.0

Streaming API の主な特徴

  1. 固定長バッファで動作
    • 計算に必要な期間(例:30 期間)のデータのみを保持
    • メモリ効率が良い
  2. 最新値のみを返す
    • 従来の API は配列全体を返すが、Streaming API は単一の値を返す
      • Streaming API は、渡された配列全体に対してインジケーターを計算し、最後の値だけを返す関数です。つまり 1000 個のデータを渡せば 1000 個分の計算をしてしまいます。
      • そのため、プログラム側で固定長バッファを管理する必要があります。
    • リアルタイムモニタリングに最適
  3. 実験的機能 (Experimental)
    • まだ開発中の機能なので、将来的に仕様が変更される可能性がある
    • プロダクション環境では慎重に使用すること

サンプル

ティックデータから単純な移動平均 MA (n=30) を算出して、株価トレンドと一緒にプロットするサンプルです。チャート作成には PyQtGraph を利用しています。

ツールバーにある ▶(再生ボタン)をクリックすると過去のティックデータ(下記サンプル)を読み込み、100 msec 間隔で新しい点を繋げてプロットします。MA を算出できるようになったら緑線で表示されるようになります。

放っておけばデータの最後までプロットしますが、途中で止める場合は、ツールバーにある ⏹(停止ボタン)をクリックします。

sample_data.zip サンプルプログラムが読み込むティックデータ

※ このティックデータは、楽天証券のマーケットスピード2 RSS 経由で自作アプリが収集した 2 秒間隔のティックデータです。

上記のサンプルデータを zip のままで、下記サンプルと同じディレクトリ内に保存してサンプルを実行してください。

import sys
from typing import Optional

import numpy as np
import pandas as pd
import pyqtgraph as pg
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMainWindow, QStyle, QToolBar, QToolButton
from talib import stream


class SampleChart(pg.PlotWidget):
    def __init__(self, ma_period: int = 30) -> None:
        super().__init__()
        self.ma_period = ma_period

        # リストで保持(append が高速)
        self.data_x: list[float] = []
        self.data_y: list[float] = []
        self.data_ma: list[float] = []

        # streaming APIの状態を保持するためのバッファ
        self.y_buffer = np.array([], dtype=float)

        self.line: pg.PlotDataItem = self.plot([], [], pen=pg.mkPen(width=0.5))
        self.ma: pg.PlotDataItem = self.plot([], [], pen=pg.mkPen((0, 255, 0, 192), width=1))

    def add_point(self, x: float, y: float) -> None:
        """新しいデータポイントを追加"""
        self.data_x.append(x)
        self.data_y.append(y)

        # バッファを更新(直近ma_period個のデータのみ保持)
        self.y_buffer = np.append(self.y_buffer, y)
        if len(self.y_buffer) > self.ma_period:
            self.y_buffer = self.y_buffer[-self.ma_period:]

        # streaming APIで移動平均を計算
        if len(self.y_buffer) >= self.ma_period:
            self.data_ma.append(stream.SMA(self.y_buffer, timeperiod=self.ma_period))

        # グラフを更新
        self.line.setData(self.data_x, self.data_y)  # type: ignore

        # MA期間に達したらMAラインを表示
        if len(self.data_ma) > 0:
            ma_start = self.ma_period - 1
            self.ma.setData(self.data_x[ma_start:], self.data_ma)  # type: ignore


class SampleTaLib(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.file_csv = "sample_data.zip"  # ZIP圧縮されたCSVファイルを読み込む
        self.df: Optional[pd.DataFrame] = None
        self.row: int = 0

        self.setWindowTitle("TA-Lib Streaming API Demo")
        self.resize(800, 600)

        # ツールバーの設定
        toolbar = QToolBar()

        but_play = QToolButton()
        but_play.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay))
        but_play.clicked.connect(self.on_play_clicked)
        toolbar.addWidget(but_play)

        but_stop = QToolButton()
        but_stop.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaStop))
        but_stop.clicked.connect(self.on_stop_clicked)
        toolbar.addWidget(but_stop)

        self.addToolBar(toolbar)

        # チャートの設定
        self.chart = SampleChart(ma_period=30)
        self.setCentralWidget(self.chart)

        # タイマーの設定
        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.set_new_data)

    def on_play_clicked(self) -> None:
        """再生ボタンクリック時の処理"""
        try:
            # データ未読み込みの場合のみ読み込む
            if self.df is None:
                self.df = pd.read_csv(self.file_csv)
                print(f"CSVファイルを読み込みました: {len(self.df)}行")

                # データの検証
                if len(self.df.columns) < 2:
                    print("エラー: CSVファイルには少なくとも2列必要です")
                    return

            if not self.timer.isActive():
                self.timer.start()
                print("タイマーを開始しました。")

        except FileNotFoundError:
            print(f"エラー: ファイル '{self.file_csv}' が見つかりません")
        except Exception as e:
            print(f"エラー: {e}")

    def on_stop_clicked(self) -> None:
        """停止ボタンクリック時の処理"""
        if self.timer.isActive():
            self.timer.stop()
            print("タイマーを停止しました。")

    def set_new_data(self) -> None:
        """新しいデータをチャートに追加"""
        if self.df is None or self.row >= len(self.df):
            self.on_stop_clicked()
            print("データの最後に到達しました。")
            return

        try:
            x, y = self.df.iloc[self.row, 0], self.df.iloc[self.row, 1]
            self.chart.add_point(float(x), float(y))
            self.row += 1
        except Exception as e:
            print(f"データ追加エラー: {e}")
            self.on_stop_clicked()


def main() -> None:
    app = QApplication(sys.argv)
    win = SampleTaLib()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()
CSVファイルを読み込みました: 9691行
タイマーを開始しました。
タイマーを停止しました。
サンプルの実行例

ベンチマーク 追記 [2026-03-12]

Anthropic Claude にベンチマーク用のコードを作ってもらいました。現行のアプリに採用している移動平均を算出するクラスのコア部分を抽出したクラスと、今回の TA-Lib の Streaming API を適用したクラスで比較しました。

import time
from collections import deque
import numpy as np
from talib import stream

# 現在の実装
class MovingAverage:
    def __init__(self, window_size: int):
        self.window_size = window_size
        self.queue = deque()
        self.running_sum = 0.0
        self.ma = 0.0
        self.prev_ma = 0.0
    
    def update(self, value: float) -> float:
        if len(self.queue) >= self.window_size:
            self.running_sum -= self.queue.popleft()
        self.queue.append(value)
        self.running_sum += value
        self.prev_ma = self.ma
        self.ma = self.running_sum / len(self.queue)
        return self.ma

# TA-Lib版
class MovingAverageTA:
    def __init__(self, window_size: int):
        self.window_size = window_size
        self.buffer = deque(maxlen=window_size)
        self.ma = 0.0
        self.prev_ma = 0.0
    
    def update(self, value: float) -> float:
        self.buffer.append(value)
        if len(self.buffer) >= self.window_size:
            self.prev_ma = self.ma
            buffer_array = np.array(self.buffer, dtype=float)
            self.ma = stream.SMA(buffer_array, timeperiod=self.window_size)
        return self.ma

# ベンチマーク
def benchmark(ma_class, iterations=100000):
    ma = ma_class(30)
    start = time.perf_counter()
    for i in range(iterations):
        ma.update(float(i))
    elapsed = time.perf_counter() - start
    return elapsed

# 実行
time_custom = benchmark(MovingAverage)
time_talib = benchmark(MovingAverageTA)

print(f"現在の実装: {time_custom:.4f}秒")
print(f"TA-Lib版:   {time_talib:.4f}秒")
print(f"速度比:     {time_talib/time_custom:.2f}倍遅い")

デイトレ・アプリを稼働させている Intel N150 搭載の Windows 11 PC 上でのベンチマークの結果は下記のとおりでした。残念ながら Python だけで記述したクラスの方が全然速いという結果になりました。

現在の実装: 0.0263秒
TA-Lib版:   0.5771秒
速度比:     21.98倍遅い

参考サイト

  1. TA-Lib - Technical Analysis Library
  2. TA-Lib/ta-lib: TA-Lib (Core C Library)
  3. TA-Lib/ta-lib-python: Python wrapper for TA-Lib
  4. TA-Lib · PyPI

 

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

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



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

2026-01-24

シンプルな PDF Viewer 〜 PySide6

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

PySide6 の GUI アプリで PDF ファイルを閲覧するサンプルを Microsoft Copilot に手伝ってもらいながら作成しました。

ここからカスタマイズしたい処理を加えていく予定なのですが、閲覧のための最低限の機能をコンパクトに実装できたので、一旦はサンプルとしてまとめました。

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

Fedora Linux 43
Workstation Edition x86_64
Python 3.13.11
PySide6 6.10.1
qt_pdfview.py

以下に実行例を示しました。

ツールバーのアイコンは、Qt にビルトイン・アイコンを利用しました。そのため、ダークモードではアイコンが見えなくなってしまう場合があります。

qt_pdfview.py の実行例

参考サイト

  1. PySide6.QtPdf.QPdfDocument - Qt for Python
  2. PySide6.QtPdfWidgets.QPdfView - Qt for Python

 

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

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



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

2026-01-22

【備忘録】xlwings と QThread

xlwings は、Python を使って Excel を直接操作したり、Excel から Python コードを呼び出したりできるライブラリです[BSD ライセンス]。VBA マクロの代替として Python で Excel の自動化(スクリプト作成、マクロ、ユーザー定義関数(UDF))を実現でき、Excel と Python のデータをシームレスに連携させ、双方の得意な部分を組み合わせて強力な自動化・データ処理を可能にします。

今回の目的
  • PySide6 の GUI アプリから Excel を操作しようと、Excel とやり取りをする部分をスレッド化したところ、うまくいかずにハマってしまいました。
  • 生成 AI から「PySide6 × xlwings を使う人が必ず通る“最大の罠”なんだよ」なんて言われていたのに、二度も同じ間違いをしてしまったので、備忘録にまとめました。

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

Windows 11 Pro 25H2
Excel 2024 MSO
Python 3.13.9
PySide6 6.10.1
xlwings 0.33.20

簡単なサンプル(GUI 無し)

まずは、xlwings を利用した、ごく簡単な GUI 無しのサンプルです。

import xlwings as xw

if __name__ == "__main__":
    wb = xw.Book()  # 新しいワークブックを開く
    sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化
    sheet["A1"].value = "Python から書き込みました。"  # 値の書き込み
「簡単なサンプル」の実行例

GUI サンプル

GUI のプッシュボタンをクリックして、簡単なサンプル(GUI 無し)と同じ処理を実行するサンプルです。

このサンプルは Excel へ文字列を書き込むだけですが、複雑な処理を加えても GUI のイベントループに影響を与えないように、「別スレッド」で Excel へアクセスする処理をするようにしています。

import sys

import xlwings as xw
from PySide6.QtCore import (
    QObject,
    QThread,
    Signal,
    Slot,
)
from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import (
    QApplication,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class ExcelWorker(QObject):
    def __init__(self):
        super().__init__()
        self.sheet = None

    @Slot()
    def initWorker(self):
        wb = xw.Book()  # 新しいワークブックを開く
        self.sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化

    @Slot(str)
    def excel_write(self, msg: str):
        self.sheet["A1"].value = msg  # 値の書き込み


class SampleXlwings(QWidget):
    requestWorkerInit = Signal()
    requestWrite = Signal(str)

    def __init__(self):
        super().__init__()
        # xlwings用スレッド
        self.thread = thread = QThread()
        self.worker = worker = ExcelWorker()
        worker.moveToThread(thread)
        thread.started.connect(self.requestWorkerInit.emit)
        self.requestWorkerInit.connect(worker.initWorker)
        self.requestWrite.connect(worker.excel_write)
        thread.start()
        # GUI
        layout = QVBoxLayout()
        self.setLayout(layout)
        but = QPushButton("新規 Excel へ文字列を書き込む")
        but.clicked.connect(self.request_write)
        layout.addWidget(but)

    def closeEvent(self, event: QCloseEvent):
        if self.thread is not None:
            self.thread.quit()
            self.thread.wait()
        if self.worker is not None:
            self.worker.deleteLater()
            self.worker = None
        event.accept()

    @Slot()
    def request_write(self):
        msg = "Python から書き込みました。"
        self.requestWrite.emit(msg)


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


if __name__ == "__main__":
    main()

なお、PySide6 では @Slot のデコレータを省略しても動作します。

「GUI サンプル」の実行例

GUI サンプルを実行すると、Excel の新しいワークブックが開きます。

新規 Excel へ文字列を書き込む ボタンをクリックすると、「簡単なサンプル」と同じように、文字列が Excel シートに書き込まれます。

注意点

ExcelWorker クラスのコンストラクタ __init__ メソッドで、以下のように xlwings のインスタンスを定義すると動作しません。

class ExcelWorker(QObject):
    def __init__(self):
        super().__init__()
        wb = xw.Book()  # 新しいワークブックを開く
        self.sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化

この場合、下記のようなエラーが出ます。

pywintypes.com_error: (-2147417842, 'アプリケーションは、別のスレッドにマーシャリングされたインターフェイスを呼び出しました。', None, None)

メインスレッドで ExcelWorker のインスタンスを定義するので、ExcelWorker.__init__ も当然、「メインスレッド」で実行されます。

「別スレッド」で Excel とやり取りをしたければ、「別スレッド」を thread.start() で開始した後に、その「別スレッド」内で xlwings のインスタンスを定義する必要があります。

そういうわけで、「別スレッド」が開始されたときに初期化用のメソッド ExcelWorker.initWorker を実行するようにしています。

self.thread = thread = QThread()
self.worker = worker = ExcelWorker()
worker.moveToThread(thread)
thread.started.connect(self.requestWorkerInit.emit)  # 「別スレッド」が開始されればシグナルを発行
self.requestWorkerInit.connect(worker.initWorker)
self.requestWrite.connect(worker.excel_write)
thread.start()  # 「別スレッド」開始
    @Slot()
    def initWorker(self):
        wb = xw.Book()  # 新しいワークブックを開く
        self.sheet = wb.sheets["Sheet1"]  # シート・オブジェクトをインスタンス化

参考サイト

  1. xlwings Documentation
  2. ASCII.jp:COM(Component Object Model)は古い技術だが、いまだに現役 あらためて解説する (1/2) [2023-04-23]

 

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

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



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

2026-01-14

【備忘録】UNIX秒とタイムゾーン 〜 Pandas

Pandas は、Python プログラミング言語向けに開発されたデータ操作・分析用ソフトウェアライブラリです(三条項 BSD ライセンス)。特に数値テーブルや時系列データの操作のためのデータ構造と演算を提供しています。Pandas という名称は、経済計量学における「パネルデータ」という用語に由来しています。また「Pythonデータ分析」という語句をもじったものともされています 。Pandas により、R 言語で利用可能な DataFrame 操作の多くの同等の機能が Python でも利用できるようになりました。Pandas は NumPy を基盤として構築されています。

Wikipedia より引用、翻訳・編集

タイムゾーンにいつも苦しむ UNIX秒

UNIX 時間(エポック秒)は、UTC(協定世界時)の 1970 年 1 月 1 日 0 時 0 分 0 秒からの経過秒数です。本記事では「UNIX秒」と呼ぶことにします。

リアルタイムのデータを扱う時、取得したデータのタイムスタンプを UNIX秒 に直して保持するようにしています。あとになって分析などをする際に表示用に時刻フォーマットをするのですがタイムゾーンが合っていません。そんなときにいつも、その場しのぎの対処をしてしまっています。

分析やチャート作成時にタイムゾーンの情報は必要ないのですが、UNIX秒をタイムゾーンが付いていないローカル時刻表記に戻す時には、一旦はタイムゾーン付きの処理、変換をしてから、タイムゾーンの情報を削除する、という流れが確実です。

Pandas の Series で .dt アクセサを利用すれば一括処理が可能なので、サンプルでは一つの時刻(UNIX秒)しか扱いませんが、わざわざ Series にしてローカル時刻へ変換する処理の流れをまとめました。丁寧に言葉でくどくど説明しても解りにくいので、コードの簡単なコメントで済ませてしまっています。

import time

import pandas as pd

print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print("UNIX秒からローカル時刻へ変換する流れ")
print("UNIX秒 → UTC (tz-aware) → JST (tz-aware) → tz-naive")
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")

print("\nUNIX秒: 現在のUNIX秒を取得")
ts_epoch = time.time()
print(ts_epoch)

print("\nUNIX秒 → tz-naive: Pandas の Timestamp(utc=True 指定無し)→ Series へ ☹")
dt_no_utc = pd.to_datetime(ts_epoch, unit="s")
ser_no_utc = pd.Series([dt_no_utc])
print(ser_no_utc)

print("\n*** .dt アクセサを使った変換[備忘録] ***")
print("UNIX秒 → UTC (tz-aware): Pandas の Timestamp(タイムゾーン付き)→ Series へ")
dt_utc = pd.to_datetime(ts_epoch, unit="s", utc=True)
ser_utc = pd.Series([dt_utc])
print(ser_utc)

print("\nUTC (tz-aware) → JST (tz-aware): タイムゾーンを日本時間に変更")
ser_jst = ser_utc.dt.tz_convert("Asia/Tokyo")
print(ser_jst)

print("\nJST (tz-aware) → tz-naive: タイムゾーン情報を削除")
ser_no_tz = ser_jst.dt.tz_localize(None)
print(ser_no_tz)
実行例
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
UNIX秒からローカル時刻へ変換する流れ
UNIX秒 → UTC (tz-aware) → JST (tz-aware) → tz-naive
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

UNIX秒: 現在のUNIX秒を取得
1768363958.6145868

UNIX秒 → tz-naive: Pandas の Timestamp(utc=True 指定無し)→ Series へ ☹
0   2026-01-14 04:12:38.614586830
dtype: datetime64[ns]

*** .dt アクセサを使った変換[備忘録] ***
UNIX秒 → UTC (tz-aware): Pandas の Timestamp(タイムゾーン付き)→ Series へ
0   2026-01-14 04:12:38.614586830+00:00
dtype: datetime64[ns, UTC]

UTC (tz-aware) → JST (tz-aware): タイムゾーンを日本時間に変更
0   2026-01-14 13:12:38.614586830+09:00
dtype: datetime64[ns, Asia/Tokyo]

JST (tz-aware) → tz-naive: タイムゾーン情報を削除
0   2026-01-14 13:12:38.614586830
dtype: datetime64[ns]

参考サイト

  1. pandasで日付・時間の列を処理(文字列変換、年月日抽出など) | note.nkmk.me

 

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

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



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