Pythonのライブラリを使って祝日の一覧を得る

最近のとあるタスクで、祝日の一覧が必要なことがありました。まぁ、たかだか1年分くらいの祝日であれば検索して出てきたカレンダーとかから転記しても良いですし、今の時代ならChatGPTに頼めばPythonで使える配列で実装してくれるでしょう。

ただ、Pythonのライブラリで専用のものがあることもわかり、使い方を確認したので記録しておきます。

使用するライブラリは workalendar です。ドキュメントはこちら

これは世界中の国の祝祭日を扱えるライブラリで、その中で日本の祝祭日も一覧を得たり判定したりできます。

ドキュメントの Basic Usageのページにフランスの事例が載っているのでそれを真似しながら日本の祝日の一覧を取ってみましょう。年を指定して実行しますが2025年を使います。

from workalendar.asia import Japan


cal = Japan()
cal.holidays(2025)
"""
[(datetime.date(2025, 1, 1), 'New year'),
 (datetime.date(2025, 1, 13), 'Coming of Age Day'),
 (datetime.date(2025, 2, 11), 'Foundation Day'),
 (datetime.date(2025, 2, 23), "The Emperor's Birthday"),
 (datetime.date(2025, 3, 20), 'Vernal Equinox Day'),
 (datetime.date(2025, 4, 29), 'Showa Day'),
 (datetime.date(2025, 5, 3), 'Constitution Memorial Day'),
 (datetime.date(2025, 5, 4), 'Greenery Day'),
 (datetime.date(2025, 5, 5), "Children's Day"),
 (datetime.date(2025, 7, 21), 'Marine Day'),
 (datetime.date(2025, 8, 11), 'Mountain Day'),
 (datetime.date(2025, 9, 15), 'Respect-for-the-Aged Day'),
 (datetime.date(2025, 9, 23), 'Autumnal Equinox Day'),
 (datetime.date(2025, 10, 13), 'Sports Day'),
 (datetime.date(2025, 11, 3), 'Culture Day'),
 (datetime.date(2025, 11, 23), 'Labour Thanksgiving Day')]
"""

このように一覧が簡単に出力できます。

ただ、注意しないといけないのは振替休日に対応してなさそうな点です。2025/02/23 は上記の結果の通り天皇誕生日ですが、日曜日なので2025/02/24が振替休日でした。しかしこれはリストには入っていません。

次に、これを使うとある日が平日(営業日)なのかのどうかの判定ができます。これはis_working_day()メソッドを使います。

日曜でも祝日でもなければTrueが帰ってきます。

from datetime import datetime
from datetime import timedelta


start_day = datetime(2025, 2, 10)
for i in range(15):
    target_day = start_day + timedelta(days=i)
    if cal.is_working_day(target_day):
        print(f"{target_day} は平日です。")
    else:
        print(f"{target_day} は平日ではありません。")
"""
2025-02-10 00:00:00 は平日です。
2025-02-11 00:00:00 は平日ではありません。
2025-02-12 00:00:00 は平日です。
2025-02-13 00:00:00 は平日です。
2025-02-14 00:00:00 は平日です。
2025-02-15 00:00:00 は平日ではありません。
2025-02-16 00:00:00 は平日ではありません。
2025-02-17 00:00:00 は平日です。
2025-02-18 00:00:00 は平日です。
2025-02-19 00:00:00 は平日です。
2025-02-20 00:00:00 は平日です。
2025-02-21 00:00:00 は平日です。
2025-02-22 00:00:00 は平日ではありません。
2025-02-23 00:00:00 は平日ではありません。
2025-02-24 00:00:00 は平日です。
"""

上記の通り、建国記念日(2025/02/11)や土日(2/15, 2/16, 2/22, 2/23) はFalseが帰ってきたので平日ではないという結果になっていますね。

ただ、やっぱり振替休日に対応していないので2025/2/24は判定をミスっています。

このほか、営業日のみカウントして特定の日付からn日後の日を求めるといったこともできます。

start_date = datetime(2025, 4, 25)
n_days = 3

workday = cal.add_working_days(start_date, n_days)
print(workday)  # 2025-05-01

4月26,27日は土日で、29日は昭和の日なので、3営業日後の5月1日が得られましたね。

