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

boto3を使ってEC2のCPUクレジットを取得する

EC2を使っていて、動作が重いなーって感じた時にCPUクレジットの状況を確認することがあります。AWSのコンソールにログインしたら見れはするのですが2要素認証も使っていてやや面倒なので、boto3で確認する方法を調べました。
(※ 個人利用しているAWSの話です。普段jupyterlabで触っていて何かしらの作業をインタラクティブに行っています。業務におけるWebサービスの運用等であればそのインスタンスにログインするのもコンソールに入るのも手間は変わらないと思います。)

僕がクラウドウォッチに詳しくなくて、あまり詳細な説明ができないというのもあるのですが、いろいろ試した結果、以下のコードで取得できることが確認できたのでそのまま掲載します。

import boto3
import requests
import datetime


# EC2メタデータからインスタンスIDを取得
def get_instance_id():
    url = "http://169.254.169.254/latest/meta-data/instance-id"
    response = requests.get(url)
    return response.text

# インスタンスIDを取得
instance_id = get_instance_id()

# CloudWatch クライアントを作成
cloudwatch = boto3.client('cloudwatch')

# 現在の UTC 時刻を取得
end_time = datetime.datetime.utcnow()
# 過去 5 分間のデータを取得
start_time = end_time - datetime.timedelta(minutes=5)

# CPUCreditBalance メトリクスを取得
response = cloudwatch.get_metric_statistics(
    Namespace='AWS/EC2',
    MetricName='CPUCreditBalance',
    Dimensions=[
        {
            'Name': 'InstanceId',
            'Value': instance_id
        },
    ],
    StartTime=start_time,
    EndTime=end_time,
    Period=300,  # 5 分間隔
    Statistics=['Average']
)

# データポイントを取得
data_points = response['Datapoints']
if data_points:
    # 最新のデータポイントを取得
    cpu_credit_balance = data_points[0]['Average']
    print(f'インスタンスID: {instance_id}')
    print(f'CPUクレジット残高: {cpu_credit_balance}')
else:
    print('CPUクレジット残高のデータが見つかりませんでした。')

これで現在のCPUクレジット残高が取得できます。
CPUクレジッド残高はインスタンスサイズによって初期値や上限値が異なるので、別途最大値を確認しておくと参考になると思います。

参考: バーストパフォーマンスインスタンスに関する主要な概念 – Amazon Elastic Compute Cloud

EC2の内部からそのインスタンスのidなどのメタデータを取得する

EC2で作業をしていて今使っているそのインスタンスのidをコードで取得したい、った場面があったのでその方法を紹介します。

最初はてっきり、環境変数か何かに入っててそこから使えるだろう、とか思っていたのですが予想に反する場所にメタデータの格納場所がありました。

インスタンスidなどのメタデータにはインスタンス内部から特定のURLにアクセスするとで取得できます。

この辺りが参考になります。
EC2 インスタンスのインスタンスメタデータにアクセスする – Amazon Elastic Compute Cloud
インスタンスメタデータを使用して EC2 インスタンスを管理する – Amazon Elastic Compute Cloud

アクセスするURLは以下です。
http://169.254.169.254/latest/meta-data/
これは情報を取得したいインスタンス内部からしかアクセスできないので気をつけてください。同じAWSアカウントであっても別のインスタンスの情報は取れないので取得したい場合はboto3などの別の手段を使う必要があります。

実際にcurlでアクセスすると、次のような情報が返ってきます。

!curl http://169.254.169.254/latest/meta-data/
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hostname
iam/
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
services/
system

これらは取得可能な情報の一覧ですね。
/で終わってるやつはまだ配下に階層が続くのでそれを手がかりに深掘りしていくことになりますが、instance-id は直下にあるようです。先ほどのURLに取得したい情報のpathを追加して実行すると欲しい情報が取れます。

!curl http://169.254.169.254/latest/meta-data/instance-id
i-*****************

Pythonでやる場合は次のようにして簡単に取得できます。

import requests


metadata_url = "http://169.254.169.254/latest/meta-data/instance-id"
print(requests.get(metadata_url).text)

これでそのコードが動いているインスタンスのidが取得できました。
boto3等を使ったコードで、実行しているインスタンス自身に対して何かやりたいのでそのインスタンスのidの指定が必要、って場合に同一のコードを使いまわせるので便利になります。

Pythonコードでimportに失敗するライブラリのバージョンを確認する

とある特殊な環境でPythonを書いていて、いくつかのライブラリがimportに失敗するという事態に遭遇しました。自分のローカルPC上であれば、pip freeze とかしてライブラリのバージョンを調べて原因を調査するのですが、その環境ではOSコマンドが打てず、同様の調査が不可能でした。importさえできれば {ライブラリ名}.__version__ みたいなプロパティから取得することもできたのですがimport自体が失敗するとあって調査に苦戦していました。

ところがどうやらimportを行わずにライブラリのメタデータにアクセスする方法がちゃんとあるようだったのでこの記事にまとめておきます。(正直、普通の環境であればpipで調べれれば済む話なので、ほとんどの人にとっては不要な知識だと思います。)

pkg_resources を使う方法

importせずにライブラリのバージョンを取得する方法の1個目は pkg_resources を使うものです。

次の例は、 arviz というライブラリのバージョンを調べたものです。

from pkg_resources import get_distribution


try:
    version = get_distribution("arviz").version
    print(f"version: {version}")
except pkg_resources.DistributionNotFound:
    print("ライブラリが見つかりません")

# version: 0.16.1

僕は上記の方法で一旦解決しました。

ただ、これはsetuptoolsに依存したライブラリなのですが、その公式ドキュメントを見ると、もう廃止されたからimportlib.metadataを使えと書いてあるのですよね。

ということで合わせてそちらを紹介します。

importlib を使う方法

importlib はPython3.8から標準ライブラリに含まれたライブラリです。

これはインストール済みのライブラリのメタデータにアクセスする機能を持っています。標準ライブラリになったわけなので、最近のPythonであればおそらくこちらを使うのが適切なんだと思います。

参考: importlib.metadata — パッケージメタデータへのアクセス — Python 3.13.0 ドキュメント

サンプルコードでもバージョンを取得していますね。それにならってやってみましょう。

import importlib.metadata


try:
    version = importlib.metadata.version("arviz")
    print(f"version: {version}")
except importlib.metadata.PackageNotFoundError:
    print("ライブラリが見つかりません")

# version: 0.16.1

同じような結果が得られました。

余談ですが、公式ドキュメントのサンプルコードでは、
from importlib.metadata import version
として versionという関数をインポートしています。僕も最初それに倣ってやったのですが、versionを結果の変数名で使いたいな、と思ったので上記のインポート方法にしました。
ただ、これはこれでイマイチな気もします。