物理の駅 Physics station by 現役研究者

テクノロジーは共有されてこそ栄える

2次元の正規分布を描画、信頼区間の設定、任意の楕円領域で確率密度を積分

まずは2次元の正規分布を描画する。簡便のために対角成分以外は0とする。

import numpy as np
from scipy.stats import multivariate_normal
import matplotlib.pyplot as plt

np.random.seed(2)

mu_x,mu_y = 5,10
sigma_x,sigma_y = 1,5
cov = np.array([[1, 0], [0, 1]])  # 共分散行列

n_samples = 100000
samples = np.random.multivariate_normal(np.array([0, 0]), cov, n_samples)

x = samples.T[0]*sigma_x+mu_x
y = samples.T[1]*sigma_y+mu_y

fig = plt.figure(figsize=(6, 6))
grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2)

main_ax = fig.add_subplot(grid[1:4, 0:3])
main_ax.scatter(x, y, alpha=0.5, s=1)
xlim = main_ax.get_xlim()
ylim = main_ax.get_ylim()

x_hist = fig.add_subplot(grid[0, 0:3], sharex=main_ax)
x_hist.hist(x, bins=40, color='tab:gray', range=xlim)

y_hist = fig.add_subplot(grid[1:4, 3], sharey=main_ax)
y_hist.hist(y, bins=40, orientation='horizontal', color='tab:gray', range=ylim)

main_ax.set_xlabel('X')
main_ax.set_ylabel('Y')
plt.show()

この正規分布における、90%信頼区間はどう引けばよいだろうか?

対角成分のみの2次元正規分布の中心からのXY成分の標準偏差で規格化した距離の平方がカイ二乗分布に従うことより、自由度2のカイ二乗分布のp値が0.1になるときのカイ二乗の平方根をとればよいと分かる。つまり sigma * chi2.isf(0.1,2) ** 0.5 である。99% C.L.であれば、sigma * chi2.isf(0.01,2) ** 0.5 である。

main_ax.scatter(x, y, alpha=0.5, s=1) の後に以下のコードを挿入しよう。matplotlibの楕円描画では、長軸と短軸の全長をそれぞれ指定するため、標準偏差の2倍を与える。

mu_x0,mu_y0 = mu_x,mu_y # 初期値を記録
sigma_x0,sigma_y0 = sigma_x,sigma_y # 初期値を記録

from scipy.stats import chi2
sigma_x = sigma_x0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1
sigma_y = sigma_y0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1

import matplotlib.patches as patches
ellipse = patches.Ellipse((mu_x, mu_y), sigma_x*2, sigma_y*2, edgecolor='r', facecolor='none',ls="-",alpha=0.5)
main_ax.add_patch(ellipse)

それっぽくできた。本当にこの楕円の中に90%のイベントが入っているか確かめてみよう。楕円内にあるかどうかを判定するコードを書く。

先程のsigma_y = sigma_y0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1 の行の直後に、以下のコードを挿入しよう

def is_in_ellipse(x, y, mu_x, mu_y, sigma_x, sigma_y):
    return ((x - mu_x)**2 / sigma_x**2 + (y - mu_y)**2 / sigma_y**2) <= 1
hitx = x[is_in_ellipse(x,y,mu_x,mu_y,sigma_x, sigma_y)]
hity = y[is_in_ellipse(x,y,mu_x,mu_y,sigma_x, sigma_y)]
main_ax.scatter(hitx, hity, alpha=0.5, s=1, color="tab:green")
main_ax.text(mu_x,mu_y,f"{len(hitx)/n_samples:.2%}",ha="center",va="center")

統計上のゆらぎの範囲で、確かに90%がこの楕円 (図上では真円に近いが軸を見ると確かに楕円) の中に入っていることが分かる。

この楕円を、任意の形、任意の場所に持っていったときの確率はどう求めればよいか。おそらく2つ方法、モンテカルロ法と積分法がある。

