|
|
import gradio as gr |
|
|
import numpy as np |
|
|
import plotly.graph_objects as go |
|
|
from plotly.subplots import make_subplots |
|
|
import time |
|
|
|
|
|
class MeanAnalyzer: |
|
|
def __init__(self): |
|
|
self.reset() |
|
|
|
|
|
def reset(self): |
|
|
"""重置所有數據""" |
|
|
self.sequence = [100.0] |
|
|
self.multipliers = [] |
|
|
self.arithmetic_means = [100.0] |
|
|
self.geometric_means = [100.0] |
|
|
self.current_step = 0 |
|
|
|
|
|
def add_step(self, min_mult=0.1, max_mult=2.1, seed=None): |
|
|
"""添加一個新的步驟""" |
|
|
if seed is not None: |
|
|
np.random.seed(seed + self.current_step) |
|
|
|
|
|
|
|
|
multiplier = np.random.uniform(min_mult, max_mult) |
|
|
self.multipliers.append(multiplier) |
|
|
|
|
|
|
|
|
new_value = self.sequence[-1] * multiplier |
|
|
self.sequence.append(new_value) |
|
|
|
|
|
|
|
|
arithmetic_mean = np.mean(self.sequence) |
|
|
self.arithmetic_means.append(arithmetic_mean) |
|
|
|
|
|
|
|
|
geometric_mean = np.exp(np.mean(np.log(self.sequence))) |
|
|
self.geometric_means.append(geometric_mean) |
|
|
|
|
|
self.current_step += 1 |
|
|
|
|
|
return self.create_plot(), self.get_stats() |
|
|
|
|
|
def create_plot(self): |
|
|
"""創建四象限圖表""" |
|
|
fig = make_subplots( |
|
|
rows=2, cols=2, |
|
|
subplot_titles=('Sequence Values & Means', 'Mean Difference (Arithmetic - Geometric)', |
|
|
'Mean Ratio (Arithmetic / Geometric)', 'Statistics Summary'), |
|
|
specs=[[{'type': 'scatter'}, {'type': 'scatter'}], |
|
|
[{'type': 'scatter'}, {'type': 'table'}]], |
|
|
vertical_spacing=0.12, |
|
|
horizontal_spacing=0.15 |
|
|
) |
|
|
|
|
|
steps = list(range(len(self.sequence))) |
|
|
|
|
|
|
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=self.sequence, |
|
|
mode='lines+markers', name='Sequence', |
|
|
line=dict(color='blue', width=2), |
|
|
marker=dict(size=6)), |
|
|
row=1, col=1 |
|
|
) |
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=self.arithmetic_means, |
|
|
mode='lines', name='Arithmetic Mean', |
|
|
line=dict(color='red', dash='dash', width=2)), |
|
|
row=1, col=1 |
|
|
) |
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=self.geometric_means, |
|
|
mode='lines', name='Geometric Mean', |
|
|
line=dict(color='green', dash='dot', width=2)), |
|
|
row=1, col=1 |
|
|
) |
|
|
|
|
|
|
|
|
differences = [a - g for a, g in zip(self.arithmetic_means, self.geometric_means)] |
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=differences, |
|
|
mode='lines+markers', name='Difference', |
|
|
line=dict(color='purple', width=2), |
|
|
marker=dict(size=6)), |
|
|
row=1, col=2 |
|
|
) |
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=[0]*len(steps), |
|
|
mode='lines', name='Zero Line', |
|
|
line=dict(color='gray', dash='dash', width=1), |
|
|
showlegend=False), |
|
|
row=1, col=2 |
|
|
) |
|
|
|
|
|
|
|
|
ratios = [a/g if g != 0 else 1 for a, g in zip(self.arithmetic_means, self.geometric_means)] |
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=ratios, |
|
|
mode='lines+markers', name='Ratio', |
|
|
line=dict(color='orange', width=2), |
|
|
marker=dict(size=6)), |
|
|
row=2, col=1 |
|
|
) |
|
|
fig.add_trace( |
|
|
go.Scatter(x=steps, y=[1]*len(steps), |
|
|
mode='lines', name='Ratio=1', |
|
|
line=dict(color='gray', dash='dash', width=1), |
|
|
showlegend=False), |
|
|
row=2, col=1 |
|
|
) |
|
|
|
|
|
|
|
|
stats_data = [ |
|
|
['Current Step', str(self.current_step)], |
|
|
['Latest Value', f'{self.sequence[-1]:.2f}'], |
|
|
['Arithmetic Mean', f'{self.arithmetic_means[-1]:.2f}'], |
|
|
['Geometric Mean', f'{self.geometric_means[-1]:.2f}'], |
|
|
['Current Difference', f'{differences[-1]:.2f}'], |
|
|
['Current Ratio', f'{ratios[-1]:.3f}'], |
|
|
['Max Difference', f'{max(differences):.2f}'], |
|
|
['Avg Difference', f'{np.mean(differences):.2f}'] |
|
|
] |
|
|
|
|
|
fig.add_trace( |
|
|
go.Table( |
|
|
header=dict(values=['Statistic', 'Value'], |
|
|
fill_color='lightgray', |
|
|
align='left', |
|
|
font=dict(size=14, color='black', family='Arial Black')), |
|
|
cells=dict(values=[[row[0] for row in stats_data], |
|
|
[row[1] for row in stats_data]], |
|
|
fill_color='white', |
|
|
align='left', |
|
|
font=dict(size=13, color='black', family='Arial'), |
|
|
height=30) |
|
|
), |
|
|
row=2, col=2 |
|
|
) |
|
|
|
|
|
|
|
|
fig.update_xaxes(title_text="Steps", row=1, col=1) |
|
|
fig.update_xaxes(title_text="Steps", row=1, col=2) |
|
|
fig.update_xaxes(title_text="Steps", row=2, col=1) |
|
|
fig.update_yaxes(title_text="Value", row=1, col=1) |
|
|
fig.update_yaxes(title_text="Difference", row=1, col=2) |
|
|
fig.update_yaxes(title_text="Ratio", row=2, col=1) |
|
|
|
|
|
fig.update_layout( |
|
|
height=700, |
|
|
showlegend=True, |
|
|
title_text="Geometric Mean vs Arithmetic Mean - Real-time Analysis", |
|
|
title_font_size=20, |
|
|
hovermode='x unified' |
|
|
) |
|
|
|
|
|
return fig |
|
|
|
|
|
def get_stats(self): |
|
|
"""獲取當前統計信息""" |
|
|
if len(self.sequence) == 0: |
|
|
return "尚無數據" |
|
|
|
|
|
diff = self.arithmetic_means[-1] - self.geometric_means[-1] |
|
|
ratio = self.arithmetic_means[-1] / self.geometric_means[-1] if self.geometric_means[-1] != 0 else 1 |
|
|
|
|
|
stats_text = f""" |
|
|
### 📊 當前狀態 |
|
|
- **步驟數**: {self.current_step} |
|
|
- **序列最新值**: {self.sequence[-1]:.2f} |
|
|
- **算術平均**: {self.arithmetic_means[-1]:.2f} |
|
|
- **幾何平均**: {self.geometric_means[-1]:.2f} |
|
|
- **差異 (算術-幾何)**: {diff:.2f} |
|
|
- **比值 (算術/幾何)**: {ratio:.3f} |
|
|
|
|
|
### 💡 觀察重點 |
|
|
- 算術平均 {'>' if diff > 0 else '='} 幾何平均 {'✓ (符合數學定理)' if diff >= 0 else ''} |
|
|
- 差異程度: {'小' if diff < 5 else '中等' if diff < 20 else '大'} |
|
|
""" |
|
|
|
|
|
if len(self.multipliers) > 0: |
|
|
stats_text += f"\n- 最後使用的乘數: {self.multipliers[-1]:.3f}" |
|
|
|
|
|
return stats_text |
|
|
|
|
|
def animate_sequence(self, seed, length, min_mult, max_mult, speed): |
|
|
"""生成動畫序列""" |
|
|
self.reset() |
|
|
frames = [] |
|
|
|
|
|
for i in range(length): |
|
|
self.add_step(min_mult, max_mult, seed) |
|
|
frames.append((self.create_plot(), self.get_stats(), i+1)) |
|
|
|
|
|
return frames |
|
|
|
|
|
|
|
|
analyzer = MeanAnalyzer() |
|
|
|
|
|
def reset_sequence(initial_value): |
|
|
"""重置序列""" |
|
|
analyzer.reset() |
|
|
analyzer.sequence[0] = initial_value |
|
|
analyzer.arithmetic_means[0] = initial_value |
|
|
analyzer.geometric_means[0] = initial_value |
|
|
return analyzer.create_plot(), analyzer.get_stats(), "✅ 已重置序列" |
|
|
|
|
|
def add_single_step(seed, min_mult, max_mult): |
|
|
"""添加單步""" |
|
|
if seed == -1: |
|
|
seed = None |
|
|
plot, stats = analyzer.add_step(min_mult, max_mult, seed) |
|
|
return plot, stats, f"✅ 已添加步驟 {analyzer.current_step}" |
|
|
|
|
|
def run_animation(seed, length, min_mult, max_mult, speed): |
|
|
"""運行動畫""" |
|
|
if seed == -1: |
|
|
seed = None |
|
|
|
|
|
analyzer.reset() |
|
|
message = f"🎬 開始動畫 (共 {length} 步)..." |
|
|
|
|
|
for i in range(length): |
|
|
plot, stats = analyzer.add_step(min_mult, max_mult, seed) |
|
|
progress = f"進度: {i+1}/{length} 步" |
|
|
yield plot, stats, message, progress |
|
|
time.sleep(1.0 / speed) |
|
|
|
|
|
|
|
|
differences = [a - g for a, g in zip(analyzer.arithmetic_means, analyzer.geometric_means)] |
|
|
final_message = f""" |
|
|
### 🎬 動畫完成!序列分析摘要: |
|
|
- **總步驟數**: {length} |
|
|
- **最終序列值**: {analyzer.sequence[-1]:.2f} |
|
|
- **最終算術平均**: {analyzer.arithmetic_means[-1]:.2f} |
|
|
- **最終幾何平均**: {analyzer.geometric_means[-1]:.2f} |
|
|
- **最大差異**: {max(differences):.2f} |
|
|
- **平均差異**: {np.mean(differences):.2f} |
|
|
- **最終比值**: {analyzer.arithmetic_means[-1]/analyzer.geometric_means[-1]:.3f} |
|
|
|
|
|
### 💡 關鍵洞察 |
|
|
- 算術平均始終 ≥ 幾何平均 ✓ |
|
|
- 差異隨著數列波動而變化 |
|
|
- 幾何平均更適合計算成長率 |
|
|
""" |
|
|
yield plot, stats, final_message, f"完成 {length}/{length} 步" |
|
|
|
|
|
def create_demo(): |
|
|
"""創建 Gradio 界面""" |
|
|
with gr.Blocks(title="幾何平均 vs 算術平均", theme=gr.themes.Soft()) as demo: |
|
|
gr.Markdown(""" |
|
|
# 🔢 幾何平均數 vs 算術平均數 - 差異分析器 |
|
|
|
|
|
此介面為-國立高雄科技大學 114學年-財管系-財管系二甲 統計學(一) 上課教材 |
|
|
|
|
|
> 探索兩種平均數在隨機序列中的差異演變,理解為什麼投資報酬率要用幾何平均計算! |
|
|
|
|
|
### 📚 數學概念 |
|
|
- **算術平均**: `(a₁ + a₂ + ... + aₙ) / n` - 一般的平均值 |
|
|
- **幾何平均**: `(a₁ × a₂ × ... × aₙ)^(1/n)` - 適合計算平均成長率 |
|
|
- **定理**: 算術平均 ≥ 幾何平均(AM-GM 不等式) |
|
|
""") |
|
|
|
|
|
with gr.Tabs(): |
|
|
|
|
|
with gr.Tab("🎮 互動模式"): |
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### 🎛️ 控制面板") |
|
|
|
|
|
seed_input = gr.Number( |
|
|
label="🎲 隨機種子 (-1 為真隨機)", |
|
|
value=42, |
|
|
precision=0 |
|
|
) |
|
|
|
|
|
initial_value = gr.Number( |
|
|
label="🏁 初始值", |
|
|
value=100, |
|
|
minimum=1 |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
min_mult = gr.Slider( |
|
|
label="📉 最小乘數", |
|
|
minimum=0.1, |
|
|
maximum=1.0, |
|
|
value=0.5, |
|
|
step=0.1 |
|
|
) |
|
|
max_mult = gr.Slider( |
|
|
label="📈 最大乘數", |
|
|
minimum=1.0, |
|
|
maximum=3.0, |
|
|
value=1.5, |
|
|
step=0.1 |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
reset_btn = gr.Button("🔄 重置", variant="secondary") |
|
|
add_btn = gr.Button("➕ 添加步驟", variant="primary") |
|
|
|
|
|
status_text = gr.Textbox( |
|
|
label="狀態訊息", |
|
|
value="✅ 就緒", |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
stats_display = gr.Markdown(analyzer.get_stats()) |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
gr.Markdown("### 📈 實時比較圖表") |
|
|
plot_display = gr.Plot(value=analyzer.create_plot()) |
|
|
|
|
|
|
|
|
with gr.Tab("🎬 動畫模式"): |
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
gr.Markdown("### 🎥 動畫設置") |
|
|
|
|
|
anim_seed = gr.Number( |
|
|
label="🎲 隨機種子 (-1 為真隨機)", |
|
|
value=42, |
|
|
precision=0 |
|
|
) |
|
|
|
|
|
anim_length = gr.Slider( |
|
|
label="📏 序列長度", |
|
|
minimum=10, |
|
|
maximum=100, |
|
|
value=30, |
|
|
step=5 |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
anim_min_mult = gr.Slider( |
|
|
label="📉 最小乘數", |
|
|
minimum=0.1, |
|
|
maximum=1.0, |
|
|
value=0.5, |
|
|
step=0.1 |
|
|
) |
|
|
anim_max_mult = gr.Slider( |
|
|
label="📈 最大乘數", |
|
|
minimum=1.0, |
|
|
maximum=3.0, |
|
|
value=1.5, |
|
|
step=0.1 |
|
|
) |
|
|
|
|
|
anim_speed = gr.Slider( |
|
|
label="⚡ 播放速度", |
|
|
minimum=1, |
|
|
maximum=10, |
|
|
value=3, |
|
|
step=1 |
|
|
) |
|
|
|
|
|
play_btn = gr.Button("▶️ 播放完整動畫", variant="primary", size="lg") |
|
|
|
|
|
anim_progress = gr.Textbox( |
|
|
label="進度", |
|
|
value="等待開始...", |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
anim_message = gr.Markdown("### 準備就緒") |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
gr.Markdown("### 🎬 動畫圖表") |
|
|
anim_plot = gr.Plot(value=analyzer.create_plot()) |
|
|
anim_stats = gr.Markdown("等待動畫開始...") |
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
### 🎓 學習指南 |
|
|
|
|
|
1. **互動模式**:手動控制每一步,仔細觀察差異變化 |
|
|
2. **動畫模式**:自動播放完整序列,看整體趨勢 |
|
|
3. **實驗建議**: |
|
|
- 嘗試不同的乘數範圍,看差異如何變化 |
|
|
- 使用相同種子,改變參數對比結果 |
|
|
- 觀察極端情況(很小或很大的乘數範圍) |
|
|
|
|
|
### 💡 重要發現 |
|
|
- 當數列波動越大,兩種平均數的差異越明顯 |
|
|
- 幾何平均對極端值(特別是接近 0 的值)更敏感 |
|
|
- 這就是為什麼計算投資報酬率要用幾何平均! |
|
|
""") |
|
|
|
|
|
|
|
|
reset_btn.click( |
|
|
fn=reset_sequence, |
|
|
inputs=[initial_value], |
|
|
outputs=[plot_display, stats_display, status_text] |
|
|
) |
|
|
|
|
|
add_btn.click( |
|
|
fn=add_single_step, |
|
|
inputs=[seed_input, min_mult, max_mult], |
|
|
outputs=[plot_display, stats_display, status_text] |
|
|
) |
|
|
|
|
|
|
|
|
play_btn.click( |
|
|
fn=run_animation, |
|
|
inputs=[anim_seed, anim_length, anim_min_mult, anim_max_mult, anim_speed], |
|
|
outputs=[anim_plot, anim_stats, anim_message, anim_progress] |
|
|
) |
|
|
|
|
|
return demo |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo = create_demo() |
|
|
demo.launch(share=True) |