# Lesson 3 | 進階資料型態與檔案讀寫
* 基於對於變數、資料型態、條件判斷以及迴圈的了解,我們已經有能力撰寫簡單的應用腳本。
* 在處理更複雜的任務時,我們可能還需要學會關於:
* 載入第三方套件(third party imports)。
* 操作高維資料型態。
* 讀取與寫入檔案。
## 第一節:NumPy簡介(1)
* 在Python裡常用這個功能強大的第三方套件(Package)[NumPy](https://numpy.org/)來處理二維、三維或是高維的資料。

* 讓我們先學習安裝第三方套件。
* 我們可以在Install的分頁看到相關說明。

* 以安裝於Windows環境為例,我們在cmd(命令提示字元)中輸入該指令來安裝NumPy。

* 安裝成功後,我們試著載入NumPy套件,這是Python常規載入套件(模組)的方式。
```python=
import numpy as np
```
* 同學們請先複製貼上下段程式碼,載入套件是不是很簡單呢?
```python=
x = np.ones(5)
print(x)
```
## 第一節:NumPy簡介(2)
* 我們要如何建構高維資料呢?可以利用NumPy套件底下的array()函式。
* 這個函式以list作為輸入:
```python=
a = np.array([2, 3, 4])
print(a)
```
* 可以建構不同資料型態的陣列(array),但陣列內必為相同的資料型態。
```python=
b = np.array([1.2, 3.5, 5.1])
print(b)
c = np.array(["A", "B", "c"])
print(c)
```
* 注意,若list內含有不同資料型態,NumPy則會強迫轉換陣列內的資料型態,使其一致。
```python=
d = np.array(["A", "B", 1])
print(d)
```
## 第一節:NumPy簡介(3)
* 我們要如何指定陣列的維度呢?
* 我們可以在NumPy的array()後面多使用一個reshape()。順序倒著數,最後一個軸(axis)是由左至右,倒數第二個是由上至下,接下來的維度皆由上至下。
```python=
# 1d array
array_1d = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
print(array_1d)
# 2d array
array_2d = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]).reshape(2, 6)
print(array_2d)
# 3d array
array_3d = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]).reshape(3, 2, 2)
print(array_3d)
```
* 我們可以用下列方法查詢ndarray的基本狀態。
* .size - 可以用來查詢此陣列的總長度。
```python=
# 1d array
array_1d = np.array([1, 2, 3, 4, 5, 6, 7, 8])
print(array_1d.size)
# 2d array
array_2d = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(2, 4)
print(array_2d.size)
# 3d array
array_3d = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(2, 2, 2)
print(array_3d.size)
```
* .shape - 用來查詢陣列的維度。
```python=
# 1d array
array_1d = np.array([1, 2, 3, 4, 5, 6, 7, 8])
print(array_1d.shape)
# 2d array
array_2d = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(2, 4)
print(array_2d.shape)
# 3d array
array_3d = np.array([1, 2, 3, 4, 5, 6, 7, 8]).reshape(2, 2, 2)
print(array_3d.shape)
```
* 值得注意的是,這些跟以往不同的變數與函式,同學們請先暫時嘗試著熟悉。
* 「.size」與「.shape」是我們第一次接觸到「物件」(object)的「屬性」(attribute)。
* 而「.reshape()」是我們第一次接觸到「物件」(object)的「方法」(method),方法就是「a function that “belongs to” an object」。
## 第一節:NumPy簡介(4)
* 我們也可以手動輸入巢狀list來創建多維矩陣。
```python=
e = np.array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10, 11]])
print(e)
print(e.shape)
```
* NumPy的好用之處在於可以進行整個陣列的四則運算。
```python=
e = np.array([[1, 2, 3],
[4, 5, 6]])
print(e + 5)
print(e - 5)
print(e * 5)
print(e / 5)
```
* 兩兩陣列之間也可以進行四則運算。
```python=
e = np.array([[1, 2, 3],
[4, 5, 6]])
f = np.array([[2, 2, 2],
[2, 2, 2]])
print(e + f)
print(e - f)
print(e * f)
print(e / f)
```
* 當然也有支援矩陣運算包含矩陣轉至、矩陣乘法以及反矩陣。
```python=
A = np.array([[1, 2, 3, 4]])
B = np.array([[4, 3, 2, 1]])
# 矩陣轉至
A_t = np.transpose(A)
print(A_t)
# 矩陣乘法
print(A_t @ B)
# 反矩陣
C = np.array([[1, 2],
[3, 4]])
print(np.linalg.inv(C))
```
## 第一節:NumPy簡介(5)
* 相信同學們已經體會NumPy在處理數學運算的好用之處,以下操作更是重要。
* 支援索引,與索引list的相同操作邏輯。
```python=
a_list = [1, 2, 3, 4, 5, 6, 7, 8]
a_array = np.array(a_list)
print(a_list[2])
print(a_list[2:5])
print(a_array[2])
print(a_array[2:5])
```
* 支援條件判斷,可以對整個矩陣進行條件判斷與索引。
```python=
b_array = np.array([1, 2, 3, 4, 5, 6, 7, 8])
print(b_array < 6)
print(b_array[b_array < 6])
```
* 還可以透過NumPy的where()函式回傳對應的位置,這些技巧相當重要!
```python=
c_array = np.array([4, 5, 6, 7, 8, 9, 10])
print(np.where(c_array > 6))
```
* 相同的索引概念可以推廣到二維,注意,索引一樣是從0開始。
```python=
d_array = np.array([[4, 5, 6, 7],
[8, 9, 10, 11]])
print(d_array[0, 0])
print(d_array[1, 2])
```
## 練習1:
* 你現在已經初步掌握了NumPy,現在我們想產生一張九九乘法表。
* 問題1:你是否能夠利用雙層迴圈將其填滿?
<details>
<summary>解答</summary>
- 關鍵在於index是從0開始
```python=
x = np.array([0]*81).reshape(9, 9)
for i in range(9):
for j in range(9):
x[i, j] = (i+1) * (j+1)
print(x)
```
</details>
* 問題2:你是否能『不依靠』迴圈產生九九乘法表呢?
<details>
<summary>解答</summary>
- 利用矩陣運算可以更簡潔的產生九九乘法表。
```python=
A = np.array(range(1, 10)).reshape(9, 1)
B = np.array(range(1, 10)).reshape(1, 9)
print(A @ B)
```
</details>
## 第二節:NumPy在資料探勘之應用(1)
* 接下來,讓同學們稍微體會NumPy的資料索引的好用之處。
* 這裡是60隻豚鼠餵食維他命C補充劑(VC)或是柳橙汁(OJ)後牙齒成長的長度,而dose代表對應的維他命C劑量。
```python=
len_of_tooth = [4.2, 11.5, 7.3, 5.8, 6.4, 10.0, 11.2, 11.2, 5.2, 7.0, 16.5, 16.5, 15.2, 17.3, 22.5, 17.3, 13.6, 14.5, 18.8, 15.5, 23.6, 18.5, 33.9, 25.5, 26.4, 32.5, 26.7, 21.5, 23.3, 29.5, 15.2, 21.5, 17.6, 9.7, 14.5, 10.0, 8.2, 9.4, 16.5, 9.7, 19.7, 23.3, 23.6, 26.4, 20.0, 25.2, 25.8, 21.2, 14.5, 27.3, 25.5, 26.4, 22.4, 24.5, 24.8, 30.9, 26.4, 27.3, 29.4, 23.0]
supp = ['VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC' ,'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ']
dose = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0]
```
* 我們可以藉由NumPy來簡單地分層出不同餵食方式下的牙齒成長長度。
```python=
# 先將資料轉換成ndarray
len_of_tooth = np.array(len_of_tooth)
supp = np.array(supp)
dose = np.array(dose)
len_vc = len_of_tooth[supp == "VC"]
len_oj = len_of_tooth[supp == "OJ"]
```
* 我們也可以拿到不同維他命攝取下的牙齒成長長度,請同學善用之前所學之函式。
```python=
# 以set來觀察有幾種dose
dose_cato = list(set(dose))
print(dose_cato)
len_dose_a = len_of_tooth[dose == dose_cato[0]]
len_dose_b = len_of_tooth[dose == dose_cato[1]]
len_dose_c = len_of_tooth[dose == dose_cato[2]]
```
* 接著我們藉由描述性統計來探勘資料。同學們請善用[官方文件](https://numpy.org/doc/1.23/reference/index.html)來查詢你所需要的函式。
```python=
# 算數平均術 mean(), 標準差 std()
print("VC:", np.mean(len_vc), "±", np.std(len_vc))
print("OJ:", np.mean(len_oj), "±", np.std(len_oj))
print("Dose A:", np.mean(len_dose_a), "±", np.std(len_dose_a))
print("Dose B:", np.mean(len_dose_b), "±", np.std(len_dose_b))
print("Dose C:", np.mean(len_dose_c), "±", np.std(len_dose_c))
```
* 這樣子同學們就完成了第一個簡單的資料探勘任務!
## 練習2:
* 問題1:在豚鼠餵食維他命C的資料集裡,似乎餵食柳橙汁有較好的牙齒成長。現在請同學幫我找出餵食柳橙汁中,不同劑量下的牙齒成長長度(以算術平均數與標準差呈現)。
```python=
len_of_tooth = [4.2, 11.5, 7.3, 5.8, 6.4, 10.0, 11.2, 11.2, 5.2, 7.0, 16.5, 16.5, 15.2, 17.3, 22.5, 17.3, 13.6, 14.5, 18.8, 15.5, 23.6, 18.5, 33.9, 25.5, 26.4, 32.5, 26.7, 21.5, 23.3, 29.5, 15.2, 21.5, 17.6, 9.7, 14.5, 10.0, 8.2, 9.4, 16.5, 9.7, 19.7, 23.3, 23.6, 26.4, 20.0, 25.2, 25.8, 21.2, 14.5, 27.3, 25.5, 26.4, 22.4, 24.5, 24.8, 30.9, 26.4, 27.3, 29.4, 23.0]
supp = ['VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC' ,'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'VC', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ', 'OJ']
dose = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0]
```
<details>
<summary>解答</summary>
- 多重判斷是這題的關鍵。
```python=
len_of_tooth = np.array(len_of_tooth)
supp = np.array(supp)
dose = np.array(dose)
dose_cato = list(set(dose))
len_oj_a = len_of_tooth[(supp == "OJ") & (dose == dose_cato[0])]
len_oj_b = len_of_tooth[(supp == "OJ") & (dose == dose_cato[1])]
len_oj_c = len_of_tooth[(supp == "OJ") & (dose == dose_cato[2])]
print("Dose A:", np.mean(len_oj_a), "±", np.std(len_oj_a))
print("Dose B:", np.mean(len_oj_b), "±", np.std(len_oj_b))
print("Dose C:", np.mean(len_oj_c), "±", np.std(len_oj_c))
```
</details>
## 第三節:讀取檔案(1)
* 不論是何種任務,資料都需要透過讀取檔案或是雲端API「讀進」Python內才能進一步處裡。
* 請按[這裡](https://linchin.ndmctsgh.edu.tw/data/monitoring_1.csv)下載等等要讀取的檔案,並將其放到下jupyter notebook指令的同個資料夾下。
* 這個檔案是105年4月25日早上5點30台北市各監測站,各空氣汙染物濃度的檔案。
* 我們將對這個檔案進行資料清理。
* 使用函式open()來讀取csv檔,第二個參數為模式,「'r'」表示只能讀取,「'w'」表示只能寫入。
```python=
f = open('monitoring_1.csv', 'r', encoding = 'CP950')
```
* 請同學們使用複合語句當中的「with」,「with」能讓程式碼在結束後自動關閉讀取的檔案。
* 可以一行一行讀成str。
```python=
with open('monitoring_1.csv', 'r', encoding = 'CP950') as f:
read_by_line = f.readline()
print(read_by_line)
read_by_line = f.readline()
print(read_by_line)
read_by_line = f.readline()
print(read_by_line)
```
* 也可以一次讀成一個由str所組成的list。
```python=
with open('monitoring_1.csv', 'r', encoding = 'CP950') as f:
read_by_lines = f.readlines()
print(read_by_lines)
```
* 注意。這裡也是用到了物件裡面的方法,但在這個任務上,這不是我們要的格式。
## 第三節:讀取檔案(2)
* 我們將使用Python的[標準函式庫(Standard Library)](https://docs.python.org/3.8/library/index.html),也就是相較於第三方套件而言,不需要額外安裝的模組。
* 我們載入```csv```模組來幫助我們讀取csv檔案。
```python=
import csv
```
```python=
with open('monitoring_1.csv', 'r', encoding = 'CP950') as file:
data = csv.reader(file)
print(data)
```
## 練習3:
* 問題1:但我們要怎麼稍微看一下讀進來的```data```怎麼用呢?請同學們試著閱讀官方說明。
<details>
<summary>解答</summary>
- 同學們可以在[這裡](https://docs.python.org/3.8/library/csv.html)找到範例。
```python=
with open('monitoring_1.csv', 'r', encoding = 'CP950') as file:
data = csv.reader(file)
for row in data:
print(row)
```
</details>
## 第四節:字典進階應用(1)
* 讀入資料並觀察時,我們湊巧發現了一件事情,那就是第5列和第6列似乎重複了,而這個檔案似乎有不只一處的重複資料,因此我們要開始做資料清理。
* 我們希望將資料轉換成字典以方便整理,在Python裡面,字典dictionary(dict)是除了list之外另一個常用處理多個變數的內建型態。
* 同學們還記得怎麼創建字典嗎?我們可以將dict視為「鍵值對」(key: value pair) 的集合。
```python=
tel = {'jack': 18574, 'sape': 18145}
```
* 字典可以直接以key作為索引,找到其value,當然也就可以重新指派變數。
```python=
class_room = dict()
class_room["醫學研究室"] = 7115
class_room["生物化學科"] = 7331
class_room["生命科學研究所"] = 7258
print(class_room)
```
## 第四節:字典進階應用(2)
* 我們可以用一些dict的方法叫出key或value。
```python=
class_room = dict() # 或是 class_room = {}
class_room["醫學研究室"] = 7115
class_room["生物化學科"] = 7331
class_room["生命科學研究所"] = 7258
# 回傳key
print(class_room.keys())
# 回傳value
print(class_room.values())
# 回傳key與value
print(class_room.items())
```
* 搭配迴圈是非常常見的使用情境。
```python=
class_room = dict() # 或是 class_room = {}
class_room["醫學研究室"] = 7115
class_room["生物化學科"] = 7331
class_room["生命科學研究所"] = 7258
for k, v in class_room.items():
print(k, v)
for k in class_room:
print(k, class_room[k])
```
## 第五節:字典與檔案讀寫(1)
* 那我們要如何將檔案讀成字典的格式呢?可以在[官方文件](https://docs.python.org/zh-tw/3.8/library/csv.html)找到關於```csv```的相關說明。
```python=
with open('monitoring_1.csv', 'r', encoding = 'CP950') as f:
reader = csv.DictReader(f)
# .fieldnames可以找到column names
headers = reader.fieldnames
air_data = {}
for i in headers:
air_data[i] = []
for row in reader:
for k, v in row.items():
# 這邊我們用到了list的方法append(),可以依序增加item。
air_data[k].append(v)
print(air_data)
```
## 第五節:字典與檔案讀寫(2)
* 現在,我們先練習將檔案寫出。
```python=
with open('monitoring_1_wback.csv', 'w', encoding = 'CP950') as f:
writer = csv.writer(f)
# 先將欄位寫出
writer.writerow(air_data.keys())
# 再將資料寫出
writer.writerows(zip(*air_data.values()))
```
* 這邊用到了另外一個沒教過的zip()函式,他可以用來"壓縮"list。
```python=
x = [1, 2, 3]
y = [4, 5, 6]
z = [7, 8, 9]
zipped = zip(x, y, z)
print(list(zipped))
# 也可以這樣觀察zip的資料型態
x1, y1, z1 = zip(x, y, z)
print(x1, y1, z1)
# 而 * 運算用來unzipped list
print(*[[1, 2, 3], [4, 5, 6], [7, 8, 9]])
# 還原回去
x2, y2, z2 = zip(*zip(x, y, z))
print(*zip(x, y, z))
print(x2, y2, z2)
```
## 練習4:
* 一口氣交了同學們很多東西,請同學先練習一下。
* 問題1:請用迴圈檢查這份空氣汙染資料有多少筆重複資料,請將下面的清理方式寫成程式碼。
1. 檢查```school```這個欄位是否重複。
2. 為```air_data```多創建一個key來儲存是否重複(key的名稱可以隨意命名)。
3. 利用迴圈,從```school```的第二個值開始檢查先前的國小中是否存在與現在位置名稱完全相等的國小。
* 我們會用到「in」這個運算。
```python=
x = True
# "in" - 在不在裡面
print(x in [False, False, False, False])
```
* 下列程式碼可以幫助同學們進行思考。
```python=
print(air_data['school'][1] in air_data['school'][0:1])
print(air_data['school'][2] in air_data['school'][0:2])
```
```python=
idx = 1
print(air_data['school'][idx] in air_data['school'][0:idx])
idx = 2
print(air_data['school'][idx] in air_data['school'][0:idx])
```
<details>
<summary>解答</summary>
```python=
air_data['duplicated'] = [False]
for i in range(1, len(air_data['school'])):
is_dupliated = air_data['school'][i] in air_data['school'][0:i]
air_data['duplicated'].append(is_dupliated)
```
- 利用迴圈來觀察結果。
```python=
for i in range(len(air_data['duplicated'])):
print(air_data['school'][i], air_data['duplicated'][i])
```
</details>
* 問題2:接著上一題,把重複的列刪除掉後請把檔案寫出。
* 請先利用迴圈將每一個key的value轉換成NumPy,轉換後利用索引以及問題1中你所創建關於重複的變數來索引出沒有重複的資料。
* 請參考本課第五節關於寫出的程式碼,將清理後的資料寫出,記得更改檔名。
<details>
<summary>解答</summary>
```python=
duplicated_row = np.array(air_data['duplicated'])
for k in air_data.keys():
np_data = np.array(air_data[k])
air_data[k] = list(np_data[duplicated_row == False])
with open('monitoring_clean.csv', 'w', encoding = 'CP950') as f:
writer = csv.writer(f)
writer.writerow(air_data.keys())
writer.writerows(zip(*air_data.values()))
```
## 總結
* 本次課程介紹了陣列以及一些基本的應用場景,利用迴圈能大量地重複執行工作。
* 本節課學到功能如下,同學務必再次熟悉本課程的內容:
* 操作高維資料
* 簡單的資料清理
* 資料讀入及寫出
* 同學在上完這堂課後,已有能力做一些簡單的資料處理,並將結果寫出,避免手動檢查。