見出し画像

ピュアランダムネス(Pure randomness)とは、完全に予測不可能で、パターンや規則性が全く存在しない純粋な無作為性や偶然性を指す概念です。アプリ化しました。

特に確率論、統計学、数学、物理学、コンピュータサイエンスなどの分野で議論される重要な概念です。


ピュアランダムネスの特徴:

  1. 予測不可能性: 過去の結果から将来の結果を予測することが不可能である

  2. 独立性: 各イベントは他のイベントとは完全に独立している

  3. 均一分布: 可能な全ての結果が等確率で発生する

  4. パターン非存在: いかなる規則性やパターンも見出せない

実世界での応用例:

  • 暗号化(セキュリティ)

  • シミュレーション

  • 統計的サンプリング

  • ゲーム理論

  • 量子力学(量子的ランダムネスなど)

興味深いのは、コンピュータ上で生成される「乱数」は通常、疑似乱数生成器(PRNG:Pseudo-Random Number Generator)を使用しており、完全なピュアランダムネスではないことです。これらは決定論的アルゴリズムに基づいているため、理論的には予測可能です。

真のランダムネスを得るためには、自然界の量子現象のような真に予測不可能な物理的プロセスを利用する必要があります。例えば、量子乱数生成器(QRNG:Quantum Random Number Generator)などがこれに当たります。


ピュアランダムネスのStreamlitシミュレーションが完成しました。このアプリケーションでは以下の機能が実装されています:

  1. 乱数生成と分析

    • 異なる乱数生成方式(NumPy、Python標準、時間ベース)の比較

    • 複数の確率分布(一様分布、正規分布、ポアソン分布など)のシミュレーション

    • 1次元、2次元、3次元データの視覚化

  2. ランダムネス評価

    • 連検定、自己相関分析、分布適合度テストなど複数の統計的検定

    • ランダムネスの各特性(予測不可能性、独立性、均一分布)に対する評価

  3. ランダムウォーク・シミュレーション

    • 1次元と2次元のランダムウォークの視覚化

    • 理論的な期待値との比較

  4. 無相関データのシミュレーション

    • サンプルサイズが相関係数の分布に与える影響の分析

    • 理論的な標準偏差との比較

  5. エントロピー測定

    • 情報理論的な観点からのランダムネスの評価

    • 相対エントロピーの計算と視覚化

このアプリケーションを使うことで、ピュアランダムネスの概念や、コンピュータで生成される疑似乱数の特性を視覚的に理解することができます。また、サンプルサイズや乱数生成方式を変えることで、それらがランダムネスの統計的特性にどのような影響を与えるかを観察できます。

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from scipy import stats
import random
import time
from collections import Counter
import base64
from io import BytesIO

# 自作のruns_test関数
def runs_test(x):
    """
    Wald–Wolfowitz runs test for randomness.
    
    Parameters
    ----------
    x : array_like
        Binary sequence (converted to True/False internally)
    
    Returns
    -------
    runs : int
        Number of runs
    p_value : float
        P-value of the test statistic
    """
    import numpy as np
    from scipy import stats
    
    # Convert to binary sequence
    x = np.asarray(x, dtype=bool)
    n = len(x)
    
    # Count runs
    runs = 1 + np.sum(x[1:] != x[:-1])
    
    # Count number of each value
    n1 = np.sum(x)
    n2 = n - n1
    
    # Expected runs and standard deviation under H0
    r_exp = 2 * n1 * n2 / n + 1
    r_var = (2 * n1 * n2 * (2 * n1 * n2 - n)) / (n**2 * (n - 1))
    
    # Z-score
    if r_var == 0:  # Avoid division by zero
        p_value = 1.0
    else:
        z = (runs - r_exp) / np.sqrt(r_var)
        p_value = 2 * (1 - stats.norm.cdf(abs(z)))  # Two-tailed test
    
    return runs, p_value

# フォント設定の試行
try:
    import japanize_matplotlib
except ImportError:
    plt.rcParams['font.family'] = ['Arial', 'Hiragino Sans', 'Yu Gothic', 'Meiryo', 'sans-serif']

# ページ設定
st.set_page_config(
    page_title="ピュアランダムネス・シミュレーション",
    page_icon="🎲",
    layout="wide",
    initial_sidebar_state="expanded"
)

# タイトルとイントロダクション
st.title("ピュアランダムネス・シミュレーション")
st.markdown("""
このアプリでは、コンピュータで生成されるランダムネス(乱数)の特性をシミュレーションし、視覚化します。
真のランダムネス(ピュアランダムネス)と疑似ランダムネス(コンピュータで生成される乱数)の違いを探索できます。
""")

# サイドバー
st.sidebar.header("設定")

# 乱数生成の種類
random_type = st.sidebar.radio(
    "乱数生成方式",
    ["NumPy(疑似乱数)", "Python標準(疑似乱数)", "時間ベース(より不確実性が高い)"]
)

# サンプル数
sample_size = st.sidebar.slider("サンプル数", 100, 10000, 1000)

# 次元数(2Dプロットと3Dプロットの選択)
dimensions = st.sidebar.radio("次元", [1, 2, 3])