振り返り休日に対応してないってのと、祝祭日に法的な変更が入ったらこまめにバージョンを上げていかないといけないというデメリットはありますが、それ以外では小回りのきくメソッドをいろいろ持っていますので、興味のある方は一度ドキュメントを読んでみてください。

pyvisで可視化したネットワークにおいてポップアップでテキストを表示する

以前紹介したpyvisの話です。
参考: pyvisでネットワーク可視化

最近久しぶりに使ったのですが、その時はそこそこ長いテキストをノードし、類似度によってエッジを貼るようなグラフを構築し可視化しました。

ノードに全文表示すると長すぎて視認性が全くなくなったので、ラベルにテキストのidや序盤の数文字を表示させていて、全文は別の場所で参照する、とやっていたのですがこれが非常に不便でした。

そこで、何かしら対応策ないかなと思っていたのですが、マウスオーバーした際にポップアップで表示するテキストを別途設定できることがわかりました。

設定自体も非常に簡単で、title という属性を指定するだけでした。

参考: Documentation — pyvis 0.1.3.1 documentation

これはノードだけでなくエッジに対しても設定できます。やってみましょう。

from pyvis.network import Network


# ネットワークのインスタンス生成
network = Network(
    notebook=True,  # これをTrueにしておくとjupyter上で結果が見れる
    cdn_resources='in_line',
    bgcolor='#ffffff',  # 背景色。デフォルト "#ffffff"
)

network.add_node(n_id=1, label="ラベル1", shape="box", title="ポップアップさせたいテキスト1")
network.add_node(n_id=2, label="ラベル2", shape="circle", title="ポップアップさせたいテキスト2")
network.add_edge(1, 2, label="辺のラベル", title="辺のポップアップ")

network.show("pyvis_sample1.html")

これで作成されたネットワーク図は、ノードやエッジにマウスオーバーした際にポップアップが表示されます。

簡単で便利ですね。

NetworkXのグラフから連結成分や孤立ノードを取り出す

久々にNetworkXを使ったのですが、その時使った関数のメモです。

とあるノードの集合に対して、ある条件を満たすペアに対してエッジを張って行き、その結果出来上がった非連結なグラフから連結成分たちを取り出すという操作をやりました。

使った関数をこのブログでまだ紹介していなかったのでその周辺の操作も含めて紹介します。

とりあえず、サンプルのグラフを準備しておきましょう。連結成分が3組と、孤立ノードが2個ある次のようなグラフを作りました。

import networkx as nx


G = nx.Graph()
G.add_edges_from([
    (1, 2), (2, 3), (3, 1),  # コンポーネント1
    (4, 5), (5, 6),  # コンポーネント2
    (7, 8),  # コンポーネント3
])
G.add_nodes_from([9, 10])  # 孤立ノードを2個追加

見ての通り、(孤立してるのも含めて)連結成分が5組ありますね。

ここから連結している成分を取り出すには、nx.connected_components() というメソッドを使います。
参考: connected_components — NetworkX 3.4.2 documentation

結果がイテレーターで帰ってくるのですが今回の例は小さいのでlistにしておきましょう。

connected_components = list(nx.connected_components(G))
print(connected_components)
"""
[{1, 2, 3}, {4, 5, 6}, {8, 7}, {9}, {10}]
"""

はい、5組がそれぞれ出てきました。

孤立成分を除きたい場合は、この中からノードが2個以上あるものに絞り込むと良いでしょう。
print([c for c in connected_components if len(c) > 1])
# [{1, 2, 3}, {4, 5, 6}, {8, 7}]

逆に、ノードが1個のものに絞ると孤立成分が抽出できます。しかし、孤立成分を探す場合は、nx.isolates() という専用メソッドもあります。
参考: isolates — NetworkX 3.4.2 documentation

print(list(nx.isolates(G)))
# [9, 10]

また、連結成分の数や孤立成分の数を返す関数として、number_connected_components, number_of_isolates がそれぞれ用意されています。

print(nx.number_connected_components(G))
# 5
print(nx.number_of_isolates(G))
# 2

最後に、このノードが含まれてる連結成分が欲しいんだ、というケースもあると思います。その場合は、node_connected_component()を使います。
参考: node_connected_component — NetworkX 3.4.2 documentation