モンテカルロ法は簡単で、先程の関数を使い回すことで求めることができる。

sigma_x sigma_y に代入した部分を下記のように書き換えよう。

# sigma_x = sigma_x0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1
# sigma_y = sigma_y0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1
mu_x,mu_y=3,5
sigma_x,sigma_y=1,2

この楕円の中には約2.23%のイベントがあることが分かった。

次に、積分法で求めてみる。2次元の正規分布の平均値を0、標準偏差を1に規格化してしまっていたので、与える平均値と標準偏差も同じく規格化して与える。

from scipy.integrate import dblquad

def pdf(x, y):
    return multivariate_normal([0, 0], cov).pdf([x, y])

norm_mu_x = (mu_x-mu_x0)/sigma_x0
norm_mu_y = (mu_y-mu_y0)/sigma_y0
norm_sigma_x = sigma_x/sigma_x0
norm_sigma_y = sigma_y/sigma_y0

P, error = dblquad(
    lambda x, y: pdf(x, y),
    norm_mu_x-norm_sigma_x, norm_mu_x+norm_sigma_x,  
    lambda x: norm_mu_y - norm_sigma_y * np.sqrt(1 - ((x-norm_mu_x) / norm_sigma_x)**2),  # y の下限
    lambda x: norm_mu_y + norm_sigma_y * np.sqrt(1 - ((x-norm_mu_x) / norm_sigma_x)**2)   # y の上限
)

print(f"楕円領域内の確率: {P:.3%}±{error:.3%}")

楕円領域内の確率: 2.213%±0.000%

と出力されるだろう。先程の 約2.23%と誤差の範囲で一致している。積分法は誤差も算出してくれるので、特に確率が低い領域での計算で役に立つだろう。

積分法と、描画を全て統合したコード

import numpy as np
from scipy.stats import multivariate_normal
import matplotlib.pyplot as plt

np.random.seed(2)

mu_x,mu_y = 5,10
sigma_x,sigma_y = 1,5
cov = np.array([[1, 0], [0, 1]])  # 共分散行列

n_samples = 100000
samples = np.random.multivariate_normal(np.array([0, 0]), cov, n_samples)

x = samples.T[0]*sigma_x+mu_x
y = samples.T[1]*sigma_y+mu_y

fig = plt.figure(figsize=(6, 6))
grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2)

main_ax = fig.add_subplot(grid[1:4, 0:3])
main_ax.scatter(x, y, alpha=0.5, s=1)

mu_x0,mu_y0 = mu_x,mu_y # 初期値を記録
sigma_x0,sigma_y0 = sigma_x,sigma_y # 初期値を記録

from scipy.stats import chi2
# sigma_x = sigma_x0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1
# sigma_y = sigma_y0 * chi2.isf(0.1,2)**0.5 # 自由度2、p値が0.1
mu_x,mu_y=3,5
sigma_x,sigma_y=1,2

norm_mu_x = (mu_x-mu_x0)/sigma_x0
norm_mu_y = (mu_y-mu_y0)/sigma_y0
norm_sigma_x = sigma_x/sigma_x0
norm_sigma_y = sigma_y/sigma_y0

x_min, x_max = norm_mu_x-norm_sigma_x, norm_mu_x+norm_sigma_x
y_lower = lambda x: norm_mu_y - norm_sigma_y * np.sqrt(1 - ((x - norm_mu_x) / norm_sigma_x) ** 2) # y の下限
y_upper = lambda x: norm_mu_y + norm_sigma_y * np.sqrt(1 - ((x - norm_mu_x) / norm_sigma_x) ** 2) # y の上限

def pdf(x, y):
    return multivariate_normal([0, 0], cov).pdf([x, y])

from scipy.integrate import dblquad
P, error = dblquad(
    lambda x, y: pdf(x, y),
    x_min, x_max,y_lower,y_upper)

# 下限と上限のy値を計算
x_values = np.linspace(x_min,x_max,1000)
y_values_lower = y_lower(x_values)
y_values_upper = y_upper(x_values)