# 乱数の分布タイプ
distribution_type = st.sidebar.selectbox(
    "分布タイプ",
    ["一様分布", "正規分布", "ポアソン分布", "指数分布", "二項分布"]
)

# 分布パラメータ
if distribution_type == "正規分布":
    mean = st.sidebar.slider("平均", -10.0, 10.0, 0.0)
    std = st.sidebar.slider("標準偏差", 0.1, 10.0, 1.0)
elif distribution_type == "ポアソン分布":
    lam = st.sidebar.slider("λ(平均到着率)", 0.1, 20.0, 5.0)
elif distribution_type == "指数分布":
    scale = st.sidebar.slider("β(スケール)", 0.1, 10.0, 1.0)
elif distribution_type == "二項分布":
    n_trials = st.sidebar.slider("試行回数", 1, 100, 20)
    p_success = st.sidebar.slider("成功確率", 0.01, 0.99, 0.5)

# シード設定
use_seed = st.sidebar.checkbox("固定シードを使用", False)
if use_seed:
    seed_value = st.sidebar.number_input("シード値", 0, 100000, 42)
else:
    seed_value = int(time.time())

# 乱数生成関数
def generate_random_data(n_samples, dims, dist_type, random_gen_type):
    if random_gen_type == "NumPy(疑似乱数)":
        np.random.seed(seed_value if use_seed else None)
        if dist_type == "一様分布":
            return np.random.random(size=(n_samples, dims))
        elif dist_type == "正規分布":
            return np.random.normal(mean, std, size=(n_samples, dims))
        elif dist_type == "ポアソン分布":
            return np.random.poisson(lam, size=(n_samples, dims)) / lam
        elif dist_type == "指数分布":
            return np.random.exponential(scale, size=(n_samples, dims))
        elif dist_type == "二項分布":
            return np.random.binomial(n_trials, p_success, size=(n_samples, dims)) / n_trials
    
    elif random_gen_type == "Python標準(疑似乱数)":
        random.seed(seed_value if use_seed else None)
        data = []
        for _ in range(n_samples):
            sample = []
            for _ in range(dims):
                if dist_type == "一様分布":
                    sample.append(random.random())
                elif dist_type == "正規分布":
                    sample.append(random.gauss(mean, std))
                elif dist_type == "ポアソン分布":
                    # 簡易的なポアソン近似
                    count = 0
                    interval = 1.0 / lam
                    total = 0
                    while total < 1.0:
                        total += random.expovariate(lam)
                        count += 1
                    sample.append(count / lam)
                elif dist_type == "指数分布":
                    sample.append(random.expovariate(1/scale))
                elif dist_type == "二項分布":
                    successes = sum(random.random() < p_success for _ in range(n_trials))
                    sample.append(successes / n_trials)
            data.append(sample)
        return np.array(data)
    
    elif random_gen_type == "時間ベース(より不確実性が高い)":
        data = []
        for _ in range(n_samples):
            # マイクロ秒単位の時間を使用してエントロピーを高める
            time.sleep(0.000001)  # 少しだけスリープして時間変化を作る
            time_ns = time.time_ns()
            sample = []
            for d in range(dims):
                # 時間の最下位ビットを使って異なる次元のランダム値を生成
                time_hash = hash(time_ns + d) / 2**(64)  # ハッシュ値を0-1に正規化
                
                if dist_type == "一様分布":
                    sample.append(time_hash % 1.0)
                elif dist_type == "正規分布":
                    # Box-Muller変換で正規分布を近似
                    u1 = time_hash % 1.0
                    u2 = (time_hash * 13.37) % 1.0
                    if u1 > 0:  # 0を避ける
                        z = mean + std * np.sqrt(-2.0 * np.log(u1)) * np.cos(2.0 * np.pi * u2)
                        sample.append(z)
                    else:
                        sample.append(mean)
                elif dist_type == "ポアソン分布":
                    # 簡易的なポアソン近似
                    count = 0
                    t = 0
                    while t < 1.0:
                        t += -np.log(max(0.001, (time_hash * (7.77 * count + 1)) % 1.0)) / lam
                        count += 1
                    sample.append((count-1) / lam)
                elif dist_type == "指数分布":
                    sample.append(-scale * np.log(max(0.001, time_hash % 1.0)))
                elif dist_type == "二項分布":
                    successes = sum((hash(time_ns + d + i) % 1000 / 1000) < p_success for i in range(n_trials))
                    sample.append(successes / n_trials)
            data.append(sample)
        return np.array(data)

# メインエリア
st.header("乱数の生成と分析")