print(nx.node_connected_component(G, 4))
# {4, 5, 6}

帰ってきているのはそのノードを含む連結成分のノードの一覧(set)であって、サブグラフオブジェクトではないのでその点は注意してください。これは先ほどの連結成分の一覧を返してきてたメソッドと同様です。

2024年のまとめ

今年も1年間お疲れ様でした。あっという間の1年間でしたね。

この1年間はデータサイエンティストとして、そして技術系のブロガーとしては激動の1年間だったと思います。なんといってもLLMの発展の影響が色々なところに出てきました。そしてプライベートでも色々イベントの多い1年でした。

このブログのこと

このブログに関しては(少し遅れた日もありますが記事数は)なんとか目標通り週1本のペースを維持することができ、この記事で今年53本目になります。合計では671本になりました。

ただしその一方で、LLMの影響か?とも考えているのですがアクセス数は徐々に下がっています。自分の学習や調べ物を考慮しても、最初からAIに聞きそれを元に公式ドキュメントをあたる対応をとることが増え、他の人の記事を参照して調べるということがかなり減りました。
そして、おそらくそれは他の人たちも同じで、何か調べてこのブログがヒットするという機会が減っているのかなとも思います。

また、データサイエンティストとか機械学習とか統計学等々のブームはLLMを除いて落ち着いてきて、新規に流入してくる人も減ったのかなぁと感じています。LLMはほとんどの人にとっては、他社が開発したサービスを利用するだけですからね。

そういったわけで、アクセスいただいたユーザー数などのこのブログの指標は昨年の半分程度に落ちており、しかも年始から年末にかけて下降トレンドなので来年はもっと下がる見通しです。

僕個人としてもアクセス数などの指標をモチベーションにするのは避けるようになってきました。というのもそれらの指標を気にしているとダイレクトにモチベーションが下がってしまって記事の質が下がったり更新が遅れたりといったことにつながってしまっているからです。

記事を書くことが自分のスキルアップにつながっている感覚はあり、直近の転職ではこのブログの存在にも大いに助けられた感覚はあるのでなんらかの形で続けたいのですがなかなか難しいところですね。

このブログ以外の発信について

さて、このブログは低調ですが、この1年間はnoteにも月1本の記事を投稿してきました。こちらも年間12本の目標達成です。noteの方は会社の看板も背負ってますし業務に関することを書きやすいということもあり結構実践に踏み込んだ記事を揃えることができたと思います。

とはいえ、このブログが4~5本、noteが1本という更新はなかなかしんどかったですね。

項目反応理論のよりテクニカルな話をもっと書きたいと思ってはいるのですがそれはこのブログになるのかnoteになるのかは未定です。

お仕事の話

昨年転職して現職について1年半ほど経過しました。教育関係のデータサイエンティストとしてだいぶ仕事が板に付いてきたと思えます。データ基盤の各種ツールが手に馴染んてきたこと、人間関係がしっかりできてきたこと、メインウェポンの項目反応理論が思いのほか面白くて学んでいて楽しいことなどが良い要因です。

仕事に関しては、来年早々の共通テストが正念場なので気を引き締めていきたいです。

私生活の話

趣味でポーカーをやっているのですが、とある大会で順調に勝ち進んで韓国で行われた決勝戦に招待していただけたのが今年のハイライトでした。

その他、マーダーミステリーを数十本やったり街歩きの謎解きに参加したり、ボードゲームで遊んだり、美術館や博物館に行くようになったりと趣味が広がる1年間でした。

この調子で来年も楽しんでいきたいと思います。

来年に向けて

来年のこのブログの更新ですが、更新ペースに関して目標を設定するのは一旦やめようと思っています。生成AIの発展により個人が運営している技術ブログというものの存在価値もだいぶ下がっていると思いますし、実際ニーズがなくなっている実感があるからです。

とはいえ何かしらの更新はしようと思うので月1本くらいは何か記事書けたらいいなと思ってはいます。noteと合わせて月に2本は何かしらの発信がある計算です。

今までのような小ネタを書いてもいいですし、それこそキャリア系の著名な方々が書いてるようなキャリア系のポエムや読み物記事を書いても良いかもしれません。

