Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +120 -83
src/streamlit_app.py
CHANGED
@@ -2,13 +2,9 @@ import streamlit as st
|
|
2 |
import pandas as pd
|
3 |
import numpy as np
|
4 |
|
5 |
-
#
|
6 |
|
7 |
def calculate_theoretical_pga(magnitude, distance):
|
8 |
-
"""
|
9 |
-
根據給定的地震動預估式(GMPE)計算理論PGA值
|
10 |
-
PGA = 1.657 * e^(0.533*M) * r^(-1.607)
|
11 |
-
"""
|
12 |
if distance <= 0:
|
13 |
return np.inf
|
14 |
exp_term = np.exp(0.533 * magnitude)
|
@@ -16,109 +12,150 @@ def calculate_theoretical_pga(magnitude, distance):
|
|
16 |
return 1.657 * exp_term * dist_term
|
17 |
|
18 |
def calculate_site_amplification(df):
|
19 |
-
"""
|
20 |
-
計算每個測站的平均場址放大因子 S
|
21 |
-
"""
|
22 |
df_copy = df.copy()
|
23 |
-
|
24 |
-
# 計算理論PGA
|
25 |
df_copy['theoretical_pga'] = df_copy.apply(
|
26 |
lambda row: calculate_theoretical_pga(row['magnitude'], row['distance_km']),
|
27 |
axis=1
|
28 |
)
|
29 |
-
|
30 |
-
# 計算單次放大效應
|
31 |
df_copy['single_event_amplification'] = df_copy['observed_pga'] / df_copy['theoretical_pga']
|
32 |
-
|
33 |
-
# 計算各站點平均放大因子
|
34 |
site_amplification_factors = df_copy.groupby('station_id')['single_event_amplification'].mean().reset_index()
|
35 |
site_amplification_factors.rename(columns={'single_event_amplification': 'site_amplification_factor_S'}, inplace=True)
|
36 |
-
|
37 |
return df_copy, site_amplification_factors
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
# --- Streamlit 介面 ---
|
40 |
|
41 |
-
st.set_page_config(page_title="
|
42 |
|
43 |
-
|
44 |
-
st.
|
45 |
-
|
46 |
-
|
47 |
-
""")
|
48 |
|
49 |
-
# --- 側邊欄:資料來源選擇 ---
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
("使用內建範例資料", "上傳自己的 CSV 檔案")
|
55 |
-
)
|
56 |
|
57 |
-
# 準備資料 DataFrame
|
58 |
input_df = None
|
59 |
-
|
60 |
if data_source == "使用內建範例資料":
|
61 |
-
st.sidebar.info("
|
62 |
data = {
|
63 |
-
'station_id': ['
|
64 |
-
'
|
65 |
-
'
|
66 |
-
'
|
67 |
-
'
|
|
|
|
|
|
|
68 |
}
|
69 |
input_df = pd.DataFrame(data)
|
70 |
-
|
71 |
else:
|
72 |
-
uploaded_file = st.sidebar.file_uploader("
|
73 |
-
if uploaded_file
|
74 |
input_df = pd.read_csv(uploaded_file)
|
75 |
-
st.sidebar.success("檔案上傳成功!")
|
76 |
-
else:
|
77 |
-
st.sidebar.warning("請上傳一個 CSV 檔。")
|
78 |
-
|
79 |
-
# --- 主畫面 ---
|
80 |
|
|
|
81 |
if input_df is not None:
|
82 |
-
st.header("
|
83 |
-
st.
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
else:
|
90 |
-
st.sidebar.header("Step 2: 開始計算")
|
91 |
-
if st.sidebar.button("🚀 計算場址放大因子 (S)"):
|
92 |
-
with st.spinner('正在進行計算中,請稍候...'):
|
93 |
-
# 執行計算
|
94 |
intermediate_df, final_factors_df = calculate_site_amplification(input_df)
|
95 |
-
|
96 |
-
|
97 |
-
st.
|
98 |
-
st.
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
else:
|
123 |
-
st.info("
|
124 |
|
|
|
2 |
import pandas as pd
|
3 |
import numpy as np
|
4 |
|
5 |
+
# --- 核心計算函式 (保持不變) ---
|
6 |
|
7 |
def calculate_theoretical_pga(magnitude, distance):
|
|
|
|
|
|
|
|
|
8 |
if distance <= 0:
|
9 |
return np.inf
|
10 |
exp_term = np.exp(0.533 * magnitude)
|
|
|
12 |
return 1.657 * exp_term * dist_term
|
13 |
|
14 |
def calculate_site_amplification(df):
|
|
|
|
|
|
|
15 |
df_copy = df.copy()
|
|
|
|
|
16 |
df_copy['theoretical_pga'] = df_copy.apply(
|
17 |
lambda row: calculate_theoretical_pga(row['magnitude'], row['distance_km']),
|
18 |
axis=1
|
19 |
)
|
|
|
|
|
20 |
df_copy['single_event_amplification'] = df_copy['observed_pga'] / df_copy['theoretical_pga']
|
|
|
|
|
21 |
site_amplification_factors = df_copy.groupby('station_id')['single_event_amplification'].mean().reset_index()
|
22 |
site_amplification_factors.rename(columns={'single_event_amplification': 'site_amplification_factor_S'}, inplace=True)
|
|
|
23 |
return df_copy, site_amplification_factors
|
24 |
|
25 |
+
# --- 創意功能函式 ---
|
26 |
+
|
27 |
+
def get_s_factor_interpretation(s_value):
|
28 |
+
"""將 S 因子轉換成白話文解說"""
|
29 |
+
if s_value > 2.0:
|
30 |
+
return f"🔴 **極顯著放大 ({s_value:.2f})**: 此處地質鬆軟,搖晃程度可能是堅硬岩盤地區的 **2倍以上**!需特別注意建築結構安全。"
|
31 |
+
elif s_value > 1.5:
|
32 |
+
return f"🟠 **顯著放大 ({s_value:.2f})**: 此處有明顯的場址放大效應,搖晃會比理論值強烈很多。"
|
33 |
+
elif s_value > 1.1:
|
34 |
+
return f"🟡 **輕微放大 ({s_value:.2f})**: 搖晃程度略高於理論值,存在一定的放大效應。"
|
35 |
+
elif s_value < 0.9:
|
36 |
+
return f"🔵 **減弱效應 ({s_value:.2f})**: 此處地質可能較為堅硬,搖晃程度反而比理論值小。"
|
37 |
+
else:
|
38 |
+
return f"🟢 **接近基準 ({s_value:.2f})**: 此處的搖晃程度與標準岩盤地區接近。"
|
39 |
+
|
40 |
+
def predict_pga_with_s(magnitude, distance, s_factor):
|
41 |
+
"""使用S因子預測一個假想地震的PGA"""
|
42 |
+
base_pga = calculate_theoretical_pga(magnitude, distance)
|
43 |
+
return base_pga * s_factor
|
44 |
+
|
45 |
# --- Streamlit 介面 ---
|
46 |
|
47 |
+
st.set_page_config(page_title="創意場址放大因子儀表板", layout="wide")
|
48 |
|
49 |
+
# 1. 標題與介紹
|
50 |
+
st.title("🌋 創意場址放大因子儀表板")
|
51 |
+
st.image("https://media.giphy.com/media/l41lGvinEgARjB2HC/giphy.gif", caption="地震波在地層中傳遞示意圖")
|
52 |
+
st.markdown("不僅計算 S 因子,更透過地圖、情境模擬,讓您『看見』並『感受』場址效應的威力!")
|
|
|
53 |
|
|
|
54 |
|
55 |
+
# 2. 側邊欄
|
56 |
+
st.sidebar.header("Step 1: 載入資料")
|
57 |
+
data_source = st.sidebar.radio("請選擇資料來源:", ("使用內建範例資料", "上傳自己的 CSV 檔案"))
|
|
|
|
|
58 |
|
|
|
59 |
input_df = None
|
|
|
60 |
if data_source == "使用內建範例資料":
|
61 |
+
st.sidebar.info("範例資料已加入經緯度(lat, lon),以用於地圖視覺化。")
|
62 |
data = {
|
63 |
+
'station_id': ['TPE', 'KHH', 'HUA', 'TPE', 'KHH', 'HUA', 'TPE', 'KHH', 'HUA', 'TPE', 'KHH', 'HUA'],
|
64 |
+
'lat': [25.0330, 22.6273, 23.9739, 25.0330, 22.6273, 23.9739, 25.0330, 22.6273, 23.9739, 25.0330, 22.6273, 23.9739],
|
65 |
+
'lon': [121.5654, 120.3014, 121.6059, 121.5654, 120.3014, 121.6059, 121.5654, 120.3014, 121.6059, 121.5654, 120.3014, 121.6059],
|
66 |
+
'earthquake_id': ['EQ1', 'EQ1', 'EQ1', 'EQ2', 'EQ2', 'EQ2', 'EQ3', 'EQ3', 'EQ3', 'EQ4', 'EQ4', 'EQ4'],
|
67 |
+
'magnitude': [6.2, 6.2, 6.2, 5.5, 5.5, 5.5, 7.0, 7.0, 7.0, 6.5, 6.5, 6.5],
|
68 |
+
'distance_km': [50, 200, 30, 80, 150, 60, 120, 250, 40, 40, 180, 25],
|
69 |
+
# KHH(高雄)的地質條件較軟,預期會有較大的放大效應
|
70 |
+
'observed_pga': [80, 40, 150, 30, 25, 60, 60, 35, 180, 150, 55, 280]
|
71 |
}
|
72 |
input_df = pd.DataFrame(data)
|
|
|
73 |
else:
|
74 |
+
uploaded_file = st.sidebar.file_uploader("上傳 CSV (需含 lat, lon 欄位)", type=["csv"])
|
75 |
+
if uploaded_file:
|
76 |
input_df = pd.read_csv(uploaded_file)
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
+
# 3. 主畫面
|
79 |
if input_df is not None:
|
80 |
+
st.sidebar.header("Step 2: 執行計算")
|
81 |
+
if st.sidebar.button("🚀 點我開始分析!"):
|
82 |
+
with st.spinner('科學計算中,請稍候...'):
|
83 |
+
required_cols = ['station_id', 'lat', 'lon', 'magnitude', 'distance_km', 'observed_pga']
|
84 |
+
if not all(col in input_df.columns for col in required_cols):
|
85 |
+
st.error(f"資料格式錯誤!請確保您的 CSV 包含以下欄位: {required_cols}")
|
86 |
+
else:
|
|
|
|
|
|
|
|
|
|
|
87 |
intermediate_df, final_factors_df = calculate_site_amplification(input_df)
|
88 |
+
# 將 station 的經緯度資訊合併到最終結果中
|
89 |
+
station_locations = input_df[['station_id', 'lat', 'lon']].drop_duplicates().set_index('station_id')
|
90 |
+
st.session_state.final_results = final_factors_df.join(station_locations, on='station_id')
|
91 |
+
st.session_state.intermediate_results = intermediate_df
|
92 |
+
st.success("計算完成!請查看下方分頁結果。")
|
93 |
+
|
94 |
+
if 'final_results' in st.session_state:
|
95 |
+
# 使用分頁呈現結果
|
96 |
+
tab1, tab2, tab3, tab4 = st.tabs(["📊 結果總覽", "🗺️ 地圖視覺化", "🔬 What-If 模擬器", "📄 資料詳情"])
|
97 |
+
|
98 |
+
final_df = st.session_state.final_results
|
99 |
+
|
100 |
+
with tab1:
|
101 |
+
st.header("📊 各測站平均場址放大因子 (S)")
|
102 |
+
col1, col2 = st.columns([0.5, 0.5])
|
103 |
+
with col1:
|
104 |
+
st.dataframe(final_df[['station_id', 'site_amplification_factor_S']].style.format({'site_amplification_factor_S': "{:.2f}"}))
|
105 |
+
st.bar_chart(final_df.set_index('station_id')['site_amplification_factor_S'])
|
106 |
+
|
107 |
+
with col2:
|
108 |
+
st.subheader("💬 結果白話文解說")
|
109 |
+
for index, row in final_df.iterrows():
|
110 |
+
st.markdown(f"**{row['station_id']} 測站:**")
|
111 |
+
st.markdown(get_s_factor_interpretation(row['site_amplification_factor_S']))
|
112 |
+
st.markdown("---")
|
113 |
+
|
114 |
+
with tab2:
|
115 |
+
st.header("🗺️ 場址放大效應地理分佈")
|
116 |
+
st.markdown("地圖上的點越大,���表該地的場址放大效應越強烈。")
|
117 |
+
|
118 |
+
# 為了讓地圖上的點大小差異更明顯,進行正規化
|
119 |
+
min_s = final_df['site_amplification_factor_S'].min()
|
120 |
+
max_s = final_df['site_amplification_factor_S'].max()
|
121 |
+
# 避免 max_s == min_s 的情況
|
122 |
+
if (max_s - min_s) > 0:
|
123 |
+
final_df['size'] = 50 + ((final_df['site_amplification_factor_S'] - min_s) / (max_s - min_s)) * 500
|
124 |
+
else:
|
125 |
+
final_df['size'] = 100
|
126 |
+
|
127 |
+
st.map(final_df, latitude='lat', longitude='lon', size='size', zoom=6)
|
128 |
+
|
129 |
+
with tab3:
|
130 |
+
st.header("🔬 What-If 情境模擬器")
|
131 |
+
st.markdown("如果現在發生一場地震,各地搖晃程度會是多少?")
|
132 |
+
|
133 |
+
sim_col1, sim_col2 = st.columns(2)
|
134 |
+
with sim_col1:
|
135 |
+
sim_mag = st.slider("設定假想地震規模 (M)", min_value=4.0, max_value=8.0, value=6.5, step=0.1)
|
136 |
+
with sim_col2:
|
137 |
+
sim_dist = st.number_input("設定您與震央的距離 (公里)", min_value=10, max_value=300, value=50)
|
138 |
+
|
139 |
+
st.markdown(f"#### 模擬結果:規模 **{sim_mag}**、距離 **{sim_dist}** 公里的地震")
|
140 |
+
|
141 |
+
sim_results = []
|
142 |
+
for index, row in final_df.iterrows():
|
143 |
+
predicted_pga = predict_pga_with_s(sim_mag, sim_dist, row['site_amplification_factor_S'])
|
144 |
+
sim_results.append({
|
145 |
+
"測站": row['station_id'],
|
146 |
+
"場址放大因子 (S)": f"{row['site_amplification_factor_S']:.2f}",
|
147 |
+
"預估搖晃程度 (PGA)": f"{predicted_pga:.2f}"
|
148 |
+
})
|
149 |
+
|
150 |
+
st.table(pd.DataFrame(sim_results))
|
151 |
+
st.info("💡 注意:PGA 越高,代表地表加速度越大,感受到的搖晃越劇烈。")
|
152 |
+
|
153 |
+
with tab4:
|
154 |
+
st.header("📄 詳細資料與計算過程")
|
155 |
+
with st.expander("點此查看原始觀測資料"):
|
156 |
+
st.dataframe(input_df)
|
157 |
+
with st.expander("點此查看詳細計算過程 (含單次放大效應)"):
|
158 |
+
st.dataframe(st.session_state.intermediate_results)
|
159 |
else:
|
160 |
+
st.info("請在左方側邊欄選擇資料來源,然後點擊按鈕開始分析。")
|
161 |
|