# 乱数を生成
if st.button("乱数を生成"):
    progress_bar = st.progress(0)
    st.write(f"シード値: {seed_value}")
    
    # 乱数生成
    random_data = generate_random_data(sample_size, dimensions, distribution_type, random_type)
    progress_bar.progress(50)
    
    # 1次元データの場合
    if dimensions == 1:
        # ヒストグラム
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
        
        sns.histplot(random_data.flatten(), kde=True, ax=ax1)
        ax1.set_title("乱数の分布")
        ax1.set_xlabel("値")
        ax1.set_ylabel("頻度")
        
        # QQプロット(正規分布との比較)
        stats.probplot(random_data.flatten(), dist="norm", plot=ax2)
        ax2.set_title("QQプロット(正規性の確認)")
        
        st.pyplot(fig)
        
        # 統計情報
        st.subheader("統計情報")
        stats_data = {
            "統計量": ["平均", "中央値", "標準偏差", "最小値", "最大値", "歪度", "尖度"],
            "値": [
                np.mean(random_data),
                np.median(random_data),
                np.std(random_data),
                np.min(random_data),
                np.max(random_data),
                stats.skew(random_data.flatten()),
                stats.kurtosis(random_data.flatten())
            ]
        }
        st.table(pd.DataFrame(stats_data))
        
        # ランダムネステスト
        st.subheader("ランダムネステスト")
        
        # 連検定 - 修正: stats.runs_test から自作の runs_test に置き換え
        runs, p_value = runs_test(random_data.flatten() > np.median(random_data))
        
        # 自己相関
        acf = pd.Series(random_data.flatten()).autocorr(lag=1)
        
        test_results = {
            "テスト": ["連検定 (p値)", "自己相関係数"],
            "結果": [p_value, acf],
            "評価": [
                "良好(真にランダム)" if p_value > 0.05 else "不良(非ランダム)",
                "良好(無相関)" if abs(acf) < 0.1 else "不良(相関あり)"
            ]
        }
        st.table(pd.DataFrame(test_results))
        
        # 連長分析
        st.subheader("連長分析(同じ傾向が続く長さ)")
        
        # 上昇/下降の連長を計算
        data = random_data.flatten()
        runs = []
        current_run = 1
        
        for i in range(1, len(data)):
            if (data[i] > data[i-1] and data[i-1] > data[i-2] if i > 1 else True) or \
               (data[i] < data[i-1] and data[i-1] < data[i-2] if i > 1 else True):
                current_run += 1
            else:
                runs.append(current_run)
                current_run = 1
        
        if current_run > 1:
            runs.append(current_run)
        
        run_counts = Counter(runs)
        run_df = pd.DataFrame({
            "連長": list(run_counts.keys()),
            "出現回数": list(run_counts.values())
        }).sort_values("連長")
        
        fig, ax = plt.subplots(figsize=(10, 5))
        ax.bar(run_df["連長"].astype(str), run_df["出現回数"])
        ax.set_title("連長の分布")
        ax.set_xlabel("連長")
        ax.set_ylabel("出現回数")
        st.pyplot(fig)
        
    # 2次元データの場合
    elif dimensions == 2:
        # 散布図
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
        
        ax1.scatter(random_data[:, 0], random_data[:, 1], alpha=0.5, s=10)
        ax1.set_title("2次元乱数の散布図")
        ax1.set_xlabel("X")
        ax1.set_ylabel("Y")
        ax1.grid(True)
        
        # ヒートマップ(2D密度)
        h = ax2.hist2d(random_data[:, 0], random_data[:, 1], bins=30, cmap="viridis")
        plt.colorbar(h[3], ax=ax2)
        ax2.set_title("2次元分布のヒートマップ")
        ax2.set_xlabel("X")
        ax2.set_ylabel("Y")
        
        st.pyplot(fig)
        
        # 相関分析
        st.subheader("相関分析")
        corr = np.corrcoef(random_data[:, 0], random_data[:, 1])[0, 1]
        
        st.write(f"X と Y の相関係数: {corr:.4f}")
        st.write(f"評価: {'良好(無相関)' if abs(corr) < 0.1 else '不良(相関あり)'}")
        
        # 分布の等高線
        st.subheader("分布の等高線図")
        fig, ax = plt.subplots(figsize=(10, 8))
        
        # 2D KDE (カーネル密度推定)
        sns.kdeplot(x=random_data[:, 0], y=random_data[:, 1], ax=ax, cmap="viridis", fill=True)
        ax.set_title("2次元分布の等高線")
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        ax.grid(True)
        
        st.pyplot(fig)
        
    # 3次元データの場合
    else:  # dimensions == 3
        # 3Dプロット
        fig = plt.figure(figsize=(10, 8))
        ax = fig.add_subplot(111, projection='3d')
        
        scatter = ax.scatter(
            random_data[:, 0], 
            random_data[:, 1], 
            random_data[:, 2],
            c=random_data[:, 2],  # Z値で色付け
            cmap="viridis",
            alpha=0.6,
            s=15
        )
        
        ax.set_title("3次元乱数の散布図")
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        ax.set_zlabel("Z")
        
        plt.colorbar(scatter, ax=ax, label="Z値")
        
        # グラフを画像として保存してStreamlitで表示(3Dプロットは直接表示できないため)
        buf = BytesIO()
        plt.savefig(buf, format="png", dpi=300, bbox_inches="tight")
        buf.seek(0)
        st.image(buf, use_column_width=True)
        
        # 相関行列
        st.subheader("相関行列")
        corr_matrix = np.corrcoef(random_data.T)
        
        fig, ax = plt.subplots(figsize=(8, 6))
        sns.heatmap(corr_matrix, annot=True, cmap="coolwarm", vmin=-1, vmax=1, ax=ax)
        ax.set_title("3次元データの相関行列")
        ax.set_xticklabels(["X", "Y", "Z"])
        ax.set_yticklabels(["X", "Y", "Z"])
        
        st.pyplot(fig)
        
        # 3次元データの各軸ごとの分布
        st.subheader("各次元の分布")
        
        fig, axes = plt.subplots(1, 3, figsize=(15, 5))
        
        for i, ax in enumerate(axes):
            sns.histplot(random_data[:, i], kde=True, ax=ax)
            ax.set_title(f"次元 {i+1} の分布")
            ax.set_xlabel("値")
            ax.set_ylabel("頻度")
        
        st.pyplot(fig)
    
    # ランダムネス評価
    st.header("ランダムネスの評価")
    
    # ピュアランダムネスの基準に照らし合わせた評価
    criteria = []
    
    # 予測可能性(自己相関)
    if dimensions == 1:
        autocorr = pd.Series(random_data.flatten()).autocorr(lag=1)
        criteria.append({
            "基準": "予測不可能性 (自己相関)",
            "結果": f"{autocorr:.4f}",
            "評価": "良好" if abs(autocorr) < 0.1 else "不良",
            "説明": "連続する値の間に相関がないことを確認" 
        })
    else:
        # 多次元データの場合は各次元の自己相関の平均
        avg_autocorr = np.mean([pd.Series(random_data[:, i]).autocorr(lag=1) for i in range(dimensions)])
        criteria.append({
            "基準": "予測不可能性 (自己相関)",
            "結果": f"{avg_autocorr:.4f}",
            "評価": "良好" if abs(avg_autocorr) < 0.1 else "不良",
            "説明": "連続する値の間に相関がないことを確認" 
        })
    
    # 均一分布(カイ二乗適合度テスト)
    for dim in range(dimensions):
        data = random_data[:, dim]
        hist, bin_edges = np.histogram(data, bins=10)
        expected = np.ones_like(hist) * (len(data) / 10)
        chi2, p = stats.chisquare(hist, expected)
        
        criteria.append({
            "基準": f"均一分布 (次元{dim+1})",
            "結果": f"p値: {p:.4f}",
            "評価": "良好" if p > 0.05 else "不良",
            "説明": "分布が理論的な分布と一致しているか確認"
        })
    
    # 次元間の独立性(相関係数)
    if dimensions > 1:
        corr_matrix = np.corrcoef(random_data.T)
        max_corr = 0
        for i in range(dimensions):
            for j in range(i+1, dimensions):
                max_corr = max(max_corr, abs(corr_matrix[i, j]))
        
        criteria.append({
            "基準": "独立性 (次元間相関)",
            "結果": f"最大相関: {max_corr:.4f}",
            "評価": "良好" if max_corr < 0.1 else "不良",
            "説明": "異なる次元の値が互いに独立しているか確認"
        })
    
    # 連検定 - 修正: stats.runs_test から自作の runs_test に置き換え
    if dimensions == 1:
        runs, p_value = runs_test(random_data.flatten() > np.median(random_data))
        criteria.append({
            "基準": "ランダム性 (連検定)",
            "結果": f"p値: {p_value:.4f}",
            "評価": "良好" if p_value > 0.05 else "不良",
            "説明": "値の増減パターンがランダムであるか確認"
        })
    
    # 評価結果をテーブルで表示
    st.table(pd.DataFrame(criteria))
    
    # 総合評価
    good_count = sum(1 for item in criteria if item["評価"] == "良好")
    total_count = len(criteria)
    
    st.subheader("総合評価")
    score = good_count / total_count * 100
    
    if score >= 90:
        quality = "非常に高品質なランダムネス"
    elif score >= 75:
        quality = "高品質なランダムネス"
    elif score >= 60:
        quality = "十分なランダムネス"
    elif score >= 50:
        quality = "やや偏りのあるランダムネス"
    else:
        quality = "偏りの大きいランダムネス(ピュアランダムネスとは言えない)"
    
    st.write(f"スコア: {score:.1f}% ({good_count}/{total_count} 基準を満たす)")
    st.write(f"評価: {quality}")
    
    if random_type == "NumPy(疑似乱数)" or random_type == "Python標準(疑似乱数)":
        st.info("このシミュレーションでは疑似ランダム生成器を使用しています。真のピュアランダムネスは、量子現象などの物理的プロセスから得られます。")
    else:
        st.info("このシミュレーションでは時間ベースのエントロピーソースを使用していますが、これも真のピュアランダムネスではありません。より高品質なランダムネスを得るには、量子乱数生成器などの物理的プロセスが必要です。")
    
    # 進捗完了
    progress_bar.progress(100)