キャリア論的なことを語りだすと自分の成長が止まるような気がしてこれまで書いてこなかったのですが、自分もそろそろ40代に突入するような年になっています。自分の成長だけ優先してるわけにもいかないので後進の方々にとって何かしら有意義な発信ができたらいいなと思います。特に前職では採用等もやっていて1000人近くの候補者を選考してたりしますし、メンバー育成等も担当していたのでその辺の経験からも何か書ければと。

それではみなさま、今年も1年間ありがとうございました。また来年もよろしくお願いいたします。

MySQLのデータのダンプとリストア

タイトルはMySQLですが、実際には AWS RDSのAurora (MySQL)を想定しています。

Aurora Serverless v2 への移行も見据えて、MySQLのデータの移行方法を調べました。dumpをとる専用のコマンドがあり、それでバックアップを取得してそれを戻せば良いようです。

ドキュメントはこちらになります。
参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 4.5.4 mysqldump — データベースバックアッププログラム

ドキュメントの一番下の方に例として一番シンプルなコマンドが載っています。

# データベース全体のバックアップを作成
shell> mysqldump db_name > backup-file.sql
# ダンプファイルをサーバーにロード
shell> mysql db_name < backup-file.sql

ただし、これはそのローカルサーバーにDBMSがあってパスワード等もかかってない場合に使うものなので、現実的にはDBのエンドポイント等を指定する必要があります。

# バックアップ
$ mysqldump -h <エンドポイント> -u <ユーザー名> -p --databases <データベース名> > aurora_backup.sql
# リストア
$ mysql -h <エンドポイント> -u <ユーザー名> -p < aurora_backup.sql

ダンプするときにデータベース名はスペース区切りで複数まとめて指定することもできます。

一方で、ダンプファイルにDB名の情報は含まれているので、リストアする時はDB名の指定は不要です。

–all-databases という全てのデータベースを対象とするオプションもありますが、システム情報的なDBまで含まれてしまうのでこれは使わず、自分で必要なDBを指定した方が安全だと思います。バージョンが違うDB間で移行するような時は特に注意が必要です。

最後に、ダンプを取るときに特にオプション等でタイムゾーンに関する設定をしなければ、datatime型のデータはタイムゾーンの情報を持たずにダンプされてしまいます。

移行前後のDB間でタイムゾーンが違うと異なる時間で解釈されるリスク等もあるので、先に設定を揃えておきましょう。

Amazon Aurora Serverless v2 がAUCの最小値を0に設定できるようになりました

Aurora Serverless v1のサポート終了が近づいてきた(が今では少しだけ延長されていますが)昨今ですが、v2 で遂に待ちに待った機能が追加されました。それがスケーリングの最小値を0にできるというものです。

リリース: Amazon Aurora Serverless v2 がゼロキャパシティへのスケーリングをサポート – AWS

以前の記事で Aurora Serveless v2 を検証したとき、キャパシティを0にできないことで僕は導入を断念してたんですよね。

参考: Amazon Aurora Serverless v2が出たので使ってみた

注意点としては、サポートされているDBのバージョンが少し限定されている点だけでしょうか。

引用すると、
0 ACU は Aurora PostgreSQL 13.15 以上、14.12 以上、15.7 以上、16.3 以上、Aurora MySQL 3.08 以上でサポートされています。
とあります。

MySQLは3.08以上なので今後新しいバージョンが出たらそれは順次対応していそうですね。

Aurora Serverless v2 で新規のデータベースを作成する画面に進むと確かに AUCのキャパシティが 0〜設定できるようになっていました!
これで僕らのような個人利用しているユーザーも本格的に v2に移行できますね。

Streamlit in SnowflakeにおけるSQLの結果取得について

今年から使いはじめた Streamlit in Snowflake についての記事です。

とはいえ、Sreamlit要素はほぼなく、Snowflake上で使うからこそ発生するSQLの結果の取得方法(=pandasのDataFrameとしての取得方法)をまとめておきます。

Streamlit in Snowflake の場合、Snowflkaeにログインして使いますので認証情報として現時点でログインしているセッションの情報が使えます。それを取得する専用の関数として、get_active_session があるので、これを呼び出すのが最初の準備です。

from snowflake.snowpark.context import get_active_session


# Snowflakeセッションの取得
session = get_active_session()