main_ax.fill_between(x_values*sigma_x0+mu_x0, y_values_lower*sigma_y0+mu_y0, y_values_upper*sigma_y0+mu_y0, color='tab:green', alpha=0.7)
main_ax.text(mu_x,mu_y,f"{P:.2%}",ha="center",va="center")

import matplotlib.patches as patches
ellipse = patches.Ellipse((mu_x, mu_y), sigma_x*2, sigma_y*2, edgecolor='r', facecolor='none',ls="-",alpha=0.5)
main_ax.add_patch(ellipse)

xlim = main_ax.get_xlim()
ylim = main_ax.get_ylim()

x_hist = fig.add_subplot(grid[0, 0:3], sharex=main_ax)
x_hist.hist(x, bins=40, color='tab:gray', range=xlim)

y_hist = fig.add_subplot(grid[1:4, 3], sharey=main_ax)
y_hist.hist(y, bins=40, orientation='horizontal', color='tab:gray', range=ylim)

main_ax.set_xlabel('X')
main_ax.set_ylabel('Y')
plt.show()

正規分布とポアソン分布の信頼区間、有意度、p値について

正規分布について考えるため、以下の図で記号を定義する。

出典: PDG

信頼区間(confidence interval) を定義したとき、有意度(σ)と、信頼区間から外れる確率(両側の場合はα、片側の場合はα/2)は以下のように計算できる。

from scipy.stats import norm

for sigma in [1,2,3,4,5,6]:
    print("δ: {:.1f}σ --> α  : {:.2e}".format(sigma,(1-norm.cdf(sigma))*2))
    print("δ: {:.1f}σ --> α/2: {:.2e}".format(sigma,1-norm.cdf(sigma)))

print()
for p_value in [0.3,0.2,0.1,0.01,0.001,1e-4,1e-5]:
    print("α  : {:.1e} --> δ: {:.2f}σ".format(p_value,norm.ppf(1-p_value/2)))
    print("α/2: {:.1e} --> δ: {:.2f}σ".format(p_value,norm.ppf(1-p_value)))

cdfはCumulative Distribution Function、つまり累積分布関数の頭文字、ppfはPercent Point Functionの頭文字でcdf(累積分布関数)の逆関数である。

出力

δ: 1.0σ --> α  : 3.17e-01
δ: 1.0σ --> α/2: 1.59e-01
δ: 2.0σ --> α  : 4.55e-02
δ: 2.0σ --> α/2: 2.28e-02
δ: 3.0σ --> α  : 2.70e-03
δ: 3.0σ --> α/2: 1.35e-03
δ: 4.0σ --> α  : 6.33e-05
δ: 4.0σ --> α/2: 3.17e-05
δ: 5.0σ --> α  : 5.73e-07
δ: 5.0σ --> α/2: 2.87e-07
δ: 6.0σ --> α  : 1.97e-09
δ: 6.0σ --> α/2: 9.87e-10

α  : 3.0e-01 --> δ: 1.04σ
α/2: 3.0e-01 --> δ: 0.52σ
α  : 2.0e-01 --> δ: 1.28σ
α/2: 2.0e-01 --> δ: 0.84σ
α  : 1.0e-01 --> δ: 1.64σ
α/2: 1.0e-01 --> δ: 1.28σ
α  : 1.0e-02 --> δ: 2.58σ
α/2: 1.0e-02 --> δ: 2.33σ
α  : 1.0e-03 --> δ: 3.29σ
α/2: 1.0e-03 --> δ: 3.09σ
α  : 1.0e-04 --> δ: 3.89σ
α/2: 1.0e-04 --> δ: 3.72σ
α  : 1.0e-05 --> δ: 4.42σ
α/2: 1.0e-05 --> δ: 4.26σ

例えば、ある観測結果が背景事象で説明できる確率が1×10-5のとき、それが正規分布の片側のみに現れる背景事象の場合は、正規分布に焼き直すと観測結果は4.26σの有意度であるといえる。