# 理論的な説明セクション
with st.expander("ピュアランダムネスとは?"):
    st.write("""
    ### ピュアランダムネス(純粋な無作為性)の定義
    
    ピュアランダムネスとは、完全に予測不可能で、パターンや規則性が全く存在しない純粋な無作為性や偶然性を指します。
    これは理論的な概念であり、真のピュアランダムネスを現実世界で実現することは極めて難しいとされています。
    
    ### ピュアランダムネスの4つの特性
    
    1. **予測不可能性**: 過去の結果がどんなに多く観測されていても、将来の結果を予測することができない
    2. **独立性**: 各イベントは他のイベントとは完全に独立している(過去の結果は未来の結果に影響しない)
    3. **均一分布**: 可能な全ての結果が等確率で発生する
    4. **パターン非存在**: どんなに長い系列であっても、そこからいかなる規則性やパターンも見出せない
    
    ### 疑似ランダムネスとの違い
    
    コンピュータで生成される「乱数」は、通常は疑似乱数生成器(PRNG)によるものです。これらは:
    
    - 決定論的アルゴリズムに基づいている
    - シード値が同じなら同じ結果を再現できる
    - 十分に長い周期の後には必ず繰り返す
    - 理論的には予測可能である
    
    これに対し、真のランダムネスは:
    
    - 決定論的に予測できない
    - 再現不可能
    - 繰り返しのパターンが存在しない
    
    ### 真のランダムネスの生成源
    
    真のランダムネスを得るためには、次のような自然界の予測不可能なプロセスを利用します:
    
    - 量子現象(量子乱数生成器)
    - 大気ノイズ
    - 放射性崩壊
    - 熱雑音
    
    ### 応用分野
    
    ピュアランダムネスは以下の分野で重要です:
    
    - 暗号化と情報セキュリティ
    - 科学的シミュレーション
    - 統計的サンプリング
    - ゲーム理論
    - 量子コンピューティング
    """)