ここから下は 「query」という変数にSQL(SELECT)文が格納されている前提になります。

sessionの sql()というメソッドでSQLを発行し、結果を得ることができます。さらに、その結果は to_pandas() というメソッドを持っており、これを使うことでDataFrame型に変換できます。

結果を表示したい場合は streamlitの dataframeとかtableといったメソッドが使えますね。

# クエリの実行と結果の取得
result = session.sql(query).to_pandas()
st.dataframe(result)
st.table(result)

続いて、 SELECT文ではなく show columns や describe table のケースも紹介します。

こちらは sqlメソッドで発行できるのは同じなのですが、結果が to_padnas()メソッドを持っていません。

ここでは、collect() というメソッドを使います。

result = session.sql(query).collect()
st.dataframe(result)

簡潔ですが以上で Streamlit in SnowflakeでSQLを発行し結果をStreamlit内で使えるようになります。

Snowflakeで配列の値やカンマ区切り等の複合値を複数行に展開する方法

要するに第1正規化ができてないデータがあるときにそれを複数行に展開する方法です。

a,b,c みたいな文字列がデータにあるときにa, b, c をそれぞれ別行のデータに展開します。

これには lateral flatten という構文を使います。参考になるドキュメントはこちらです。
参考: FLATTEN | Snowflake Documentation

元々のデータが配列型の場合は split が不要になるのでその分を読み替えてください。
最初のwith句でダミーデータを生成して実験しています。

from 句で指定してるテーブルの後、joinで複数テーブルを結合している場合はそれらの一番最後にカンマを打ってから使う点に注意してください。また、 inputとか values とかも決まった単語ですのでそのまま使います。

with
dummy_data as (
    select
        1 as id,
        'a,b,c' as text
    union all select
        2 as id,
        None as text
    union all select
        3 as id,
        'd' as text
)

select
    id,
    value::varchar as text
from
    dummy_data, lateral flatten(input => split(text, ','))

これで、
id, text
1, a
1, b
1, c
3, d
という結果が得られます。

さて、id = 2のレコードについてはtextの値がNullだったので結果に出てきませんでした。
これがNullではなく空白文字列であれば、id=3のレコードと同様に行は作られたのですけどね。

このように展開する値がNullのレコードも残したい場合は、 outerオプションを使います。= でではなく => で trueに指定するSQLでは見慣れない記法ですが、次のような形になります。

with
dummy_data as (
    select
        1 as id,
        'a,b,c' as text
    union all select
        2 as id,
        Null as text
    union all select
        3 as id,
        'd' as text
)

select
    id,
    value::varchar as text
from
    dummy_data, lateral flatten(input => split(text, ','), outer => true)

そもそも正規化がちゃんと行われているデータだけ使えばいいのであれば滅多に使わない記法なのですが、残念ながら自分はよく使う場面が頻繁にあることと、その割になかなか覚えられなくて毎回調べているので今回記事にしました。

Scipyのminimizeで関数の最小値を探すときに探索範囲を制限する方法

minimize関数の使い方の記事2本目です。

前回の記事で基本的な使い方を紹介しましたが、それは探索範囲に特に制限を設けず、各変数について実数値全体から探索するものでした。

しかし、現実の問題では特定の範囲に絞って探索したいってこともよくあります。一番よくあるのは特定の変数は正の値に絞るってケースでしょうか。また、各値がそれぞれの範囲内に絞られる、長方形や直方体系の制限だけでなく、円の内側のような指定も可能です。順番に見ていきましょう。

まずは、bounds 引数を使った境界制約です。
これは、単純に各変数の探索範囲にそれぞれ下限上限を指定できます。下限だけ指定したいとか上限だけ指定したい、といった場合はnp.inf で無限大を利用してください。

次のように、bounds引数に各変数の下限上限の配列を渡します。境界を指定しなければ最小値がない関数(ただの2変数の和)で実験してみましょう。

import numpy as np
from scipy.optimize import minimize


# 目的関数の定義
def sample_function(x):
    return x[0]+x[1]


# 初期値
x0 = [0, 0]

# 最適化の実行
result = minimize(sample_function, x0, method='L-BFGS-B', bounds=[[-2, 5], [3, np.inf]])

