P04 Practice02 Search & Maximum#
for-loop + if-else: conditional operators
Counting:
把所有文字看過一遍,計算文字的出現次數。讀取一個文字字符串,使用for迴圈和if條件語句來計算每個字元或單詞出現的次數。例子: “apple” 中,’p’出現了2次。Filtering:
透過for迴圈和if條件語句來選擇並顯示特定的youbike站台,例如只顯示滿載的站台。例子: 從10個站台中找出滿載的站台。Detecting prominants:
透過比較來找到最大值和最小值Top value: 找出數字列表中的最大值。例子: [3, 9, 2] 中,最大值是9。
Top site:從youbike站台列表中找出擁有最高評分或最多使用者的站台。例子: “站台A”, “站台B”, “站台C” 中,”站台B” 最熱門。
Top 3 sites:找出前三個最熱門的站台。
Conditional Replacing:
使用if條件語句來檢測AQX數據中的缺失值,並規定當缺失值出現時如何進行替換或處理。也可用於檢查輸入日期的合法性。例子: 如果AQI值為NaN,則替換為平均AQI值。Sorting:
使用for迴圈來遍歷AQI數據,然後找出具有最高AQI或者前三高AQI的站台。 例子: 從10個AQI值中找出最大的或前三大的。Rescaling:
透過一個標準的映射表,將AQI數值轉換為五個等級:非常高、高、中、低、非常低,以便使用者更容易理解。例子: AQI 67轉換為”高”等級。
空氣品質指數(AQX)案例研究#
簡介與資料來源
這個案例研究旨在分析來自行政院環保署的即時空氣品質數據。這些數據可以在行政院環保署的空氣品質監測網站上查詢,並會按照AQI值用不同顏色代表其嚴重程度。更即時的數據也可在行政院環保署的開放資料平台找到。
分析策略
對於空氣品質或相似數據(如水污染、噪音、紫外線)的分析,以下是幾個主要的分析方向:
識別突出的數據點:找出AQI值最高(最嚴重)的地區。如果有多個地區同樣嚴重,那麼所有這些地區都應該被列出。這涉及到遍歷所有的數據,並透過比較大小找出最高的AQI值。
排序與篩選:除了找出最嚴重的地區,我們也可能對前三、前五或前10%的地區感興趣。這通常需要對數據進行排序。
數據摘要:計算各種統計量,例如平均值、四分位數,以及數據的分佈型態(如常態分佈或幂律分佈)。
描述數據分佈:了解不同等級的空氣品質在各測站中的分佈情況。這通常涉及到「分類」和「計數」的基本操作。
目標觀察與比較:例如,對比大都市和非大都市的空氣品質,特別是PM2.5或O3等指標。
重要性與溝通
為何需要這樣的分析?主要是為了將複雜的數據以容易理解的方式傳達給大眾。儘管數據分析主要是由專家進行,但其目的通常是為了解釋或傳達某種特定信息給一般人。為了做到這一點,使用簡單明瞭的語言是關鍵——例如,「大部分地區」、「最嚴重地區」或「前幾名」等。有了數據和統計支持,我們就可以更自信地,也更準確地傳達這些信息。
Load AQX data#
本案例所使用的資料仍然是網頁上json格式的資料,因此,仍需要用
requests函式庫將其取回,再用json()將json格式的文字解析為Python的dict與list。原始網頁為https://airtw.moenv.gov.tw/cht/EnvMonitoring/Central/CentralMonitoring.aspx。
json資料網址為https://airtw.moenv.gov.tw/gis_ajax.aspx?Type=GetAQInfo&Layer=EPA&QueryTime=2025/03/12%2011:00,其中QueryTime為查詢的時間,格式為YYYY/MM/DD HH:MM,例如2025/03/12 11:00。需要改為合理且近日的日期。
import requests
import json
url = "https://airtw.moenv.gov.tw/gis_ajax.aspx?Type=GetAQInfo&Layer=EPA&QueryTime=2025/09/19%2011:00"
aqdata = requests.get(url).json()
print("Type of aqdata:", type(aqdata))
print("Type of the first element in aqdata:", type(aqdata[0]))
Type of aqdata: <class 'list'>
Type of the first element in aqdata: <class 'dict'>
如果要更清楚地觀察,可以用json.dumps()將其轉回json格式的文字,並用縮排來顯示。
# print aqdata in pretty format
print(json.dumps(aqdata[:3], indent=4, ensure_ascii=False))
[
{
"SiteID": "1",
"SiteName": "二林",
"AQI": "43",
"ViewName": "二林",
"AreaName": "中部空品區",
"AreaID": "3",
"COUNTY_Eng": "Changhua",
"POLLUTANT": "",
"SiteAddres": "彰化縣二林鎮萬合里江山巷1號",
"TWD97_Lon": 120.409653,
"TWD97_Lat": 23.925175,
"SiteType": "一般站",
"SiteType2": "中央政府",
"siteowner": "環境部"
},
{
"SiteID": "2",
"SiteName": "三重",
"AQI": "29",
"ViewName": "三重",
"AreaName": "北部空品區",
"AreaID": "1",
"COUNTY_Eng": "Newtaipei",
"POLLUTANT": "",
"SiteAddres": "新北市三重區三和路重陽路交口",
"TWD97_Lon": 121.493806,
"TWD97_Lat": 25.072611,
"SiteType": "交通站",
"SiteType2": "中央政府",
"siteowner": "環境部"
},
{
"SiteID": "3",
"SiteName": "三義",
"AQI": "78",
"ViewName": "三義",
"AreaName": "竹苗空品區",
"AreaID": "2",
"COUNTY_Eng": "Miaoli",
"POLLUTANT": "細懸浮微粒",
"SiteAddres": "苗栗縣三義鄉西湖村上湖61-1號",
"TWD97_Lon": 120.75956754,
"TWD97_Lat": 24.38248443,
"SiteType": "一般站",
"SiteType2": "中央政府",
"siteowner": "環境部"
}
]
Traverse AQI content#
既然傳回來的資料看起來是個list of dict的結構,外層為一個list,不失一般性地取index為0的第一筆資料來觀察。
法則一:如果偵測出他是一個list,那就取出第0筆資料往下追蹤。
法則二:如果偵測出他是一個dict,就用
dict.keys()將其所有的key給列印出來,然後挑你所需要的key往下追蹤。但如果.keys()印出來的結果是類似流水號的id(e.g., youbike data),那就代表設計者把id對應到該id的資料。此時,也是不失一般性,取第一個id作為索引來往下存取即可。
# print the data type
# Print the content of the first data entry
# Print the length of the data
# Print all keys of one data entry
Print and observe each data entry#
列印出相關資料來做觀察,包含AQI、POLLUTANT(污染物種類)。如果資料筆數過多不易觀察的話,應該用List Slicing取出前數筆(例如alist[:10])或後數筆(例如alist[-3:])來觀察
二林 79 細懸浮微粒
三重 58 細懸浮微粒
三義 57 細懸浮微粒
土城 51 懸浮微粒
士林 57 細懸浮微粒
# Print AQI and POLLUTANT of each site
# for site in aqdata:
# print("{}\t{}\t{}".format(site['SiteName'], site['AQI'], site['POLLUTANT']))
# Print AQI and POLLUTANT of the first 10 sites
range() to traverse list by index#
因為
aqdata是一個list,也可以透過index去accessaqdata中第0個、第1個、第2個dict的內容。
但此時你就要去用index存取aqdata內的值,你的程式碼會稍微長一點點。range(n)函式,相當於range(0, n),可以產生0至n-1的list。
用list()轉換後(例如list(range(10)))便會顯示出其內容。range(s, t)表從s至t-1。range(s, t, u)表示從s至t-1,每間隔u,如果u等於2的話,就等於隔一個數字。所以range(3, 10, 2)會是[3, 5, 7, 9]。
Question: 什麼時候會用到?取樣的時候,例如多加一個if判斷式,來取出偶數位置的樣本。
print(list(range(10)))
print(list(range(0, 10)))
print(list(range(1, 10)))
print(list(range(1, 10, 2)))
for i in range(1, 10, 2):
print(i)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 3, 5, 7, 9]
1
3
5
7
9
而aqdata這個list的長度為len(aqdataa),其list index從0至len(aqdataa),所以使用for i in range(len(aqdata)):可以走訪整個list的內容。(註:以下的例子只列印出前10個)
enumerate() to access index and item simultaneously#
前面我們介紹到兩個版本,
for site in aqdata["Data"]:是用site作為臨時變數以在for-loop中存取所有的資料項目。for i in range(len(aqdata["Data"])):的做法則是用i作為index來存取原本的aqdata["Data"]。
但其實還有第三種選擇就是用enumerate()來同時存取index和資料項目。
當需要存取這是第幾個項目時,那我就用index,但我如果只是要判斷一下大小,那我就可以用被取出來的資料項目。
寫法如下:
for i, site in enumerate(aqdata):`
print(type(enumerate(aqdata)))
<class 'enumerate'>
Prepare data for search and query#
顯然AQI資料是一個非常標準的JSON格式,轉換為Python的資料型態就會變成list of dict。
但我希望可以用空品站名稱來查詢空品站的指標,要怎麼做呢?有兩種方法:
我可以用for-loop把所有資料跑一遍,如果等於該空品站名稱的話,那就列印出來。
我想要建立一個新的
aqdict,希望可以用aqdict["三重"]就直接列印出三重的資料。
Method 01. Search with for-loop#
就把所有資料用for-loop走訪一遍,如果站台名稱等於所欲查詢的站台的話,那就把他列印出來。
query = "三重"
queries = ["三重", "萬里", "萬華"]
Method 02. Convert data structure - hashing#
如果希望可以用
aqdict["三重"]就直接列印出三重的資料,有一個方法是重建一個新的以測站作為key的dict,這樣只需要有站名,就可以直接藉由key-to-value的對應找到該站的資料,所以要建的dict是站名對應到該空品站資料的對應。建立方法是把整個list of dict走訪過一遍,把list中每個dict assign給該dict中的測站名稱
site["SiteName"]
aqdict = {} # Initialize a new dictionary
Dump new dict to .json#
在Python中把資料dump成json會遠比csv來得方便,在R中則剛好相反。原因是Python的dictionary與list兩項資料結構恰好json的格式一一相符,所以非常容易就可以把資料dump出去或者read進來。如果是要dump成csv檔的話,就必須想辦法把資料整理成pandas dataframe的二維型態。
# json.dump(aqdict, open('aqi.json', 'w'))
Rescale AQI index#
前面說到一般大眾其實不了解AQI數值的意義,也不知道最大最小值是多少,所以在對外作呈現的時候,呈現AQI數值是沒意義的。大眾比較容易了解的是「現在嚴不嚴重」。因此,我們可以依照環保署對於空氣品質的等級劃分方法來將所有站台區分成幾個等級,相當於是按照數值做統計上的分層,將數值轉為factor的操作。
而程式邏輯上要做的事情是,用if-elif-else來設計好不同的區間判斷,然後用for-loop把所有AQI的值走訪過一遍,看每個站台落入哪個區間。

# for site in aqdata:
# print(site["SiteName"], site["AQI"], site["Quality"])
(Option) Rescale better#
為了避免過多的if-elif-else或者switch的使用,可以先把scale和category寫成兩個List,然後用多一層for-loop來trace該value會落在List的哪個區間。

degree = ["Unknown", "良好", "普通", "對敏感族群不健康", "對所有族群不健康", "非常不健康", "危害"]
scaler = [-1, 50, 100, 150, 200, 300, 1000]
sdict = {k:v for k, v in zip(scaler, degree)}
print(sdict)
# scaler = {-1:"Unknown", 50:"良好", 100:"普通", 150:"對敏感族群不健康", 200:"對所有族群不健康", 300:"非常不健康", 1000:"危害"}
{-1: 'Unknown', 50: '良好', 100: '普通', 150: '對敏感族群不健康', 200: '對所有族群不健康', 300: '非常不健康', 1000: '危害'}
Detect prominant data#
有了這類的資料後,可以做出什麼樣的資料描述呢?通常第一個想到的分析方法就是找到最嚴重、最差、變化最劇烈的地區。以下這是個非常好的例子說明要如何運用if和for找出AQI最高的地區。在過程中,必須要注意,原本的資料的資料型態為何,甚至要注意,原本的資料是否有缺漏。
這個案例企圖找出AQI最高的是哪些站台,且把該站台列印出來。注意,要列印的是AQI最高的站台,而不是最高的AQI值;此外還要注意,AQI最高的站台可能不只一個(這時候該怎麼處理?)。
Find the maximum#
找到最大值或最小值的概念:就個人的邏輯思考一下,要怎麼找到一群數的最大值?解題邏輯:把第一個數先拿來當標準,之後確認過每個數,有沒有比這個數更大的,若有的話,最大值就代換成找到的那個數,沒有的話,那第一個數就是最大值。
# finding the maximum value
# mina, maxa = 100, 0
# print maxa, a
# print(max_site, maxa)
# print(min_site, mina)
Use built-in functions#
這類比較大小的簡單功能一定都有人幫忙寫好了(如以下的例子),只要記得使用它就好。但是,要記得這樣的函式其實就是透過類似上述方法來比較大小以找到最大值。
alist = [5, 3, 2, 4, 1, 3, 2, 4, 7, 82, 19, 23, 42]
print(min(alist), max(alist), sum(alist), len(alist), float(sum(alist))/len(alist))
# sorted(aqlist, reverse = True)[0]
1 82 197 13 15.153846153846153
Find top n#
前面的例子是只要找最大值,所以只需要用一個變數來存放最大值。但現在的任務如果是要找到前n大,那就必須要有n個變數來儲存。往往實用上這個n還常常開放給使用者自行輸入,必須是可變動的。那最好的解決方法就是,乾脆把所有數值由大到小排列好,放在一個List裡面,然後看看要取前n大,就用List Slicing取出前面n個數值。
Sorted by sorted function#
開一個空的List,用For將所有資料走訪一遍,把所有的AQI的值給
append()在該List中。存起來以後,我用
sorted()這個函式把這些值做排序。
## Use a list to store all PM2.5 value
aqlist = []
for site in aqdata:
aqlist.append(int(site["AQI"]))
print(aqlist)
## sort the list by sorted()
[43, 29, 78, 15, 23, 29, 68, 33, 48, 48, 19, 58, 44, 46, 23, 13, 44, 55, 10, 38, 42, 13, 32, 59, 66, 68, 17, 61, 17, 24, 17, 38, 19, 28, 34, 38, 50, 40, 9, 33, 71, 51, 19, 19, 32, 10, 42, 55, 39, 41, 35, 97, 17, 12, 79, 11, 20, 34, 48, 48, 21, 22, 51, 42, 18, 42, 45, 38, 58, 45, 66, 78, 61, 27, 68, 25, 52, 19, 33, 34, 31, 17, 25, 14, 53, 58]
(Practice) Print out the site which values are larger than the first quarter#
有了排序後的List,就可以很方便地取一個閾值把要的資料取出來(最大值、前四分之一大的值或者是前三大的值),例如把大於該閾值的站台都給列印出來,即為所求。
(Practice) Print out sites with top 3 serious values#
取出Top-3的範例
print(sorted(aqlist, reverse=True)[3])
for site in aqdata:
if int(site['AQI']) >= aqlist[2]:
print(site['SiteName'], site['AQI'])
78
三義 78
湖口 97
新竹 79
龍潭 78
(Option) By comperhensive style#
aqlist = sorted([int(site['AQI']) for site in aqdata], reverse=True)
[(site['SiteName'], site['AQI']) for site in aqdata if int(site['AQI']) >= aqlist[2]]
[('三義', '78'), ('湖口', '97'), ('新竹', '79'), ('龍潭', '78')]
(Option) Sorting Algorithm by hand#
前例用了一個sorted函式就排序了所有的AQI的值,那這類sorted的排序函式是怎麼寫的呢?排序邏輯可以有很多種,但最容易理解的一種可以用兩層的排序演算法來達成。觀念很簡單:
先抓住第一個,一一和後面比較,看看有沒有人比它大,有的話,就和後面做置換(Swap),沒有的話就不動,這樣跑過一輪,就可以保證第一個最大。
接下來抓住第二個,一一和後面比較,看看有沒有人比它大,,有的話,就和後面做置換(Swap),沒有的話就不動,這樣跑過一輪,就可以保證第二個是次大的。
依此類推。
Reference
alist = [8, 3, 1, 3, 2, 4, 5]
alist = [8, 5, 1, 3, 2, 3, 4]
alist = [8, 5, 4, 1, 2, 3, 3]
alist = [8, 5, 4, 3, 1, 2, 3]
alist = [8, 5, 4, 3, 3, 1, 2]
alist = [8, 5, 4, 3, 3, 2, 1]
aqlist = []
site_list = []
for site in aqdata:x
if site['AQI'] != "" or site['AQI'] != "ND":
aqlist.append(int(site['AQI']))
site_list.append(site['SiteName'])
for i in range(len(aqlist)):
for j in range(i+1, len(aqlist)):
if aqlist[i] < aqlist[j]:
aqlist[i], aqlist[j] = aqlist[j], aqlist[i]
print(aqlist)
Input In [20]
if site['AQI'] != "" or site['AQI'] != "ND":
^
IndentationError: unexpected indent
Convert data to dataframe#
操作開放資料的時候,經常會為了方便處理,把資料轉成未來pandas可以處理的形式。而這樣的形式通常是list of dictionary。
AQX data to pandas dataframe#
import requests
import pandas as pd
url = "https://airtw.moenv.gov.tw/gis_ajax.aspx?Type=GetAQInfo&Layer=EPA&QueryTime=2025/09/12%2011:00"
aqdata = requests.get(url).json()
pd.DataFrame(aqdata).head()
| SiteID | SiteName | AQI | ViewName | AreaName | AreaID | COUNTY_Eng | POLLUTANT | SiteAddres | TWD97_Lon | TWD97_Lat | SiteType | SiteType2 | siteowner | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 二林 | 48 | 二林 | 中部空品區 | 3 | Changhua | 彰化縣二林鎮萬合里江山巷1號 | 120.409653 | 23.925175 | 一般站 | 中央政府 | 環境部 | |
| 1 | 2 | 三重 | 57 | 三重 | 北部空品區 | 1 | Newtaipei | 二氧化氮 | 新北市三重區三和路重陽路交口 | 121.493806 | 25.072611 | 交通站 | 中央政府 | 環境部 |
| 2 | 3 | 三義 | 27 | 三義 | 竹苗空品區 | 2 | Miaoli | 苗栗縣三義鄉西湖村上湖61-1號 | 120.759568 | 24.382484 | 一般站 | 中央政府 | 環境部 | |
| 3 | 4 | 土城 | 52 | 土城 | 北部空品區 | 1 | Newtaipei | 細懸浮微粒 | 新北市土城區學府路一段241號 | 121.451861 | 24.982528 | 一般站 | 中央政府 | 環境部 |
| 4 | 5 | 士林 | 33 | 士林 | 北部空品區 | 1 | Taipei | 臺北市北投區文林北路77號 | 121.516664 | 25.103340 | 一般站 | 中央政府 | 環境部 |
Youbike data to pandas dataframe#
新的Youbike的原始資料為list-of-dict的型態,相當容易可以被轉換成pandas的dataframe。
import requests
import json
response = requests.get('https://tcgbusfs.blob.core.windows.net/dotapp/youbike/v2/youbike_immediate.json')
data = response.json()
print("Type of ubike data:", type(data))
pd.DataFrame(data).head()
Type of ubike data: <class 'list'>
| sno | sna | sarea | mday | ar | sareaen | snaen | aren | act | srcUpdateTime | updateTime | infoTime | infoDate | Quantity | available_rent_bikes | latitude | longitude | available_return_bikes | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 500101001 | YouBike2.0_捷運科技大樓站 | 大安區 | 2025-09-20 22:52:15 | 復興南路二段235號前 | Daan Dist. | YouBike2.0_MRT Technology Bldg. Sta. | No.235, Sec. 2, Fuxing S. Rd. | 1 | 2025-09-20 22:52:29 | 2025-09-20 22:52:52 | 2025-09-20 22:52:15 | 2025-09-20 | 28 | 0 | 25.02605 | 121.54360 | 28 |
| 1 | 500101002 | YouBike2.0_復興南路二段273號前 | 大安區 | 2025-09-20 22:42:02 | 復興南路二段273號西側 | Daan Dist. | YouBike2.0_No.273, Sec. 2, Fuxing S. Rd. | No.273, Sec. 2, Fuxing S. Rd. (West) | 1 | 2025-09-20 22:52:29 | 2025-09-20 22:52:52 | 2025-09-20 22:42:02 | 2025-09-20 | 21 | 5 | 25.02565 | 121.54357 | 15 |
| 2 | 500101003 | YouBike2.0_國北教大實小東側門 | 大安區 | 2025-09-20 22:30:03 | 和平東路二段96巷7號 | Daan Dist. | YouBike2.0_NTUE Experiment Elementary School (... | No. 7, Ln. 96, Sec. 2, Heping E. Rd | 1 | 2025-09-20 22:52:29 | 2025-09-20 22:52:52 | 2025-09-20 22:30:03 | 2025-09-20 | 28 | 17 | 25.02429 | 121.54124 | 11 |
| 3 | 500101004 | YouBike2.0_和平公園東側 | 大安區 | 2025-09-20 22:10:02 | 和平東路二段118巷33號 | Daan Dist. | YouBike2.0_Heping Park (East) | No. 33, Ln. 118, Sec. 2, Heping E. Rd | 1 | 2025-09-20 22:52:29 | 2025-09-20 22:52:52 | 2025-09-20 22:10:02 | 2025-09-20 | 11 | 6 | 25.02351 | 121.54282 | 5 |
| 4 | 500101005 | YouBike2.0_辛亥復興路口西北側 | 大安區 | 2025-09-20 22:42:02 | 復興南路二段368號 | Daan Dist. | YouBike2.0_Xinhai Fuxing Rd. Intersection (Nor... | No. 368, Sec. 2, Fuxing S. Rd. | 1 | 2025-09-20 22:52:29 | 2025-09-20 22:52:52 | 2025-09-20 22:42:02 | 2025-09-20 | 16 | 15 | 25.02153 | 121.54299 | 1 |