with st.expander("乱数生成方式の説明"):
    st.write("""
    ### このシミュレーションで利用可能な乱数生成方式
    
    #### 1. NumPy(疑似乱数)
    NumPyの乱数生成器は、メルセンヌ・ツイスタアルゴリズムに基づいています。高品質な疑似乱数を生成し、2^19937-1という非常に長い周期を持ちます。統計的なシミュレーションなどに広く使われています。
    
    #### 2. Python標準(疑似乱数)
    Python標準のrandomモジュールは、メルセンヌ・ツイスターアルゴリズムを改良したものを使用しています。品質の良い疑似乱数を生成しますが、暗号論的に安全ではありません。
    
    #### 3. 時間ベース(より不確実性が高い)
    システムの時間情報をエントロピーソースとして利用する方法です。マイクロ秒単位の時間は予測が難しいため、ある程度の不確実性が得られますが、真のランダムネスではありません。このアプリでは、時間にハッシュ関数を適用し、さらに不確実性を高める工夫をしています。
    
    ### 実際の真のランダムネス生成
    
    このシミュレーションでは実装されていませんが、真のランダムネスを得るための主な方法には以下があります:
    
    - **量子乱数生成器 (QRNG)**: 量子力学の原理に基づく真の不確定性を利用
    - **熱雑音**: 電子回路の熱雑音を測定して乱数を生成
    - **大気ノイズ**: ラジオやマイクで検出される大気ノイズを利用
    - **放射性崩壊**: 放射性同位体の崩壊は本質的に予測不可能
    
    これらの方法を使った乱数生成サービスも存在します(例:random.org、NIST乱数ビーコンなど)。
    """)

with st.expander("ランダムネステストの説明"):
    st.write("""
    ### ランダムネスを評価するための統計的テスト
    
    #### 基本的なテスト
    
    1. **頻度テスト**: 各値がほぼ同じ頻度で出現するか
    2. **連検定**: 連続して同じ傾向(上昇や下降)が続く長さが期待通りか
    3. **自己相関分析**: 連続する値の間に相関がないか
    4. **分布適合度テスト**: 理論的な分布と一致しているか
    
    #### 高度なテスト
    
    1. **NIST統計的テストスイート**: 米国標準技術研究所が開発した15種類のテスト
    2. **Diehard テスト**: George Marsaglia が開発した複数のテスト
    3. **DieharderテストおよびTestU01**: 上記の拡張版
    
#### このアプリで使用しているテスト
    
    1. **連検定 (Runs Test)**: ランダムなデータでは、値の増減のパターンが特定の統計的な性質に従います
    2. **自己相関係数**: ランダムなデータでは、連続する値の間に相関がないはずです
    3. **カイ二乗適合度テスト**: データの分布が理論的な分布と一致しているかを確認します
    4. **相関分析**: 多次元データの場合、各次元は互いに独立しているべきです
    
    ### テスト結果の解釈
    
    - **p値 > 0.05**: 一般的にはデータがランダムであることを示唆(帰無仮説を棄却できない)
    - **相関係数 < 0.1**: 相関がほとんどないことを示唆
    
    複数のテストを組み合わせることで、より信頼性の高い評価ができます。ただし、真のランダムネスでも偶然テストに失敗することがあり、逆に非ランダムなデータでも特定のテストには合格することがあります。
    """)