# 結果の表示
print("最適化の結果:", result)
print("最小値をとる点 x:", result.x)
print("最小値 f(x):", result.fun)
"""
最適化の結果:   message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
  success: True
   status: 0
      fun: 1.0
        x: [-2.000e+00  3.000e+00]
      nit: 2
      jac: [ 1.000e+00  1.000e+00]
     nfev: 9
     njev: 3
 hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>
最小値をとる点 x: [-2.  3.]
最小値 f(x): 1.0
"""

想定通り、それぞれの変数が最小値だったときに和も最小値でしたね。

methodは例として’L-BFGS-B’を使いましたが、boundsを指定できるmethodは数種類に限られます。もし何かしらエラーが発生したらドキュメントを確認して対応しているか確かめましょう。これは次の方法でも同じです。

次は、不等式を使って領域を絞り込む方法です。
これは制約を課す関数を定義し、その関数が「正の値」をとる範囲で探索します。

例えば、中心が(5, 3) 半径が2 の円の内側だけ探索するようにしてみましょう。
constraints引数に渡す方法はちょっとクセがあります。

# 目的関数の定義
def sample_function(x):
    return x[0]+x[1]


# 不等式制約
def constraint(x):
    return 4 - (x[0]-5)**2 - (x[1]-3)**2

inequality_constraint = {'type': 'ineq', 'fun': constraint}

# 初期値
x0 = [5, 3]

# 最適化の実行
result = minimize(sample_function, x0, method='SLSQP', constraints=inequality_constraint)

# 結果の表示
print("最適化の結果:", result)
print("最小値をとる点 x:", result.x)
print("最小値 f(x):", result.fun)
"""
最適化の結果:  message: Optimization terminated successfully
 success: True
  status: 0
     fun: 5.171572875250672
       x: [ 3.586e+00  1.586e+00]
     nit: 6
     jac: [ 1.000e+00  1.000e+00]
    nfev: 18
    njev: 6
最小値をとる点 x: [3.58578644 1.58578644]
最小値 f(x): 5.171572875250672
"""

method では今までと違って、 SLSQP を使いました。というのも、COBYLA, COBYQA, SLSQP  でしかサポートされてないのです。この辺りも気をつけて使う必要がありますね。

SciPyの optimize.minimizeを使って関数が最小値を取る点を探す

最近よく使っている、scipyの最適化関数の一つであるminimizeについて、まだ記事を書いてなかったので紹介します。

公式ドキュメントはこちらです。
参考: minimize — SciPy v1.14.1 Manual

これはタイトルの通りで、数値を返す関数を渡すとその関数が最小値をとる引数を探してくれるものです。ちなみに、最大値になる引数を探すメソッドはないので最大値を探したかったら、その関数に-1をかけて符号を反転させた関数を用意してください。

サクッと一つやってみましょう。正解がわかりやすいよう、2次関数でも例にとりましょうか。次の関数を使います。

$$x^2-6x+5 = (x-3)^2 -4 $$

平方完成から分かる通り、$x=3$で最小ですね。

import numpy as np
from scipy.optimize import minimize

# 目的関数の定義
def sample_function(x):
    return x**2 - 6*x + 5

# 初期値
x0 = 0

# 最適化の実行
result = minimize(sample_function, x0, method='L-BFGS-B')

# 結果の表示
print("最適化の結果:", result)
print("最小値をとる点 x:", result.x)
print("最小値 f(x):", result.fun)
"""
最適化の結果:   message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
  success: True
   status: 0
      fun: -3.9999999999999982
        x: [ 3.000e+00]
      nit: 2
      jac: [ 1.776e-07]
     nfev: 6
     njev: 3
 hess_inv: <1x1 LbfgsInvHessProduct with dtype=float64>
最小値をとる点 x: [3.00000004]
最小値 f(x): -3.9999999999999982
"""

上のサンプルコードのように、最初の引数で最適化したい関数、2個目の引数で初期値、そしてオプションで利用するアルゴリズムなどの各種設定を行います。

ここで注意しないといけないのは、渡した関数の第1引数だけが最適化の対象ということです。なので、元の関数が複数の引数をとる多変数関数の場合、最適化したい引数等を一個の配列にまとめて渡す関数でラップしてあげる必要があります。また、最適化対象外の値を固定する引数については、argsで固定します。

