🐉
相関係数ハック
狙った相関係数になるように新しくデータを作るって方法あるんだっけ?
ってことで相関係数ハックしてみる。
考え方
相関係数といえば、これ
今回は、これにガウスノイズを加えていくとする。
ガウスノイズを
とすると、
となる。 さらに、
でもある。
相関係数の式に代入すると、
これをnumpy風にかくと、
となる。 (これを書いててysのままでも良い気もしてきたが、そこはツッコミなしで)
なので、このrが狙った相関係数となるようなsの値を探索すれば良いのではと思った。
つまり、アルゴリズムとしては、↓のような感じになればいいんじゃないかと思った。
## 大域的なsの探索
for s in [search_min, search_max]:
r_tmp = {ni ~ N(0, s) で上式から、rを計算}
best_s = {r_tmpが探索範囲内で最小の時のs}
## sを固定した上で、何回かランダムにサンプリングして相関係数を探す
## ただし、狙った総関係数にならない場合もある(r_diff >= epsilon) ので、最大の試行回数を予め決めておく
while (r_diff < epsilon) and (iter_n < max_iter):
ns ~ N(0, best_s)
ys = xs + ns
r_tmp = corr(xs, ys)
r_diff = (r_tmp - r_target)
iter_n ++;
pythonコード
ということで、雑にpythonで書いてみる。(ちょっと変数名が違うが、そこは許してください。)
- tqdmで処理の様子を可視化していたりするので、コードは上のpseudoコードとは少し違う。
- 最初のscaleの探索の上限下限はなんとなく、この範囲ならいけるっしょという勘()
import numpy as np
import matplotlib.pyplot as plt
from tqdm.auto import tqdm
np.random.seed(42)
def get_corr_data(xs, corr=0.8, search_n=1000, eps=1e-6, iter_n=10_000, verbose=0):
_data_n = len(xs)
## global scale search
search_range = np.linspace(0.001 * np.ptp(xs), np.ptp(xs), search_n)
best_diff = np.inf
scale = -1
pbar = tqdm(search_range, total=len(search_range), disable=verbose==0)
for _s in pbar:
ns = np.random.normal(0, _s, _data_n)
_corr = np.mean( (xs - xs.mean()) * (xs + ns - xs.mean())) / np.std(xs) / np.std(xs + ns)
_diff = abs(corr - _corr)
if _diff < best_diff:
best_diff = _diff
scale = _s
pbar.set_description(f'scale search : {best_diff = } {scale = }')
best_diff = np.inf
ys_new = None
## correlation search
pbar = tqdm(range(iter_n), total=iter_n, disable=verbose==0)
for iter in pbar:
ns = np.random.normal(0, scale, _data_n)
_ys = xs.copy() + ns
_corr = np.corrcoef(xs, _ys)[0, 1]
_diff = abs(corr - _corr)
if _diff < best_diff:
best_diff = _diff
best_corr = _corr
ys = _ys.copy()
pbar.set_description(f'corr search : {best_corr = }')
if best_diff < eps:
break
return ys
適当なxsで試す
データ数10,000で適当なsin波で試してみる。
data_n = 10_000
freq = 2
xs = np.sin(2 * np.pi * np.arange(data_n) / data_n * freq)
ちょっと可視化も含めて関数化しておく。
def sim1(xs):
ys = get_corr_data(xs, corr=0.8, iter_n=10_000, eps=1e-5, search_n=1000, verbose=1)
corr = np.corrcoef(xs, ys)[0, 1]
print(f'{corr =}')
fig, ax = plt.subplots(1, 2, figsize=(20, 5))
ax[0].scatter(xs, ys)
ax[0].set_title('xs vs ys')
ax[1].plot(np.arange(data_n), xs, alpha=0.5, label='xs')
ax[1].plot(np.arange(data_n), ys, alpha=0.5, label='ys')
ax[1].legend()
plt.show()
そして、これで試してみると、
sim1(xs)
うまくいってるんじゃないかな??
他にもいろいろなxsを用意して試してみる。
for xs in [
np.random.normal(0.4, 0.1, data_n),
np.sin(2 * np.pi * np.arange(data_n) / data_n * freq) + np.linspace(-1, 1, data_n),
np.sin(2 * np.pi * np.arange(data_n) / data_n * freq) * np.exp(-np.linspace(0, 2, data_n))
]:
sim1(xs)
ガウスノイズもOKぽい。
にょきにょき上がっていくsin波もOKぽい。
高校の数学でよくみるこれもうまくいってるぽい。
まとめ
ということで、相関係数ハックできたと信じてる。
Discussion