# ランダムウォークのシミュレーション機能
st.header("ランダムウォーク・シミュレーション")
st.write("""
ランダムウォークは、ランダムな方向への連続的な移動の軌跡です。
真にランダムな場合、長期的な方向性や偏りは発生しません。
""")

random_walk_type = st.radio(
    "ランダムウォークの種類",
    ["1次元ランダムウォーク", "2次元ランダムウォーク"]
)

steps = st.slider("ステップ数", 100, 10000, 1000)

if st.button("ランダムウォークを実行"):
    if random_walk_type == "1次元ランダムウォーク":
        # 1次元ランダムウォーク
        if random_type == "NumPy(疑似乱数)":
            np.random.seed(seed_value if use_seed else None)
            steps_array = 2 * np.random.randint(0, 2, size=steps) - 1
        elif random_type == "Python標準(疑似乱数)":
            random.seed(seed_value if use_seed else None)
            steps_array = [2 * random.randint(0, 1) - 1 for _ in range(steps)]
        else:  # 時間ベース
            steps_array = []
            for _ in range(steps):
                time.sleep(0.000001)
                time_val = time.time_ns()
                step = 1 if time_val % 2 == 0 else -1
                steps_array.append(step)
        
        # 累積和で位置を計算
        position = np.cumsum(steps_array)
        
        # プロット
        fig, ax = plt.subplots(figsize=(12, 6))
        ax.plot(range(steps), position)
        ax.set_title("1次元ランダムウォーク")
        ax.set_xlabel("ステップ")
        ax.set_ylabel("位置")
        ax.grid(True)
        
        # 期待値(理論値)と比較
        expected_final_position = 0
        expected_distance = np.sqrt(steps)
        
        ax.axhline(y=expected_final_position, color='r', linestyle='--', label="期待値(平均)")
        ax.axhline(y=expected_distance, color='g', linestyle='--', label=f"+1σ (√n = {expected_distance:.1f})")
        ax.axhline(y=-expected_distance, color='g', linestyle='--', label=f"-1σ (-√n = {-expected_distance:.1f})")
        
        ax.legend()
        st.pyplot(fig)
        
        # 統計情報
        final_position = position[-1]
        st.write(f"最終位置: {final_position}")
        st.write(f"理論的な期待位置: {expected_final_position}")
        st.write(f"理論的な標準偏差: {expected_distance:.2f}")
        st.write(f"標準化された最終位置 (Z-score): {final_position / expected_distance:.2f}")
        
        # 自己相関分析
        acf_values = [pd.Series(position).autocorr(lag=i) for i in range(1, 11)]
        
        fig, ax = plt.subplots(figsize=(10, 5))
        ax.bar(range(1, 11), acf_values)
        ax.set_title("自己相関関数")
        ax.set_xlabel("ラグ")
        ax.set_ylabel("自己相関")
        ax.grid(True)
        st.pyplot(fig)
        
        # 長期的なトレンドの有無を評価
        normalized_position = position / np.sqrt(np.arange(1, steps+1))
        trend_coefficient = np.polyfit(range(steps), normalized_position, 1)[0]
        
        st.write(f"長期的トレンド係数: {trend_coefficient:.6f}")
        st.write(f"評価: {'ランダム性が高い' if abs(trend_coefficient) < 0.01 else 'トレンドが存在する可能性あり'}")
        
    else:  # 2次元ランダムウォーク
        # 2次元ランダムウォーク(上下左右への移動)
        if random_type == "NumPy(疑似乱数)":
            np.random.seed(seed_value if use_seed else None)
            angles = 2 * np.pi * np.random.random(steps)
            dx = np.cos(angles)
            dy = np.sin(angles)
        elif random_type == "Python標準(疑似乱数)":
            random.seed(seed_value if use_seed else None)
            directions = [random.choice([(0, 1), (1, 0), (0, -1), (-1, 0)]) for _ in range(steps)]
            dx = [d[0] for d in directions]
            dy = [d[1] for d in directions]
        else:  # 時間ベース
            dx, dy = [], []
            for _ in range(steps):
                time.sleep(0.000001)
                time_val = time.time_ns()
                direction = time_val % 4
                if direction == 0:
                    dx.append(0)
                    dy.append(1)
                elif direction == 1:
                    dx.append(1)
                    dy.append(0)
                elif direction == 2:
                    dx.append(0)
                    dy.append(-1)
                else:
                    dx.append(-1)
                    dy.append(0)
        
        # 累積和で位置を計算
        x = np.cumsum(dx)
        y = np.cumsum(dy)
        
        # プロット
        fig, ax = plt.subplots(figsize=(10, 10))
        
        # 色付きの軌跡(時間経過を表す)
        points = np.array([x, y]).T.reshape(-1, 1, 2)
        segments = np.concatenate([points[:-1], points[1:]], axis=1)
        
        norm = plt.Normalize(0, steps)
        lc = plt.matplotlib.collections.LineCollection(segments, cmap='viridis', norm=norm)
        lc.set_array(np.arange(steps))
        line = ax.add_collection(lc)
        fig.colorbar(line, ax=ax, label="ステップ")
        
        # 始点と終点
        ax.plot(x[0], y[0], 'go', markersize=10, label="開始点")
        ax.plot(x[-1], y[-1], 'ro', markersize=10, label="終了点")
        
        # 理論的な期待到達距離(円)
        expected_distance = np.sqrt(steps)
        circle = plt.Circle((0, 0), expected_distance, color='r', fill=False, linestyle='--', label=f"期待距離 (√n = {expected_distance:.1f})")
        ax.add_patch(circle)
        
        ax.set_title("2次元ランダムウォーク")
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        ax.grid(True)
        ax.axis('equal')
        ax.legend()
        
        st.pyplot(fig)
        
        # 統計情報
        final_x, final_y = x[-1], y[-1]
        final_distance = np.sqrt(final_x**2 + final_y**2)
        
        st.write(f"最終位置: ({final_x:.2f}, {final_y:.2f})")
        st.write(f"原点からの距離: {final_distance:.2f}")
        st.write(f"理論的な期待距離: {expected_distance:.2f}")
        st.write(f"標準化された最終距離 (Z-score): {final_distance / expected_distance:.2f}")
        
        # 方向統計
        theta = np.arctan2(y, x)
        
        fig, ax = plt.subplots(figsize=(10, 6), subplot_kw={'projection': 'polar'})
        ax.hist(theta, bins=36, edgecolor='black')
        ax.set_title("方向分布")
        st.pyplot(fig)

