資料科學第六週-期中了!用Streamlit展示文字資訊與圖表

在本週的部落格文章中,介紹了如何將DataFrame和資料視覺化圖表上線,使用的工具有Streamlit、Pandas和Plotly。透過建置網站和Python腳本,可以快速展示資料。另外,也介紹了Streamlit的安裝、文字顯示方式、欄位排版以及利用Plotly製作折線圖和長條圖。使用滑桿和側邊欄進行資料篩選和互動,讓資料呈現更生動。详细教學可參考[完整程式碼連結](https://taoyuan-data.streamlit.app/)。

前言-會使用到什麼讓網站上線

在這週,我們要將前面所學到的DataFrame與資料視覺化圖表的上線,可參考動態展示網站:

https://taoyuan-data.streamlit.app/

Image

若只是在本地(Local)端呈現,會使用到的技術包含:

  • Streamlit - 建置網站

  • Pandas and Plotly - 資訊分析與視覺化工具

如果要上雲端,讓人能夠連上,要學會的包含(我會寫在下一篇):

  • Venv - Python內建虛擬環境設定

  • Git and Github - 儲存和協作軟體開發專案的線上平台

  • Streamlit Cloud - 雲端部署網站的平台服務

Streamlit簡介

Streamlit 是一個開源的 Python 框架,專為快速建立美觀且互動性強的資料展示而設計,只要寫幾行程式碼,就可以將 Python 腳本轉變成互動式網頁應用。Let’s try it!

Streamlit安裝與匯入

1
pip install streamlit

創建一檔案這取名為app.py ,將套件匯入

1
import streamlit as st

Streamlit 文字類型

在使用文字時與平常記筆記的Notion類似,使用Markdown語法可以設定標題

Image

如圖,標題可以使用

1
st.title('桃園市相關數據展示')

使用title 等同於使用write 加上markdown語法#如下:

1
st.write('# 桃園市相關數據展示')

所以不想記太多函式的話,可以全部用write 也OK

次標題可以用##

1
st.write('## 中壢高商乙丙級通過率')

Streamlit 上線

這樣,執行後就可以在本地端測試了:

Image

會看到streamlit run 你的檔案位置 複製並輸入在命令提示字元就可以看到結果。

Image

接者每當你改動程式碼後存檔,只要重新整理頁面,就會跟改動了

更方便的是,你可以點選右上角的… > Settings

Image

再點選Run on save ,網頁就會在你存檔時,不用重整網頁,自動更新囉,非常快速方便吧!

Image

Streamlit 欄位排版

三欄式切版

可以在一行中指定要切成幾欄,例如我要切成三欄

1
col1, col2,col3 = st.columns(3)

每欄中有多行指令要設定的話,可以配上with ,還進行欄位的設定

官網範例

Image

metric

實作中使用metric 這個函式,當欄位是要強調「某個值或訊息」的時候,很適合使用,其中參數包含:

  • label:指標的名稱,用於描述數值的內容。

  • value:主顯示數值。

  • delta(可選):和之前數值的變化,用於顯示增加或減少的趨勢(通常是數字或百分比)。

  • delta_color(可選):控制變化值的顏色。

    • "normal":預設,增加顯示綠色,減少顯示紅色。

    • "inverse":反轉顏色,增加顯示紅色,減少顯示綠色。

  • help(可選):一段說明文字,當使用者將滑鼠懸停在指標名稱上時會顯示。

實作:

1
2
3
4
5
col1, col2,col3, col4 = st.columns(4)
col1.metric('112年度電乙本校參檢人數', 84, delta=-20, delta_color="inverse", help=None)
col2.metric('112年度電乙全國參檢人數', 6874, delta=7651-6874, delta_color="inverse", help=None)
col3.metric('112年度電乙本校通過率', f'{48/84*100:.2f}%', delta='-13.1%', delta_color="inverse", help=None)
col4.metric('112年度電乙全國通過率', f'{3370/6874*100:.2f}%', delta=f'{((3370/6874)-(3654/7651))*100:.2f}%', delta_color="inverse", help=None)

Image

和台灣不同,預設「漲是綠色、跌是紅色」,所以在delta_color 要使用inverse

不同比例切版

除了可以在一行中指定要切成幾欄,也可以利用list 的方式設定比例,如官網範例:

Image

Plotly製圖I - 繪折線圖

實作前幾節的資料,用plotly來呈現,幾個plotly的用法可以注意一下:

  • 載入Plotly中可以互動的元件
1
import plotly.graph_objects as go
  • 建立空的圖表
1
fig = go.Figure()
  • 將線型加入,將我要加入的地區用list 呈現

    • add_trace() :在fig中加入軌跡(線圖)

    • Scatter() :散布圖/折線圖,根據後面的mode 決定

    • name :這個參數呈現在圖例(legend)當中

1
2
for area in ['臺灣地區-元', '新北市-元', '臺北市-元', '桃園市-元']:
fig.add_trace(x=df['年'],y=df[area],mode='lines+markers',name=area)
  • 圖的額外設定,其中hovermode 可以設定x, y, closet ,互動時會依據方式來呈現資料
1
2
3
4
5
6
7
fig.update_layout(
#title='各地區收入變化',
xaxis_title='年份',
yaxis_title='收入 (元)',
hovermode='x',
width=700 # 設定圖表寬度
)
  • 在streamlit呈現出來,use_container_width=True 能讓圖形根據網頁大小而做變化,更符合RWD建議加入
1
st.plotly_chart(fig,use_container_width=True)

此區段程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import plotly.graph_objects as go
data = {
'年': [1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022],
'臺灣地區-元': [1070268, 1089575, 1091478, 1064136, 1064153, 1064825, 1074665, 1082168, 1099739, 1108674, 1099994, 1074180, 1071938, 1104265, 1122379, 1140271, 1157926, 1167284, 1194572, 1231112, 1249031, 1274196, 1293719, 1314023, 1340848],
'新北市-元': [1087242, 1102563, 1095192, 1098408, 1100003, 1093053, 1109639, 1135398, 1125605, 1099472, 1159279, 1110774, 1071131, 1116342, 1101389, 1129598, 1146991, 1171978, 1223867, 1265798, 1292753, 1319841, 1352548, 1381603, 1421385],
'臺北市-元': [1442675, 1472979, 1530735, 1505506, 1514440, 1501916, 1488180, 1514069, 1526228, 1550134, 1538257, 1515793, 1564298, 1537890, 1570778, 1545415, 1575819, 1581899, 1568945, 1648122, 1649348, 1723021, 1716591, 1732126, 1752411],
'桃園市-元': [1163181, 1174654, 1209363, 1143050, 1192731, 1163092, 1203215, 1192419, 1179444, 1210511, 1182721, 1146080, 1122238, 1183732, 1238698, 1257146, 1327963, 1307158, 1317790, 1337361, 1378732, 1392199, 1424027, 1448909, 1449549],
'臺中市-元': [1102701, 1137007, 1038480, 1055944, 1015972, 1023452, 990633, 988496, 1048174, 1105239, 1026631, 977731, 987259, 1100346, 1067060, 1126875, 1152523, 1169183, 1140325, 1245350, 1279865, 1298497, 1289700, 1304508, 1338826],
'臺南市-元': [911488, 896601, 933736, 872189, 898270, 936057, 943051, 881412, 950332, 972699, 976116, 924950, 886924, 948383, 927231, 996434, 991990, 1007093, 1063495, 1079199, 1086077, 1079174, 1086475, 1120580, 1143699],
'高雄市-元': [1029317, 1051762, 1031569, 1003108, 974592, 972280, 1011408, 1046729, 1030212, 1081799, 1068765, 1052162, 1052260, 1043941, 1085971, 1107383, 1112287, 1145895, 1166824, 1186204, 1219246, 1224668, 1224100, 1231562, 1263068],
'宜蘭縣-元': [976550, 1022506, 972996, 877199, 846963, 943028, 874576, 994967, 931588, 909097, 1006018, 904809, 971775, 877016, 1075706, 979013, 1071335, 1160320, 1085846, 1098807, 1069997, 1093475, 1138365, 1136419, 1165558],
'新竹縣-元': [1113042, 1167454, 1232173, 1106603, 1127123, 1133617, 1196515, 1304360, 1164274, 1098234, 1289463, 1281933, 1300116, 1372358, 1367712, 1346768, 1389453, 1283995, 1365150, 1616327, 1519478, 1539555, 1619782, 1689337, 1702134],
'苗栗縣-元': [943047, 945975, 935836, 944066, 921405, 949888, 895950, 931906, 1030851, 1000447, 919930, 926267, 964594, 1014144, 1012306, 1020185, 1100084, 1008241, 1166196, 1029485, 1045881, 1073028, 1161999, 1214424, 1273250],
'彰化縣-元': [951047, 891310, 897000, 845708, 943182, 962720, 945506, 906328, 961572, 922001, 902838, 890929, 887707, 920937, 953701, 936595, 940572, 926717, 994353, 970491, 997162, 1026792, 996066, 1086809, 1108221],
'南投縣-元': [875980, 1005079, 874344, 836262, 830936, 819394, 879780, 897891, 964953, 860836, 851992, 978331, 932725, 881743, 986881, 896557, 919551, 878760, 916199, 894368, 967369, 940893, 997605, 985686, 1048879],
'雲林縣-元': [782106, 889682, 810634, 873803, 817942, 809617, 849594, 769997, 906358, 794205, 767431, 744181, 748256, 817778, 824211, 838094, 865131, 876670, 896101, 868663, 860237, 933883, 988062, 1024191, 1016913],
'嘉義縣-元': [707349, 766905, 857615, 783819, 730084, 738385, 778574, 797895, 810323, 750236, 764933, 741766, 775017, 804668, 880625, 858253, 789406, 890742, 896217, 901022, 940780, 850597, 906554, 916631, 891498],
'屏東縣-元': [919710, 879362, 913848, 838027, 927042, 919906, 847872, 894772, 899657, 953471, 842372, 877498, 850116, 866795, 889560, 861063, 832681, 869818, 911258, 958999, 985681, 964547, 1064508, 1046302, 1083933],
'臺東縣-元': [702299, 725065, 780967, 760989, 774329, 737947, 755711, 695369, 753191, 860701, 699334, 805395, 674899, 733168, 796622, 799026, 820549, 746981, 797395, 889108, 884510, 874386, 891340, 911136, 916928],
'花蓮縣-元': [887914, 883537, 933625, 933484, 845923, 880361, 858345, 795376, 862688, 878356, 843646, 808632, 904472, 920602, 927400, 966607, 860613, 924706, 948501, 910090, 937898, 956973, 1032499, 1026488, 1010041],
'澎湖縣-元': [634053, 723916, 831230, 732306, 760406, 739670, 874714, 778659, 853872, 789752, 902952, 851835, 986954, 828441, 887077, 922916, 932694, 792696, 1069545, 857821, 959761, 1037554, 1091011, 796484, 919357],
'基隆市-元': [999088, 1058154, 1052113, 964170, 893884, 881716, 1048399, 1052699, 948817, 999562, 976639, 1026166, 1040931, 1018118, 955197, 1002341, 957360, 1072433, 1074314, 1096361, 1070748, 1140481, 1106963, 1111556, 1099526],
'新竹市-元': [1207558, 1268541, 1395702, 1419946, 1320717, 1344434, 1479816, 1361016, 1478303, 1554456, 1462204, 1426854, 1448209, 1479675, 1439066, 1535411, 1576797, 1427572, 1537317, 1572296, 1426379, 1602826, 1618903, 1602415, 1722889],
'嘉義市-元': [1059622, 958462, 1005451, 975877, 1015957, 927635, 832402, 984171, 966863, 899387, 898229, 836551, 842337, 925444, 1095203, 1241161, 1157962, 1106004, 1154411, 1090947, 1152499, 1208298, 1225219, 1217617, 1282798]
}


df = pd.DataFrame(data) #各地區年收資料
col_rowB_1, col_rowB_2 = st.columns((7,3))
with col_rowB_1:
st.write('### 桃園區與臺灣地區收入變化')
# 使用 plotly 建立折線圖
fig = go.Figure()

# 添加每個選取的地區到圖表中
for area in ['臺灣地區-元', '新北市-元', '臺北市-元', '桃園市-元']:
fig.add_trace(go.Scatter(x=df['年'], y=df[area], mode='lines+markers', name=area))

# 設定圖表標題和軸標籤
fig.update_layout(
#title='各地區收入變化',
xaxis_title='年份',
yaxis_title='收入 (元)',
hovermode='x',
width=700 # 設定圖表寬度

)
# 在 Streamlit 中顯示圖表
st.plotly_chart(fig,use_container_width=True)


with col_rowB_2:
st.write('### 原始資料')
st.write(df)

Image

側邊欄設定

側邊欄是Streamlit一個有趣的設計,可以讓使用者自行篩選資料,讓圖表視覺化的呈現更為彈性,以上面的地區收入變化為例:

側邊欄的標題

分為主標題st.sidebar.header 和次標題st.sidebar.subheader 非常直觀

1
st.sidebar.subheader('收入-選擇地區')

多選條件

可以選定不同地區做即時的比較,所以使用multiselect ,其中

  • option :代表所有可選擇的標籤

  • default :代表網頁一開始預設有的標籤

1
2
3
4
5
6
selected_areas = st.sidebar.multiselect(
'選擇地區',
options=df.columns[1:], # 不包含 '年' 欄位
default=['臺灣地區-元', '新北市-元', '臺北市-元', '桃園市-元'] # 預設選擇

)

側邊欄就被叫出來拉!

Image

因為要讓圖可以跟著調整,所以對原本圖的呈現進行修改:

要加入軌跡add_trace() 的線,改為由selected_areas 這個變數所控制,就這麼簡單!

1
2
for area in selected_areas:
fig.add_trace(go.Scatter(x=df['年'], y=df[area], mode='lines+markers', name=area))

修改後就可以及時控制圖囉

Image

側邊欄風格調整

另外因為Streamlit預設主視覺顏色很鮮豔,一樣可以透過Setting來做修改,點擊Setting後選擇「Edit active theme」即可變換主題顏色

Image

繪製原餅圖

這裡使用之前廟宇的資料,先透過sidebar進行設定,這樣在算「主祀神祇的比例」的時候,可以篩選,要計入的神明。

這邊要注意的是option_list ,因為原本df_count_ratio.index 得到的是一個Series,要轉成文字串列才能被選擇。

1
2
3
4
5
6
7
st.sidebar.subheader('比例-選擇主祀神祇')
options_list = df_count_ratio.index.astype(str).tolist()
selected_labels = st.sidebar.multiselect(
'選擇主祀神祇',
options=options_list,
default=options_list # 預設顯示全部
)

再來就差不多了,label 代表圓餅圖要出現的標籤,values 代表各個值,跟前面畫圖是一樣的

hole 則是用來設計中間的空心,改稱「甜甜圈圖」,可有可無。

1
2
3
4
5
6
7
8
9
10
11
12
# 使用 Plotly 繪製互動式圓餅圖
fig = go.Figure(
go.Pie(
labels=filtered_data.index,
values=filtered_data,
hole=0.3, # 设置为甜甜圈图表
textinfo='percent+label' # 显示百分比和标签
)
)

# 在 Streamlit 中顯示圖表
st.plotly_chart(fig)

此段完整程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
st.write('## 桃園區廟宇登記數量分析')
col_rowC_1, col_rowC_2 = st.columns((5,5))
with col_rowC_1:
st.write('### 桃園市各廟宇主祀神祇比例')

# 從 API 讀取 JSON 數據
data = pd.read_json('http://data.tycg.gov.tw/api/v1/rest/datastore/b2247404-3d92-4829-9855-0cd5e71b92b3?format=json&limit=500')
df = pd.DataFrame(data['result']['records'])

# 計算各主祀神祇的數量和比例
df_count = df['主祀神祇'].value_counts(normalize=True)
df_count_ratio = df_count / df_count.sum()
# 計算 "其他" 的比例
df_count_ratio['其他'] = df_count_ratio[df_count_ratio < 0.03].sum()

# 保留比例大於等於 3% 的項目,並包含 "其他"
df_count_ratio = df_count_ratio[df_count_ratio >= 0.03]
# 在 Streamlit sidebar 中創建多選篩選
# 轉換為純字串列表
st.sidebar.subheader('比例-選擇主祀神祇')
options_list = df_count_ratio.index.astype(str).tolist()
selected_labels = st.sidebar.multiselect(
'選擇主祀神祇',
options=options_list,
default=options_list # 預設顯示全部
)


# 根據選擇篩選數據
filtered_data = df_count_ratio[selected_labels]

# 使用 Plotly 繪製互動式圓餅圖
fig = go.Figure(
go.Pie(
labels=filtered_data.index,
values=filtered_data,
hole=0.3, # 设置为甜甜圈图表
textinfo='percent+label' # 显示百分比和标签
)
)

# 在 Streamlit 中顯示圖表
st.plotly_chart(fig)

繪製長條圖 和 滑桿側邊欄

樞紐分析表Pivot_table

在前面我們使用pivot_table 來做神明與教別的交叉數量分析,將pivot_table 顯示出來更方便了解:

1
2
3
4
5
6
7
8
9
# 建立交叉表,顯示不同主祀神祇所對應的教別數量
pivot_table = pd.pivot_table(
df,
index='主祀神祇',
columns='教別',
values='寺廟名稱',
aggfunc='count',
fill_value=0
).reset_index()

Image

因為等等要建置的滑桿最小值和最大值和「總廟數」有關,於是又加了一個欄位,將所有列,第一欄以外(iloc[:,1:])進行橫向加總(axis=1)

1
2
3

# 添加 "總寺廟數量"
pivot_table['總寺廟數量'] = pivot_table.iloc[:, 1:].sum(axis=1)

滑桿Slider

設定滑桿,找到最大值和最小值即可

1
2
3
4
5
6
min_count, max_count = st.sidebar.slider(
'選擇寺廟數量篩選範圍',
min_value=int(pivot_table['總寺廟數量'].min()),
max_value=int(pivot_table['總寺廟數量'].max()),
value=(int(pivot_table['總寺廟數量'].min()), int(pivot_table['總寺廟數量'].max()))
)

Image

長條圖繪製前,講一下長表格melt

因為要透過「教別」來進行分組的長條圖,可以想見,x軸是依教別分好組的神祇,y軸是寺廟數量。

因此我想讓表格如下圖,每個神祇對應到四種教別的寺廟數量

Image

但原本pivot_table中,教別在欄,這時候就適合用melt將欄融化成列!

melt 的三個參數:

  • id_vars - 代表主要不轉換的鍵

  • var_name - 代表要融化的欄

  • value_name - 代表表中還要保留的值

1
2
3
4
5
6
7
8
# 根據數量範圍篩選神祇
filtered_table = pivot_table[(pivot_table['總寺廟數量'] >= min_count) & (pivot_table['總寺廟數量'] <= max_count)]

# 去除 "總寺廟數量" 列,轉換為長格式
filtered_table_long = filtered_table.drop(columns='總寺廟數量').melt(id_vars='主祀神祇', var_name='教別', value_name='寺廟數量')
st.write(filtered_table_long)
# 去除寺廟數量為 0 的項目
filtered_table_long = filtered_table_long[filtered_table_long['寺廟數量'] > 0]

Image

繪製長條圖

轉換為長表格後,就可以用Plotly來進行Bar Chart的繪製,其中barmode 代表長條圖的顯示模式,也可以試著調整看看其他效果。

  • barmode='group':並排顯示條形,用於對比不同類別。

  • barmode='stack':堆疊顯示條形,用於展示類別總和及其貢獻。

  • barmode='overlay':覆蓋顯示條形,容易遮擋信息,不常用。

  • barmode='relative':相對堆疊顯示,展示各類別在 x 軸標籤下的比例。

1
2
3
4
5
6
7
8
9
10
11
12
13
# 使用 Plotly 繪製長條圖
fig = px.bar(
filtered_table_long,
x='主祀神祇',
y='寺廟數量',
color='教別',
barmode='group',
title='',
labels={'寺廟數量': '寺廟數量', '主祀神祇': '主祀神祇', '教別': '教別'}
)

# 在 Streamlit 中顯示長條圖
st.plotly_chart(fig)

以上為將之前資料放上Streamlit的過程教學,在實作時應該很夠大家消化一下了。

我把地圖folium 的功能放在下一篇分開講!順便記錄一下,十月的颱風,放了兩天!

最後放上完整程式碼連結:
我的Github連結,喜歡可以按星星!


資料科學第六週-期中了!用Streamlit展示文字資訊與圖表
https://codinglu.tw/2024/10/streamlit-dashboard/
作者
阿盧
發布於
2024年10月2日
許可協議
📌 本文瀏覽量: 0