次に、ポアソン分布を考える。

Wikipediaより

例えば、設定した条件内に入ってくる背景事象の期待値が1の場合、観測イベントが0イベントになる確率は0.368、1イベントの確率は0.368、2イベントの確率は0.184となる。これを計算するには、

from scipy.stats import poisson

for expected in [1,4,10]:
    for k in [0,1,2,3,4,5,10,15,20]:
        print("λ={} k={} P={:.3f}".format(expected, k, poisson.pmf(k, expected)))

とすればよい。出力は、

λ=1 k=0 P=0.368
λ=1 k=1 P=0.368
λ=1 k=2 P=0.184
λ=1 k=3 P=0.061
λ=1 k=4 P=0.015
λ=1 k=5 P=0.003
λ=1 k=10 P=0.000
λ=1 k=15 P=0.000
λ=1 k=20 P=0.000
λ=4 k=0 P=0.018
λ=4 k=1 P=0.073
λ=4 k=2 P=0.147
λ=4 k=3 P=0.195
λ=4 k=4 P=0.195
λ=4 k=5 P=0.156
λ=4 k=10 P=0.005
λ=4 k=15 P=0.000
λ=4 k=20 P=0.000
λ=10 k=0 P=0.000
λ=10 k=1 P=0.000
λ=10 k=2 P=0.002
λ=10 k=3 P=0.008
λ=10 k=4 P=0.019
λ=10 k=5 P=0.038
λ=10 k=10 P=0.125
λ=10 k=15 P=0.035
λ=10 k=20 P=0.002

背景事象の期待値が1イベントで、実験で観測されたイベントが10イベントの場合、この10イベントが全て背景事象で説明できる確率はどう求めればよいか?

これは、期待値が1イベントで、観測が0~9イベントまでの累積分布を求めれば良い。

from scipy.stats import poisson

bg = 1
events = 10

print("p-value: {:.2e}".format(1-poisson.cdf(events-1, bg)))
print("p-value: {:.2e}".format(poisson.sf(events-1, bg)))

この結果はどちらも 1.11e-07となるだろう。このp値が小さすぎる場合は、poisson.sf関数の方が正確な結果になる。

このp値を正規分布における有意度に焼き直すには、背景事象がポアソン分布の片側だけに出てくることに注意すると、norm.ppf(1-poisson.sf(events-1, bg))より5.2σが導出できるだろう。

Windows11 でNet-SNMPを動かす

Net-SNMP のインストール

sourceforge.net

5.5-binaries から x64 のバイナリをダウンロードしてインストール。オプションは特に変更しない。

snmpd.confの設置

C:\usr\etc\snmphttps://gitlab.com/-/snippets/3736552 から snmpd.conf をダウンロードして置く。コミュニティ名をpublicにして、全てのホスト 0.0.0.0/0からのアクセスを許可している。

サービスに登録

スタート -> すべてのアプリ -> Net-SNMP -> Register Agent Service -> 詳細からファイルの場所を開く -> 管理者として実行

サービスの開始

サービスから「Net-SNMP Agent」を探して開始。開始できない場合は、「SNMPサービス」を停止して、スタートアップの種類を「手動」にしておく。

PATHの追加

(ローカルでプログラムを実行しないなら必要なし)

C:\usr\bin を環境変数のPATHに追加

実行

(ローカルでプログラムを実行しないなら必要なし)

コマンドプロンプトで snmpwalk -v 2c -c public localhost を実行

ファイアウォールで外部からのアクセスを許可

他のパソコンからSNMPのデータ取得を許可するには、ファイアウォールの受信の規則で

新しい規則 -> ポート ->UDP、161 -> 接続を許可する -> 全てにチェック -> Net-SNMP

とする。

参照

www.eaton-daitron.jp

Python: PNG形式の画像ファイルのDPIだけバイナリで書き換える

JPG形式の記事 と基本的には同じである。