# 標本相関係数のシミュレーション機能
st.header("無相関データのシミュレーション")
st.write("""
真にランダムな2つの変数間には相関がないはずです。
このシミュレーションでは、サンプルサイズと試行回数を変えて、無相関データの相関係数の分布を観察できます。
""")

sample_sizes = st.multiselect(
    "サンプルサイズ",
    [10, 30, 50, 100, 200, 500, 1000],
    default=[30, 100, 500]
)

n_trials = st.slider("試行回数", 100, 10000, 1000)

if st.button("相関シミュレーションを実行") and sample_sizes:
    # サンプルサイズごとの相関係数分布をシミュレーション
    correlations = {}
    
    for n in sample_sizes:
        corrs = []
        for _ in range(n_trials):
            if random_type == "NumPy(疑似乱数)":
                np.random.seed(None)  # 毎回違う乱数を使用
                x = np.random.normal(0, 1, n)
                y = np.random.normal(0, 1, n)
            elif random_type == "Python標準(疑似乱数)":
                random.seed(None)
                x = [random.gauss(0, 1) for _ in range(n)]
                y = [random.gauss(0, 1) for _ in range(n)]
            else:  # 時間ベース
                x, y = [], []
                for i in range(n):
                    time.sleep(0.000001)
                    time_val = time.time_ns()
                    hash_x = hash(time_val) / 2**(64)
                    hash_y = hash(time_val + 1) / 2**(64)
                    
                    # Box-Muller変換で正規分布を生成
                    if hash_x > 0 and hash_y > 0:
                        x.append(np.sqrt(-2.0 * np.log(hash_x)) * np.cos(2.0 * np.pi * hash_y))
                        y.append(np.sqrt(-2.0 * np.log(hash_x)) * np.sin(2.0 * np.pi * hash_y))
                    else:
                        x.append(hash_x)
                        y.append(hash_y)
            
            corr = np.corrcoef(x, y)[0, 1]
            corrs.append(corr)
        
        correlations[n] = corrs
    
    # サンプルサイズごとの相関係数分布をプロット
    fig, ax = plt.subplots(figsize=(12, 8))
    
    for n in sample_sizes:
        sns.kdeplot(correlations[n], label=f"n = {n}", ax=ax)
    
    ax.set_title("無相関データの相関係数分布")
    ax.set_xlabel("相関係数")
    ax.set_ylabel("密度")
    ax.axvline(x=0, color='k', linestyle='--')
    
    # 理論的な標準偏差(1/√n)を表示
    for n in sample_sizes:
        sd = 1 / np.sqrt(n)
        ax.axvline(x=sd, color='r', linestyle=':', alpha=0.5)
        ax.axvline(x=-sd, color='r', linestyle=':', alpha=0.5)
        ax.text(sd, 0.1, f"+1σ (n={n})", rotation=90, alpha=0.7)
    
    ax.legend()
    ax.grid(True)
    st.pyplot(fig)
    
    # 統計情報
    stats_data = {
        "サンプルサイズ": [],
        "平均": [],
        "標準偏差": [],
        "理論的標準偏差": [],
        "95%信頼区間": []
    }
    
    for n in sample_sizes:
        stats_data["サンプルサイズ"].append(n)
        stats_data["平均"].append(np.mean(correlations[n]))
        stats_data["標準偏差"].append(np.std(correlations[n], ddof=1))
        stats_data["理論的標準偏差"].append(1 / np.sqrt(n))
        
        # 95%信頼区間
        lower = np.percentile(correlations[n], 2.5)
        upper = np.percentile(correlations[n], 97.5)
        stats_data["95%信頼区間"].append(f"({lower:.4f}, {upper:.4f})")
    
    st.table(pd.DataFrame(stats_data))
    
    st.write("""
    ### 解説
    
    - 真に無相関なデータでも、有限のサンプルからは非ゼロの相関係数が得られます
    - サンプルサイズが大きいほど、相関係数の分布は0周辺に集中します
    - 理論的には、相関係数の標準偏差は 1/√n に近似されます
    - 大規模データで小さな相関が検出された場合、統計的には有意でも実質的な意味はない可能性があります
    """)