例えば、$f(x, y, a, b) = x^2 + ax + y^2 + by$ みたいな関数があって、a, bは固定したとき、これを最小にする $x, y$を求めたい場合次のようにします。

# 元の関数(オリジナルの目的関数)
def original_function(x, y, a, b):
    return x**2 + a*x + y**2 + b*y


# 最適化用にラップする関数
# 元の関数のx, y を xという配列で渡してx[0], x[1]として内部で使っている
def optimization_target_function(x, *params):
    return original_function(x[0], x[1], *params)


# 最適化の実行
x0 = [0, 0]
result = minimize(optimization_target_function, x0, args=(4, -6), method='L-BFGS-B')

# 結果の表示
print("最適化の結果:", result)
print("最小値をとる点 x:", result.x[0])
print("最小値をとる点 y:", result.x[1])
print("最小値 f(x, y):", result.fun)

"""
最適化の結果:   message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
  success: True
   status: 0
      fun: -12.999999999999925
        x: [-2.000e+00  3.000e+00]
      nit: 2
      jac: [-1.776e-07 -7.105e-07]
     nfev: 9
     njev: 3
 hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>
最小値をとる点 x: -2.0000000804663682
最小値をとる点 y: 2.9999997404723753
最小値 f(x, y): -12.999999999999925
"""

最適化したい引数が何変数なのか?ってのを一見どこでも指定してないように見えて不安になるのですが、これは初期値の配列の要素数から自動的に判断してくれています。

結果は result.x に入っているのでここから自分で取得します。

本当に基本的な使い方は以上になります。

少し発展的な内容なのですが、最適化のアルゴリズムは何種類もある中から選べますが、中には導関数を利用するものもあります。このminimizeに導関数を渡さない場合は、数値微分でいい感じにやってくれるのですが、明示的に導関数を指定することも可能です。

やり方は簡単で、元の関数と同じ形で引数を受け取るように導関数を定義して、jac引数に渡すだけです。1変数の場合は簡単すぎるので2変数の例を出しますが、2変数の場合は第1引数による微分と第2引数による微分の2つがあるので、その結果を配列で返す関数を定義してそれを渡します。

例えば、$f(x,y)=x^2+y^2+4x+6y+13$ みたいなのを考えてみましょう。$x, y$での微分はそれぞれ、$2x+4$, $2y+6$ですね。

# 目的関数
def objective_function(vars):
    """
    2変数関数 f(x, y) = x^2 + y^2 + 4x + 6y + 13
    vars: [x, y]
    """
    x, y = vars
    return x**2 + y**2 + 4*x + 6*y + 13

# 勾配(偏微分値)
def gradient_function(vars):
    """
    勾配 ∇f(x, y) = [∂f/∂x, ∂f/∂y]
    vars: [x, y]
    """
    x, y = vars
    grad_x = 2 * x + 4
    grad_y = 2 * y + 6
    return np.array([grad_x, grad_y])  # 勾配ベクトルを返す

# 初期値
x0 = [0, 0]

# 最適化の実行 (勾配を指定)
result = minimize(objective_function, x0, jac=gradient_function, method='L-BFGS-B')

# 結果の表示
print("最適化の結果:", result)
print("最小値をとる点 [x, y]:", result.x)
print("最小値 f(x, y):", result.fun)

"""
最適化の結果:   message: CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL
  success: True
   status: 0
      fun: 0.0
        x: [-2.000e+00 -3.000e+00]
      nit: 2
      jac: [ 0.000e+00  0.000e+00]
     nfev: 3
     njev: 3
 hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>
最小値をとる点 [x, y]: [-2. -3.]
最小値 f(x, y): 0.0
"""

関数が簡単で計算量も小さい場合は数値微分でも特に問題ないのですが、そうでない場合は明示的に導関数を渡すことでリソースを節約することもできます。

長くなってきたので今回の記事はここまでにしようと思います。境界条件とうの制約をつける方法とかを今後紹介したいですね。

最後にminimizeを使う時の注意点ですが、これ、極小値が複数あるような関数の場合、最小値ではなく極小値の一つを返してくることがあります。結果が初期値に依存してしまうのです。複雑な形の関数を最適化する場合は注意してください。