- 投稿日:2021-12-05T23:55:12+09:00
Pythonで始めるテストツール製作
1. はじめに Pythonはライブラリが充実していて、使いやすくてパワフルなテストツールをお手軽に作れます。本稿ではMenu Based CLI(メニュー形式のコマンドラインインターフェース)で操作するテストツールの基本形とそのカスタマイズ例をご紹介します。 例1: 数値計算 例2: 画像の一部を切り出して保存 PythonやOpenCVのインストール方法は付録Aを、動作確認環境は付録Bをご覧ください。 2. 基本形 カスタマイズのベースとなるプログラムを以下に示します1。 myTools.py # myTools.py by ka's(https://qiita.com/pbjpkas) 2021 # python 3.x import sys def function_a(): print("Hello, World.") def main(): while True: print("== myTools ==") print("a: function_a") print("x: exit") s = input(">") if s == "a": function_a() if s == "x": print("Bye.") sys.exit() main() step.1 Cドライブの直下にworkというフォルダを作成し上記プログラムをmyTools.pyという名前で保存します。 NOTE: ソースコードのウィンドウにマウスポインタを乗せると右上にアイコンが出現します。 これをクリックするとソースコードがクリップボードへコピーされます。 step.2 次に、コマンドプロンプトを起動しpythonで実行します。 cmd cd c:\work python myTools.py step.3 以下のようにメニューとプロンプト(>)が表示されればOKです。 == myTools == a: function_a x: exit > aを入力してEnterするとfunction_a()が実行され、メニューに戻ります。 xを入力してEnterするとBye.と表示され終了します。 それ以外は無視され、メニューが再度表示されます。 実行例 c:\work>python myTools.py == myTools == a: function_a x: exit >a Hello, World. == myTools == a: function_a x: exit >b == myTools == a: function_a x: exit >x Bye. c:\work> 3. 数値計算 一つ目の例として「先月と今月のメトリクス」のような2つの数値の比較を想定します。OSに標準でインストールされている電卓でも計算できますが、「差」と「何倍か」の両方を知りたいといった、計算する項目が複数あるとちょっと手間です。そこでテストツールに組み込みます。 要件 新旧の値を入力すると「差」および「何倍か」を計算すること 旧の値が0の場合は「何倍か」は0とすること 何倍かは小数点二けたで表示すること 実装 input()の戻り値は文字列(str型)のためfloat型にキャストして計算します。 myTools.py(Ver.2) # myTools.py by ka's(https://qiita.com/pbjpkas) 2021 # python 3.x import sys def comparison_value(): try: old_value = float(input("old>")) new_value = float(input("new>")) except: print(sys.exc_info()) return False delta = new_value - old_value if old_value != 0: times = new_value / old_value else: times = 0 print("delta:%f, times:%.2f" % (delta, times)) return True def main(): while True: print("== myTools ==") print("a: comparison of old and new value") print("x: exit") s = input(">") if s == "a": comparison_value() if s == "x": print("Bye.") sys.exit() main() 実行例 oldに256、newに384を入力した例と、oldに384、newに256を入力した例を以下に示します。電卓では手間だけどExcelを使うほどでもないような計算が手軽にできるようになりました。 実行例 c:\work>python myTools.py == myTools == a: comparison of old and new value x: exit >a old>256 new>384 delta:128.000000, times:1.50 == myTools == a: comparison of old and new value x: exit >a old>384 new>256 delta:-128.000000, times:0.67 == myTools == a: comparison of old and new value x: exit > 4. 画像の一部を切り出して保存 Pythonでテストツールを製作する一番のメリットは様々なライブラリの恩恵を受けられることです。そこで、本節ではスクリーンショットの一部を切り出してバグ票に貼り付ける作業を想定し、OpenCVで画像を切り出してファイルに保存する機能を実装します。 要件 以下のパラメータを入力できること 加工元の画像ファイル名 切り出す座標(x0, y0, x1, y1)の値 保存するファイル名 以下の場合はエラーメッセージを表示してメニューに戻ること 加工元ファイルの読み込みに失敗した場合 切り出す座標の値が加工元ファイルの範囲外、x0>x1、y0>y1の場合 切り出した画像の保存に失敗した場合 実装 cv2をimportします。 パラメータの入力を受け持つcrop_image_menu()と画像を切り出すcrop_image()を実装しmenu()から呼べるようにします。 参考 【OpenCV】imreadでちゃんと読み込まれたか確認したいとき myTools.py(Ver.3) # myTools.py by ka's(https://qiita.com/pbjpkas) 2021 # python 3.x import sys import cv2 def comparison_value(): try: old_value = float(input("old>")) new_value = float(input("new>")) except: print(sys.exc_info()) return False delta = new_value - old_value if old_value != 0: times = new_value / old_value else: times = 0 print("delta:%f, times:%.2f" % (delta, times)) return True def crop_image(filename_in, x0, y0, x1, y1, filename_out): img = cv2.imread(filename_in, cv2.IMREAD_COLOR) # 加工元ファイルを開けることの確認 if img is None: print("Input File Error.") return False # 座標がint型にキャストできることを確認 try: v0 = int(y0) v1 = int(y1) h0 = int(x0) h1 = int(x1) except: print("Coordinate Value Error(1).") print(sys.exc_info()) return False # 座標が加工元画像の範囲内、かつ、v0<v1、h0<h1であることの確認 # print("original image size v:%d, h:%d" % (img.shape[0],img.shape[1])) if 0<=v0 and v0<=img.shape[0] and \ 0<=v1 and v1<=img.shape[0] and \ 0<=h0 and h0<=img.shape[1] and \ 0<=h1 and h1<=img.shape[1] and \ v0<v1 and h0<h1 : img2 = img[v0:v1, h0:h1] else: print("Coordinate Value Error(2).") return False try: ret = cv2.imwrite(filename_out, img2) if ret != True: print("Output File Error.") return False except: print("Output File Error.") print(sys.exc_info()) return False return True def crop_image_menu(): filename_in = input("input file>") x0 = input("x0>") y0 = input("y0>") x1 = input("x1>") y1 = input("y1>") filename_out = input("output file>") s = input("OK? y/n>") if s == "y": ret = crop_image(filename_in, x0, y0, x1, y1, filename_out) return ret else: return False def main(): while True: print("== myTools ==") print("a: comparison of old and new value") print("b: crop image") print("x: exit") s = input(">") if s == "a": comparison_value() if s == "b": crop_image_menu() if s == "x": print("Bye.") sys.exit() main() 実行例 以下のサンプル画像(sample.png2)をC:\workに配置し、赤枠部分(x0:120、y0:38、x1:230、y1:57)を切り出してout.pngに保存します。 実行例 c:\work>python myTools.py == myTools == a: comparison of old and new value b: crop image x: exit >b input file>sample.png x0>120 y0>38 x1>230 y1>57 output file>out.png OK? y/n>y == myTools == a: comparison of old and new value b: crop image x: exit > 赤枠部分を切り出すことができました。 5. おわりに テストツール製作の一歩目として、基本形のプログラムをカスタマイズしながら数値計算のプログラムと画像を切り出して保存するプログラムを実装しました。Pythonとライブラリを組み合わせることでメニュー形式ならではの操作性、取っつきやすさとライブラリのパワー3を兼ね備えたテストツールを作れます。 筆者の考えるPythonでテストツールを作るメリットを以下に挙げます。 ライブラリが豊富 UART(RS-232C)通信(PySerial) 計測器の制御(PyVISA) 画像処理(OpenCV、pyocr) シェルコマンドの実行(subprocess) マルチスレッドの処理(threading) たいていのことはググると見つかる Raspberry Piのようなシングルボードコンピュータを治具として利用できる UIをメニュー形式からコマンドライン形式にすることで自動化ツールに流用できる 構造化プログラミング、オブジェクト指向プログラミングのどちらもOK returnで複数のパラメータを返すのが簡単にできる デメリットとしてPythonやライブラリのバージョン依存問題(はまるとちょっと面倒…)がありますがこれはPythonに限ったことでもないと思います。 Pythonで始めるテストツール製作、おススメです。 付録A. 環境構築 A.1 Pythonのインストール Python.org → Download Latest Version(2021/12/4時点では3.10.0) 一番下の、RecommendedとなっているWindows Installer(64-bit)をダウンロードする Add Python 3.10 to PATHにチェックを入れてからInstall Now Disable path length limitを押下してからClose(お好みで) A.2 OpenCVのインストール コマンドプロンプトで以下のpipコマンドを実行します。 pip install opencv-python pip install opencv-contrib-python 付録B. 動作確認環境 Windows10 64bit バージョン21H1です。Pythonやライブラリのバージョンを以下に示します。 >python --version Python 3.10.0 >pip list Package Version --------------------- -------- numpy 1.21.4 opencv-contrib-python 4.5.4.60 opencv-python 4.5.4.60 pip 21.2.3 setuptools 57.4.0 付録C. 画像切り出しプログラムの動作確認内容 sample.pngは横643ピクセルx縦513ピクセルの画像です。 正常系 1x1ピクセル(切り出しサイズの最小値)、任意のサイズ、全体(切り出しサイズの最大値)の3パターン+αを確認しています。 No. 切り出す箇所 input file x0 y0 x1 y1 output file 期待結果 実行結果 1 赤枠部分 sample.png 120 38 230 57 out.png 赤枠部分がout.pngに保存される OK 2 1x1 pixel sample.png 0 0 1 1 1x1-1.png 元画像左上1x1 pixelが1x1-1.pngに保存される OK 3 1x1 pixel sample.png 642 512 643 513 1x1-2.png 元画像右下1x1 pixelが1x1-2.pngに保存される OK 4 全体 sample.png 0 0 643 513 643x513.png 元画像の全体が643x513.pngに保存される OK 5 左上 sample.png 0 0 20 20 corner-ul.png 元画像左上20x20 pixelがcorner-ul.pngに保存される OK 6 左下 sample.png 0 493 20 513 corner-bl.png 元画像左下20x20 pixelがcorner-bl.pngに保存される OK 7 右上 sample.png 623 0 643 20 corner-ur.png 元画像右上20x20 pixelがcorner-ur.pngに保存される OK 8 右下 sample.png 623 493 643 513 corner-br.png 元画像右下20x20 pixelがcorner-br.pngに保存される OK エラー処理 Input File Error No. エラー発生条件 input file x0 y0 x1 y1 output file 期待結果 実行結果 1 存在しないファイルをinput fileに指定する aaa.png 10 10 20 20 bbb.png Input File Error.が返ってくる OK 2 input fileを空欄にする (空欄) 10 10 20 20 bbb.png Input File Error.が返ってくる OK Output File Error No. エラー発生条件 input file x0 y0 x1 y1 output file 期待結果 実行結果 1 output fileのパスに存在しないディレクトリを含める sample.png 10 10 20 20 foo\bbb.png Output File Error.が返ってくる OK 2 output fileを空欄にする sample.png 10 10 20 20 (空欄) Output File Error.が返ってくる OK 3 output fileの拡張子を画像以外のものにする sample.png 10 10 20 20 bbb.exe Output File Error.が返ってくる OK Coordinate Value Error(1) No. 確認方法 input file x0 y0 x1 y1 output file 期待結果 実行結果 1 切り出す座標の値にアルファベットを与える sample.png a 10 20 20 bbb.png Coordinate Value Error(1).が返ってくる OK 2 切り出す座標の値に小数を含む数値を与える sample.png 10 10 20 20.5 bbb.png Coordinate Value Error(1).が返ってくる OK Coordinate Value Error(2) No. 確認方法 input file x0 y0 x1 y1 output file 期待結果 実行結果 1 切り出す座標の値に負の整数を与える sample.png -10 10 20 20 bbb.png Coordinate Value Error(2).が返ってくる OK 2 切り出す座標の値に元画像の範囲外の数値を与える sample.png 10 10 20 514 bbb.png Coordinate Value Error(2).が返ってくる OK 3 切り出す座標の値にx0>x1となる数値を与える sample.png 20 10 10 20 bbb.png Coordinate Value Error(2).が返ってくる OK 4 切り出す座標の値にy0>y1となる数値を与える sample.png 10 20 20 10 bbb.png Coordinate Value Error(2).が返ってくる OK 付録D. ソフトウェアテストのアドベントカレンダー参加記事 M5StackとPythonで受入テスト自動化の要素技術を試す (ソフトウェアテスト #2 Advent Calendar 2018 | 13日目) ストップウォッチを使う性能テストを実ステップ300行に満たない自動テストシステムで自動化する (ソフトウェアテストの小ネタ Advent Calendar 2019 | 13日目) 自作の自動テストツールをJenkinsで実行する (ソフトウェアテストの小ネタ Advent Calendar 2020 | 3日目) 投稿内容の利用についてはQiitaの利用規約をご確認ください。 ↩ 参考:Lチカで始めるテスト自動化(21)キーボード入力待ちの実装 ↩ 例えばsample.pngのタイムスタンプ部分はOpenCVで矩形の白背景や文字列を描画しています。OpenCVを組み込むとWebカメラの映像を動画で撮影したり、フレームを指定して動画から静止画像を切り出したり、画像に図形や文字列を重畳したりといったことも容易にできます。 ↩
- 投稿日:2021-12-05T23:51:13+09:00
[AutoML]Pycaretをとりあえず使ってみた。
内容と想定読者 Pycaretを使ってみようと思ったときのメモになります。 やることは回帰(regression)です。 導入から分析までの流れをメモしました。 初めてPycaretを触る方の一助になればと思います。 Pycaretのインストール pipで行けるんだろーなとか思ってやったらエラー。 原因を解消するより仮想環境を作る方が簡単そうだったのでそうしました。 ってことで、ターミナルを開いて、condaで仮想環境の作成。 今回は仮想環境名PyCaret、Pythonバージョンは3.6で作成しました。 仮想環境作る conda create --name PyCaret python=3.6 作成した環境に移動。 作成した仮想環境へ移動 conda activate PyCaret pycaretのインストール pycaretのインストール pip install pycaret これで完了。(pycaretいらんってなったら、仮想環境ごと削除) 自分はJupyter Notebookを使いたいので、次の設定も行いました。 Anaconda Navigator を開いて、Application on で作成した仮想環境を選択。 Jupyter Notebook をインストール インストールが完了したら、 Jupyter Notebook を起動。 Pycaretの呼び出しとlightGBM周りのエラー 上記の手順でPycaretのインストールに成功したので、早速Pycaretを使っていきたいところですが、まだもうちょっとインストールしないといけないものがあります。それがlightgbmとlibompとbrewになります。 <↓なんでこれらが必要なのか気になる人向け↓> pycaretはいくつかのモデルを自動で生成し、モデル毎のテスト結果を比較出力ができます。その際、デフォルトでlightgbmが呼ばれるのでインストールしておかないとエラーになります。また、lightgbmは並列処理を行うのでそのためにlibompが必要になります。ただ、このlibompがbrew経由でのインストールになるので、brewも合わせてインストールが必要になります。 <説明終わり>> ってことでインストールします。 lightgbmのインストール pip install lightgbm brewは公式サイトからダウンロード用URLをとってきて、実行する。 これめっちゃ時間かかります。 brewのインストール /bin/bash -c "$(curl -fsSLhttps://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brewがインストールできたので、それを使ってlibompのインストール libompのインストール brew install libomp 以下簡単な動作確認。 jupyter_notebookを開いて、以下を実行 import pycaret これで pycaret がインポートできたら下準備はおしまいです。 ようやく本編?〜分析開始!〜 ってなことで、importまでもなかなか大変だったわけですが、ここから実際に使っていこうと思います。 今回はBostonデータを使った家賃予想をやってみたいと思います。(有名なやつですね) 色々なサイトでBostonデータに対して高精度となるようなパラメーターの調整やらなんやらされてると思いますが、 この記事ではそんなことは目指しません。なんとなく触ってみる。そんな感じです。 では、まずデータの取得から sample.py from pycaret.regression import * from pycaret.datasets import get_data data_set = get_data("boston") 続いて、pycaret独特のsetupってのを行います。 まぁ簡単に言うと前処理なんですが、pycaretを使って分析を行うにはsetupは必須作業です。 デフォルトでも、特徴量のデータタイプ推定、IDや日付データの削除、欠損値の補完、train_test_split、乱数シード設定、データサンプリングを行ってくれます。(詳しくは公式サイト:https://pycaret.org/setup/) もちろんデフォルトで設定されていないものはオプション指定可能です。 今回は多重共線性の項目だけオプション設定してみてます。 sample.py exp = setup(data_set, target='medv', remove_multicollinearity=True) そうすると各特徴量に対して、推定されるデータタイプが返されます。 一応目視で間違いないかを確認して、問題なければEnter。今回は問題なさそうなのでEnterでsetupを完了させます。 問題があればquitを入力し、手動で調整が必要です。(公式サイト:https://pycaret.org/setup/) さて、ここから従来の分析であればいくつかのモデルを試して、モデルを選定していくって感じになるかと思います。 ↑この作業めんどくさくないですか? pycaretには、モデル比較用の関数が用意されており、これがめちゃくちゃ便利!! sample.py compare_models() この一文で複数のモデルでスコアを算出してくれます! (ちなみにデータを入れんでいいの?と言われそうですが、どのデータを対象にするかなどはsetupで設定が完了しているみたいです。) ってことで今回の結果はこんな感じ。 ※各モデルごとに必要なパラメータはデフォルト値 Modelのところには分析モデルが書かれていて、MAEから右のところはスコア。結果も見やすくていいですね。 今回は回帰なのでR2が大きいExtra Tree を使って話を進めます。 モデルは以下のように選択します。 sample.py model = create_model("et") モデル比較結果の一番左の値を引数に取る感じです。 ちなみに、compare_models()の返り値はbestモデル(結果の一番上のやつ)になってるので、 model = compare_models() ってな感じで受けてもいいかもです。 本来は、上位三つくらいのモデルに対してパラメータ調整とかやって精度を高めた上で どのモデルを採用するかを検討すべきなんでしょうけど、今回はやりません。 次はハイパーパラメータのチューニングですね。やっちゃいましょう! sample.py tuned_model = tune_model(model) はい!これでハイパーパラメータのチューニングも完了!!! と言いたいところですが、よく見てみるとチューニング前後でR2スコアが下がってしまっていることに気づきます。 compare_models()を行った時のスコアは0.86669ですよね。 (この辺は乱数のせいじゃない?と思う方もいるとは思いますが、setup時に乱数シードの設定をしているので何度やってもこの結果は変わりません。) 今回のハイパーパラメータチューニングは、デフォルト値ではうまく行きませんでした。 仕方ないので、ここはいつも通り?引数を設定してあげましょ〜。 sample.py import numpy as np params = {'max_depth':np.random.randint(1,(len(data.columns)*.85),20), "max_features": np.random.randint(1, len(data.columns),20), "min_samples_leaf": [2,3,4,5,6], } tuned_custom_model = tune_model(model, custom_grid = params) そうすると少しですが、R2スコアが大きくなったのが確認できますね。(元々は0.86669) まぁ、これが最適です!なんては到底言えないですが、、、 グリッドサーチするための引数を渡すことでハイパーパラメータの調整もできるよーってことがわかったのでOKとします。 結果の可視化 主に、plot_modelとevaluate_modelの二つで結果の確認が可能 sample.py plot_model(tuned_custom_model) evaluate_model()では、グラフ上のボタンを押すことで様々な特徴量を確認することができる。 sample.py evaluate_model(tuned_custom_model) 予測精度の確認 predict_model()を使うことで予測精度の確認ができます。 特にデータは指定していませんが、setup時にトレーニングデータとテストデータに分けられているので、問題なし! ちなみに、predict_modelではテストデータが使われます。 sample.py predict_model(tuned_custom_model) スクロールバーで一番右にすると予測結果(Label)と正解データ(medv)が確認できます。 R2も0.8459と言うことで過学習も特にして無さそうな感じかなー、まずまずって感じですかね。 振り返り(個人の感想) と言うことで、まずはここまで読んでいたただいてありがとうございます。 pycaretのインストール、モデル構築/予測をやってみました。個人的にはすごく便利だなーといった印象でした。 ただ、pycaretも一応前処理を行ってくれますが、pycaretに入れるデータの前処理はpandas等で行った方がいいのかなーと思いました。 (欠損値補間で平均が入るのですが、個人的にはラベル別の平均値とかを入れたいので) 「pandasでインプットデータを整えて、pycaretに渡す」 こんな感じになるのかな〜、まぁ使いやすいように使えってことですね。 以上、お疲れ様でした!
- 投稿日:2021-12-05T23:37:45+09:00
【Python】Altair で様々な散布図を作成する
概要 本稿ではグラフ可視化ライブラリ Altair で作成可能な散布図をいくつか紹介する。この作成例を参考に、様々な散布図を作成してみよう。 参考 散布図以外にも Altair では様々な Figure を作成できる。まずはこちらの記事を参考にしよう。 データの作成 前稿と同様、Figure 作成用のテストデータとして架空の学校で行われた期末試験の得点を使用する。この学校には学生が 300 人在籍し、普通、特進、理数の 3 クラスが存在する。期末試験の科目は国語、数学、理科、社会、英語で各教科 100 点満点とする。 デモデータ作成 import numpy as np import pandas as pd np.random.seed(1)# 乱数の固定 n = 300 # 学生の人数 s = np.random.normal(55,10,n) # 学生の学力(score) c = np.random.randint(0,3,n) # クラス s = s * (1 + c * 0.015) # クラスの学力差をつける g = np.random.randint(0,2,n) # 性別 # 得点データの生成 s1 = np.random.uniform(0.75,1.1,n) * s * (1 + g * 0.02) s2 = np.random.uniform(0.9,1.1,n) * s * (1 - g * 0.05) s3 = np.random.uniform(0.9,1.05,n) * s * (1 + g * 0.03) s4 = np.random.uniform(0.9,1.2,n) * s * (1 - g * 0.02) s5 = np.random.uniform(0.8,1.1,n) * s * (1 + g * 0.01) sex = ['男','女'] # 性別 cl = ['普通','理数','特進'] # クラス sub = ['国語','数学','理科','社会','英語'] # 教科 df = pd.DataFrame() df['学生番号'] = list(map(lambda x: 'ID'+str(x).zfill(3), range(1,1+n))) df['国語'] = list(map(lambda x: round(x), s1)) df['数学'] = list(map(lambda x: round(x), s2)) df['理科'] = list(map(lambda x: round(x), s3)) df['社会'] = list(map(lambda x: round(x), s4)) df['英語'] = list(map(lambda x: round(x), s5)) df['合計'] = df['国語'] + df['数学'] + df['社会'] + df['理科'] + df['英語'] df['クラス'] = list(map(lambda x: cl[x], c)) df['性別'] = list(map(lambda x: sex[x], g)) print(df.head(10)) df.head(10) 学生番号 国語 数学 理科 社会 英語 合計 クラス 性別 0 ID001 65 68 68 72 76 349 普通 男 1 ID002 48 52 49 56 47 252 普通 男 2 ID003 52 45 50 49 45 241 普通 女 3 ID004 48 39 46 45 39 217 普通 女 4 ID005 52 62 71 68 63 316 特進 女 5 ID006 27 31 32 32 33 155 特進 女 6 ID007 74 63 77 80 78 372 普通 女 7 ID008 53 48 48 52 50 251 特進 男 8 ID009 58 55 60 58 55 286 特進 女 9 ID010 58 53 48 63 48 270 理数 男 各種設定 各種設定 import altair as alt from altair_saver import save # 図のサイズ width = 300 height = 300 # Figure に使用する色 color_lst = ['steelblue','darkorange'] # 軸に表示させる値 values_lst = [0,20,40,60,80,100] 周辺分布をヒストグラムで表示させた散布図 ヒストグラムと散布図 # Figure 作成 class_radio = alt.binding_radio(options=cl) class_select = alt.selection_single( fields=['クラス'], bind=class_radio, name="class",init={'クラス': cl[0]}) base = alt.Chart(df).add_selection(class_select).transform_filter(class_select) scatter = base.mark_circle(size=30).encode( x=alt.X('国語', scale=alt.Scale(domain=[0,100]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, values=values_lst,title='国語の得点') ), y=alt.Y('数学', scale=alt.Scale(domain=[0, 100]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, values=values_lst,title='数学の得点') ), color=alt.Color('性別', scale=alt.Scale(domain=sex,range=color_lst), ), tooltip=['国語', '数学'], ).properties( width=width,height=height ) hist_x = base.mark_bar().encode( x=alt.X("国語", bin=alt.Bin(step=10,extent=[0,100]), axis=None ), y=alt.Y('count(国語)', axis=alt.Axis(labelFontSize=15, titleFontSize=18,title='人数'), ), color=alt.Color('性別', scale=alt.Scale(domain=sex,range=color_lst), ), ).properties( width=width,height=height//3 ) hist_y = base.mark_bar().encode( y=alt.Y("数学", bin=alt.Bin(step=10,extent=[0,100]), axis=None ), x=alt.X('count(数学)', axis=alt.Axis(labelFontSize=15, titleFontSize=18,title='人数'), ), color=alt.Color('性別', scale=alt.Scale(domain=sex,range=color_lst), ), ).properties( width=width//3,height=height ) # 図の保存 save(hist_x&(scatter|hist_y),'histgram_scatter.html',embed_options={'actions':True}) 【注意点1】 今回はヒストグラムの厚みが散布図のサイズの 1/3 になるように指定している。properties から変更可能。 【注意点2】 ヒストグラムの軸は、散布図にあわせて指定すること。X 軸 と Y 軸を混同しやすい。 【ポイント】 ラジオボタンで普通・理数・特進の切り替えができる。 単回帰分析と散布図 Altair ではパラメトリックモデルおよびノンパラメトリックモデルの単回帰分析が可能である。 パラメトリック回帰 scatter = alt.Chart(df).mark_circle(size=30).encode( x=alt.X('国語', scale=alt.Scale(domain=[0,100]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, values=values_lst,title='国語の得点') ), y=alt.Y('数学', scale=alt.Scale(domain=[0, 100]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, values=values_lst,title='数学の得点') ), color=alt.Color('性別', scale=alt.Scale(domain=sex,range=color_lst), legend=alt.Legend(labelFontSize=15,titleFontSize=18) ), ).properties( width=width,height=height ) # パラメトリック回帰 regress = scatter + scatter.transform_regression( '国語', '数学', method="poly", order = 1,groupby=['性別']).mark_line(shape='mark') regress = regress.facet( column = alt.Column('クラス', header=alt.Header(labelFontSize=15, titleFontSize=18) ) ) save(regress, 'scatter_regression.html',embed_options={'actions':True}) 【ポイント1】 transform_regression を用いることで様々なパラメトリックモデルの単回帰分析ができる。ただし poly を用いるときは次元数を order で指定する。order=1 の場合 linear` と同じ一次関数の単回帰分析が行われる。 method="XXXXXXX" Model linear y = a + b * x log y = a + b * log(x) exp y = a + exp(b * x) pow y = a * xb quad y = a + b * x + c * x2 poly y = a + b * x + … + k * xorder 【ポイント2】 transform_regression では groupby を用いて男女別々に回帰している。 【ポイント3】 ノンパラメトリック回帰には transform_loess を用いる。 ノンパラメトリック回帰 # ノンパラメトリック回帰 regress = scatter + scatter.transform_loess( '国語', '数学', groupby=['性別']).mark_line(shape='mark') ヒストグラムと連動した散布図 合計得点の分布と国語・数学の得点分布を対応させた Figure を作成する。 ヒストグラムと連動した散布図 # interval selection in the scatter plot brush = alt.selection(type="interval", encodings=['x']) # left panel: scatter plot points = alt.Chart().mark_point(filled=True, color="black").encode( x=alt.X('国語', scale=alt.Scale(domain=[0,100]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, values=values_lst,title='国語の得点') ), y=alt.Y('数学', scale=alt.Scale(domain=[0, 100]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, values=values_lst,title='数学の得点') ), color=alt.condition(brush, alt.Color('性別:N', scale=alt.Scale(domain=sex,range=color_lst), legend=alt.Legend(labelFontSize=15,titleFontSize=18) ), alt.value("#ddd") ), ).add_selection( brush ).properties(width=300,height=300) # right panel: histogram base = alt.Chart().mark_bar().encode( x=alt.X('合計:Q', bin=alt.Bin(step=50,extent=[0,500]), scale=alt.Scale(domain=[0,500]), axis=alt.Axis(labelFontSize=15, titleFontSize=18, title='合計') ), y=alt.Y('count()', axis=alt.Axis(labelFontSize=15, titleFontSize=18, title='人数') ) ).properties( width=300,height=300 ) # gray background with selection background = base.encode( color=alt.value('#ddd') ).add_selection(brush) # blue highlights on the transformed data highlight = base.transform_filter(brush).encode( color=alt.Color('性別:N', scale=alt.Scale(domain=sex,range=color_lst), legend=alt.Legend(labelFontSize=15,titleFontSize=18) ) ) hist = alt.layer(background,highlight) # build the chart: chart = alt.hconcat(points,hist,data=df) save(chart,'histgram_scatter_interactive.html',embed_options={'actions':True}) 【ポイント1】 例えば下図では,ヒストグラムで合計得点 300~350 点を選択して、その得点帯にいる人のプロットを散布図でハイライトさせている。 【ポイント2】 一方で下図では,散布図で国語の得点 40~60 点を選択して、その得点帯にいる人の合計得点をヒストグラムでハイライトさせている。
- 投稿日:2021-12-05T23:24:29+09:00
カラーのPDFを白黒2値に変換するやつ
はじめに この記事は苫小牧高専アドベントカレンダー2021 6日目の記事です。 これを作った話です 導入 とある先生がノート提出を白黒2値のpdfで出せとのたまいました。 そこでカラーのPDFを白黒2値で印刷する必要があったのですが、調べた感じAcrobat Proが必要っぽかったです。 奴その先生はライセンスを持っていますが、我々学生はそんな高いものは持っていません。 大変キレたので自分用にこれを作成しました。 MacOSなら普通に二値化できるらしい 成果物 使い方 CTRL+ENTERでセル内のコードを実行しつつ次のセルに進むことができます。 セルを順番に実行したいときはCTRL+ENTERを連打すると良いでしょう 1つ目のセルを実行します。 (pdfをアップロードするためのフォルダ(pdf_file)が生成されます。) 左のファイルアイコンをクリックしてファイル管理する画面を開きます。 pdf_file/以下に変換したいPDFファイルをアップロードします。 複数のpdfをアップロードできます!! 残ったすべてのセルを上から順に実行します。 ファイルを開きなおします(更新するため)。 ↓開きなおした後のファイル画面 増えたフォルダについての解説は以下の通りです。 result_dir 白黒変換後のpdfが格納されているフォルダ。 img_dir 白黒変換後のpdfの内容がページ毎に画像化したものが格納されているフォルダ こんな感じで格納されています。 ほしいデータをダウンロードします。 補足:二値化のしきい値は1つ目のセルにある変数threshに代入する値をいじることで変更できます(0~255) 実行例 before after(thresh==170) after(thresh==200) ソースコード ここに載ってます。 やってることはpdf2imageでpdfを画像に変換し、その画像をcv2で二値化してimg2pdfでpdfにくっつけなおしているだけです。 終わりに 奴先生は多分この課題を出し続けると思われます。 そうなったときにこの記事を思い出して楽をしてもらえたらなと思いこの記事を執筆しました。 ちなみに紙でノートを取っている人はスキャンの段階で二値化できるのでそうしたほうが速いですね。 私はiPadのGoodNotesを使ってノートを取っていたのでバックアップのpdfファイルをこれに投げて提出しました。締め切り後に 苫小牧高専アドベントカレンダー2021、明日は[nullpointerexception]さんです!! それでは!!
- 投稿日:2021-12-05T23:20:48+09:00
色々置いといて取り敢えずpythonでスクレイピング
はじめに 初めまして。おおのと申します。最近はパワポケやダンガンロンパ、月姫など昔のゲームのリメイクやら移植作品がたくさん出てうれしくて全部買ってしまったのに一つもやる時間が無くてやばいクレーマーと化しそうです。あと金もなくなりました。 そういえばポケモンのダイヤモンドとパールのリメイクも出ましたね。当然私も購入したのですが、バッジ一つも取れておらずもうパルキアにでもなってあたりを焼け野原にしたい気持ちでいっぱいです。切に中学時代の友達と会って対戦がしたいです。 ところで、突然ですが日常生活でスクレイピングしないといけない事態に陥ったことはありませんか?僕は無いですが、先日なんとなくスクレイピングをしてみたので、軽くまとめていきたいなと思います。自分も初心者でググりながら色々やってみたクチなのでより詳しい解説欲しい人はこんなばかみたいな「はじめに」書いてる記事なんか読んでないで別の記事探してください。(ごめんなさい) ここでは大雑把に自分が書いたコードの説明だけします。 対象としてはスクレイピングやってみたいけどどうしたらいいかさっぱりわからない初心者さんでしょうか。一度読んでもらって処理の大雑把な流れを把握してもらえたらいいと思います。 それから、僕も普通にプログラミングも初心者なので、コードの可読性とか変数名とかゴミなのもある程度許容できる人でないとストレスで消化器官の67パーセントくらいが機能不全になるかもしれないのでマジで注意してください。 次のセクションから説明を書いていきます。 目次 小説家になろうのランキング取得 適当なポケモンの名前とそのポケモンが覚える技取得 終わりに 小説家になろうの月間総合ランキング取得 まずは簡単なプログラムから載せていきます from bs4 import BeautifulSoup from urllib import request novel_names = [] url = 'https://syosetu.com/' res = request.urlopen(url) soup = BeautifulSoup(res,'html.parser') for i in range(10): novel = soup.find(class_ = 'p-ranking__item p-ranking__item--col2 p-ranking__item--' + str(i+1) + ' c-novel-item c-novel-item--ranking') url2 = novel.get('href') res2 = request.urlopen(url2) soup2 = BeautifulSoup(res2,'html.parser') title = soup2.find('title') novel_names.append(title.get_text()) print('第' + str(i+1)+ '位:' + title.get_text()) 何をするプログラムかわかりますか?スクレイピングです。 すみません許してください。なんでもはしませんがこのプログラムについて説明します。 このプログラムは「小説家になろう」の月間総合ランキングを出力するプログラムです。import文を見るとわかるのですが、bs4(beautifulsoup4)というライブラリとurllibというライブラリを使っています。上から順にプログラムに現れる変数の説明をしていきます。 変数 novel_names:実は何もしていません。それに気づけた貴方はえらい url:みんな大好き「小説家になろう」さんのトップページURLです res:requestのメソッドurlopenで指定URLをオープンします soup:BeautifulSoupオブジェクトです。resとオプション(今回はhtmlの解析を行うのでhtml.parser)を引数に渡してあげることでurlの先のhtmlを解析し、簡単にデータを取り出せる形にしてくれます novel:soupのfind関数を使って指定したclassの要素を得ています。小説家になろうのhtmlソースを読むとわかるのですが、月間総合ランキングは'p-ranking_item p-rankingitem--col2 p-ranking_item--' + str(i+1) + ' c-novel-item c-novel-item--ranking'というclassの(i+1)の部分が順位で、指定してあげるとhtmlからその順位のタイトルを含むhtmlの要素が取れるようになっています。for文で10位まで回していますね。 url2:頭の悪い変数名でごめんなさい。本当は上のnovelの時点でタイトル情報は取得できているんですが、いかんせん最近のなろう小説はタイトルが長くて枠に入りきらず、この時点では折りたたまれてしまっていることがしょっちゅうなのです…(ソースでも折りたたまれていることを知らなかった情弱はこちら)。なのでget関数で取得したhtmlの要素からhref(リンク)を取得し、ランキングのページではなくその小説のページに飛んでから正式なクソ長ぇタイトル(投稿者さんごめんなさい)を取得することが必要になります。 res2、soup2:resとsoupと同様です。小説のページにとんだ先でhtmlを取得します。 title:飛んだ先のhtmlからtitleの要素をfindで取得します。ちなみに、この段階では出力してもまだゴミがついてくるためget_text関数を使って単なるテキストのみ取り出します。それを下でprintしています。 サラッとnovel_namesにtitleの追加を行っていますがこれ何しようとしてたんでしょうねははは。 そこそこ細かめに見ていくとこんな感じです。だらだら書くんじゃねぇよなぐるぞって言われたくないので簡単に流れを説明しなおすとまず小説家になろうのトップページのhtmlの内容取得、次にランキングの部分からトップ10の小説ページへのリンクを取得しそのページに飛ぶ、そしてタイトルを取得するという流れです。何気にページ遷移+タイトル取得という流れがめんどくさいですね。でも入門としては悪くないと思うのでまず最初にこんな感じで取れそうなものを取ってみるということをやるのがおすすめです。 ちなみに本プログラム実行結果はこんな感じ 時期によって当然変わるのですが半分くらいクソ長タイトルでしたね 取り敢えずまずはただプログラムを動かしてもらって、それからhtmlソースを実際に眺めてもらうのが良いと思います。 ポケモンの名前と適当な覚える技取得 最初の方に述べたのですが、私はポケモンそこそこ好きです。ポケモンは対戦ゲームなのですが、対戦するにあたってポケモンを育てる必要があります。しかし、どんなポケモンを育てたらいいか思いつかない!!!そんなときの強い味方みたいなプログラムを作りました。ランダムでポケモンの名前とそのポケモンが覚える技から4つ取得してくれます。これでもうポケモン選びにも技選びにも迷うことはありませんね!!!!早速プログラムを見ていきましょう。 import requests from bs4 import BeautifulSoup import random pokemon =[] i = 0 wazalist = [] for count in range(6): url1 = "https://yakkun.com/swsh/zukan/n" a = random.randint(1,898) url = url1 + str(a) r = requests.get(url) soup = BeautifulSoup(r.content,'html.parser') pokename1 = soup.find('title') pokename2 = pokename1.get_text() pokename = pokename2.split("|") while pokename[0] in pokemon: url = url1 + str(random.randint(1,898)) r = requests.get(url) soup = BeautifulSoup(r.content,'html.parser') pokename1 = soup.find('title') pokename2 = pokename1.get_text() pokename = pokename2.split("|") print("ポケモン名:" + pokename[0]) pokemon.append(pokename[0]) wazamei = soup.find_all(class_ ='move_name_cell') for k in wazamei: c = k.find('a') wazalist.append(c.get_text()) i = i + 1 if a == 235: print("技:スケッチ") i = 0 wazalist =[] print() elif a == 132: print("技:へんしん") i = 0 wazalist =[] print() elif a == 789: print("技1:はねる") print("技2:テレポート") i = 0 wazalist =[] print() else: num1 = random.randint(0,i-1) num2 = random.randint(0,i-1) while wazalist[num2] == wazalist[num1]: num2 = random.randint(0,i-1) num3 = random.randint(0,i-1) while wazalist[num1] == wazalist[num3] or wazalist[num2] == wazalist[num3]: num3 = random.randint(0,i-1) num4 = random.randint(0,i-1) while wazalist[num4] == wazalist[num1] or wazalist[num4] == wazalist[num2] or wazalist[num4] == wazalist[num3]: num4 = random.randint(0,i-1) print("技1:" + wazalist[num1]) print("技2:" + wazalist[num2]) print("技3:" + wazalist[num3]) print("技4:" + wazalist[num4]) i = 0 wazalist =[] print() スクレイピング元は「ポケモン徹底攻略」さんです。情報がとてもよくまとまっています。 ちなみにURLを開くのに使ったライブラリは先ほどのurllibではなくrequestsです。元々requestsを使っていたのですが、なぜかうまくいかずurllibで上手くいったということが何度かあったのでどっちかだめならどっちか試してみると良いと思います。 このサイトではポケモンの名前から覚えるポケモンの技名までしっかり一つのページで完結します。なんとかになろうとかとは違いますね(ごめんなさい) ちなみにこのプログラムはパーティ単位で出力できるようにポケモン6体分出力してます(なのでfor文6回)最初に宣言した配列pokemonの中にすでに出たポケモンは入れて、その後に出たときにはじくようにしています。 urlの指定はhttps://yakkun.com/swsh/zukan/n の後ろにそのポケモンのぜんこく図巻番号を入れるので、ランダムで1~898までの整数を入れてあげることでランダムに全てのポケモンのページを取得できるようにしています。今回は一つ一つ変数を解説しませんがsoupにfind_allという見たことない関数があると思います。これはhtmlの特定のclassを持つhtmlの要素を配列形式で上から順番に全部返してくれる関数です。先ほど出てきたfindはそのclassを持つ一番上のhtmlの要素だけ返します。 move_name_cellというclassにはそのポケモンが覚える技がすべて格納されています。終わりの方ではダブり処理をしながら中から技を4つ選んでいます。確実にもっといい処理はあるのでマネしないでください。普通に配列の中身参照すればよかったと思います。あとコード中にへんしんとかスケッチとか書いてあるのはポケモンやってる人ならわかると思うんですけど、技4つ覚えないポケモンが何匹かいるのでそのための例外処理です。 またやってることをまとめると、ポケモンのページをランダムで決定、覚える技全部取得、中から4つ選び出力という非常にシンプルな流れです。プログラム内でsplitとか使ってるのは手に入る文字列がそのまま扱えるわけではないので上手いこと切り出したりしています。 実行例はこんな感じ。(本当は6体出ます)なんか攻撃的なハピナスが生まれてしまいました。 終わりに 今回は初心者がスクレイピングをやってみたのを振り返るため一度記事を作ってみました。このプログラムが参考になるかはわかりませんがこれの動きを理解してもらってなんとなくスクレイピングのやり方を知ってもらえたらいいかなと思います。 最後まで読んでいただきありがとうございました。
- 投稿日:2021-12-05T23:15:09+09:00
取り敢えずpythonでGUIを使ってみる
はじめに こんにちは、おおのと申します。僕の趣味はゲーセンで音ゲー(主にチュウニズム)をすることなのですが、僕よりずっと後から始めた友達があまりに上手すぎて絶望しています。 それはおいといて今回の本題です。プログラミングの勉強を始めてコマンドラインで動くツールを幾つか作ったりしたのですが普段使うツールって大体GUI使ってますよね。自分でもGUIで何か作りたいなと思い簡単なものを作ってみたので、プログラミング超初心者仲間さんに共有したいと思い記事を書きました。マジで使ってみただけで何の工夫もできておらず基本の基本を動かしてみたレポートという感じですがそれでも見てみたい人は見てくれると嬉しいです。 目次 チュウニズムのスコア計算ツール 終わりに チュウニズムのスコア計算ツール 「はじめに」で述べたように僕はチュウニズムが好きなのですが、チュウニズムに限らずゲームでスコアを狙う時どの程度を目指せばいいかを事前に知っておくと遊びやすくなると思います。このゲームのスコア計算式は既に解明されているので、狙っているスコアを達成するにはどのくらいミスを削ればいいかというのを計算することができます。 チュウニズムやら音ゲーやらを知らない人も多いと思うので、具体的に何がしたいかということを説明します。 チュウニズムのプレイ画面ってこんな感じなのですが、画面上の方にJUSTICE CRITICAL, JUSTICE, ATTACK, MISSと書いてあると思います。左から順番にスコアの高い判定を示しており、右に行くほど得られるスコアの低い判定になります。画面にノーツというものが流れてくるのですが、それをタイミングぴったりで押せると一番左が加算され、タイミングがずれるにつれ右の方の判定に加算されてしまうという感じです。 チュウニズムでは満点が101万点になっていて、全てのノーツをJUSTICE CRITICALで取ると満点が取れます。対して、右のJUSTICEはJUSTICE CRITICALの100/101のスコア、その右のATTACKはJUSTICE CRITICALの50/101のスコア、その右のMISSはスコアが0となっています。ちなみに、普通にこのゲームをする上ではJUSTICEまでしか出さないでクリアするとALL JUSTICEというものが取れて、基本はそれを目指すことになるかなと思います(満点はやばい人しか狙いません) 点数計算をしたいときに必要になる情報はノーツが全部でいくつあるかということとJUSTICE CRITICAL以外の判定をいくつずつ出したかです。101万点が満点なので、出した判定に応じて削られる点数分101万から引いていけばよいですね。ちなみにこのゲームのスコアは小数点以下切り捨てのようなのでそれも織り交ぜることといたします。 参照:https://chunithm.gamerch.com/%E3%82%B2%E3%83%BC%E3%83%A0%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0#content_2_2 今回はGUIを使ってチュウニズムのスコア計算をしてくれるツールを作りました。以下にコードを載せます import tkinter as tk import tkinter.ttk as ttk from tkinter import messagebox import math def calcscore(): if int(entry0.get()) < (int(entry1.get()) + int(entry2.get()) + int(entry3.get())): messagebox.showinfo("Error",'ノーツ数とミスカウントが合いません') else: score = math.floor(1010000 - (int(entry1.get())*(1010000/int(entry0.get()))*(1/101) + int(entry2.get())*(1010000/int(entry0.get()))*(51/101) + int(entry3.get())*(1010000/int(entry0.get())))) #math.floorは小数点以下切り捨て、entry1はJUSTICEの数を示しており、その分だけ1/101を減点、entry2,3はATTACK、MISSに対応しており以下同様 rank = "" if score >= 1009000: rank = 'SSS+' elif score >= 1007500 and score < 1009000: rank = "SSS" elif score >= 1005000 and score < 1007500: rank = "SS+" elif score >= 1000000 and score < 1005000: rank = "SS" elif score >= 990000 and score < 1000000: rank = "S+" elif score >= 975000 and score < 990000: rank = "S" else: rank = "under S" messagebox.showinfo("score",str(score) + " " + rank) # rootフレームの設定 root = tk.Tk() root.title("CHUNITHMのスコア計算") root.geometry("200x120") # 各種ウィジェットの作成 label0 = ttk.Label(root, text="ノーツ数:") entry0 = ttk.Entry(root) label1 = ttk.Label(root, text="justice数:") entry1 = ttk.Entry(root) label2 = ttk.Label(root, text="attack数:") entry2 = ttk.Entry(root) label3 = ttk.Label(root, text="miss数:") entry3 = ttk.Entry(root) button_execute = ttk.Button(root, text="実行", command=calcscore) # 各種ウィジェットの設置 label0.grid(row=0, column=0) entry0.grid(row=0, column=1) label1.grid(row=1,column=0) entry1.grid(row=1,column=1) label2.grid(row=2,column=0) entry2.grid(row=2,column=1) label3.grid(row=3,column=0) entry3.grid(row=3,column=1) button_execute.grid(row=4, column=1) entry1.insert(0,"0") entry2.insert(0,"0") entry3.insert(0,"0") root.mainloop() 算術ライブラリのmathとGUIを使うためのライブラリtkinterを使ってみました。 実行すると以下のようなウィンドウが開きます。 JUSTICE、ATTACK、MISSのカウントは先に0埋めしてあるので適宜数字を入れてください。ノーツ数は曲によって違うので調べて自分で入れてください。これで下のように適当な数字を入れて実行すると このように結果が出ます。スコアの実数値とスコアランクですね。 説明していませんでしたが、このゲームではスコアに応じてSSS+~Dというランクが付きます。申し訳程度にそれにも対応してみました。(どのスコアがどのランクに対応しているかは上の参考ページに載ってます) 2300ノーツの曲ならこれくらいでSSSが取れるのがわかりますね。(ちなみに2300はかなり物量のある方だと思います) では、このプログラムの説明をしていきたいと思います。 まずGUIのウィンドウの設計です。 # rootフレームの設定 root = tk.Tk() root.title("CHUNITHMのスコア計算") root.geometry("200x120") rootはTKオブジェクト(画面上のウィンドウに対応) titleでウィンドウの名前を指定 geometryでウィンドウの縦横比を指定 次はウィンドウ内に要素を置いていきます # 各種ウィジェットの作成 label0 = ttk.Label(root, text="ノーツ数:") entry0 = ttk.Entry(root) label1 = ttk.Label(root, text="justice数:") entry1 = ttk.Entry(root) label2 = ttk.Label(root, text="attack数:") entry2 = ttk.Entry(root) label3 = ttk.Label(root, text="miss数:") entry3 = ttk.Entry(root) button_execute = ttk.Button(root, text="実行", command=calcscore) # 各種ウィジェットの設置 label0.grid(row=0, column=0) entry0.grid(row=0, column=1) label1.grid(row=1,column=0) entry1.grid(row=1,column=1) label2.grid(row=2,column=0) entry2.grid(row=2,column=1) label3.grid(row=3,column=0) entry3.grid(row=3,column=1) button_execute.grid(row=4, column=1) entry1.insert(0,"0") entry2.insert(0,"0") entry3.insert(0,"0") root.mainloop() ウィンドウに各種情報の入力フォームと実行ボタンを設置します。 ttk.Labelで入力フォーム横のテキストを設定し、ttk.Entryで入力フォームを、ttk.Buttonでボタンを設定します。 それぞれgridで配置する行と列をrowとcolumnで指定しながら配置します。最後にentry1~3に0を標準入力するようにしてroot.mainloopでイベントループを開始します。 これでツールのウィンドウが出来上がりました。 次に実行ボタンを押したときに動かす関数を定義します。 button_execute = ttk.Button(frame, text="実行", command=calcscore) ここでボタンの作成と実行する関数の設定をしてますね。 以下が関数calcscoreです。 def calcscore(): if int(entry0.get()) < (int(entry1.get()) + int(entry2.get()) + int(entry3.get())): messagebox.showinfo("Error",'ノーツ数とミスカウントが合いません') else: score = math.floor(1010000 - (int(entry1.get())*(1010000/int(entry0.get()))*(1/101) + int(entry2.get())*(1010000/int(entry0.get()))*(51/101) + int(entry3.get())*(1010000/int(entry0.get())))) rank = "" if score >= 1009000: rank = 'SSS+' elif score >= 1007500 and score < 1009000: rank = "SSS" elif score >= 1005000 and score < 1007500: rank = "SS+" elif score >= 1000000 and score < 1005000: rank = "SS" elif score >= 990000 and score < 1000000: rank = "S+" elif score >= 975000 and score < 990000: rank = "S" else: rank = "under S" messagebox.showinfo("score",str(score) + " " + rank) entryの要素を読み取ったりしてscore及びrankを算出し、新たなポップアップウィンドウに表示します(messagebox.showinfoの引数で指定) 結局GUIを使ってツールを作るにはどうすればよいかといえばGUI用のウィンドウと実行時に動かしてあげる関数を作ってあげればいいわけですね。 おわりに 今回はかなりシンプルな題材で作ってみました。複数入力の必要になるツールにはコマンドラインよりこういった形の方が良いのではないかと思います。装飾など一切考えずとりあえず動くテンプレートみたいになってしまいましたが、実際にGUIを使って何かしらのきっかけになればと思います。 今回は読んでいただきありがとうございました。
- 投稿日:2021-12-05T22:53:09+09:00
TouchDesigner×Pythonで軽率にOSC通信する方法を初心者向けに説明する
この記事はIwaken Lab.アドベントカレンダーの6日目です。 背景 同一ネットワークのリアルタイム通信を行う場合、UDPやTCP通信など行う場合、OSC通信が有用である場合があります。 例えば、私が以前HoloLensプレゼンを行った時の、HoloLens→Unreal Engineへの通信もOSC通信で行いました。 ここで、入力と出力を増やしたいといった場合に、OSC通信を束ねる役割があると便利になります。(リレーサーバー的な役割) それをTouch Designerで行う場合のヒントになる記事がこちらになります。 OSC通信とは OpenSound Control(OSC)とは、電子楽器(特にシンセサイザー)やコンピュータなどの機器において音楽演奏データをネットワーク経由でリアルタイムに共有するための通信プロトコルである https://ja.wikipedia.org/wiki/OpenSound_Control OSC(Open Sound Control)とは,送信者(クライアント)から受信者(サーバ)へ,つぎのようなデータを送るための仕組みです. https://www.ei.tohoku.ac.jp/xkozima/lab/espTutorial4.html もともとは電子楽器のコンピュータ制御のためにつくられましたが,今ではさまざまな用途に使われています.通信の実装部分には UDP を利用することが多いようです.UDP とは,TCP と同様に,IP(Internet Protocol)を使ったデータ通信方法のひとつです. UDP通信を利用することが多いというのもポイントです。 つまり、IPアドレスとPort番号が分かればデータを投げつけることができます。 投げられるデータは 整数 (int) 実装 (float) 文字列 (string) などです。 なぜOSC通信が有用か 同一ネットワーク内でのリアルタイム通信を行いたいケース、例えば撮影スタジオ、VJイベント、XR展示ではOSC通信が便利になるケースが多いです。 個人的意見としては以下です 複数のツール/プログラミング言語に対応している Madmapper,TouchDesigner Unity,Unreal Engine Python,C#,C++... 実装が楽 グローバルに行う場合、サーバーを準備する必要がある→ローカルネットワーク内の通信を行うには too Much UDP通信、TCP通信→ 既存のツールで対応していない場合がある OSC通信→既存ツールで対応している + 実装もシンプル TouchDesingerとは TouchDesignerというのは、カナダのDerivative社が開発しているアプリケーションで、コードを書かなくてもインタラクティブなコンテンツが作成できるため、デザイナーからプログラマー、アーティストまで、幅広くコンテンツ開発ができるということが魅力のひとつです。では、コードを書かずにプログラミングができるというのはどういうことでしょうか。以下で実際の画面を見ていただくとわかるかと思います。 https://www.taki.co.jp/blog/lab_takagi_20190902/ ノードベースで3Dのレンダリングや、データのやり取りをできるツールです。 メディアアートやVJなどに使われることが多いそうです。 なぜTouchDesignerでOSCを行いたいか OSC通信を束ねるリレーサーバー的な役割に関して、ぶっちゃけ手段は何でもOKです チームに合わせて「どの技術力が高いか」「現場での柔軟性が高い手段が何か」を照らし合わして技術選定すると良いです。 私の場合 現場ではソースコードを書けない人も微修正対応する可能性がある アプリ化してUI的に使いやすいようにしたい TouchDesigner触ってみたい という理由から、C#やC++などではなく、TouchDesignerを選びました。 この記事が解決すること 今までTouchDesinger触っていなかったプログラマが、TouchDesignerを触ると 「どこから触ればいいかわからない」 となり、調べようにも 「Pythonでカスタマイズしたいが、英語の記事しかない」 「意外とピンポイントで知りたい記事が見つからない」 といったケースが多かったです。 この記事では、TouchDesigner×PythonでOSC通信を最低限行う実装とオススメ設定をご紹介します。 手順 最初の準備を行う OSCIn/Outを作る Pythonでリレー通信の処理を書く 最初の準備を行う TouchDesignerを立ち上げると、NewProject.toeが立ち上がります。 まずこれらのノードたちを削除しましょう。 ノードを選択してBackSpaceキーを押します。 OSCInノードを作って設定 OSC通信を受け取るノードになります。 まずTabキーを押すとOP Create Dialogが現れます。これを選ぶと様々なノードを生成できます。 ここから「DAT」タブを選択し、OSC Inを選択します。 OSC Inを生成。 生成されたOSCInをクリックして「Pキー」をクリックします (パラメータ画面を開くショートカットキーになります) OSC Inの設定について以下について設定します 項目 設定 Active On Protocol Messaging(UDP) Port 7000 (なんでもOK) LocalAddress (自分のPCのIPAddress) OSCOutノードを作って設定 OSC通信を送信するノードになります。 送信する目的地の数だけ作る想定です。 まずTabキーを押して「DAT」タブを選択し、OSC Outを選択します。 パラメータ設定を以下のようにします。 項目 設定 Active On Protocol Messaging(UDP) NetWorkAddress (送り先のIPAddress) Port 7001 (なんでもOK/OSC Inと変えてみる) Pythonでリレー通信の処理を書く 狙い、OSC通信を受け取って、別のIPAddressに通信する。 oscin1にくっついているこのノードをPythonで書き換える。 お気に入りのEditorで編集 ノードを選択し「Ctrl+E」もしくは右クリックで「Edit Contents」を押すと、Editorが立ち上がります。 この時のEditorを設定するには、 [Edit]>[Preferences]でダイアログを開き [DATsタブ]>[Text Editor]で選択 [Save]ボタン 私はVSCodeを選択しています。 Pythonで書いてみる 開くとこのような画面が開きます。 onReceiveOSC関数を書き換えましょう。 def onReceiveOSC(dat, rowIndex, message, bytes, timeStamp, address, args, peer): # oscout1という名まえのノードを取得 osc_out = op("oscout1") # address: OSCAddress 例:"/hoge/fuga" # args: 変数のリスト 例:[0,0,1] osc_out.sendOSC(address,args) return これで、OSCInから受け取ったOSCAddressと引数がOSCOutの目的地へ送られる実装ができました。 これで完成です!!! その他便利設定 TouchDesignerを最小化しても、OSC通信を止めないようにする デフォルトではTouchDesingerを最小化すると、OSC通信のプロセスが止まってしまいます。これでは不便です。それを回避する設定は [Edit]>[Preferences]>[General]>[Stop Playing when Minimizedのチェックを外す] デバッグログを吐き出す print("hogehoge")と書くと書き出される。 ログの場所は[Dialogs]>[Textport and DATs]を選択する。すると登場する。 この記事では書きませんでしたが、やりたいと思われること ボタンなどUIを作る 複数のOSC Outに送る 受け取ったOSCAddressによってフィルタリングを行う
- 投稿日:2021-12-05T22:46:21+09:00
数値流体力学3~数値流束~
はじめに 今回は、数値流体力学シリーズの第3回です。移流方程式を数値流束という考え方によって数値計算していきます。 何か間違いなどありましたら気軽にコメントしてください! シリーズ構成 1. スカラー移流方程式 1.1 輸送速度が正の場合 1.2 輸送速度の符号が不明の時の線形問題 1.3 数値流束を利用した方法←本記事 1.4 輸送速度が未知量であるとき(Burgers方程式) 1.5 多次元への拡張(2次元スカラー移流方程式) 1.6 実践的な計算法(TVD方程式) 2. 移流方程式の時間積分法 2.1 陽解法と陰解法について 2.2 時間陰解法について(Crank-Nicholson法、近似LU分解) 3. 拡散方程式 3.1 1次元熱伝導方程式の数値計算法 3.2 楕円方程式の数値計算法 4. 圧縮性流れの数値計算法 4.1 オイラー方程式の数値計算法 4.2 MacCormack法による数値計算法 4.3 TVD法による数値計算法 今回扱う内容 今回は、数値流束がテーマとなっています。まず、数値流束のアイデアを考えたのち、それを第1回、第2回で扱った、移流方程式の数値計算法に適用していきます。最後にスカラー移流方程式の数値計算では有名なGodunovの定理について考えてます。 数値流束とは? 流束$f$を$f=cq$と定義して、スカラー移流方程式を考えると、 $$\frac{\partial{q}}{\partial{t}}+\frac{\partial{f}}{\partial{x}}=0\tag{1}$$ となります。 上図のように空間格子の間に仮想的に境界を考えてみましょう。さて、時間$n$ステップでの格子点$j$と格子点$j+1$との間には、仮想的な境界$j+1/2$があることになります。このような境界面を考えることによって、式(1)は q^{n+1}_{j}=q^{n}_{j}-\frac{\Delta{t}}{\Delta{x}}\Big(\tilde{f}^{n}_{j+1/2}-\tilde{f}^{n}_{j-1/2}\Big)\tag{2} のように離散化できます。ここで、$\tilde{f}^{n}_{j+1/2}$などを数値流束といいます。数値流束には、構成の任意性がある(空間格子の半分のところでなくてもいいわけですね)ので、定義点(空間格子点)での流束と区別して一般的にチルダ「~」をつけることが慣習となっています。 数値流束を数値計算法に適用してみる 第1回、第2回で考えた移流方程式の数値計算法に対して、$j+1/2$における数値流束の評価方法を考えてみましょう。 FTCS法 $j+1/2$における数値流束を評価するとき、左右の平均を利用するとFTCS法となります。 \tilde{f}^{n}_{j+1/2}=\frac{f^{n}_{j+1}+f^{n}_{j}}{2}\tag{3} 1次精度風上法 $j+1/2$における数値流束を流れの風上方向、つまり格子点$j$の流束で評価すると1次精度風上法となります。 \tilde{f}^{n}_{j+1/2}=f^{n}_{j}\tag{4} Lax法 \tilde{f}^{n}_{j+1/2}=\frac{1}{2}\Big\{\Big(1-\frac{1}{\nu}\Big)f^{n}_{j+1}+\Big(1+\frac{1}{\nu}\Big)f^{n}_{j}\Big\}\tag{5} Lax-Wendroff法 \tilde{f}^{n}_{j+1/2}=\frac{1}{2}\Big\{c(1-\nu)f^{n}_{j+1}+c(1+\nu)f^{n}_{j}\Big\}\tag{6} 輸送速度cの符号が不明のとき FTCS法、Lax法、Lax-Wendroff法は離散点に対して対称な評価式であるから、$c$の符号に依存しません。1次精度風上法を考えてみましょう。$f=cq$と定義したことにより、 \tilde{f}^{n}_{j+1/2}=\frac{c+|c|}{2}q^n_j+\frac{c-|c|}{2}q^n_{j+1}=\frac{1}{2}\Big\{\Big(f^{n}_{j+1}+f^{n}_{j}\Big)-|c|\Big(q^n_{j+1}-q^n_{j}\Big)\Big\}\tag{7} これは、圧縮性流体解析にも出てくる重要な考え方になります。 数値流束を用いた高精度化(Godunovの定理) これまでは移流方程式を1次精度で評価してきました。1次精度であっても格子点の数を増やすことによって、それなりに正確な解を得られることができるということは分かったと思います。しかし、実際の数値計算では格子点の数が限られている場合があり、2次精度以上の計算を利用することが必要になってきます。 輸送速度が正のとき スカラー移流方程式は、 $$\frac{\partial{q}}{\partial{t}}+c\frac{\partial{q}}{\partial{x}}=0\tag{7}$$ である。ここで、格子点$j-1$と$j-2$でのテイラー展開を考えてみましょう。 q_{j-1}=q_{j}-\left.\frac{\partial{q}}{\partial{x}}\right|_{j}\Delta{x}+\frac{1}{2}\left.\frac{\partial^2{q}}{\partial^2{x}}\right|_{j}\Delta{x^2}-\frac{1}{3!}\left.\frac{\partial^3{q}}{\partial^3{x}}\right|_{j}\Delta{x^3}+・・・\tag{8} q_{j-2}=q_{j}-\left.\frac{\partial{q}}{\partial{x}}\right|_{j}(2\Delta{x})+\frac{1}{2}\left.\frac{\partial^2{q}}{\partial^2{x}}\right|_{j}(2\Delta{x})^2-\frac{1}{3!}\left.\frac{\partial^3{q}}{\partial^3{x}}\right|_{j}(2\Delta{x})^3+・・・\tag{9} 式(8)の4倍から式(9)を引くと、 \left.\frac{\partial{q}}{\partial{x}}\right|_{j}=\frac{3q_{j}-4q_{j-1}+q_{j-2}}{2\Delta{x}}+O(\Delta{x^2})\tag{10} この式は、2次精度であることがわかります。つまり、これを移流方程式に適用することで、空間微分項を1次精度から2次精度へと高精度化することができますね。 では、式(10)を式(7)に適用してみましょう。 q^{n+1}_{j}=q^{n}_{j}-c\Delta{t}\frac{3q^n_{j}-4q^n_{j-1}+q^n_{j-2}}{2\Delta{x}}\tag{11} これを変形すると、 q^{n+1}_{j}=q^{n}_{j}-c\Delta{t}\frac{\Big(\frac{3}{2}q^n_{j}-\frac{1}{2}q^n_{j-1}\Big)-\Big(\frac{3}{2}q^n_{j-1}-\frac{1}{2}q^n_{j-2}\Big)}{\Delta{x}}\tag{12} ここからわかるように、1次精度の離散化における$q^n_j$を$\frac{3}{2}q^n_{j}-\frac{1}{2}q^n_{j-1}$に置き換えることにより2次精度になることがわかります。 数値流束としては、 \left \{ \begin{array}{l} \tilde{f}^{n}_{j+1/2}=f^{n}_{j} (1次精度) \tag{13} \\ \tilde{f}^{n}_{j+1/2}=\frac{3}{2}f^n_{j}-\frac{1}{2}f^n_{j-1} (2次精度) \end{array} \right. と書けることになります。 さて、この2次精度化を使ってスカラー移流方程式を数値計算してみましょう。プログラムは以下の通りです。 import numpy as np import matplotlib.pyplot as plt def init(q1, q2, dx, jmax): x = np.linspace(0, dx * (jmax-1), jmax) q = np.array([(float(q1) if i < 0.5 * jmax else float(q2)) for i in range(jmax)]) return (x, q) def UPWIND1(q, c, dt, dx, j): ur = q[j+1] ul = q[j] fr = c * ur fl = c * ul return 0.5 * (fr + fl - abs(c) * (ur - ul)) def UPWIND2(q, c, dt, dx, j): ur = 1.5 * q[j+1] - 0.5 * q[j+2] ul = 1.5 * q[j] - 0.5 * q[j-1] fr = c * ur fl = c * ul return 0.5 * (fr + fl - abs(c) * (ur - ul)) def do_computing(x, q, c, dt, dx, nmax, ff, order = 1, interval = 2, ylim = None, yticks = None): plt.figure(figsize=(7,7), dpi=100) plt.rcParams["font.size"] = 22 plt.plot(x, q, marker='o', lw=2, label='n=0') for n in range(1, nmax+1): qold = q.copy() for j in range(order, jmax-order): ff1 = ff(qold, c, dt, dx, j) ff2 = ff(qold, c, dt, dx, j-1) q[j] = qold[j] - dt/dx * (ff1 - ff2) if n % interval == 0: plt.plot(x, q, marker='o', lw=2, label=f'n={n}') plt.grid(color='black', linestyle='dashed', linewidth=0.5) plt.xlabel('x') plt.ylabel('q') plt.legend() if ylim is not None: plt.ylim(ylim) if yticks is not None: plt.yticks(yticks) plt.show() c = 1 dt = 0.05 dx = 0.1 jmax = 21 nmax = 6 q1 = 1 q2 = 0 x, q = init(q1, q2, dx, jmax) do_computing(x, q, c, dt, dx, nmax, UPWIND1, order = 1, interval = 2) c = -1 dt = 0.05 dx = 0.1 jmax = 21 nmax = 6 q1 = 0 q2 = 1 x, q = init(q1, q2, dx, jmax) do_computing(x, q, c, dt, dx, nmax, UPWIND1, order = 1, interval = 2) c = 1 dt = 0.05 dx = 0.1 jmax = 21 nmax = 6 q1 = 1 q2 = 0 x, q = init(q1, q2, dx, jmax) do_computing(x, q, c, dt, dx, nmax, UPWIND2, order = 2, interval = 2, \ ylim = [-1, 1.1], \ yticks = np.arange(-1, 1.1, 0.2)) c = -1 dt = 0.05 dx = 0.1 jmax = 21 nmax = 6 q1 = 0 q2 = 1 x, q = init(q1, q2, dx, jmax) do_computing(x, q, c, dt, dx, nmax, UPWIND2, order = 2, interval = 2, \ ylim = [-1, 1.1], \ yticks = np.arange(-1, 1.1, 0.2)) 実行すると、 ①1次精度かつ$c>0$のとき ②1次精度かつ$c<0$のとき ③2次精度かつ$c>0$のとき ④1次精度かつ$c<0$のとき この結果からもわかる通り、スカラー移流方程式に対しては、以下の形の2次以上の精度をもつスキームを使用すると解の単調性が保てないことが知られています。これをGodunovの定理といいます。 q^{n+1}_{j}=\displaystyle\sum_{k}c_kq^{n}_{j+k} \tag{14} 解の単調性を保つためには係数$c_k$のすべてが正でなければならないのです。具体的にこれまで見てきた計算法で確認してみましょう。それぞれの方法について$q^{n}_{k}$の係数を抜き出してその正負についてみていきましょう。 1次精度風上法 $$c_j=1-\nu \tag{15}$$ $$c_{j-1}=\nu \tag{16}$$ 1次精度風上法では安定な時間積分のためにCFL条件があるので、これらの係数は常に正です。つまり、単調性が維持できることになります。 2次精度風上法 $$ c_j=1-\frac{3}{2}\nu \tag{17}$$ $$c_{j-1}=2\nu \tag{18}$$ $$c_{j-2}=-\frac{1}{2}\nu \tag{19}$$ 係数$c_{j-2}$は必ず負の値をとってしまうから、単調性が維持できないことがわかります。 Lax-Wendroff法 $$c_j=1-\nu^2 \tag{20}$$ $$c_{j-1}=\frac{1}{2}\nu(1+\nu)\tag{21} $$ $$c_{j-2}=-\frac{1}{2}\nu(1-\nu)\tag{22}$$ こちらも、係数$c_{j-2}$は必ず負の値をとってしまうから、単調性が維持できないことがわかります。 このように、各係数を見ることによってその計算法の数値振動の有無を調べることができます。 まとめ 今回は数値流束を用いて、移流方程式の評価式を考えてみました。そのあと、評価式の高精度化を図り、数値解析を行いました。それを通して、Godunovの定理の確認をしました。 参考文献 藤井孝蔵、立川智章、Pythonで学ぶ流体力学の数値計算法、2020/10/20
- 投稿日:2021-12-05T22:20:11+09:00
HTTPXとasyncioを利用したPythonの非同期HTTPリクエスト
HTTPXとasyncioを利用したPythonの非同期HTTPリクエスト この記事はJSL(日本システム技研) Advent Calendar 2021の3日目の記事です! やりたいこと 今年のQiitaアドベントカレンダーのスポンサーされているTwilioさんのとても良い記事をお見かけしました。 asyncioで非同期にPokemonのAPIを呼び出し並行処理を実現するとても良い記事です!ぜひご一読ください!! 上記の記事ではaiohttpを利用していたので、推しのasyncio対応HTTPクライアントであるHTTPXで書き換えたくなりました。そして生まれたのが本記事です。 HTTPX 1.0リリースまであと少し?!ということでよろしくお願いします。 HTTPX HTTPXは、asyncio対応のHTTPクライアントです。 PythonのHTTPクライアントであるrequestsにインスパイアされており、使い方がよく似ています。 多機能なaiohttpと比べ、よりシンプルなクライアントです。 また、HTTPXはEncodeの管理するOSSです。 EncodeはDjango REST FrameworkやUvicorn、Starletteを管理しているイギリスの会社です。(UK limited company) 他にも様々なOSSを公開していますので、興味のある方はぜひ覗いてみてください。 動作環境 MacOS Python 3.10.0 HTTPX 0.21.1 事前準備 HTTPXをpipでインストールしてください。 pip install httpx asyncioによる非同期リクエストで大切なこと asyncioは外部IOの待ち時間でタスクを切り替えます。 大切なのは、外部のIOを行う処理がasyncioに対応していることです。 asyncioを利用した非同期処理を実装する場合、標準ライブラリであるurllib.requestやrequestsでなく、aiothttpやHTTPXを利用する必要があります。 今回の例ではHTTPアクセスですが、データベースアクセスでも同様です。 postgresの場合、psycpg2ではなく、asyncpgや今年の夏asyncioに対応したベータがリリースされたpsycpg3を利用する必要があります。 HTTPXを利用する httpx.AsyncClient() がasyncio対応のクライアントを返すコンテキストマネージャーです。with文で使います。 アクセスする先は元記事同様、PokemonのAPIです。ケロマツが好きです。 import asyncio from time import time import httpx URL = "https://pokeapi.co/api/v2/pokemon/" async def access_url_poke(client, num: int) -> str: r = await client.get(f"{URL}{num}") pokemon = r.json() # JSONパース return pokemon["name"] # ポケ名を抜く async def main_poke(): """httpxでポケモン151匹引っこ抜く""" start = time() async with httpx.AsyncClient() as client: tasks = [access_url_poke(client, number) for number in range(1, 151)] result = await asyncio.gather(*tasks, return_exceptions=False) print(result) print("time: ", time() - start) asyncio.run(main_poke()) 実行してみる 上記のコードを実行してみましょう。処理が1秒で終わっていることがわかります。 py3.10.0 ❯ python poke.py ['bulbasaur', 'ivysaur', 'venusaur', 'charmander', 'charmeleon', 'charizard', 'squirtle', 'wartortle', 'blastoise', 'caterpie', 'metapod', 'butterfree', 'weedle', 'kakuna', 'beedrill', 'pidgey', 'pidgeotto', 'pidgeot', 'rattata', 'raticate', 'spearow', 'fearow', 'ekans', 'arbok', 'pikachu', 'raichu', 'sandshrew', 'sandslash', 'nidoran-f', 'nidorina', 'nidoqueen', 'nidoran-m', 'nidorino', 'nidoking', 'clefairy', 'clefable', 'vulpix', 'ninetales', 'jigglypuff', 'wigglytuff', 'zubat', 'golbat', 'oddish', 'gloom', 'vileplume', 'paras', 'parasect', 'venonat', 'venomoth', 'diglett', 'dugtrio', 'meowth', 'persian', 'psyduck', 'golduck', 'mankey', 'primeape', 'growlithe', 'arcanine', 'poliwag', 'poliwhirl', 'poliwrath', 'abra', 'kadabra', 'alakazam', 'machop', 'machoke', 'machamp', 'bellsprout', 'weepinbell', 'victreebel', 'tentacool', 'tentacruel', 'geodude', 'graveler', 'golem', 'ponyta', 'rapidash', 'slowpoke', 'slowbro', 'magnemite', 'magneton', 'farfetchd', 'doduo', 'dodrio', 'seel', 'dewgong', 'grimer', 'muk', 'shellder', 'cloyster', 'gastly', 'haunter', 'gengar', 'onix', 'drowzee', 'hypno', 'krabby', 'kingler', 'voltorb', 'electrode', 'exeggcute', 'exeggutor', 'cubone', 'marowak', 'hitmonlee', 'hitmonchan', 'lickitung', 'koffing', 'weezing', 'rhyhorn', 'rhydon', 'chansey', 'tangela', 'kangaskhan', 'horsea', 'seadra', 'goldeen', 'seaking', 'staryu', 'starmie', 'mr-mime', 'scyther', 'jynx', 'electabuzz', 'magmar', 'pinsir', 'tauros', 'magikarp', 'gyarados', 'lapras', 'ditto', 'eevee', 'vaporeon', 'jolteon', 'flareon', 'porygon', 'omanyte', 'omastar', 'kabuto', 'kabutops', 'aerodactyl', 'snorlax', 'articuno', 'zapdos', 'moltres', 'dratini', 'dragonair', 'dragonite', 'mewtwo'] time: 1.0884361267089844 結果を見てみると、bulbasaurとなっています。はて・・・?? 初代ポケモンの名前を覚えている世代ではないため、分かりません。(どの世代のポケモンもほぼ分かりません) 一番最後を見てみるとmewtwoとなっていることから、想定どおり初代ポケモン全ての名前を取得できてはいるようです。 結果を問い合わせる そう、ポケモンの名前が全部英語になっていますね。このままではどれがだれなのかわかりません。 APIの仕様を確認しても、日本語名はないようです。 取得した結果をもとに日本語名を問い合わせるようにしてみましょう。 ポケモンの日本語名を返すAPIを探してみましたが、どうやらすぐには見当たりません。 少し調べるとJSONを作ってくださっている方がいらっしゃいました。 PonDad/pokemon.json こいつをデータベースに突っ込んで、問い合わせるようにしてみましょう。 なぜデータベース?と思った方もいらっしゃるかもしれません。 それは、asyncioのためです(非同期IO)。 asyncioはI/Oバウンドな処理で効果を発揮するため、データベースから引っ張ります(英語名→日本語名だけならJSONを直接みてももちろん良いです)。 次回へ続く! また来週!
- 投稿日:2021-12-05T21:49:27+09:00
paizaの問題文用に作成した文章校正ツールProofLeaderの仕組み
はじめに paizaアドベントカレンダー 2020 7 日目を担当する xryuseix です.paiza ではpaiza ラーニングの学生アルバイトをしています. 今までのアドベントカレンダーはこちら(Adventar).昨日のアドベントカレンダーはもじゃさんの「みて うちのかわいい魚たちを」です. さて,今日は文章構成ツールについてお話しします.社内ドキュメントも社外用公開ドキュメントも,複数人で書く場合はフォーマットを統一する必要があります1.例えば以下の二つの文章をみてみましょう. 文章を綺麗に書く事はとても美しい.何故なら,世界で一番文章が麗しくなるからだ. 文章を綺麗に書くことはとても美しい。なぜなら、世界で 1 番文章がうるわしくなるからだ。 文章の意味は置いといて,読んだ時の印象が多かれ少なかれ異なるとおもいます(どちらがいいとかはまた別の話).これが文章ごとに色々入ってたら違和感で文の内容が頭に入ってこないかもしれません. 本記事ではそのような表記ゆれを訂正するための自動化ツールについてお話しします. (↑ 作った OSS はこれです.クリックすると GitHub へ飛びます.) 対象となる文章表現 具体的には人はどのような文章が表記ゆれしやすいのでしょうか.例としてこのようなものが挙げられます. 内容 変更前 (良さそうな)変更後 句読点 , . 、 。 漢字 よろしくお願い致します よろしくお願いいたします 計算式 1+1=2 1 + 1 = 2 英単語 平和とはpeaceということです 平和とは peace ということです 数値 1 全体の50%が中央値以下でした 全体の 50% が中央値以下でした 数値 2 5000000000000000円欲しい! 5,000,000,000,000,000円欲しい! 他にもありますがざっとこんなもんでしょう.ここからはこれらの変更をどのように実現しているのかについて考えていきます.基本的にすべて正規表現でどうにかします(ここから出てくる言語は全部 Python です). 句読点 ( .,→、。 ) これは一番簡単です.正規表現モジュールを用いて,re.subを 2 回やればよいです. converter.py # .,を、。に変換 def dot_to_comma(text): replacedText = re.sub(",", r"、", text) return re.sub(".", r"。", replacedText) 漢字 ( 致します→いたします ) まずはワードリストを作る必要があります.なぜなら,漢字のルール一般的なものがあるわけではなく(ほんと?),基本的に会社やプロジェクト単位で定めるからです. ワードリストの例を下記に示します. word_list.csv 致します,いたします わたし,私,わたくし,俺,わい,イッチ 一般的にはこのように記述するように定義しました. word_list.csv After1,Before1 After2,Before2_1,Before2_2,Before2_3 After3,Before3_1,Before3_2 すると以下のように Before が文章に入っていた場合 After にした方がいいと警告します. WARNING: ファイル名:行数:行頭から何文字目: (致します) => (いたします) さて,実装について話を進めていきます.このような「大きな文章の中から特定の部分文字列を探索する」という処理は Rolling Hash を用いたりしますが,今回はユースケースを考えて,あまり大きな文章に対して実行しない(文書はいくつか分けるだろう)ので,単純にワードリストの単語の回数分re.searchで探索しています. converter.py for word in word_list: # 文字列警告 re_obj = re.search(word[0], text) if re_obj: warning_list.append([i + 1, re_obj.start(), re_obj.group(), word[1]]) for c in warning_list: print("\033[33mWARNING\033[0m: %s:%s:%s: (%s) => (%s)" % (file, c[0], c[1], c[2], c[3])) 計算式 ( 1+1→1 + 1 ) ここから少しずつ難しくなっていきます.計算式の記号の前後に空白を入れる際は(何か)(記号)(何か)を(何か)(スペース)(記号)(スペース)(何か)に置換しています.割り算がここに入っていないのはタグ<a></a>が<a>< / a>にならないようにするためです.ProofLeader は英字+一部の記号列の前後にスペースを入れる機能があり,その機能で別途1/1を1 / 1にするようにしています. converter.py # 記号の前後に空白 op = r"\+\-\*" text = re.sub(r"([^%s\n ])([%s]+)" % (op, op), r"\1 \2", text) text = re.sub(r"([%s]+)([^%s ])" % (op, op), r"\1 \2", text) 数値 1 ( トリオは3人→トリオは 3 人 ) 難易度高めの正規表現が出てきました.もう一生書きたくありません(実は間違ってる説まである). converter.py # 数値の前に空白 text = re.sub(r"([^\n\d, \.])((?:\d+\.?\d*|\.\d+))", r"\1 \2", text) # 数値の後ろに空白 text = re.sub(r"([\+\-]?(?:\d+\.?\d*|\.\d+))([^\n\d, \.])", r"\1 \2", text) 要は下記のような置換ができるような正規表現を書く必要がありました. 置換前 置換後 説明 あ1い あ 1 い 単純にスペースを入れる あ+1い あ +1 い 数値の前に+が入っている場合 あ-1い あ -1 い 数値の前に-が入っている場合 (行頭)1い (行頭)1 い 行頭にはスペースを入れたくありません あ3.5い あ 3.5 い 小数にも対応したいです (行頭)-3.5い (行頭)-3.5 い 全部のせ それがいい感じになるようなんとなく正規表現を書くとあんな感じになりました(?) 数値 2 ( 1000→1,000 ) これもなかなか難しいです.まずは数値の箇所を切り抜きます. converter.py # textから数値の場所のみを切り出す def cut_out(self): return re.sub(r"\d+[.,\d]*\d+", self.__digit_comma, self.text) ここからは小数の可能性もある(先頭に+-はない,カンマが含まれる可能性はある),数値を受け取り,いい感じにカンマを足します.数値を整数部と小数部に分け,整数部にだけre.sub("(\d)(?=(\d\d\d)+(?!\d))", r"\1,", ここに整数部の文字列)を適用させています. converter.py # 数字を三桁ごとに区切ってカンマ def __digit_comma(self, num: str): num = num.group() integer_decimal = num.split(".") commad_num = re.sub( "(\d)(?=(\d\d\d)+(?!\d))", r"\1,", integer_decimal[0] ) # 整数部 if len(integer_decimal) > 1: commad_num += "." + integer_decimal[1] # 小数部 return commad_num 急に出てきた(\d)(?=(\d\d\d)+(?!\d))って正規表現こそが三桁ごとにカンマを入れる正規表現です(正確には入れるべきカンマの直前にある数値を抽出する).正規表現の肯定先読みで後続の文字列の数値が 3 の倍数個であればマッチします.3 の倍数個かどうかの判定は少なくとも 3 の倍数個ある((\d\d\d)+)かつ 3 の倍数個の数値の後ろに数値がない(否定先読み(?!\d))で実装)しています. その他の機能 実はまあまああくまで自分が使いやすいよう色々やっています.例えばコードブロック(```)内や code タグ(<code></code>)内では置換しないだとか,一部のファイルを置換対象から除外する設定ができたりだとか,ワードリストはわたし,(私|わたくし|俺|わい|イッチ)と書けるよう正規表現を採用したりだとか,まあそんなところです. さいごに ここまで雑な文章を読んでいただきありがとうございました.もし良ければGitHub レポジトリにスターを押していただけるとうれしいです.押すとたぶん来年のおみくじは大吉になります.たぶん. 明日はtoshiki imai さんの「友達の研究室に Arduino 制御のカードキーを設置した話」です.お楽しみに! とはいえ,これは社内ツールではなく個人的に使ってるだけのただの自分の文章を綺麗に見せたいだけのツールです. ↩
- 投稿日:2021-12-05T21:00:38+09:00
Opencvで画像を読み込んだときのsizeとshapeの意味
気になったこと opensvで読み込んだ画像から、サイズや幅を出力するときにsizeやshapeを使うと思いますが、実際の画像ファイルとの関係が気になったので確認してみました。 確認した内容 適当な画像ファイル「a.jpg」のファイルをimreadで読み込んで、いろいろ表示させてみました。 import cv2 path = "C:\\XXXXXXXXXXXXXXX\\a.jpg" img = cv2.imread(path) print(type(img)) print(img.size) print(img.shape) print("高さ " + str(img.shape[0])) print("幅 " + str(img.shape[1])) print("色 " + str(img.shape[2])) 実行結果 <class 'numpy.ndarray'> 15116544 (1944, 2592, 3) 高さ 1944 幅 2592 色 3 確認に使用した「a.jpg」のプロパティはこのようになってました。 つまり、cv2で読み込むとndarray型になって、shape[0]で高さ、shape[1]で幅、shape[2]はビットの深さ(24ビット→3バイト)。 shape[0] × shape[1] × shape[2]の結果がsizeで出力される結果ということみたいです。
- 投稿日:2021-12-05T21:00:38+09:00
OpenCVで画像を読み込んだときのsizeとshapeの意味
気になったこと OpenCVで読み込んだ画像から、サイズや幅を出力するときにsizeやshapeを使うと思いますが、実際の画像ファイルとの関係が気になったので確認してみました。 確認した内容 適当な画像ファイル「a.jpg」のファイルをimreadで読み込んで、いろいろ表示させてみました。 import cv2 path = "C:\\XXXXXXXXXXXXXXX\\a.jpg" img = cv2.imread(path) print(type(img)) print(img.size) print(img.shape) print("高さ " + str(img.shape[0])) print("幅 " + str(img.shape[1])) print("色 " + str(img.shape[2])) 実行結果 <class 'numpy.ndarray'> 15116544 (1944, 2592, 3) 高さ 1944 幅 2592 色 3 確認に使用した「a.jpg」のプロパティはこのようになってました。 つまり、cv2で読み込むとndarray型になって、shape[0]で高さ、shape[1]で幅、shape[2]はビットの深さ(24ビット→3バイト)。 shape[0] × shape[1] × shape[2]の結果がsizeで出力される結果ということみたいです。
- 投稿日:2021-12-05T20:51:05+09:00
フィンガープリントを可視化してみる~TopologicalTorsionFingerprint~
はじめに こちらの続き。 前回の AtomPairFingerprint に続き、今回はさらに情報の少ないTopologicalTorsionFingerprintの可視化方法を検討したので共有したい。 TopologicalTorsionFingerprintとは? 端的にいうと二面角に関するフィンガープリントだ。 二面角についてはこちらの定義を参照してほしい。 https://www.weblio.jp/wkpja/content/%E4%BA%8C%E9%9D%A2%E8%A7%92_%E5%8C%96%E5%AD%A6 フィンガープリントについては RDKitでフィンガープリントを使った分子類似性の判定 を見てほしい(またも丸投げ) 本記事の目的 本記事では、TopologicalTorsionFingerprintのビットとして「4303634976 」のようなコードが与えられたときに、そのコードが化合物のどの部分に該当するのかを可視化する方法をお伝えすることである。 環境 RDKit 2021.09.2 cairosvg やり方 準備 例えば、 from rdkit import rdBase, Chem from rdkit.Chem import AllChem, Descriptors, Draw from rdkit.ML.Descriptors import MoleculeDescriptors from rdkit.Chem.AtomPairs import Torsions tt = Torsions.GetTopologicalTorsionFingerprint(mol) for torsion in tt.GetNonzeroElements().keys(): print(torsion, '\t', Torsions.ExplainPathScore(torsion), tt.GetNonzeroElements()[torsion]) のように、分子のTopologicalTorsionFingerprintを計算し、ExplainPathScore関数を使うと、 4303634976 (('C', 1, 0), ('C', 3, 0), ('C', 3, 0), ('C', 1, 0)) 1 4437590049 (('C', 2, 0), ('C', 2, 0), ('C', 2, 0), ('C', 2, 0)) 1 4437590560 (('C', 1, 0), ('C', 3, 0), ('C', 2, 0), ('C', 2, 0)) 2 4437852704 (('C', 1, 0), ('C', 3, 0), ('C', 3, 0), ('C', 2, 0)) 2 4437852705 (('C', 2, 0), ('C', 3, 0), ('C', 3, 0), ('C', 2, 0)) 1 4571807777 (('C', 2, 0), ('C', 2, 0), ('C', 2, 0), ('C', 3, 0)) 2 4572069921 (('C', 2, 0), ('C', 2, 0), ('C', 3, 0), ('C', 3, 0)) 2 のように、分子に含まれるTopologicalTorsionFingerprintのビットを表す各コードについて、それがどのような4つの原子コードから構成されているかを得ることができる。 4つの原子コードの内、真ん中の2つが、2組の3原子が共有する2原子と考えられるので、この真ん中の2つと分子中の全結合における両端の原子のコードを照合し、一致結合があった場合、1,4番目の原子コードと、結合の両端の原子それぞれの1つ先の原子のコードとを照合することで、コードに対応する二面角を構成する4原子を容易に特定できるのである。 実装コード 以下はkey「4303634976」に該当する二面角を構成する4原子を特定し、そのインデックス番号の一覧をresutlsに格納する処理である。molには可視化対象のRDKitのMOLオブジェクトが格納されている前提としている。 結果が複数ある場合もあるため、resutls はリストのリストになっている。 key = 4303634976 explained = Torsions.ExplainPathScore(key) results = set() # 全結合でループ for i, bond in enumerate(mol.GetBonds()): #print(f"=={i}==") # 結合の両端の原子を取得 atom1 = bond.GetBeginAtom() atom2 = bond.GetEndAtom() # 結合の両端の原子コードを取得 explain1 = Utils.ExplainAtomCode(Utils.GetAtomCode(atom1)) explain2 = Utils.ExplainAtomCode(Utils.GetAtomCode(atom2)) # 結合の両端の原子が、explainの真ん中の2つの結合に一致する場合(ケース1) # atom1, 2の順にexplained[1], explained[2]とマッチした場合 if (explain1 == explained[1]) and (explain2 == explained[2]): # atom1の結合の反対側の原子とexplain0を比較 atom0s = [] for tmpBond in atom1.GetBonds(): if bond.GetIdx() != tmpBond.GetIdx(): atom0 = tmpBond.GetOtherAtom(atom1) explain0 = Utils.ExplainAtomCode(Utils.GetAtomCode(atom0)) if explain0 == explained[0]: atom0s.append(atom0.GetIdx()) # atom2の結合の反対側の原子とexplain3を比較 atom3s = [] for tmpBond in atom2.GetBonds(): if bond.GetIdx() != tmpBond.GetIdx(): atom3 = tmpBond.GetOtherAtom(atom2) explain3 = Utils.ExplainAtomCode(Utils.GetAtomCode(atom3)) if explain3 == explained[3]: atom3s.append(atom3.GetIdx()) # 全ての原子の組合わせにより結果を生成 for atom0_idx in atom0s: for atom3_idx in atom3s: result = (atom0_idx, atom1.GetIdx(), atom2.GetIdx(), atom3_idx) result2 = tuple(sorted(result)) results.add(result2) # 結合の両端の原子が、explainの真ん中の2つの結合に一致する場合(ケース2) # atom2, 1の順にexplained[1], explained[2]とマッチした場合 if (explain2 == explained[1]) and (explain1 == explained[2]): # atom2の結合の反対側の原子とexplain0を比較 atom0s = [] for tmpBond in atom2.GetBonds(): if bond.GetIdx() != tmpBond.GetIdx(): atom0 = tmpBond.GetOtherAtom(atom2) explain0 = Utils.ExplainAtomCode(Utils.GetAtomCode(atom0)) if explain0 == explained[0]: atom0s.append(atom0.GetIdx()) # atom1の結合の反対側の原子とexplain3を比較 atom3s = [] for tmpBond in atom1.GetBonds(): if bond.GetIdx() != tmpBond.GetIdx(): atom3 = tmpBond.GetOtherAtom(atom1) explain3 = Utils.ExplainAtomCode(Utils.GetAtomCode(atom3)) if explain3 == explained[3]: atom3s.append(atom3.GetIdx()) # 全ての原子の組合わせにより結果を生成 for atom0_idx in atom0s: for atom3_idx in atom3s: result = (atom0_idx, atom2.GetIdx(), atom1.GetIdx(), atom3_idx) result2 = tuple(sorted(result)) results.add(result2) results = list(results) print(results) これを実行すると次のように、results変数に「4303634976 」に該当する二面角を構成する4原子のリストが複数分格納される。 [(0, 1, 6, 7)] 該当箇所の原子が特定できたため、これを可視化してみよう。まず、可視化関数を用意する。 from rdkit.Chem.Draw import rdMolDraw2D from IPython.display import SVG from io import BytesIO from PIL import Image from cairosvg import svg2png def generate_image(mol, highlight_atoms, size, isNumber=False): view = rdMolDraw2D.MolDraw2DSVG(size[0], size[1]) tm = rdMolDraw2D.PrepareMolForDrawing(mol) option = view.drawOptions() if isNumber: for atom in mol.GetAtoms(): option.atomLabels[atom.GetIdx()] = atom.GetSymbol() + str(atom.GetIdx() + 1) view.DrawMolecule(tm, highlightAtoms=highlight_atoms) view.FinishDrawing() svg = view.GetDrawingText() SVG(svg.replace('svg:', '')) ret = svg2png(bytestring=svg.encode('utf-8')) img = Image.open(BytesIO(ret)) return img 次に、上の関数にresultsの0番目のリストを与えて可視化してみる。 img = generate_image(mol, results[0], (400,200), True) img Jupyter Labであればこんな感じで表示されるはずだ。 これにより当該のリストが、ハイライトされている部分の2面角を示していることが一目に分かる。 同様にresults内の全リストについて色を変更する等して可視化すれば、分子中における「4303634976」に該当する全2面角を一目で表示させることができるはずだ。 おわりに 今回、ロジックが複雑になってしまったが、RDkitの機能を使えばもっと楽ができそうな気がする。 もっとRDKitついて勉強せねば。
- 投稿日:2021-12-05T20:08:17+09:00
[Python]デフォルト値を持つ引数の前にはアスタリスクを設けておくと堅牢で良いかも、という話
TL;DR Pythonでデフォルト値を取る引数に関してはその前にアスタリスク(*)の引数設定があるとキーワード引数を前提とする縛りを設けられて引数順や引数の追加などをした際に影響が少なくて堅牢で良さそう、という話です。 きっかけ 先日タイムラインに以下のようなツイートが流れてきて拝見しました。 PythonのOSSのレビューで、新しいキーワード引数を入れる時に、前に*を入れるように指摘されるのをよく見るんだけど、なるほど、の後はキーワード引数じゃないといけなくなるのか。確かに``無しだと、間違って引数を入力しちゃうこともあるし、キーワード引数の順番も変えられなくなるから、これはいい pic.twitter.com/PL6aHorINW— Atsushi Sakai (@Atsushi_twi) December 4, 2021 Effective Pythonとかの本(私が読んだのは初版なので第二版以降はどう書かれているか把握できていませんが)にも確か「デフォルト値を取る引数に関してはキーワード引数で指定すべき」といったTIPSが書かれていた気がしますが(昔のことすぎて若干記憶が曖昧ですが・・・)、前述のものをコードを書く際に守るように気を付けるよりもそもそも守らないとコードが動かない・・・形の方が確実で良さそうに思えました。 実際にapyscという趣味で作っている以下のPythonライブラリで反映してみたのですが、うっかり前述のデフォルト値を取る引数に対してキーワード引数が使われていない・・・といったケースが少し見つかったりと、Lintなどと同様に人に頼らずに確実に制限される形が好ましそうと感じました。 どういった話なのか 例えば以下のような関数があったとします。a, c, eとそれぞれ整数の型の引数を受け付けます。cとeの引数はデフォルト値をそれぞれ持っています。 def any_func(a: int, c: int = 30, e: int = 50) -> str: ... この関数のユーザーはaの引数のみ必須で指定する必要がありますが、cとeの引数の設定は任意です(aだけ指定すれば呼び出すことができます)。 any_func(10) また、cやeなどの引数は以下のように位置引数で指定したりキーワード引数としてもどちらでも指定できます。 位置引数で指定する場合 any_func(10, 20, 30) キーワード引数で指定する場合 any_func(10, c=20, e=30) ただしデフォルト値を取る引数に関しては順番が変わったりその前に別に引数がアップデートなどで追加などがされがちなのでキーワード引数の指定をするようになっていると関数のアップデートで影響を受けづらくなります。 例えばbというこれまた整数で且つデフォルト値を取る引数が関数にアップデートで追加されたとします。エンジニアとしてはb引数はaとcの引数の間に追加するのが自然だ・・・と感じてその位置に引数を追加したとします(aのようなデフォルト値を持たない引数に関しては順番が変わるような破壊的変更が入ることは稀ですが、デフォルト引数を取る引数に関してはこのような変更が入ることがあったり追加を行った方が自然なケースがあります)。 bの引数が追加になったケース def any_func(a: int, b: int = 20, c: int = 30, e: int = 50) -> str: ... すると、位置引数でcやeの引数に値を指定していた呼び出し箇所ではそれぞれの引数の指定がcとeからbとcに変わってしまい、想定外の挙動となってバグの原因になったりする可能性があります(特にライブラリアップデートなどしてインターフェイスが変わった際などは気づきづらいケースがあります)。 テストで引っかかるケースが大半だとは思いますし各引数の型が異なっていればmypyなどの型チェックでCIのタイミングなどで検知できるケースも多くありますが、運が悪いとこれらのチェックもすり抜けてしまうケースも無いとは言えません。 以下のような呼び出し箇所のパラメーターの指定が引数a,b,cへの指定に変わってしまいます。 any_func(10, 20, 30) 一方でcやeの引数指定をキーワード引数で行っていた場合はb引数が追加された後でもcとeへの指定のままの挙動となり影響が出ずに済みます。 関数にb引数が追加になってもキーワード引数で指定してあれば影響が出にくくなります。 any_func(10, c=20, e=30) そのためデフォルト値を取る引数に関しては基本的にキーワード引数で指定する・・・としておくと関数の更新時やライブラリアップデートなどで影響が少なくなります。 しかしながらついうっかり位置引数で指定してしまったり、もしくは途中からデフォルト値を取るようになった関数などの呼び出し元では位置引数で指定しまっているといったケースが発生し得ます。他にもライブラリなどを提供している場合には、全てのユーザーがこのようにキーワード引数で使ってくれるわけではないのでアップデート時に破壊的更新となってしまいユーザーに迷惑がかかってしまうケースなどもあるかもしれません。 そういったケースを軽減し破壊的な影響を出しにくくするために引数の途中でアスタリスクの*を入れることで、以降の必ずキーワード引数で指定しないといけなくなるようにすることができます。 例えば関数の引数の記述を以下のようにaとbの間にアスタリスクを追加する形で対応ができます。 def any_func(a: int, *, b: int = 20, c: int = 30, e: int = 50) -> str: ... アスタリスクの後の引数はキーワード引数で指定しないといけなくなるというPythonの言語仕様がありますので、これでユーザーはb以降のデフォルト値を取る引数はキーワード引数で指定しないといけなくなります。 試しに位置引数で指定してみるとVS Code上のPylanceでリアルタイムにエラーが表示されます(mypyなどでも引っかかることは確認済みですので、CIなどでmypyを通す形になっていれば事前にミスに気づけます)。 any_func(10, 20, 30) プログラム自体も動かしてみるとエラーとなって位置引数では動かせられないことが確認できます。 TypeError: any_func() takes 1 positional argument but 3 were given これによって「必ずユーザーはデフォルト引数付きの引数はキーワード引数で指定している」という状態を担保できるため、安全に破壊的変更になりにくい形で関数などの引数内容の追加や位置調整などのアップデートをかけることができるようになります。 お手軽ですし堅牢性が高まったりとメリットが多いため、今後は積極的に使っていきたいところです。 一部ビルトインやライブラリとの兼ね合いで使えないケースも発生する 自作ライブラリで反映作業をしていて気づいたのですが、デフォルト値を取る引数で必ずしもその引数の前にアスタリスクが配置できるわけではない・・・というケースを見つけています。例えばビルトインなどで位置引数が必要になるケースです。 並列処理のPoolで考えてみます。Poolのiterable引数に指定する値は位置引数として指定されるため、キーワード引数での指定を強制してあるとエラーになります(Pylanceなどでも引っかかります)。 from multiprocessing import Pool def any_func(*, a: int = 10) -> None: print(a) with Pool(processes=4) as p: p.map(func=any_func, iterable=[10, 20, 30]) こういったケースではそもそもPoolの代わりにconcurrent.futuresパッケージなどのキーワード引数が指定できるビルトインのものを使う・・・といったケースも出てくるかもしれません。 from concurrent.futures import ProcessPoolExecutor, Future, as_completed from typing import List def any_func(*, a: int = 10) -> None: print(a) futures: List[Future] = [] with ProcessPoolExecutor(max_workers=4) as executor: for a in (10, 20, 30): future = executor.submit(fn=any_func, a=a) futures.append(future) _ = as_completed(fs=futures) 参考 : 参考文献・参考サイトまとめ https://twitter.com/Atsushi_twi/status/1467110845243326465?s=20
- 投稿日:2021-12-05T19:57:36+09:00
なろう小説のブクマ数予測コンペに参加した話
はじめに こんにちは、estieで機械学習エンジニアをやっている、ぴーまんです。 最近、自分の体の細さに嫌気が差し、ジム通いを始めたものの、カウンセリングで体の状態を測定した結果「プロアスリート級」の体という診断を受け、少し満足してしまっている自分がいます。今回はestie Advent Calendar 2021 7日目の記事として、趣味で取り組んでいるデータサイエンスコンペについて書こうと思います。 データサイエンスコンペとは まず、皆さんはデータサイエンスコンペをご存知でしょうか。有名なものだと、Kaggle社が運営するKaggleというコンペがあり、こちらは耳にしたことのある人もいるんじゃないでしょうか。(ちなみに2017年にgoogleがKaggle社を買収したらしいです。知らなんだ。)データサイエンスコンペでは、運営・企業から与えられたデータに統計手法・機械学習を駆使して、様々なお題に対して予測を行い、その精度を競います。estieも過去にKaggleの「医薬品の作用機序」を推定するコンペに出場したことがあります。その際の記事はこちら。データサイエンスコンペで一番有名なKaggle社はアメリカの企業ですが、日本にもSignateやProbspace、そして今回参加したNishikaなどがあります。 Nishikaは2019年11月にサービスをローンチした比較的歴史の浅いサイトで、ローンチコンペの「AIは芥川龍之介を見分けられるのか?」というコンペや、レコメンドエンジンの開発コンペ、仮想通貨の価格予想、日本絵画の顔分類など、ユニークなコンペを数多く開催しています。(ちなみにローンチコンペでは金メダル圏の4位に入賞しました!3位以内だと賞金がもらえたのですが。。。惜しかった。。。 下図はローンチコンペのランキングです。) 今回参加したコンペ 今回は、Nishikaが開催する「小説家になろう ブクマ数予測 ~”伸びる”タイトルとは?~」というコンペに出場しました。このコンペでは、「小説家になろう」という小説投稿サイトの各小説のあらすじやタイトル、そのジャンル情報などから、ブックマーク数を予測するお題が与えられています。書籍の小説の場合、例えば売上数を予測しようとすると、まず実売部数(発行部数ではない)は極秘事項となっていて、そもそもデータが取れなかったり、メディアや口コミでの紹介などの様々な事象に影響を受けるため、予測は非常に困難です。一方で投稿サイトの小説の場合、関連するデータが比較的オープンで網羅的に取得できるため、そこから得られる特徴量から読者数やファンの数、ブックマーク数を予測できる可能性がある、という背景で今回のコンペは開催されています。予測対象となるブックマーク数はブックマーク数を5段階にビニングしたものとして設定されており、コンペでは、各小説が5段階評価のそれぞれに入る確率を算出したものに対して、正解と比較した時の精度をMulti-class loglossを用いて計算(計算式は下図の通り)し、その精度スコアの順位で競います。 結果!! 先月末11月29日までこのコンペは開催されていましたが、最終日締め切り後に最終ランキングが発表され、私は参加者576人中54位でした。(まだ入賞候補の精査中なため、メダル圏は確定していませんが、暫定スコアベースだと銅メダル圏です! 下図は今回コンペの最終ランキングです) 構築したモデル 学習・推論パイプライン構成は下図の通りです。 設定した特徴量 最終的に予測に寄与した重要特徴量は上から順に下記のようになりました(上位10つの特徴量を記載) 大ジャンル 連載/短編 ユーザーID 作者名 キーワード数 あらすじ内の名詞数 あらすじの長さ 初回掲載日からの日数 投稿順 あらすじ内の動詞数 ・・・ 与えられたtrainデータには、そのまま特徴量として採用できるものと、加工しないと機能しない特徴量が含まれています。例えば、「大ジャンル」や「小ジャンル」などは、そのままどんなジャンルだったらブックマーク数が多くなりそうか、というのはなんとなく統計的に有意差が得られそうですが、「あらすじ」「タイトル」などは特に固有の文字列の並びでしかなく、どんな「あらすじ」「タイトル」だったらブックマーク数が多くなりそうかというデータの特徴を抽出するには一工夫が必要です。 そこで文字列を単語ごとに分かち書きをしてくれるtokenizerを用い、特徴を抽出しやすい形にしています。分かち書き処理で得られたデータから抽出した特徴量の例は下記の通りです。 品詞ごとの使用数 (例えば名詞は何回使われているか、もしくは否定系の助動詞が含まれているか、等)などを特徴量として採用しました。 頻繁に登場する単語のあらすじ/タイトル内での登場回数 あらすじをベクトル化したもの あらすじについては、より詳細な特徴を抽出するために、Doc2Vecを使用して文章を固定長のベクトルに変換しています。(Doc2Vecについてはこちらの記事がわかりやすいです。)ベクトル化をすることで、文章間の意味の類似性をベクトル間の距離で表現することができるようになります。 その他の簡単な特徴量の例は下記の通りです。 あらすじ/タイトルの長さ ジャンル 初回掲載日から何日経っているか 投稿順 この特徴量が少し面白くて、「ncode」という、そのままでは各小説の固有IDとしか使えないような特徴量が実は、投稿された順番の番号をある数式で変換したものだという情報が、コンペ内の掲示板で挙がっており、この数式を逆算することで得られる投稿順も特徴量に採用しました。 学習方法 上記で作成した特徴量データセットとそれぞれに対応するブックマーク度の正解データのセットに対して、交差検証して学習を行いました。後述しますが、ブックマーク度の分布は偏っているため、交差検証のためのデータ分割にはStratifiedKFoldを採用しています。(StratifiedKFoldについてのわかりやすい記事はこちら )これにより、データを10つのfoldに分割し、それぞれに対して学習を行います。パラメータチューニングにはOptunaを使用しました。 推論方法 上記で学習したモデル10つに対し、テストデータを投入し、結果は単純平均を採用しました。 交差検証の結果 交差検証におけるスコアは 0.808(-> 最終スコア: 0.719)でした。検証で正しいラベルを予測したデータセットと不正解のデータセットとの差を簡単に考察してみます。 まず、大ジャンルの割合について、正解のデータセットに比べて不正解のデータセットで割合が目立って大きい大ジャンルは「恋愛」「ファンタジー」「SF」等。小ジャンルの割合については「異世界〔恋愛〕」「ハイファンタジー〔ファンタジー〕」「VRゲーム〔SF〕」等が目立って不正解データセット内の割合が高い結果となりました。これらのジャンルは人気が高く、またブックマーク度のばらつきが大きいジャンルです。このばらつきによって、傾向の学習がしにくくなってしまったと考えられます。 大ジャンル ブックマーク度平均 ブックマーク度分散 恋愛 1.72 1.55 ファンタジー 1.14 1.21 SF 0.86 0.97 文芸 0.66 0.68 その他 0.54 0.38 ノンジャンル 0.41 0.42 小ジャンルについても同様の考察ができそうです。 小ジャンル ブックマーク度平均 ブックマーク度分散 異世界〔恋愛〕 2.40 1.16 VRゲーム〔SF〕 1.58 1.43 ハイファンタジー〔ファンタジー〕 1.53 1.35 現実世界〔恋愛〕 1.02 1.00 歴史〔文芸〕 0.93 1.01 また、タイトルの長さ、タイトル内の単語数、キーワード数について、不正解のデータセットの方が長く、多い傾向がありました。今回、あらすじにフォーカスして自然言語処理を用いて特徴量を抽出し、タイトルに対しては最低限の単語数カウントや頻出単語の有無の分析しか行いませんでした。あらすじと比べると長さが短く、特徴が抽出しにくいタイトルですが、品詞分析を初めもう少し詳細な分析をかけることでより有効な特徴量が抽出できた可能性が考えられます。 以上、簡単にですが、今回の学習/推論結果について考察してみました。 データセット分析 コンペの本筋とは外れますが、なろう小説初心者なりに、データ全体に対して特徴量抽出の際に色々分析を行って、得られた分析結果をいくつか書いてみます。 ブックマーク度の分布 まずブックマーク度の分布は、上図のようにかなり偏っています。5段階評価で3以上(グラフでは0始まりなので2以上)の投稿は全体の1/5足らずなのがわかります。かなり評価が厳しい世界であることがわかります。 人気の(ブックマーク度が高い)ジャンル 小ジャンル ブックマーク度平均 異世界〔恋愛〕 2.40 VRゲーム〔SF〕 1.58 ハイファンタジー〔ファンタジー〕 1.53 現実世界〔恋愛〕 1.02 歴史〔文芸〕 0.93 前章と同様の分析ですが、各小ジャンルのブックマーク数を集計して、上位のジャンルを見てみました。堂々の1位は「異世界 [恋愛]」。なろう小説初心者としては正直どんな話があるのかわかりませんが、異世界転生ものが流行っているというのはなんとなく聞いたことがある気がしますし、漫画サイトの広告で良くみるような。。。(ターゲティングされてる?)3位に挙がっている「ハイファンタジー [ファンタジー]」については、全く聞いたことの無いジャンルでしたが、wikipedia曰く、要は異世界ものらしいです。やっぱり異世界が人気なんですね。ちなみにハイファンタジーに対立するローファンタジーというのは、例えば現実世界に魔法が入ってるような、現実と少しリンクしているようなファンタジーのことを言うそうです。ということはハリー・ポッターとかはローファンタジーなんですかね?(私は結構ハリー・ポッター好きなんですが、小学生の頃、何もわからない同級生相手にハリー・ポッターの知識をひけらかして恐れらている子供でした。元祖マウンティング野郎です。) あらすじの長さによるブックマーク度の分布 あらすじの長さごとのレコード数を揃えつつ(pandasのqcutを使用)、あらすじの長さごとのブックマーク度平均を可視化してみました。これをみると、あらすじが長い投稿ほどブックマーク度が高いようです。あらすじをしっかり書いている作者はプロットもしっかりしている傾向があるのでしょうか。この辺りは一度しっかり中身を読んでみたいところですね。 複数の作者名を使っている人 ユーザーごと・作者ごとに、作者の経験年数や文章のうまさ、ファンの多さなどでブックマーク度の傾向がありそうだというのはなんとなくわかりやすいですが、では1ユーザーで複数の作者名(ペンネーム)を使っている人はいるんだろうかという単純な興味で分析をかけてみたところ、trainデータに含まれる18630人のうち、664人が複数の作者名を使っていることがわかりました。(同一User_idで複数の作者名を持っているUser_id数をカウントしています。)さらに一番多くの作者名を持つユーザーはなんと57つも作者名を使って投稿していました。作者ごとに作風を分けたりしてるんでしょうか、と考えたり、なかなか興味深いです。(ちなみにこのユーザーごとの作者名の数も特徴量として入れましたが、予測にはほとんど効いていないようでした。。。笑) 他にもあらすじ・タイトルに含まれているキーワードごとのブックマーク数について有意差検定を取ってみたり、色々な分析をかけてみましたが、キリが無いので、この辺で終わりにしたいと思います。 さいごに いかがでしたでしょうか。今回は、estie Advent Calendar 2021 7日目の記事として、Nishikaの「小説家になろう ブクマ数予測 ~”伸びる”タイトルとは?~」というデータサイエンスコンペに参加してみた話について書いてみました。結果として銅メダル圏内でフィニッシュできましたが、個人的には時間の都合で試せなかった特徴量やモデル選定などがあり、まだ上を目指せた部分はあったなと思っています。 普段は社内でオフィス物件の賃料推定モデルを作っており、基本的にオフィスに関するデータを毎日いじっている身としては、こういうコンペでは普段触らないようなデータに触れることができるため、非常に刺激的で楽しく、いい気分転換になります。もちろん機械学習技術やデータ加工のTIPSなど、毎回キャッチアップできることは多く、自己研鑽としても大変ためになります。引き続き、色々なコンペに参加していこうと思います。 さて、estieでは全職種でメンバーを募集しています! とりあえず、まずは話を聞いてみたい方はこちらから! - 株式会社estie estie エンジニア採用情報 我こそはestieのデータでおもろいことするんじゃという方。オフィス不動産のデータってどんなの?って興味を持たれた方、是非カジュアルにお話しさせてください。 明日は、estieが誇る最強デザイナーあらけんの、「2人目デザイナーの入社前にやったこと」です。お楽しみに!
- 投稿日:2021-12-05T19:57:19+09:00
フィンガープリントを可視化してみる~AtomPairFingerprint~
はじめに フィンガープリントを使って予測モデルを作成した場合、説明変数として利用されたフィンガープリントのビットがどういうものか分からないと、予測のメカニズムの理解が難しいだろう。 Morganフィンガープリントについては、参考文献に示したようにビットを可視化するための情報がいくつかある. 今回は、情報が少ない AtomPairFingerprint についての可視化方法を検討したので共有したい。 AtomPairFingerprintとは? AtomPairFingerprintとは、簡単に言うと分子内における原子のペアの出現に関するフィンガープリントだ。もう少し詳しく言うと、C原子とN原子が同時に出現したというような単純なものではなく、同じC原子であっても結合の違いを考慮したり、同じペアであってもペア同士の距離の違いを考慮したりしている。 詳しくは、アトムペアやドナーアクセプターペアフィンガープリントは分子の形状を捉えた化学表現 を見てほしい(丸投げ) 本記事の目的 本記事では、AtomPairFingerprintのビットとして「689188」のようなコードが与えられたときに、そのコードが化合物のどの部分に該当するのかを可視化する方法をお伝えすることである。 環境 RDKit 2021.09.2 cairosvg やり方 準備 例えば、 from rdkit import rdBase, Chem from rdkit.Chem import AllChem, Descriptors, Draw from rdkit.ML.Descriptors import MoleculeDescriptors from rdkit.Chem.AtomPairs import Pairs, Utils fp = AllChem.GetAtomPairFingerprint(mol) d = fp.GetNonzeroElements() for key in d.keys(): explained = Pairs.ExplainPairScore(key) print(key, explained) のように、分子のAtomPairFingerprintを計算し、ExplainPairScore関数を使うと、 541730 (('C', 1, 0), 2, ('C', 1, 0)) 541734 (('C', 1, 0), 6, ('C', 1, 0)) 590881 (('C', 1, 0), 1, ('C', 4, 0)) 590885 (('C', 1, 0), 5, ('C', 4, 0)) 590980 (('C', 4, 0), 4, ('C', 4, 0)) 689187 (('C', 1, 0), 3, ('C', 2, 1)) 689188 (('C', 1, 0), 4, ('C', 2, 1)) 689191 (('C', 1, 0), 7, ('C', 2, 1)) 689193 (('C', 1, 0), 9, ('C', 2, 1)) 689194 (('C', 1, 0), 10, ('C', 2, 1)) 689195 (('C', 1, 0), 11, ('C', 2, 1)) のように、分子に含まれるAtomPairFingerprintのビットを表す各コードについて、それがどのような原子ペア、距離を表しているかを得ることができる。具体的には、ExplainPairScoreの戻り値のタップルの1番目は、アトムペアの1つめの原子コード、2番目はアトムペアの距離、3番目はアトムペアの2つめの原子コードである。 従って、分子内の原子の組み合わせを総当たりで調べ、原子コード・距離と、コードにExplainPairScore関数をかましたタプルを照合することで、コードに対応する原子ペアを容易に特定できるのである。 実装コード 以下はkey「689188」に該当するペア特定し、そのペア間に存在する原子のインデックス番号の一覧をresutlsに格納する処理である。molには可視化対象のRDKitのMOLオブジェクトが格納されている前提としている。 結果が複数ある場合もあるため、resutls はリストのリストになっている。 key = 689188 explained = Pairs.ExplainPairScore(key) results = [] for i, atom1 in enumerate(mol.GetAtoms()): code1 = Utils.GetAtomCode(mol.GetAtomWithIdx(i)) explain1 = Utils.ExplainAtomCode(code1) for j, atom2 in enumerate(mol.GetAtoms()): code2 = Utils.GetAtomCode(mol.GetAtomWithIdx(j)) explain2 = Utils.ExplainAtomCode(code2) if j > i: pathLength = len(AllChem.GetShortestPath(mol, i, j)) - 1 if ((explain1 == explained[0] and explain2 == explained[2] or explain2 == explained[0] and explain1 == explained[2]) and (pathLength == explained[1])): results.append(AllChem.GetShortestPath(mol, i, j)) これを実行すると次のように、results変数に「689188」に該当する原子ペアを結ぶパス上にある原子のリストが複数分格納される。 [(0, 1, 4, 9, 8), (2, 1, 4, 9, 8), (3, 1, 4, 9, 8), (8, 7, 6, 25, 26), (8, 7, 6, 25, 27), (8, 7, 6, 25, 28)] 該当箇所の原子が特定できたため、これを可視化してみよう。まず、可視化関数を用意する。 from rdkit.Chem.Draw import rdMolDraw2D from IPython.display import SVG from io import BytesIO from PIL import Image from cairosvg import svg2png def generate_image(mol, highlight_atoms, size, isNumber=False): view = rdMolDraw2D.MolDraw2DSVG(size[0], size[1]) tm = rdMolDraw2D.PrepareMolForDrawing(mol) option = view.drawOptions() if isNumber: for atom in mol.GetAtoms(): option.atomLabels[atom.GetIdx()] = atom.GetSymbol() + str(atom.GetIdx() + 1) view.DrawMolecule(tm, highlightAtoms=highlight_atoms) view.FinishDrawing() svg = view.GetDrawingText() SVG(svg.replace('svg:', '')) ret = svg2png(bytestring=svg.encode('utf-8')) img = Image.open(BytesIO(ret)) return img 次に、上の関数にresultsの0番目のリストを与えて可視化してみる。 img = generate_image(mol, results[0], (400,200), True) img Jupyter Labであればこんな感じで表示されるはずだ。 これにより当該のリストが、ハイライトされている部分の両端であるC1とC9のアトムペアを示していることが一目に分かる。 同様にresults内の全リストについて色を変更する等して可視化すれば、分子中における「689188」に該当する全アトムペアを一目で表示させることができるはずだ。 参考文献 Fingerprintの可視化について RDKitでフィンガープリントの可視化 【RDKit】Morganフィンガープリントの生成ルールを解釈してみた
- 投稿日:2021-12-05T19:32:49+09:00
連携先のシステムがgzip対応してなくてもやりくりする方法
はじめに システム間の結合後をおさえるために、処理に必要なデータをファイルでやりとりするケースがあります。 それに加えて、やりとりするデータが大量の場合、ディスク使用量やIOをおさえるために、そのファイルをgzipなどで圧縮したいこともあります。 しかし、連携先のシステムがgzip圧縮したファイルでのやりとりを対応していない場合も、ままあります。 そういった場合に、自分のシステムのところだけではgzip圧縮させつつ、連携ファイルは圧縮なしのものでやりとりするための小技を集めたのが、この記事です。 基本方針 連携先からファイルをもらう場合は、自分のサーバに、gzip圧縮したコピーを作成 連携先にファイルを渡す場合は、自分のサーバにgzip圧縮したものを作成して、それを解凍しながらコピー これによって、自分のサーバ上は圧縮したファイルのみを扱い、連携先には圧縮していない状態でやりとりできます。 Pythonはgzipファイルの操作はgzip.open で、比較的簡単にできます。 NFSで連携する場合 ファイルをもらう場合 /nfs/input/data.csv が、NFSに配置されるとします。 /input/data.csv.gz として、圧縮コピーします。 input_file = Path('nfs', 'input', 'data.csv') compressed_file = Path('input', 'data.csv.gz') with open(input_file, 'rb') as src, gzip.open(compressed_file, 'w') as dst: shutil.copyfileobj(src, dst) 単純なコピーなのでバイナリモードとshutil.copyfileobjを使います。 やってることは cat /nfs/input/data.csv | gzip > /input/data.csv.gz と同等です。 ファイルを渡す場合 バッチで、連携するデータをgzip圧縮したファイルとして、自分のサーバ上の /output/result.csv.gz に作成するとします。 これを、NFSの /nfs/output/result.csv となるように、解凍コピーします。 入力のときのコードの役割を逆にするだけです。 compressed_file = Path('output', 'result.csv.gz') output_file = Path('nfs', 'output', 'result.csv') with gzip.open(compressed_file, 'r') as src, open(output_file, 'wb') as dst: shutil.copyfileobj(src, dst) やってることは zcat /output/result.csv.gz > /nfs/output/result.csv と同等です。 SFTPで連携する場合 セキュリティの関係で、SFTPでやりとりするケースもあります。 Pythonの場合、ParamikoというSFTPクライアントライブラリがあります。 このライブラリでは、リモート上のファイルに対して、file-like object相当のものを返してくれるメソッドがあります。 これを使えば、NFSのときとほとんど同じコードで、同じことができます。 ファイルをもらう場合 sftp_client = ... remote_file = sftp_client.file('data.csv') compressed_file = Path('input', 'data.csv.gz') with open(remote_file, 'rb') as src, gzip.open(compressed_file, 'w') as dst: shutil.copyfileobj(src, dst) ファイルを渡す場合 compressed_file = Path('output', 'result.csv.gz') sftp_client = ... output_file = sftp_client.file('result.csv') with gzip.open(compressed_file, 'r') as src, open(output_file, 'wb') as dst: shutil.copyfileobj(src, dst) Appendix gzipファイルを解凍せずに、DBに取り込む 大量データの場合、RDBMSが提供するファイル取り込みの機能(MySQLだとLoad data構文)を使いたくなります。 このファイル取り込みの機能が、gzip圧縮されたものをサポートしていないケースがあります。 こういった場合、 ファイルを解凍 解凍したファイルに対してファイル取り込みを実施 とやると、折角、圧縮してディスク使用量をおさえたのが、なかったことになります。 これを回避するために、 別プロセスで、named pipeに解凍した内容を書き込み named pipeに対してファイル取り込みを実施 とすれば、いったん、解凍したファイルをつくらずにすみます。 シェルだと、下記のようになります。 mkfifo /tmp/named_pipe zcat data.csv.gz > /tmp/named_pipe & mysql -e "LOAD DATA INFILE '/tmp/named_pipe' INTO TABLE sample_table" Pythonでも、以下の2つと、これまでの内容を組み合わせることで同様のことができます。 os.mkfifo multiprocessing.Process まとめ 知ってしまえば、gzip圧縮したまま、様々な操作をする方法があり、そのやり方も、通常のファイルとさほど変わらない(gzipを扱うためのコマンドやクラスを一段かませるだけ)ことが多いです。 データが大きいほど、圧縮したときの差がでてくるので、大量データを扱うプロジェクトでは、地味に効いてくるテクニックです。
- 投稿日:2021-12-05T18:34:06+09:00
RDKitのフィンガープリントをPandasのデータフレームに取り込んで使い倒そう②
はじめに RDKitのフィンガープリントをPandasのデータフレームに取り込んで使い倒そう の続編。 前回は、Morgan FingerprintとAtomPair Fingerprintについてはハッシュの例しかなかったため、今回はアンハッシュの例を示す。 環境 RDKit 2020.09.3 前準備 前回同様、本記事のソースを動かすにあたり、以下のモジュールをインポートしておく必要がある。 import numpy as np import pandas as pd from rdkit import rdBase, Chem from rdkit import DataStructs from rdkit.DataStructs import ExplicitBitVect from rdkit.Chem import AllChem, Descriptors from rdkit.ML.Descriptors import MoleculeDescriptors ハッシュ方式とアンハッシュ方式の違い ハッシュ方式はビット数固定だが、アンハッシュ方式はビット数が可変となる。 ハッシュ方式はビットが衝突するため、一部の情報が失われてしまう。 フィンガープリントの説明 では、ここからMorgan Fingerprint(アンハッシュ)とAtomPair Fingerprint(アンハッシュ)について、フィンガープリントを求めてpandasに取り込む方法について説明する。 なお、以下のコード例は全て前提としてmolsという変数にRDkitのMolオブジェクトのリストが格納されていることを前提としている。 また、Pandasのデータフレームのindexは指定していないので、化合物名のリスト等、適宜補ってほしい。 Morgan Fingerprint (アンハッシュ) Pandasに取り込む方法 ここでは、GetMorganFingerprint関数に半径3を指定して計算した結果を取り込む例を示す。 前回のTopologicalTorsion Fingerprintでもふれたが、計算を行うまでどんなビットがでてくるか分からないという特徴がある。化合物によって得られるビットも異なってくる。 従って計算しながらビット名をキーとして保持し、後から全化合物に対し存在しないビットの値を0で補完する処理が必要となる。 # Morganのフィンガープリントの計算 from collections import defaultdict mg_fps_tmp = [] keys = set() for mol in mols: fp = AllChem.GetMorganFingerprint(mol, 3) mg_fps_tmp.append(fp.GetNonzeroElements()) for key in fp.GetNonzeroElements(): keys.add(key) mg_fps = defaultdict(list) for fp in ap_fps_tmp: for key in keys: if key in fp: mg_fps[key].append(fp[key]) else: mg_fps[key].append(0) df = pd.DataFrame(data=mg_fps) AtomPair Fingerprint(アンハッシュ) こちらもMorgan と全く同様の処理となる。 Pandasに取り込む方法 from collections import defaultdict ap_fps_tmp = [] keys = set() for mol in mols: fp = AllChem.GetAtomPairFingerprint(mol) ap_fps_tmp.append(fp.GetNonzeroElements()) for key in fp.GetNonzeroElements(): keys.add(key) ap_fps = defaultdict(list) for fp in ap_fps_tmp: for key in keys: if key in fp: ap_fps[key].append(fp[key]) else: ap_fps[key].append(0) df = pd.DataFrame(data=ap_fps) おわりに アンハッシュ方式は、情報の損失がなくなる半面、説明変数の数が多くなるため変数選択が重要となる。うまく使い分けてモデルを作成しよう。 参考
- 投稿日:2021-12-05T18:28:22+09:00
【環境構築】macOS Big Sur 11.6.1 + Dockerのpython containerにpandasをinstall
会社貸与のMacBook Proに飲み物をこぼしてしまい動かなくなったので交換してもらった。 前のマシンにはPythonやらPandasやら使いたいライブラリーを直接インストールしていたので、環境のトラブルがあった場合に色々と面倒だった。今回は素直にDockerで環境構築して、いざとなればimage捨てて作り直せばいいやと思い、環境構築の手順をロギング。 pyhtonのcontainer実行できるようになったので、自分の仕事の効率化のために書いたpythonコード実行する環境を再構築する。 ということで、今回はpandsをinstall 前回までの記事 【環境構築】macOS Big Sur 11.6.1 にDockerをInstall https://qiita.com/cocoapuff/items/e3eee747b1aeafa32037 【環境構築】macOS Big Sur 11.6.1 + Dockerでpython https://qiita.com/cocoapuff/items/1daaf9e91b24bc3818ad pandsをインストール pythonのcontainerは動いている前提 自分macにはpipをinstallしていないので、間違ってMacのterminalで実行しても問題ないけど、cpntainer側のCLIで実行するよう気をつける # pip3 install pandas これだけです。 pandasを使ったpythonコードを実行してみる pandasをimportして使えていることを確かめる簡単なコードを書いて実行してみる pandas_test.py import pandas as pd df = pd.DataFrame( { 'name':['itoh', 'saitoh'], 'age':['20', '30'] }) print(df) # python ./pandas_test.py name age 0 itoh 20 1 saitoh 30 問題なく動いたので、pandasは使える 今日はここまで。 container内でコード書くの大変なのでVScodeで書けるようにしたい。
- 投稿日:2021-12-05T18:20:59+09:00
【Python】ネストされた辞書型配列のKeyを検索・値を取得する方法
はじめに pythonでは、JSON形式のデータをdict型(辞書型配列)に変換して扱うケースは多いと思います。その際、JSONの階層が可変であったり、list型を含んでいたりするなどの複雑な構造を持つデータに対して、特定のKeyを指定して値を検索・取得できる関数があると便利だなと考え、勉強の一環で作ってみました。 ※既にあるのかも知れませんが、ググっても出てこなかったのであくまで教材・課題のつもりで... ※ご指摘、ご指南いただけるとありがたいです 環境 macOS Monterey 12.0.1 Python 3.9.2 Visual Studio Code コード 関数内部で関数自身を呼び(回帰関数)、ネストが可変でも対応できるように実装。list型が含まれる場合にも対応しています。 util.py def dict_search(d,key): if not d or not key: return None elif isinstance(d, dict): if key in d: return d.get(key) else: l = [dict_search(d.get(dkey),key) for dkey in d if isinstance(d.get(dkey),dict) or isinstance(d.get(dkey),list)] return [lv for lv in l if not lv is None].pop(0) if any(l) else None elif isinstance(d,list): li = [dict_search(e,key) for e in d if isinstance(e,dict) or isinstance(e,list)] return [liv for liv in li if not liv is None].pop(0) if any(li) else None else: return None 動作検証 実際にネスト深めのJSONファイル(sumple.json)を用意して検証してみます。同じ階層のディレクトリに以下のファイルがある環境とします。 ・sumple.json ・util.py ・test.py sumple.json { "data": { "orders": { "edges": [ { "node": { "id": "4595490455781", "nodeName": "#123456", "customer": { "id": "abcdefg@hijklmn.com", "name": "kajuta" } } } ] } } } :使用例: 別ファイル(util.py)に関数を定義したので、from ~ importを使って関数をインポートして使用してみます。jsonファイルの読み込みについての説明は割愛します。 test.py from util import dict_search import json # jsonを読み込んでdictに変換 with open('sumple.json','r') as f: j_dict = json.load(f) # keyを指定してdict内の要素を取得(この例では'name'の値を取得) result = dict_search(j_dict,'name') # 出力 print(result) 実行結果は以下のとおり。途中でlistを含む階層の奥にある"name"の値を取得できているのがわかります。 console kajuta :検証: 「name」というkeyが別階層に重複して存在するサンプルで検証 sumple2.json { "node": { "id": "4595490455781", "name": "#123456", "customer": { "id": "abcdefg@hijklmn.com", "name": "kajuta" } } } 実行結果は予想通り、浅い階層から先にヒットしました。 console #123456 このようにkeyが重複して存在する場合に、何番目の要素にヒットさせるかを、引数で指定できるようにしても面白いかもしれません。 おわりに 最近、API連携などの開発を行なっていて、その際に得られたJSON形式のデータから特定の値を取得する方法をいろいろ模索していました。大抵は与えられるJSONの階層やKeyが事前にわかっている場合が多いとは思いますが、階層が複雑な場合や可変の場合のベストな方法はないかと思い、勉強のつもりで自分なりに考えてみたので投稿しました。 ご指摘・ご指南等あれば、是非ともよろしくお願いします。
- 投稿日:2021-12-05T18:20:59+09:00
【Python】ネストされた辞書型配列の要素を検索・取得する方法
はじめに pythonでは、JSON形式のデータをdict型(辞書型配列)に変換して扱うケースは多いと思います。その際、JSONの階層が可変であったり、list型を含んでいたりするなどの複雑な構造を持つデータに対して、特定のKeyを指定して値を検索・取得できる関数があると便利だなと考え、勉強の一環で作ってみました。 ※既にあるのかも知れませんが、ググっても出てこなかったのであくまで教材・課題のつもりで... ※ご指摘、ご指南いただけるとありがたいです 環境 macOS Monterey 12.0.1 Python 3.9.2 Visual Studio Code コード 関数内部で関数自身を呼び(再帰関数)、ネストが可変でも対応できるように実装。list型が含まれる場合にも対応しています。 util.py def dict_search(d,key): if not d or not key: return None elif isinstance(d, dict): if key in d: return d.get(key) else: l = [dict_search(d.get(dkey),key) for dkey in d if isinstance(d.get(dkey),dict) or isinstance(d.get(dkey),list)] return [lv for lv in l if not lv is None].pop(0) if any(l) else None elif isinstance(d,list): li = [dict_search(e,key) for e in d if isinstance(e,dict) or isinstance(e,list)] return [liv for liv in li if not liv is None].pop(0) if any(li) else None else: return None 動作検証 実際にネスト深めのJSONファイル(sumple.json)を用意して検証してみます。同じ階層のディレクトリに以下のファイルがある環境とします。 ・sumple.json ・util.py ・test.py sumple.json { "data": { "orders": { "edges": [ { "node": { "id": "4595490455781", "nodeName": "#123456", "customer": { "id": "abcdefg@hijklmn.com", "name": "kajuta" } } } ] } } } :使用例: 別ファイル(util.py)に関数を定義したので、from ~ importを使って関数をインポートして使用してみます。jsonファイルの読み込みについての説明は割愛します。 test.py from util import dict_search import json # jsonを読み込んでdictに変換 with open('sumple.json','r') as f: j_dict = json.load(f) # keyを指定してdict内の要素を取得(この例では'name'の値を取得) result = dict_search(j_dict,'name') # 出力 print(result) 実行結果は以下のとおり。途中でlistを含む階層の奥にある"name"の値を取得できているのがわかります。 console kajuta :検証: 「name」というkeyが別階層に重複して存在するサンプルで検証 sumple2.json { "node": { "id": "4595490455781", "name": "#123456", "customer": { "id": "abcdefg@hijklmn.com", "name": "kajuta" } } } 実行結果は予想通り、浅い階層から先にヒットしました。 console #123456 このようにkeyが重複して存在する場合に、何番目の要素にヒットさせるかを、引数で指定できるようにしても面白いかもしれません。 おわりに 最近、API連携などの開発を行なっていて、その際に得られたJSON形式のデータから特定の値を取得する方法をいろいろ模索していました。大抵は与えられるJSONの階層やKeyが事前にわかっている場合が多いとは思いますが、階層が複雑な場合や可変の場合のベストな方法はないかと思い、勉強のつもりで自分なりに考えてみたので投稿しました。 ご指摘・ご指南等あれば、是非ともよろしくお願いします。
- 投稿日:2021-12-05T18:20:59+09:00
【Python】ネストされた辞書型配列の要素を再帰的に取得する方法
はじめに pythonでは、JSON形式のデータをdict型(辞書型配列)に変換して扱うケースは多いと思います。その際、JSONの階層が可変であったり、list型を含んでいたりするなどの複雑な構造を持つデータに対して、特定のKeyを指定して値を検索・取得できる関数があると便利だなと考え、勉強の一環で作ってみました。 ※既にあるのかも知れませんが、ググっても出てこなかったのであくまで教材・課題のつもりで... ※ご指摘、ご指南いただけるとありがたいです 環境 macOS Monterey 12.0.1 Python 3.9.2 Visual Studio Code コード 関数内部で関数自身を呼び(再帰関数)、ネストが可変でも対応できるように実装。list型が含まれる場合にも対応しています。 util.py def dict_search(d,key): if not d or not key: return None elif isinstance(d, dict): if key in d: return d.get(key) else: l = [dict_search(d.get(dkey),key) for dkey in d if isinstance(d.get(dkey),dict) or isinstance(d.get(dkey),list)] return [lv for lv in l if not lv is None].pop(0) if any(l) else None elif isinstance(d,list): li = [dict_search(e,key) for e in d if isinstance(e,dict) or isinstance(e,list)] return [liv for liv in li if not liv is None].pop(0) if any(li) else None else: return None 動作検証 実際にネスト深めのJSONファイル(sumple.json)を用意して検証してみます。同じ階層のディレクトリに以下のファイルがある環境とします。 ・sumple.json ・util.py ・test.py sumple.json { "data": { "orders": { "edges": [ { "node": { "id": "4595490455781", "nodeName": "#123456", "customer": { "id": "abcdefg@hijklmn.com", "name": "kajuta" } } } ] } } } :使用例: 別ファイル(util.py)に関数を定義したので、from ~ importを使って関数をインポートして使用してみます。jsonファイルの読み込みについての説明は割愛します。 test.py from util import dict_search import json # jsonを読み込んでdictに変換 with open('sumple.json','r') as f: j_dict = json.load(f) # keyを指定してdict内の要素を取得(この例では'name'の値を取得) result = dict_search(j_dict,'name') # 出力 print(result) 実行結果は以下のとおり。途中でlistを含む階層の奥にある"name"の値を取得できているのがわかります。 console kajuta :検証: 「name」というkeyが別階層に重複して存在するサンプルで検証 sumple2.json { "node": { "id": "4595490455781", "name": "#123456", "customer": { "id": "abcdefg@hijklmn.com", "name": "kajuta" } } } 実行結果は予想通り、浅い階層から先にヒットしました。 console #123456 このようにkeyが重複して存在する場合に、何番目の要素にヒットさせるかを、引数で指定できるようにしても面白いかもしれません。 おわりに 最近、API連携などの開発を行なっていて、その際に得られたJSON形式のデータから特定の値を取得する方法をいろいろ模索していました。大抵は与えられるJSONの階層やKeyが事前にわかっている場合が多いとは思いますが、階層が複雑な場合や可変の場合のベストな方法はないかと思い、勉強のつもりで自分なりに考えてみたので投稿しました。 ご指摘・ご指南等あれば、是非ともよろしくお願いします。 参考 ・Pythonで再帰関数を作る方法【初心者向け】 ・Pythonで理解する再帰関数 ・Pythonのリストの深さを再帰的に計算する:空のリストの扱いに注意しよう
- 投稿日:2021-12-05T18:01:17+09:00
LINEのトークスクリプトから、ネガティブ・ポジティブ感情を判定する
はじめに LINEのトークスクリプト分析記事の続きです! 今回は、単語感情極性対応表を利用して、トークのネガポジ判定をしていきたいと思います。 いつもの注意 分析対象のデータは、パーソナルデータとなっております。 利用する際は、相手に必ず確認を取ること。また、その取り扱いには十分注意するようお願いいたします。 本記事は単なるHowTo記事であり、個人情報流出等のいかなる責任であっても負いかねます。 利用辞書 ここでは、単語感情極性対応表から日本語版を使用させていただきたいと思います。 こちらからダウンロードできます! ダウンロードしたファイルをデータフレームにして確認するとこんな感じです。 pn_dfを作成するコードはこちら。 pn_df = pandas.read_csv('{{downloadした辞書のpath}}/pn_ja.dic',\ sep=':', encoding='shift-jis', names=('Word','Reading','POS', 'PN') ) 注意事項としては、encodingで日本語を読み取れるようにしておくことくらいでしょうか。 分析対象データフレーム 前回のLINE分析記事を参考にしてください。 こちらを参考にしていただくと、このようなデータフレームが取得できます。 これに対して、トークスクリプトのネガポジを数値化していきたいと思います。 作戦 ネット記事には色々ありましたが、僕の数値化作戦は以下でいきたいと思います。 分析対象のDFを行ごとに形態素解析する 各形態素と極性辞書から一致をかけて、極性値を取得 文章の極性値平均を取り、新しいカラムに保存する 1.行ごとに形態素解析 形態素解析にはmecabを使用します。 また、業界の標準っぽい、neologdというシステム辞書を使うことにします。 mecabの使い方等の説明は一旦省きたいと思います。 今回は分かち書きされた単語のみを分析したいので、mecabのparceToNodeを利用して、文章を分かち書きします。 for text in temp_df['talk'].head(30): node = tagger.parseToNode(text) while node: print(node.surface) node = node.next 余談ですが、僕はここで久しぶりに、whileの無限ループにハマってしまい凹みました。笑 また、お気づきかとは思いますが、形態素解析は実行に時間がかかります。 過去のテキスト全てに対して解析をかけるとなると、分散処理をかましてあげないとやってられないくらいには実行時間くが長いです。 一旦動かしてみたいので、ここの分散処理はまた記事を書きます。 [執筆中 : 形態素解析の処理を、マルチスレッド化させるpythonの分散処理で時短する] 2.辞書を使って極性値を取得 node.surfaceで分かち書きした単語を取得することに成功したので、pn_dfから極性値を引っ張ってきたいと思います。 pn_value = [] tagger = MeCab.Tagger("{{neologdのpath}}/mecab-ipadic-neologd") text = "並行分散処理かけてあげないと終わらないわ。" node = tagger.parseToNode(text) while node: print(node.surface) word_value = pn_df[pn_df['Word']==node.surface]['PN'].values pn_value.append(word_value[0]) if len(word_value)>0 else pn_value.append(0) node = node.next 1つのtextに対して、このようにnodeを回してあげると、pn_valueというリストに極性値が入っていきます。 ここでは、pn_dfの名前一致に引っかからない単語の極性値は0としています。 ちなみに、「並行分散処理かけてあげないと終わらないわ。」というテキストに対する極性値はこんな感じです。 3.文章の極性値平均をとる ( 2で作成した極性値リストのsum ) / ( リストの数 ) をしてあげれば、その文章の平均の極性値を取得することができます。 データの確認 全体感はこんな感じです。 写真や通話に対して、ネガティブの極性がついているので、その分マイナスに寄っていますね。 ここで注意していただきたいのが、利用している辞書の特性についてなのですが、 このように、登録されている単語数にも、ネガポジで偏りがあります。 なので、そもそもの話ですが、ネガティブな単語はポジティブなものと比較して抽出されやすい。という特徴を理解しておかないといけません。 最後に 極性値の合計を出してみました。 この結果を見る限り、僕よりも相手の方が、ネガティブワードを使っているようです。笑 me -244.388739 you -273.091161 ネガポジはあくまでも参考として、楽しむものとして使うのが良さそうでした。 ご精読ありがとうございます。
- 投稿日:2021-12-05T17:58:06+09:00
mlflowとluigiによるML実験管理例
はじめに 本記事ではMLの実験を行うときの、コード、パラメータ、モデル、評価結果を管理するための構成例を紹介します。 サンプルコードはこちら 前提知識 Must python docker Want mlflow luigi 思想 前処理を加えたデータや学習したモデルなどプログラムで出力されるファイルは全てmlflowの管理下におく。 コードはgitで管理し、実験結果とcommit hashを紐づける。 前処理、学習、推論などタスク同士の依存関係を管理して、依存しているタスクを自動で実行できるようにする。また、既に実行されたタスクは実行しないようにする。 構成概要 titanicのdataに対して、前処理、学習、推論を行う例を紹介する。 ディレクトリ構成は以下のような感じ。 src/tasks/下に前処理などの具体的なタスクを行うファイルを作成する。 tomlファイルで実行するタスクを指定してsrc/runner.pyを実行する。 各タスクの出力は、mlruns下のmlflowの出力ファイルの管理先のデフォルト位置に実験ごとに出力する。 ディレクトリ構成 . ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── input │ ├── gender_submission.csv │ ├── test.csv │ ├── titanic.zip │ └── train.csv ├── luigi.toml ├── mlruns/ ├── requirements.txt └── src ├── runner.py └── tasks ├── prediction.py ├── preprocessor.py ├── run_task.py └── trainer.py 環境構築 mlflowなど必要なライブラリを入れるDockerfileを適当に作成して、下記のようなdocker-compose.ymlを用意して、プロジェクト直下でdocker-compose up dなどでmlflow trackingサーバを立ち上げる。 mlflow trackingでは、サーバを立ち上げるときにmetricなどを管理するTracking URIと、出力されるファイルを管理するArtifact URIという2つのURIを指定することができる。デフォルトではどちらも./mlruns/下に保存される。 サーバを立ち上げた後{サーバのIP}:{指定したポート番号}をブラウザで開くことでアクセスできる。 docker-compose.yml version: "3" services: mlflow: image: ml_experiment_management_demo container_name: ml_experiment_management_demo_mlflow build: context: . dockerfile: Dockerfile volumes: - $PWD:$PWD working_dir: $PWD ports: - "5000:5000" command: mlflow ui -h 0.0.0.0 restart: always コードの構成 .envに、プロジェクトのパス、利用するconfigのパス、configの種類を指定する。 .env PROJECT_ROOT={プロジェクトのパスを指定} LUIGI_CONFIG_PATH=luigi.toml LUIGI_CONFIG_PARSER=toml luigiの機能で環境変数LUIGI_CONFIG_PATHに指定したファイルをconfigとして読み込むことができる。ここではluigi.tomlとしている。 RunConfigはrunner.pyで利用するconfigで、実験名や概要、実行するtaskを指定する設計になっている。 PathConfigでは各タスクの入出力先ディレクトリのpathを定義する。 前処理、学習、推論の各タスクのconfigはPreprocessor、Trainer、Predictionに定義する。 luigi.toml [RunConfig] #実験名:mlflowの実験単位 experiment_name="demo" description="概要" # 実行するTask task="Prediction" [PathConfig] # input input="./input/" # Preprocessorのoutput、Trainer、Evaluatorのinput # デフォルトはartifact_uri下、"./mlruns/{experiment_id}/{run_id}/artifacts/preprocessor/"となる。 #preprocessor="./mlruns/" # Trainerのoutput、Evaluatorのinput # デフォルはトartifact_uri下"./mlruns/{experiment_id}/{run_id}/artifacts/trainer/"となる。 #trainer="./mlruns/" # Predictionのoutput、 # デフォルトはartifact_uri下"./mlruns/{experiment_id}/{run_id}/artifacts/prediction/"となる。 #trainer="./mlruns/" [Preprocessor] train_columns=['PassengerId','Survived', 'Pclass', 'SibSp', 'Parch'] test_columns=['PassengerId','Pclass', 'SibSp', 'Parch'] [Trainer] seed=0 [Prediction] runner.pyが実際に実行するスクリプトになる。 .envとRunConfigを読み込んで指定したtaskを実行する。 experiment_nameを実験名として設定して、利用したconfigファイルや実行するtaskをlogとして記録している。詳しくはmlflow trackingのドキュメント参照。 runner.py import os import sys import luigi import mlflow from dotenv import load_dotenv from luigi.configuration import add_config_path, get_config from tasks.prediction import Prediction from tasks.preprocessor import Preprocessor from tasks.trainer import Trainer def main(): load_dotenv() sys.path.append(f"{os.getenv('PROJECT_ROOT')}src/") add_config_path(os.getenv("LUIGI_CONFIG_PATH")) run_config = get_config(os.getenv("LUIGI_CONFIG_PARSER"))["RunConfig"] mlflow.set_experiment(run_config["experiment_name"]) mlflow.start_run() mlflow.set_tag('mlflow.note.content', run_config["description"]) mlflow.log_param("description", run_config["description"]) mlflow.log_param("task", run_config["task"]) mlflow.set_tag('mlflow.runName', mlflow.active_run().info.run_id) mlflow.log_artifact(os.getenv("LUIGI_CONFIG_PATH")) luigi.run([run_config["task"], "--local-scheduler"]) mlflow.end_run() if __name__ == '__main__': main() タスクの定義 runner.pyで実行するタスクはRunTaskクラスを継承して作成する。 RunTaskクラスでは、PathConfigで各タスクの出力先ディレクトリのpathが指定されているかを確認し、指定されていない場合./mlruns/{experiment_id}/{run_id}/artifacts/{task_name}/に設定する。 build_input_output_path_dictsはサブクラスで入出力pathの一覧を返すように実装することを想定しており、サブクラスでは設定されたpath_configを用いて、入出力のプロジェクトルートからの相対パスか絶対パスを返すように実装する。 RunTaskクラスはluigiのTaskを継承しており、outputメソッドでoutput_paths_dictの値をluigiのLocalTargetとして指定する。 luigi.Taskの機能でコンストラクタの実行後outputメソッドで指定したファイルが既にある場合、実行済のタスクと見なされる。これにより、RunTaskを継承した各タスクはoutput_paths_dictに設定したファイルが既にあれば、実行しなくて済むようになっている。 また、PathConfigを指定しなかった場合、run_idは毎回変わるのでそのタスクは必ず実行される。 run_task.py import os from abc import abstractmethod from typing import Tuple import luigi import mlflow from luigi.configuration import get_config class RunTask(luigi.Task): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.config = get_config( os.getenv("LUIGI_CONFIG_PARSER") )[self.__class__.__name__] experiment_id = mlflow.active_run().info.experiment_id run_id = mlflow.active_run().info.run_id self.path_config = get_config( os.getenv("LUIGI_CONFIG_PARSER"))["PathConfig"] self.artifacts_uri_path = f"./mlruns/{experiment_id}/{run_id}/artifacts/" # タスクごとにPathConfigが指定されていなければデフォルト値設定 # デフォルトパス ./mlruns/{experiment_id}/{run_id}/artifacts/{task_name}/ configs_keys = get_config(os.getenv("LUIGI_CONFIG_PARSER")).data.keys() task_list = [i for i in configs_keys if i not in [ "RunConfig", 'PathConfig']] for task_name in task_list: self.path_config.setdefault( task_name, f"{self.artifacts_uri_path}{task_name}/") # 入出力パス output_paths_dictのvaluesのパスにファイルが既にある場合、そのタスクは行われない self.input_paths_dict, self.output_paths_dict = self.build_input_output_path_dicts() for output_path in self.output_paths_dict.values(): os.makedirs(os.path.dirname(output_path), exist_ok=True) @abstractmethod def build_input_output_path_dicts(self) -> Tuple[dict, dict]: """ クラスの入出力パスをそれぞれdictで返す Returns: input_paths_dict,output_paths_dict """ pass def output(self): return list(map(lambda x: luigi.LocalTarget(x), self.output_paths_dict.values())) 前処理 RunTaskの説明で述べたように、前処理のタスクPreprocessorクラスはRunTaskを継承して作成する。 build_input_output_path_dictsでpath_configの値を利用して、入出力pathを設定している。 runメソッドがtaskの実行する処理で、ここでは前処理の内容を実装している。 ここで行っている前処理は、シンプルにtrain、testをconfigで指定したcolumnsのみにしているだけである。 preprocessor.py from typing import Tuple import pandas as pd from tasks.run_task import RunTask class Preprocessor(RunTask): def build_input_output_path_dicts(self) -> Tuple[dict, dict]: input_paths_dict = { "train": f"{self.path_config['input']}train.csv", "test": f"{self.path_config['input']}test.csv", } output_paths_dict = { "train": f"{self.path_config['Preprocessor']}train.csv", "test": f"{self.path_config['Preprocessor']}test.csv", } return input_paths_dict, output_paths_dict def run(self): train = pd.read_csv(self.input_paths_dict["train"]) test = pd.read_csv(self.input_paths_dict["test"]) train = train[self.config["train_columns"]] test = test[self.config["test_columns"]] train.to_csv(self.output_paths_dict["train"], index=False) test.to_csv(self.output_paths_dict["test"], index=False) 学習 前処理と同様にRunTaskを継承して作成している。 luigiの機能の@requiresへルパを用いて、Preprocessorに依存していることを定義している。これによりTrainerを実行する前にPreprocessorが先に実行される。詳しくはluigiのdocument参照。 また、Trainerではsklearnのモデルで学習しているが、mlflowのmlflow.sklearn.autologを用いることで、Metricsなどがいくつか自動で保存することができる。自分で指標を設定したいときはlog_metricなどを用いる。詳しくはmlflowのLogging Functionsのdocument参照。 trainer.py import pickle from typing import Tuple import mlflow.sklearn import pandas as pd from luigi.util import requires from sklearn import linear_model from tasks.preprocessor import Preprocessor from tasks.run_task import RunTask @requires(Preprocessor) import pickle from typing import Tuple import mlflow.sklearn import pandas as pd from luigi.util import requires from sklearn import linear_model from tasks.preprocessor import Preprocessor from tasks.run_task import RunTask @requires(Preprocessor) class Trainer(RunTask): def build_input_output_path_dicts(self) -> Tuple[dict, dict]: input_paths_dict = { "train": f"{self.path_config['Preprocessor']}train.csv", "test": f"{self.path_config['Preprocessor']}test.csv", } output_paths_dict = { "model": f"{self.path_config['Trainer']}model", } return input_paths_dict, output_paths_dict def run(self): train = pd.read_csv( self.input_paths_dict["train"], index_col='PassengerId') X = train.drop("Survived", axis=1) y = train["Survived"] mlflow.sklearn.autolog() reg = linear_model.RidgeClassifier(random_state=self.config["seed"]) reg.fit(X, y) pickle.dump(reg, open(self.output_paths_dict["model"], "wb")) 推論 前処理・学習と同様にRunTaskを継承して作成する。 実行に前処理済みのtestと学習したmodelが必要になるため、Preprocessor, Trainerを@requiresに指定している。 prediction.py import pickle from typing import Tuple import pandas as pd from luigi.util import requires from tasks.preprocessor import Preprocessor from tasks.run_task import RunTask from tasks.trainer import Trainer @requires(Preprocessor, Trainer) class Prediction(RunTask): def build_input_output_path_dicts(self) -> Tuple[dict, dict]: input_paths_dict = { "test": f"{self.path_config['Preprocessor']}test.csv", "model": f"{self.path_config['Trainer']}model", } output_paths_dict = { "test_prediction": f"{self.path_config['Prediction']}test_prediction.csv", } return input_paths_dict, output_paths_dict def run(self): test = pd.read_csv( self.input_paths_dict["test"], index_col="PassengerId") model = pickle.load(open(self.input_paths_dict["model"], "rb")) test_prediction = pd.DataFrame( model.predict(test), columns=["Survived"], index=test.index ) test_prediction.to_csv(self.output_paths_dict["test_prediction"]) 実行例 プロジェクトルートでpython src/runnr.pyを実行することで、configのtaskで指定したタスクを実行する。例えばtask="Prediction"としていれば、Predictionのrunを実行する。ただし、PredictionはPreprocessor,Trainerに依存しているため、先に前処理、学習が実行される。 上記の実行後、下記のようにPreprocessor,Trainerのpathを各タスクの実行結果のディレクトリに指定して、再びtask="Prediction"として実行した場合、Predictionの入力として先程の実行結果が用いられる。この場合、前処理、学習の処理は省略できる。 [PathConfig] Preprocessor="./mlruns/1/ba79349b64ac4493ac1f6c6eac306e68/artifacts/Preprocessor/" Trainer="./mlruns/1/ba79349b64ac4493ac1f6c6eac306e68/artifacts/Trainer/" 課題 mlflowのartifact URIがデフォルトの前提で、コード内でパスを指定している。つまり、実行しているプロジェクトルート直下のmlruns/ディレクトリ下で管理する前提になっている。環境変数やconfigファイルなどで指定できるようにしたほうが望ましい。 1つのファイルに全てのconfigをまとめているので、設定項目が多くなると頻雑になる。ただし、半端に分けても扱いづらくなると思うので難しいところ。
- 投稿日:2021-12-05T17:54:03+09:00
[研究]ジェスチャー認識で色々遠隔操作してみる
現在、私は大学で骨格認識技術を使用してIOTデバイスだったりアプリケーションをジェスチャーで遠隔操作すると言った研究をしています。この記事は、進捗報告件、やってみた記事として上げていきたいと思います。 これまでは、OpenPoseを使用して、色々試してきましたがOpenposeの環境構築がとても大変で今回は、簡単に実装ができるMediapipeを使用しました。 今回作る全体構成図 ハンドジェスチャーのみで様々なデバイスを遠隔操作できるようにする 利用技術とシステムの全体図 実装する機能 モード 機能 メインモード 音楽モードかライトモードに移れる 音楽モード 音楽を再生、停止、ボリューム調整ができる ライトモード ライトを点灯、消灯、光量調整、色調整ができる 音声はPygameを使用する ライトはyeelightを使用する。 注)メインモード以外は機能提供中にモードを移ることができない。 ※音楽モードからライトモードに移動したい時は、必ず、メインモードに戻る必要がある。 実装したもの 2021年11月29日〜2021年12月05日で実装した内容。 判定できるようにしたジェスチャー モード切り替えの実装 色んな所で呼ばれているので、リファクタリング 雑に書かれていたのでclassで書き直した。 スレッドを作成してその中でサービスを制御する [問題点]whileの中で認識した後、機能を提供すると、何回も呼び出されてしまう。 浮上した問題点と出来なかったところ。 ハンドジェスチャーは人によって認識しなくなる時がある。 今後の進めていくもの Lightモードを進める yeelightの実装 仕様 https://yeelight.readthedocs.io/en/latest/ ハンドジェスチャー認識の閾値の調査 調査用UI構築?(時間がなかったらデータ集めだけ) 今後のスケジュール yeelightの繋ぎこみ中心に作業を行う [参考]これまで作ってきたもの ジェスチャー認識技術と簡易ドローンを利用した審判補助用システムとその検証用シミュレータの試作
- 投稿日:2021-12-05T17:19:17+09:00
今更だけどもテスト駆動開発
はじめに こんにちは、3年の阿左見です。 来年の卒業研究が怖い今日この頃。 昨日は@nobu_wt君のAWSのお話でした。僕にとってAWSはちんぷんかんぷんです。 さてMYJLab Advent Calendar 2021 6日目、書いていこうと思います。TDDです。 詳細は『テスト駆動開発入門』 | Kent Beck 著 | 和田 卓人 訳を読んでもらうと詳しくわかるかなって思います。 TDDとは Test Driven Developmentの略称でTDDです。 日本語で言うところのテスト駆動開発ですね。BDDとか言われることもあるみたいです。 Kent Beckによって提唱された概念らしいです。 「Test-Driven Development: By Example」が2003年に書かれているので僕の方が年上ですね TDDの目的 「動作するきれいなコード」、ロン・ジェフリーズのこの簡潔な言葉は、TDD(テスト駆動開発)の目標である。動作するきれいなコードは、あらゆる理由で価値がある。*1 がTDDの目的らしいです。綺麗なコードってほんと大事ですよね。 僕の友人は汚いコードはプログラミングできないと同義とか過激なこと言ってました。言い過ぎでは。 レッド・グリーン・リファクタリング TDDの基本となる概念がこちらになります。 1.レッド:動作しない、おそらく最初のうちはコンパイルも通らないテストを1つ書く。 2.グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。 3.リファクタリング:テストを通すために発生した重複をすべて除去する。 *2 TDDでは最初に必ず失敗するコードを書きます。(テストする機能を書く前にテストを書きます)テストツールを使っている場合にテストの失敗は赤色で表示されるのでレッドと呼びます。 その後テストが通る(グリーンになる様に)処理を記述していきます。上記の「このステップでは罪を犯しても良い」の罪とは「コードの重複」です。以降「罪=重複」とします。 最後に重複を解消していきます。 この作業を経ることで綺麗でかつ重複の少ないコードが生まれるのです。 実際にやってみる。 今回は2分探索でやってみましょう。 最初にテストコードを書きます。 test_binary.py import unittest class TestCase(unittest.TestCase): def test_binary(self): pass 現状は実行すれば成功するテストコードです。 今回はサンプルなのでテストは一つだけにします。 python -m unittest test_binary.py or python3 -m unittest test_binary.py 環境によりますがどちらかのコードを実行してみましょう。(ファイルの指定をしなければ作業ディレクトリの全テストを実行できます。) python3 -m unittest test_binary.py .. ---------------------------------------------------------------------- Ran 1 tests in 0.000s OK こんな感じの画面が出ると思います。一旦は成功です。 通らないテストを書く前に空の関数を用意します。 binary_search.py def binary_search(): pass 準備が出来たら通らないテストを書いていきます。 test_binary.py import unittest from binarysearch import binary_search class TestCase(unittest.TestCase): def test_binary(self): #探索用のテストデータ test_data = [1,2,3,4,5,6,7,8,9] #9を探索する時、正しいインデックスが表示されるか self.assertEqual(8,binary_search(test_data,9)) 実行してみましょう E ====================================================================== ERROR: test_binary (test_binary.TestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/ren/myjlab/my-note/python/test_binary.py", line 7, in test_binary self.assertEqual(8,binary_search(test_data,9)) TypeError: binary_search() takes 0 positional arguments but 2 were given ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (errors=1) 上記のような結果が出てきました。 しっかりと失敗してくれています。 ソートされていない配列が渡された時と値が存在しなかった時のテストを加えましょう。(記事書いてて気持ち悪くなった) tset_binary.py class TestCase(unittest.TestCase): class TestCase(unittest.TestCase): def test_binary(self): #探索用のテストデータ test_data = [1,2,3,4,5,6,7,8,9] #9を探索する時、正しいインデックスが表示されるか self.assertEqual(8,binary_search(test_data,9)) def test_not_exists(self): #テストデータ② test_data = [1,2,3,4,5,6,7,8,9] #存在しないときは-1を返す仕様にする。 self.assertEqual(-1,binary_search(test_data,20)) def test_unsorted_binary_search(self): #テストデータ③ test_data = [1,9,3,4,5,2,7] #二分探索の仕様にそぐわないので-1を返します。 self.assertEqual(-1,binary_search(test_data,9)) こんな感じです。 次はbinar_ysearch.pyを編集してテストが通るようにしていきます。 binary_searh.py def binary_search(data:list,value): #探索する範囲の左右を設定 left = 0 right = len(data) - 1 while left <= right: #探索範囲の中央値を計算 mid = left + right if data[mid] == value: #中央値と等しければ値を返す。 return mid elif data[mid] < value: #大きい場合は左端を変える left = mid + 1 else: #小さい場合は右端を変える right = mid - 1 #見つからなかったら-1を返す。 return -1 この実装で1つ目のテストと2つ目のテストは成功するはずなので実行してみましょう。 % python3 -m unittest test_binary.py ..F ====================================================================== FAIL: test_unsorted_binary_search (test_binary.TestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "/Users/ren/myjlab/my-note/python/test_binary.py", line 21, in test_unsorted_binary_search self.assertEqual(1,binary_search(test_data,9)) AssertionError: 1 != -1 ---------------------------------------------------------------------- Ran 3 tests in 0.000s FAILED (failures=1) 失敗しました。最高ですね。 残りのテストも通るようにコードを書き換えていきたいと思います。 binary_search.py def binary_search(data,value): sorted_data = sorted(data) #エラーハンドリングの追加 if sorted_data != data: return -1 #探索する範囲の左右を設定 left = 0 right = len(data) - 1 while left <= right: #探索範囲の中央値を計算 mid = left + right if data[mid] == value: #中央値と等しければ値を返す。 return mid elif data[mid] < value: #大きい場合は左端を変える left = mid + 1 else: #小さい場合は右端を変える right = mid - 1 #見つからなかったら-1を返す。 return -1 テストを実行してみましょう。 python3 -m unittest test_binary.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK ren@ren-pro python % python3 -m unittest test_binary.py ... ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK 成功です。テストが通りました。 以上で二分探索アルゴリズムのテスト駆動開発が終わりました。 サンプルなのでこのくらいにしましょう。 まとめ テスト駆動開発は動作する綺麗なコードを書くための開発手法である。 レッド・グリーン・リファクタリングの順番に開発を進めていく。 重複のないコードが書ける(らしい) もう少し詳しくTDDについて理解を深めていきたいときは「実践テスト駆動開発」を読んでみるのもいいかもしれません。 今日はこれで終わりです。ありがとうございました。次回は少し毛色を変えた記事を書きたいと思います。 参考文献及び記事 『テスト駆動開発入門』 | Kent Beck 著 | 和田 卓人 訳 [1][2]
- 投稿日:2021-12-05T17:00:08+09:00
【図解解説】JOI2021-2022 一次予選 第2回 問題4 希少な数
図解解説シリーズ 競技プログラミングを始めたばかりでAtCoderの解説やJOIの解説ではいまいちピンと来ない…という人向けに、図解を用いて解説を行います。 問題文 情報オリンピック日本委員会に掲載されている問題 AtCoderに掲載されている問題 入出力など実際に確認して自分の作成したプログラムを採点することができます。 図解解説 今年度の一次予選のD問題は、以下の5つのスキルを確認する問題になっています。 1.入力・出力を正しく利用できる 2.算術演算子を正しく利用できる 3.条件分岐(if)を正しく利用できる 4.繰り返し処理を正しく利用できる 5.配列(リスト)を正しく利用できる 問題文を整理するために、具体的な数字を用いて、図示して考えてみます。 出現回数が入っているリストAを先頭から一つずつ確認し、数え上げていきます。出現回数をまとめると、2,5,8が1回、4が2回出現します。出現回数が最小の数字の中で最小の整数が答えとなるので、入力例2では2が正解となります。 ここで、どのように数え上げていけばいいか考えます。問題文から、1<=Ai<=2000となっているため、最大2000種類の整数が出現します。また、リストAの要素数は1<=N<=100となっていることから、出現回数は最大100回となります。 そこで、2通りの方法を考えてみます。リストAの先頭から確認していく作業はどちらも同じです。 1つ目の方法は、出現回数を保存するリストBを作成します。1~2000までの整数を数え上げることができればよいので、要素数を2001にしたリストを用意します(0は使用しませんが、そのほうが操作が楽になります)。リストAの先頭から1つずつ数字を確認し、該当する数字の要素番号の値を1増やします。そうすることで、リストBにそれぞれの数字の出現回数を保存することができます。最後に、リストBの先頭から出現回数が1以上の値を確認し、最小の出現回数が出てきたらその値を変数に保存します。以上で最小の整数を求めることができます。 2つ目の方法は、Python固有のcountメソッドを使用して数字が何個あるか確認を行います。同じ数字を何度も調べてしまい効率が悪いと感じるかもしれませんが、この方法で考えてみます。現在の出現回数の最小値と数字を保存する変数が必要になりますが、解答例のように丁寧に整理してプログラムを記述してください。リストAの数字は小さい順に並んでいませんので、出現回数が同じ場合は必ず数字の大小関係を比較するように注意してください。 解答例 d1.py N = int(input()) A = list(map(int,input().split())) #出現回数を保存するリストを作成し、初期値をすべて0にする B = [] for i in range(2001): B.append(0) #要素番号=数値として、先頭からリストAをチェックし出現したら+1する for i in range(N): B[A[i]] = B[A[i]] + 1 #答えを探すために数字と出現回数を記録するための変数を用意 #最初に大きな値を設定しておいて、これよりも小さい値が出てきたら書き換える suji = 3000 kaisu = 1000 #先頭から順番に確認を行い出現回数が1以上のものが見つかったら、 #保存している出現回数と大小関係を比較する #もし、保存している値よりも出現回数が小さかったら数字と出現回数を書き換える for i in range(2001): if B[i]>=1: if kaisu>B[i]: kaisu=B[i] suji=i #最後に数字を表示 print(suji) 採点サイトに提出したプログラム d2.py N = int(input()) A = list(map(int,input().split())) #答えを探すために数字と出現回数を記録するための変数を用意 #最初に大きな値を設定しておいて、これよりも小さい値が出てきたら書き換える suji = 3000 kaisu = 1000 #先頭から順番に確認を行いcountメソッドを使って出現回数を数え保存している出現回数と大小関係を比較する #もし、保存している値よりも出現回数が小さかったら数字と出現回数を書き換える #保存している値と出現回数が同じだったら、保存している数字と大小関係を比較し、数字が小さかったら数字を書き換える for i in range(N): if A.count(A[i])<kaisu: kaisu = A.count(A[i]) suji = A[i] elif A.count(A[i])==kaisu: if A[i]<suji: suji = A[i] #最後に数字を表示 print(suji) 採点サイトに提出したプログラム イラスト スライド内で使用しているイラストはすべて「いらすとや」の素材を利用しています。
- 投稿日:2021-12-05T17:00:05+09:00
Streamlitアプリのページ切り替えの流れとページ再読み込みについて
Streamlitでアプリを作る際にページ切り替えを入れ込む際の作り方と、能動的にページを再読み込み(=アプリ再起動)させたいけどどうやるんだ!という時のための記事です。 streamlitの挙動と状態保持変数の説明 streamlitではボタンなどのウィジェットの値が変更される度に全てのコードが再実行されます。 なので、ページ遷移を行うためにはページ管理用の変数を作成し、実行の最初でその値により処理を分岐させるという方法を取るのが一般的かと思います。 イメージとしては下記のようなコードになります。 streamlit_page_change.py import streamlit as st def main(): # 1ページ目表示 st.sidebar.title("test_streamlit") st.markdown("## ボタンでページを変えましょう") st.sidebar.button("ページ切り替えボタン", on_click=change_page) def change_page(): # ページ切り替えボタンコールバック st.session_state["page_control"]=1 def next_page(): # 2ページ目表示 st.sidebar.title("ページが切り替わりました") st.markdown("## 次のページです") # 状態保持する変数を作成して確認 if ("page_control" in st.session_state and st.session_state["page_control"] == 1): next_page() else: st.session_state["page_control"] = 0 main() ※ st.session_state[変数名]でコード再実行が行われても値を保持する変数を定義出来ます 各ウィジェットの引数にあるon_clickやon_changeなどのコールバックを設定した場合、コールバック関数を実行→アプリが再実行という順番で動作します。 上記コードはコールバック関数内で変数の値を更新することでページ遷移を行っています。 ※ この時、最初のページから次のページに持ち越したい値がある場合は、同様にst.session_stateで定義しておくか、コールバック関数にargs,kwargsなどのキーワード引数として持ち越すという方法があります ページ再読み込みのユースケース あまり実用的ではないかもしれませんが、例えばファイルのアップロードを契機に解析用のページに移動させたい場合などに使えます。 まずは下記のコードを見てみましょう。 streamlit_file_upload.py import streamlit as st def main(): # 1ページ目表示 st.sidebar.title("test_streamlit") st.session_state["file"]=st.sidebar.file_uploader("upload", on_change=change_page) def change_page(): # ページ遷移ボタンコールバック st.session_state["page_control"]=1 def next_page(): # 2ページ目表示 st.sidebar.title("ページが切り替わりました") st.markdown("## 次のページです") # 状態保持する変数を作成して確認 if ("page_control" in st.session_state and st.session_state["page_control"] == 1): next_page() else: st.session_state["page_control"] = 0 ページ遷移後の関数内でst.session_state["file"]の中身を確認するとわかるのですが、値はNoneとなってしまっています。 ページ遷移をさせずに下記のような形でファイルの中身を読むと、ちゃんと値が格納されています。 streamlit_file_upload.py st.session_state["file"]=st.sidebar.file_uploader("upload") if st.session_state["file"] != None: st.markdown("ファイルの中身") st.markdown(st.session_state["file"]) 関数の中身を追えていないのでどのような仕様でこうなっているのかわかりませんが、兎にも角にもページ遷移+ファイルアップロード情報を持ち越しをしたい場合は、コールバックのon_changeを使わずになんとかするしかありません。 ここで表題にもあるページ再読み込みの機能が役に立ちます。 解決法 1行加えるだけです。 streamlit_file_upload.py import streamlit as st def main(): # 1ページ目表示 st.sidebar.title("test_streamlit") st.session_state["file"]=st.sidebar.file_uploader("upload") if st.session_state["file"] != None: st.session_state["page_control"] = 1 raise st.experimental_rerun() def next_page(): # 2ページ目表示 st.sidebar.title("ページが切り替わりました") st.markdown("## 次のページです") st.markdown("ファイルの中身") st.markdown(st.session_state["file"]) # 状態保持する変数を作成して確認 if ("page_control" in st.session_state and st.session_state["page_control"] == 1): next_page() else: st.session_state["page_control"] = 0 main() 無事に遷移後のページにアップロードファイルの情報を持ち越すことが出来ました。
- 投稿日:2021-12-05T16:41:58+09:00
ジョイマン生成器つくってみた
はじめに この記事は、以下の記事に感銘を受けて書いたものです。 また、一部実装および記事の内容も参考にさせて頂きました。 ジョイマン ジョイマンは、2人組の日本のお笑いコンビです。 韻を踏むボケ担当の高木晋哉(たかぎしんや)と、ツッコミの池谷和志(いけたにかずゆき)からなります。 韻を踏むネタとその独特の世界観が特徴的です。 詳しくは、以下のYouTubeをご覧ください。 定式化 ジョイマンのネタは、韻を踏むのが特徴です。 代表的なネタについては、こちらの一覧を参考にしました。 調べたところ、以下のパターンがあることがわかりました。 パターン 説明 例 ① 単純に韻を踏むパターン。一番単純。一番多いネタもコレ。 「コラーゲン 主電源」「毛根 ベーコン」「広辞苑 中耳炎」 ② 部分文字列で韻を踏むパターン。たとえば、「おなかのたるみ テルミーショウミー」では、「おなかのたるみ」という文字の一部「たるみ」で韻を踏んでいる 「おなかのたるみ テルミーショウミー」「母さんのぬくもり 甘栗 山盛り」 ③ 部分文字列同士で韻を踏むパターン。たとえば、「人で賑わう繁華街 家帰ったらうがい」では、「繁華街」と「うがい」というそれぞれの部分文字列で韻を踏んでいる 「人で賑わう繁華街 家帰ったらうがい」「汗はしょっぺー 椎名は桔平」 ④ 部分文字列で韻を踏み、かつ対になっているパターン。たとえば、「運動は大事 板東は英二」では、「運動」と「板東」、「大事」と「英二」というペアで韻を踏んでいます。 「運動は大事 板東は英二」「ニコール・キッドマン イコールコッペパン」「七転び八起き 生わさび歯ぐき」 ⑤ その他。特に規則性はなさそう。 「母さんがよなべをして手袋アンドクリエイター」など パターン⑤は規則性が見つけられなかったため、対象外としました。 また、将来的にはパターン④の「運動は大事 板東は英二」を出力したいのですが、いきなりは難しそうです。 今回は、簡単に取り組めそうなパターン①をやってみました。 韻を踏む そもそも「韻を踏む」とは、同じ言葉や同じ母音を持つ言葉を繰り返し使う手法(参考)です。 「同じ言葉の繰り返し」は、「同じ母音の繰り返し」の集合に含まれるので、今回は「同じ母音の繰り返し」を条件とします。 また、正確には母音ではありませんが、リズム感が感じられるので「ん(n)」も母音に含めることにします。 次に、同じ母音の繰り返しは何文字とするか、を決めます。 たとえば、「運動(unou)」「板東(anou)」は最後の3文字、「大事(aii)」「英二(eii)」は最後の3文字の母音が一致します。 全体的にネタを見る限り、最後の2文字だけ同じというものが多く、それでも十分にリズム感を感じることができるかと思います。 よって、「母音の最後の2文字が同じ」という条件にします。 さらに、単語の母音の文字数は同じものに限る、という条件を付け加えます。 これは、極端に文字数が異なるとリズム感が損なわれると考えたためです。 たとえば、「運動(unou)」「板東(anou)」はともに4文字、「大事(aii)」「英二(eii)」はともに3文字の母音になっていますね。 これらをまとめると、条件は以下になります。 韻を踏む、とは同じ母音の繰り返し。ただし、ここでは母音はaiueo+nとする 母音の最後の2文字が同じ 母音の文字数は同じ 実装 それでは、いよいよ実装していきます。 言語はPython、環境はGoogle Colabを使用します。 まずは使用する単語のリストを入手します。 今回は、Wikipediaのタイトルのリストを使うことにしました。 まずはダウンロード・展開しましょう。 !wget http://download.wikimedia.org/jawiki/latest/jawiki-latest-all-titles-in-ns0.gz !gunzip jawiki-latest-all-titles-in-ns0.gz 展開したファイルを読み込みます。 # ファイル読み込み with open('jawiki-latest-all-titles-in-ns0') as f: titles = f.readlines() titlesには、Wikipediaのタイトルがリストとして格納されています。 使いやすさのため、dataframeにしておきましょう。 import pandas as pd df_org = pd.DataFrame({'title': titles}) df_org 以下のように出力されました。 dataframeを整形します。 dataframeには不要な文字列が含まれているので削除します。 また、今回は話を簡単にするため、英数字を含まないワードのみを対象にすることとします。 df = df_org[1:] # 1行目以降を抽出 df = df.replace('\s', '', regex=True) # 改行コードを削除 df = df[~df['title'].str.contains('[a-zA-Z0-9]')] # 半角英数字を含まないもののみ抽出 次に、文字をカタカナやローマ字に変換するpykakasiをインストールします。 !pip install pykakasi それでは母音を抽出してみましょう。 「ゴー☆ジャス(宇宙海賊)をつくる」のコードを参考にしました。 import pykakasi kks = pykakasi.kakasi() def romanize(text): """ローマ字に変換""" result = '' for i in kks.convert(text): result += i['hepburn'] return result def extract_vowel(text): """母音(aiueo)とン(n)を抽出""" return ''.join([i for i in text if i in ['a', 'i', 'u', 'e', 'o', 'n']]) 試しにこれらの関数を使ってみます。 text = 'ジョイマン' romanized_text = romanize(text) extract_vowel_text = extract_vowel(romanized_text) print(romanized_text) print(extract_vowel_text) # joiman # oian ちゃんと、ローマ字にした後、母音を抽出できていることがわかります。 それでは、これらの関数をdataframeに適用しましょう。 以下ではapplyを使って、title列の全ての値に対して、romanize関数、extract_vowel関数と順に適用しています。 df['vowel'] = df['title'].apply(romanize).apply(extract_vowel) # 母音の抽出 df = df[df['vowel'] != ''] # 母音のないものは削除 母音を抽出したvowel列ができています。 それではいよいよ大詰めです! 韻を踏む言葉を表示する関数をつくりましょう! import random def show_rhyming_words(input_text, df): """韻を踏む言葉を抽出する""" input_romanize = romanize(input_text) input_extract_vowel = extract_vowel(input_romanize) tmp_list = df[ (df['vowel'].str.endswith(input_extract_vowel[-2:], na=False)) # 母音の最後の2文字が等しい & (df['vowel'].str.len() == len(input_extract_vowel)) # 母音の文字数が等しい ]['title'].to_list() for i in random.sample(tmp_list, 10): print('{} {}'.format(input_text, i)) # ランダムに10コ表示 まず、input文字列をローマ字表記(input_romanize)し、さらに母音を抽出(input_extract_vowel)します。 そして、dataframeのvowel列と比較して、母音の最後の2文字が等しいかつ母音の文字数が等しいものを抽出します。 こうして得られたワードのリストから、ランダムに10コ表示するという関数になっています。 結果を〜セイッ! それでは結果を見ていきましょう! ※10コの出力はランダムなので、実行するたびに結果は変わります。 コラーゲン☆主電源 「コラーゲン 主電源」の結果は以下になりました。 ※入力は「コラーゲン」です(以下同様)。 input_text = 'コラーゲン' show_rhyming_words(input_text, df) # コラーゲン 饋電線 # コラーゲン クカス県 # コラーゲン ジム・バッケン # コラーゲン ニトレン # コラーゲン ボールペン # コラーゲン 趙之謙 # コラーゲン 甲仙 # コラーゲン ジャッキー・チェン # コラーゲン 処方せん # コラーゲン 関和典 聞き慣れない単語も多く出ていますね。 この中でネタとして使えそうなのは「コラーゲン ボールペン」「コラーゲン 処方箋」ぐらいでしょうか。 「ジャッキー・チェン」も馴染みのある単語ですが、文字数が多くリズム感が少し損なわれそうです。 ※「コラーゲン」の母音の数は5(koraagen→oaaen)、「ジャッキー・チェン」の母音の数も5(jakkii・chen→aiien)で一致。 ちなみに「饋電線(きでんせん)」「趙之謙(ちょうしけん)」「関和典(せきかずのり)」と読むようですが、最後の関和典はpykakasiで「典」を「てん」と読んでしまったようですね。 毛根☆ベーコン 「毛根 ベーコン」の結果は以下です。 input_text = '毛根' show_rhyming_words(input_text, df) # 毛根 亡魂 # 毛根 ローソン # 毛根 カムオン # 毛根 ピアソン # 毛根 わおーん! # 毛根 ヘブロン # 毛根 天文 # 毛根 メイ・ロン # 毛根 クルトン # 毛根 みさぽん ネタとして使えそうなのは「毛根 ローソン」「毛根 クルトン」かと思われます。 「メイ・ロン」は恐竜、「みさぽん」はおそらくアイドルの愛称だと思われます。 広辞苑☆中耳炎 最後に「広辞苑 中耳炎」です。 input_text = '広辞苑' show_rhyming_words(input_text, df) # 広辞苑 電波源 # 広辞苑 佐野県 # 広辞苑 ハヌッセン # 広辞苑 清江苑 # 広辞苑 デリー県 # 広辞苑 慕輿虔 # 広辞苑 姚思廉 # 広辞苑 デロン県 # 広辞苑 三次元 # 広辞苑 ビビン麺 ネタになりそうなのは「広辞苑 三次元」「広辞苑 ビビン麺」といったところでしょうか。 「慕輿虔(ぼよけん)」「姚思廉(ようしれん)」は歴史上の人物のようです。 まとめ いかがだったでしょうか? ジョイマンのように韻を踏む単語を出力することができました! また一方で、馴染みのない言葉も多く出てきていました。 今回はWikipediaのタイトルを使いましたが、馴染みがない単語は除外する、あるいは他の単語リストを使ってみるという方法が考えられます。 何かコメント等ございましたら テルミーショウミー
- 投稿日:2021-12-05T16:40:23+09:00
postgresで1000万件データをいれてみる
きっかけ 大量のデータを投入してテストをしてみたいと思い、まずは大量のデータを投入してみることにしました。 結論からいうと、こんな感じに select count(*)して1000万件を確認 csvファイルを投入して30秒ほどでinsert完了しました。 もっと時間かかるかと思いましたが、早い テストのテーブル作成を作成する 適当にテーブルを作成 SQL CREATE TABLE public.test ( id character varying(256) NOT NULL, num integer, created_at timestamp with time zone ); CSVは以下のようなものを作ります GUIDに数字に日付 ↓sql_many.csvの中身 id,num,created_at c6afae11-6be6-4f5b-b9c6-6e626b299301,9149,2019-06-02 10:34:59+00 ...省略 pythonでcsv作成プログラムを作ってみる ランダムなデータを入れ込んでいます。 日付はちょっと適当かも。 最初、大量のinsert into命令を作成するプログラムを作っていたのですが、CSVファイルimportしたほうがinsert処理時間が早いためcsv作成プログラムに変更しました (途中で変更したのでpythonのcsvライブラリを使っていない・・) python import string import uuid import datetime import random #開始関数 def main(): fileName = 'sql.csv' maxRowNum = 1000 * 10000 createCsvFile(fileName, maxRowNum) #追記モードでファイルに書く def createCsvFile(fileName: string, maxRowNum: int): with open(fileName, 'at') as fb: header = createHeader() fb.write(header) for rowNum in range(maxRowNum): sql = getOneSql(rowNum) fb.write(sql) print(str(rowNum) + ' - ' + sql) fb.close() #ヘッダーを作成 def createHeader(): return "id,num,created_at\n" #1件分のCSV作成 def getOneSql(num: int) -> string: #GUID作成 csv = '' csv += str(uuid.uuid4()) csv += ',' #ランダムな数字 csv += str(random.randrange(0, 10000 +1, 1)) csv += ',' #ランダムな日付を作成 csv += random_date(datetime.datetime(2018, 1, 1), datetime.datetime(2021, 2, 1)).replace(tzinfo=datetime.timezone.utc).isoformat() csv += '\n' return csv #ランダムな日付を作成する関数 他サイトから拝借 def random_date(start, end): """Generate a random datetime between `start` and `end`""" return start + datetime.timedelta( # Get a random amount of seconds between `start` and `end` seconds=random.randint(0, int((end - start).total_seconds())), ) #開始する main() pgAdmin 4でimport 1.対象のテーブルで右クリック 2.Import/Exportを選択 3.以下の画面が開くので、設定して終わり これから SQLを実行して、どのぐらい時間がかかるか、実行計画などを見ていこうと思います