# 乱数のエントロピー測定
st.header("エントロピー測定")
st.write("""
エントロピーは情報理論において、ランダム性や不確かさの度合いを示す指標です。
真にランダムな系列は、最大のエントロピーを持ちます。
""")

entropy_sample_size = st.slider("エントロピー計算のサンプル数", 100, 10000, 1000)
bin_count = st.slider("ビン数(離散化の粒度)", 2, 256, 8)

if st.button("エントロピーを計算"):
    # 異なる方法で乱数を生成
    if random_type == "NumPy(疑似乱数)":
        np.random.seed(seed_value if use_seed else None)
        data = np.random.random(entropy_sample_size)
    elif random_type == "Python標準(疑似乱数)":
        random.seed(seed_value if use_seed else None)
        data = [random.random() for _ in range(entropy_sample_size)]
    else:  # 時間ベース
        data = []
        for _ in range(entropy_sample_size):
            time.sleep(0.000001)
            time_val = time.time_ns()
            data.append((time_val % 1000) / 1000)
    
    # データを離散化
    hist, _ = np.histogram(data, bins=bin_count, range=(0, 1))
    pk = hist / entropy_sample_size
    
    # シャノンエントロピーを計算
    entropy = -np.sum(pk * np.log2(pk + 1e-10))
    max_entropy = np.log2(bin_count)
    rel_entropy = entropy / max_entropy
    
    st.write(f"計算されたエントロピー: {entropy:.4f} ビット")
    st.write(f"最大可能エントロピー: {max_entropy:.4f} ビット")
    st.write(f"相対エントロピー: {rel_entropy:.4f} ({rel_entropy*100:.1f}%)")
    
    # 視覚化
    fig, ax = plt.subplots(figsize=(10, 6))
    
    bin_centers = np.linspace(0, 1, bin_count)
    ax.bar(bin_centers, pk, width=1/bin_count, edgecolor='black')
    ax.set_title(f"乱数の分布(ビン数: {bin_count})")
    ax.set_xlabel("値")
    ax.set_ylabel("確率")
    ax.grid(True)
    
    # 一様分布との比較
    uniform_pk = np.ones(bin_count) / bin_count
    ax.plot(bin_centers, uniform_pk, 'r--', linewidth=2, label="理想的な一様分布")
    
    ax.legend()
    st.pyplot(fig)
    
    # エントロピーの評価
    if rel_entropy > 0.99:
        st.success("非常に高いエントロピー - ほぼ理想的なランダムネスと言えます")
    elif rel_entropy > 0.95:
        st.success("高いエントロピー - 良好なランダムネスです")
    elif rel_entropy > 0.9:
        st.info("十分なエントロピー - 実用的なランダムネスです")
    elif rel_entropy > 0.8:
        st.warning("中程度のエントロピー - 若干の偏りがあります")
    else:
        st.error("低いエントロピー - 偏りが大きく、ランダムとは言えません")


# 結論セクション
st.header("まとめ:コンピュータと真のランダムネス")

st.write("""
### ピュアランダムネスの難しさ

コンピュータは基本的に決定論的なマシンであり、真にランダムな数を生成することは原理的に困難です。
このシミュレーションでは、以下の3つの方法で乱数を生成しましたが、いずれも「疑似ランダム」に過ぎません:

1. **NumPy(疑似乱数)**: 数学的アルゴリズムに基づく高品質な疑似乱数
2. **Python標準(疑似乱数)**: 同様に数学的アルゴリズムに基づく疑似乱数
3. **時間ベース(より不確実性が高い)**: システム時間を利用した方法(ただし真のランダムとは言えない)

### 真のランダムネスを得るには

真のランダムネスを得るには、量子力学的現象や熱雑音など、決定論的に予測できない物理現象を利用する必要があります:

- **量子乱数生成器 (QRNG)**: 量子現象の本質的な不確定性を利用
- **環境ノイズ**: 大気ノイズや熱雑音など、予測困難な物理現象を測定

### 実用的な観点

多くのアプリケーションでは、高品質な疑似乱数で十分です。ただし、暗号や情報セキュリティなど一部の分野では、真のランダムネスが重要になることがあります。

### ランダムネスを評価する方法

ランダムネスを評価するには、複数の統計的テストを組み合わせることが重要です:

- 分布テスト(均一性)
- 自己相関分析(独立性)
- ランダムウォーク特性
- エントロピー測定

このシミュレーションで見たように、真にランダムなデータでも偶然パターンが現れることがあり、逆に非ランダムなデータでも特定のテストには合格することがあります。そのため、総合的な評価が必要です。
""")

# フッター
st.markdown("---")
st.caption("ピュアランダムネス・シミュレーション - ランダムネスの性質を探る")

いいなと思ったら応援しよう!