こんな人にオススメ
curve_fit
で得られたパラメータがどのパラメータ名だったのかが分かりにくい!
なんとかしてパラメータ値とパラメータ名の対応づけがしたい!
ということで、今回はdef
で定義した関数のパラメータの値とその名前を対応づける方法について解説する。
どういうことかというと、例えば以下のようにパラメータの値が決まった場合を考える。
# 実はパラメータa=1, b=2, c=3 par = [1, 2, 3]
この時、それぞれの値が初めに決めていたパラメータのどれに該当するかを対応づけするということ。今でいえばa=1
, b=2
, c=3
ということ。
標準ライブラリのinspect
のsignature
関数を使用することで、パラメータ名を取得可能。あとは値とこの名称をうまく結びつければいい。
python環境は以下。
- Python 3.9.6
- numpy 1.21.1
- scipy 1.7.0
- plotly 5.1.0
- plotly-orca 3.4.2
下準備
import inspect import sys import numpy as np import scipy.optimize import plotly.graph_objects as go import plotly.io as pio sys.path.append('../../') import plotly_layout_template as template
まずは下準備としてのimport
関連。plotly
は初めの簡単な例でしか使わないので、グラフを描く必要がないならimport
しなくてもいい。
また、plotly_layout_template
はplotlyの自作テンプレートを入れている。パスが2つ上なので'../../'
としている。
-
-
【随時更新 備忘録】plotlyのグラフ即席作成コード
続きを見る
パラメータの値と名前を対応づけたい
まず初めに、対応づけとはいったいどういうことかということについてお話しする。実際にコードを書いた方がわかりやすいだろう。
ガウシアンっぽいデータを作成
def get_gaussian(x): # ガウシアンっぽいデータの作成 arr = [] for num, i in enumerate(x): np.random.seed(seed=num) coefficient1 = np.random.rand() / 100 # 各xで乱数を変えながらガウシアン関数を作成 eq1 = 1 / (np.sqrt(2 * np.pi)) eq2 = np.exp(-((i - 1) ** 2) / 2) val = eq1 * eq2 + coefficient1 arr.append(val) arr = np.array(arr) return arr # 横軸の値を変更(-5から4.995まで0.01刻み) x = np.arange(-5, 5, 0.005) arr = get_gaussian(x=x) print(f"x: {x}") # x: [-5. -4.995 -4.99 ... 4.985 4.99 4.995] print(f"arr: {arr}") # arr: [0.00548814 0.00417023 0.00435996 ... 0.00785357 0.00573374 0.00838173]
説明するためにはデータが必要なのでサクッと作成する。今回使用するデータはガウシアンっぽいデータ。
以下の記事で実際に使用した関数をそのまま使用している。
-
-
【python&フィッティング】polyfitとcurve_fitでfitting
続きを見る
データをfitさせてパラメータ値を取得
def gaussian_func(x, sigma, mu, b): eq1 = 1 / (np.sqrt(2 * np.pi) * sigma) eq2 = np.exp(-((x - mu) ** 2 / (2 * sigma ** 2))) y = eq1 * eq2 + b return y # データをガウシアンでfitting par, cov = scipy.optimize.curve_fit(f=gaussian_func, xdata=x, ydata=arr)
データのfitはscipy
のcurve_fit
を使用した。fit対象の関数はgaussian_func
関数でガウシアンの形状。
curv_fit
でfitさせるとパラメータと共分散行列が手に入るが、ここではパラメータpar
を使用する。
# パラメータの出力はできるけど、対応関係がわからない print(f"par: {par}") # par: [0.99997103 1.00021249 0.0048593 ]
今回のfitで得られたパラメータは上の3種類で、それぞれgaussian_func
関数のsigma
, mu
, b
に対応している。
んだが、どのパラメータがsigma
か、mu
か、b
かがわかりにくい。3つだけだから簡単だが、それでも自分で対応させるのは面倒。
なら勝手に対応させるようにすればいい、というのが今回の記事。
ガウシアンのグラフとそのfit
蛇足ではあるが、元のデータとそのfitでできるグラフが上のグラフ。グラフ作成用のコードは以下。
inspect.signature
で対応づけ
結果からいうとinspect.signature
を使えばいい。ほぼこれで大丈夫。だが、これだけではうまくいかないので対策を解説。
inspect.signature
を使う
args_names = inspect.signature(gaussian_func) print(args_names) # (x, sigma, mu, b) print(type(args_names)) # <class 'inspect.Signature'>
既に書いたが、inspect.signature
を使うと、その関数の引数名を得ることができる。今で言うとこの引数名がパラメータ名。
type
はinspect.Signature
。個人的には聞き慣れない型。
そのままで使えない
# そのまま使おうとしたらエラー print(args_names[1:]) # TypeError: 'Signature' object is not subscriptable
これで解決かといえばそうではない。関数の引数には横軸の値として使用するx
が入っている。だから、パラメータ名を取得するにはx
を省く必要がある。
じゃあシンプルにスライスすればいいじゃんとなるが、実はスライスできない。事はそう単純ではない。
list
とかにも変換できない
# tupleに変換しようとしてもエラー args_names_tpl = tuple(args_names) # TypeError: 'Signature' object is not iterable
なら一旦tuple
とかに変換して仕舞えばいいなじゃないのか、となるがこれも許されない。イテラブルじゃないとな。ということはlist
もダメだ。
# listに変換しようとしてもエラー args_names_lst = list(args_names) # TypeError: 'Signature' object is not iterable
parameter部分を取り出し
args_pair = args_names.parameters print(args_pair) # OrderedDict([('x', <Parameter "x">), ('sigma', <Parameter "sigma">), ('mu', <Parameter "mu">), ('b', <Parameter "b">)]) print(type(args_pair)) # <class 'mappingproxy'>
じゃあどうするかというと、実は.parameters
でパラメータ部分の取り出しが可能。早く言ってよ。
この場合、パラメータ名とその状態(と勝手に呼んでる)が同時に出力される。状態というのは今回で言えば名称と同じだが、後々異なる。
で、この出力はdict
に似た関係になっているので、.keys
を使用する事で、パラメータ名を取り出すことができる。
par_names = args_pair.keys() print(par_names) # odict_keys(['x', 'sigma', 'mu', 'b']) print(type(par_names)) # <class 'odict_keys'>
.keys
で取り出せたらもうこっちのもの。tuple
やlist
に変換可能なのでスライスもできるようになる。
par_names_tpl = tuple(par_names) print(par_names_tpl) # ('x', 'sigma', 'mu', 'b') par_names_lst = list(par_names) print(par_names_lst) # ['x', 'sigma', 'mu', 'b']
ちなみに、.values
とするとパラメータの状態を出力することも可能。
par_values = args_pair.values() print(par_values) # odict_values([<Parameter "x">, <Parameter "sigma">, <Parameter "mu">, <Parameter "b">]) print(type(par_values)) # <class 'odict_values'>
パラメータ値と名前を対応づけ
gaussian_par = par_names_tpl[1:] par_dct = {} for name, val in zip(gaussian_par, par): par_dct[name] = val print(f"par_dct: {par_dct}") # par_dct: {'sigma': 0.9999710320855185, 'mu': 1.0002124931675611, 'b': 0.004859304255264688}
ということで、パラメータ名が手に入ったのでこれをパラメータの値と対応づければいい。今回はdict
を使った。
初めの[1:]
はパラメータ名にあるx
を省くためだ。これで当初の目的は達成。あっさり。
初期値とかargs
とかある関数で試す
inspect.signature
を使うことでパラメータ値とパラメータ名を結びつけることに成功した。
ここからは、色んな種類の関数にinspect.signature
を適用してどんな挙動を見せるのかを確認する。
パラメータ名と組み合わせを出力する関数
# 関数のパラメータ名を出力する関数 def func_parmeters(func): args_names = inspect.signature(func) args_pair = args_names.parameters print(args_names) print(args_pair)
の前にサクッと出力を確認できる関数を作成。この関数に、調べたい関数名を入れることでパラメータ名を出力可能。
print
だけ・返り値のある関数
# シンプルにprintするだけの関数 def print_func(a, b): print(a, b) func_parmeters(print_func) # (a, b) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">)])
まずはシンプルにprintだけの関数。この場合はガウシアンの時と同じようにパラメータ名が2種類取り出せる。
これは返り値だけの関数でも同様。
# 返り値のある関数 def return_func(a, b): return a, b func_parmeters(return_func) # (a, b) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">)])
途中に変数が入る関数
def middle(a, b): c = 10 print(a, b, c) func_parmeters(middle) # (a, b) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">)])
途中に変数が入ったとしても、ちゃんとパラメータ名が出力される。これは安心。
初期値がある関数
def print_func_ini(a, b, c=10): print(a, b, c) # 初期値もしっかり示してくれる func_parmeters(print_func_ini) # (a, b, c=10) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('c', <Parameter "c=10">)])
初期値がある関数でもパラメータ名を出力してくれるが、この場合はパラメータの状態に初期値情報が記載される。
ということで、型指定や色んな初期値を試してみる。全部うまくいく。いいね。
# 引数の種類を増やしてみた def print_func_ini2(a: list, b=[1, 2, 3], c=10, d=True, e='e', f={10, 20}): print(a, b, c, d, e, f) # 型指定だけでも反映される func_parmeters(print_func_ini2) # (a: list, b=[1, 2, 3], c=10, d=True, e='e', f={10, 20}) # OrderedDict([('a', <Parameter "a: list">), ('b', <Parameter "b=[1, 2, 3]">), ('c', <Parameter "c=10">), ('d', <Parameter "d=True">), ('e', <Parameter "e='e'">), ('f', <Parameter "f={10, 20}">)])
*args
のある関数
# argsのある関数 def print_func_args(*args, a, b): print(args, a, b) func_parmeters(print_func_args) # (*args, a, b) # OrderedDict([('args', <Parameter "*args">), ('a', <Parameter "a">), ('b', <Parameter "b">)])
可変長引数*args
がある場合、パラメータ名にはargs
だけが、その状態には*args
がつく。可変長引数については以下。
-
-
【python&関数化】defとかargsとかを使って関数を作成する
続きを見る
*args
の位置を変更
# argsを最後に持ってきた def print_func_args2(a, b, *args): print(args, a, b) func_parmeters(print_func_args2) # (a, b, *args) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('args', <Parameter "*args">)])
可変長引数を最初ではなく最後に持ってきても大丈夫。単に出力される位置が変わるだけ。これは*args
を真ん中に持ってきた時も同様。
# argsを真ん中に持ってきた def print_func_args3(a, *args, b): print(args, a, b) func_parmeters(print_func_args3) # (a, *args, b) # OrderedDict([('a', <Parameter "a">), ('args', <Parameter "*args">), ('b', <Parameter "b">)])
**kwargs
のある関数
# kwargsのある関数 def print_func_kwargs(a, b, **kwargs): print(a, b, kwargs) func_parmeters(print_func_kwargs) # (a, b, **kwargs) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('kwargs', <Parameter "**kwargs">)])
可変長引数**kwargs
も同じようにパラメータ名には*kwargs
、状態には**kwargs
と出力される。
**kwargs
の場合は引数の一番最後にしか書けないためこれで終わり。
args
のある関数で値と名前の対応づけ
args
やkwargs
がない関数でのパラメータ値とパラメータ名の対応づけは既に書いた通り。ではある場合はどうなるのか。
さっきと同じコードで対応づけをすると、args
ではうまくいかない。kwargs
ではうまくいく。
そもそも、args
とかを使って対応づけという状況が起きるかは分からんがとりあえずやってみる。
単純に対応づけするとズレる
# print_func_args関数のパラメータ名 args_pair = inspect.signature(print_func_args).parameters print(args_pair) # OrderedDict([('args', <Parameter "*args">), ('a', <Parameter "a">), ('b', <Parameter "b">)])
まずはパラメータ名を出力。これは今まで通り、というよりさっきやった。で、これにパラメータ値を対応づける。
今回は簡単に4種類のパラメータ値を用意した。前2つがargs
に、あとはa
, b
にそれぞれ対応するようにした。
# print_func_args関数にパラメータ値 # 10, 20がargs、3がa、4がbに対応 par = (10, 20, 3, 4)
で、ガウシアンの時と同じコードを使用して書いてみると、args
には1つしか要素が入らずにa
, b
がズレてしまう。
# 普通にdictに入れるとa=3とb=4がズレる par_dct = {} for name, val in zip(args_pair, par): par_dct[name] = val print(f"par_dct: {par_dct}") # par_dct: {'args': 10, 'a': 20, 'b': 3}
args
の部分をtuple
にする
# パラメータ名とパラメータ値の数 print(len(args_pair)) # 3 print(len(par)) # 4 # パラメータ名とパラメータ値の数の差分 diff = (len(par) - len(args_pair))
何が問題だったのかというと、args
に対応するパラメータが独立していたという点。ならtuple
とかで囲めばいい。
ということで、まずはargs
に対応するパラメータ数がいくつあるかを探るべく、パラメータ名と値の数を勘定して差分を取る。
次にパラメータの中でargs
がどのインデックスにあるかを検索。今回で言えば0
番目。
# argsが何番目にあるのか index = list(args_pair).index('args') print(index) # 0
続いて、par
はtuple
だがtuple
のままだとargs
に対応する要素をtuple
で囲めない。したがって、一旦list
に変換。
list
に変換後はargs
に対応する要素がどれかを抽出。今回で言えば10
と20
。
# tupleだと変更不可能なので一旦listに変換 par_lst = list(par) # argsの対象となる要素 print(par_lst[index: index + 1 + diff]) # [10, 20]
args
の対象となる要素をtuple
で囲めばいい。tuple
は「,
」がないと成立しないのでこの点には注意。
# 要素の変更 par_lst[index: index + 1 + diff] = (par[index: index + 1 + diff],) # tupleに戻す par = tuple(par_lst) print(par) # ((10, 20), 3, 4)
これでargs
部分を囲めたので、先程のコードを使用して対応づけるとうまくいく。
# argsをtupleにしたのでズレがなくなる par_dct = {} for name, val in zip(args_pair, par): par_dct[name] = val print(f"par_dct: {par_dct}") # par_dct: {'args': (10, 20), 'a': 3, 'b': 4}
kwargs
のある関数で値と名前の対応づけ
# print_func_args関数のパラメータ名 args_pair = inspect.signature(print_func_kwargs).parameters print(args_pair) # OrderedDict([('a', <Parameter "a">), ('b', <Parameter "b">), ('kwargs', <Parameter "**kwargs">)]) # print_func_args関数にパラメータ値 # kwargsの場合はdictにしないといけないので、元からdictに par = (1, 2, dict(A=3, B=4, C=5))
これもこんな状況が生まれるのか分からないけどやってみる。kwargs
の場合は一部がdictになるので元のコードでそのままいける。
# この方法なら初めからkwargs部分がdictで独立しているから大丈夫 par_dct = {} for name, val in zip(args_pair, par): par_dct[name] = val print(f"par_dct: {par_dct}") # par_dct: {'a': 1, 'b': 2, 'kwargs': {'A': 3, 'B': 4, 'C': 5}}
__code__
でも可能だけど面倒
def print_func(a, b, c=3): d = 1 print(a, b, c, d)
今回はinspectライブラリを使用したが、実は__code__
という書き方でも同じように関数の変数名を取得することができる。
しかし、この場合はローカル変数も反映されたりそもそもコードが長くなりやすいという点から個人的にはオススメしない。
# __code__はpythonドキュメント曰く # > 関数をコンパイルした バイトコード を格納するコードオブジェクト # とのこと(<https://docs.python.org/ja/3/library/inspect.html)> print_func_info = print_func.__code__ print(print_func_info) # <code object print_func at 0x10271d7c0, file "(ファイル名)", line (関数を定義した行番号)> # ローカル変数も入ってしまう print_variable_names = print_func_info.co_varnames print(print_variable_names) # ('a', 'b', 'c', 'd') # 引数の数は合ってる print_arg_nums = print_func_info.co_argcount print(print_arg_nums) # 3
__code__
については以下に詳しく書いてある。
一応、__code__
には色々派生系があるから、それを使う際には使えるかもしれないが、基本はinspect
でいい気がする。
fittingの時に使えそう
今回はinspect.signature
を使用して、関数の引数名を取得する方法を解説した。さらに、これを応用してパラメータ値と対応づける方法も解説した。
実際に対応づけるタイミングがあるかといえばあまりないかもしれない。しかし、curve_fit
でのfittingではパラメータが出力されるので、この時には活躍しそう。
ニッチな分野かもしれないが、知ってるだけで一歩くらいは先に進めるだろう。
関連記事
-
-
【python&表出力】tabulateモジュールで出力を表形式にする
続きを見る
-
-
【Python&Excel】openpyxlでフォルダ内の複数csv→1つのExcelへ
続きを見る
-
-
【python3&四捨五入】四捨五入すると1.5も2.5も2.0になる問題
続きを見る
-
-
【collections.Counter】配列の要素を要素名と個数で勝手に集計
続きを見る
-
-
【python&~】数値にチルダ(~)をつけると値が+1されて負の数になる(ビット反転)
続きを見る
-
-
【辞書の結合】dictのマージ
続きを見る
-
-
【辞書&pandas】dict{name: , val: {a: [~], b:[~]}}のpandas化
続きを見る
-
-
【PEP8&flake8】pythonにおけるPEP8とflake8
続きを見る