違いは物理サイズ指定の単位が JPGではDPI (dots per inch) であるのに対して、PNGではPPM (pixels per meter) であること、チャンク(セグメント)ごとにCRC-32の巡回冗長検査があるのでそれを書き換える必要があることである。

以下、PNG形式の画像ファイルの物理サイズをA4の横幅にするコード。

def parse_png(file_path):
    with open(file_path, 'rb') as file:
        data = file.read()
    
    i = 0
    signature = data[i:i+8]
    assert("PNG" in str(signature))
    i+=8
    
    chunks = {}
    while True:
        length = int.from_bytes(data[i:i+4], 'big')
        chunk_type = data[i+4:i+8].decode('ascii')
        if chunk_type == 'IHDR':
            chunks[chunk_type] = (i,length)
        elif chunk_type == 'pHYs':
            chunks[chunk_type] = (i,length)
        if chunk_type == 'IEND':
            break
        i += (12+length)
        
    return chunks

import zlib

def modify_ppm(file_path, chunk_IHDR, chunk_pHYs):
    with open(file_path, 'rb') as f:
        data = f.read()
    
    width = int.from_bytes(data[chunk_IHDR[0]+8:chunk_IHDR[0]+12], 'big')
    height = int.from_bytes(data[chunk_IHDR[0]+12:chunk_IHDR[0]+16], 'big')
    precision = data[chunk_IHDR[0]+16]
    colors = data[chunk_IHDR[0]+17]
    
    x_density = int.from_bytes(data[chunk_pHYs[0]+8:chunk_pHYs[0]+12], 'big')
    y_density = int.from_bytes(data[chunk_pHYs[0]+12:chunk_pHYs[0]+16], 'big')
    units = data[chunk_pHYs[0]+16]
    
    #CRC-32検証
    # chunk_data = data[chunk_pHYs[0]+4:chunk_pHYs[0]+chunk_pHYs[1]+8]
    # crc_calculated = zlib.crc32(chunk_data) & 0xffffffff
    # crc_from_file = int.from_bytes(data[chunk_pHYs[0]+chunk_pHYs[1]+8:chunk_pHYs[0]+chunk_pHYs[1]+12],'big')
    # print(crc_calculated,"vs",crc_from_file)
    
    # print(f"PPM_Units : {units}")
    # print(f"X_PPM     : {x_density}")
    # print(f"Y_PPM     : {y_density}")
    # print(f"Precision : {precision} bits")
    # print(f"Height    : {height} pixels")
    # print(f"Width     : {width} pixels")
    # print(f"Colors    : {colors}")

    a4_width = 210
    a4_ppm = round(width/a4_width*1000) # PPMを計算
    
    if x_density == a4_ppm and units == 1:
        print(f"DPI: {x_density} == {a4_ppm}")
        return
    print(f"PPM: {x_density} -> {a4_ppm}")
    data = bytearray(data)
    data[chunk_pHYs[0]+8:chunk_pHYs[0]+12] = a4_ppm.to_bytes(4, 'big')
    data[chunk_pHYs[0]+12:chunk_pHYs[0]+16] = a4_ppm.to_bytes(4, 'big')
    data[chunk_pHYs[0]+16] = 1
    
    #新CRC-32書き込み
    chunk_data = data[chunk_pHYs[0]+4:chunk_pHYs[0]+chunk_pHYs[1]+8]
    crc_new = zlib.crc32(chunk_data) & 0xffffffff
    data[chunk_pHYs[0]+chunk_pHYs[1]+8:chunk_pHYs[0]+chunk_pHYs[1]+12] = crc_new.to_bytes(4, 'big')
    
    with open(file_path, 'wb') as f:
        f.write(data)

# ディレクトリ内のすべての png ファイルを変換
import glob
for file_path in glob.glob("*.png"):
    chunks = parse_png(file_path)
    modify_ppm(file_path,chunks["IHDR"],chunks["pHYs"])

色々対応させたバージョン

gitlab.com