# Data Science HW1 - Weather Forecast
[toc]
## 資料屬性
| 屬性 | 說明 |格式
| ----------------- | ----------------------- | ----------------------------- |
| Attribute1 | 當天日期 | yyyy/MM/dd
| Attribute2 | 氣象站的地區 | 數字表示地區
| Attribute3 | 最低溫度 | 數字 (攝氏)
| Attribute4 | 最高溫度 | 數字 (攝氏)
| Attribute5 | 降雨量 | 數字 (單位: 毫米)
| Attribute6 | 蒸發量 | 數字
| Attribute7 | 當天陽光出現的時數 | 數字
| Attribute8 | 最強陣風方向 | 英文方位 (e.g. NE)
| Attribute9 | 最強陣風速度 | 數字 (單位: 公里/小時)
| Attribute10 | 下午三點的風向 | 英文方位 (e.g. NNW)
| Attribute11 | 下午三點前的平均風速 | 數字 (單位: 公里/小時)
| Attribute12 | 下午三點的相對溼度 | 數字
| Attribute13 | 下午三點前的平均大氣壓 | 數字(hpa)
| Attribute14 | 下午三點,雲層覆蓋天空的比例 | 數字 (0 完全晴朗無雲~8 完全多雲)
| Attribute15 | 下午三點的溫度 | 數字
| Attribute16 | 今天有沒有下雨 | Yes/No
| Attribute17 | 明天會不會下雨 | Yes/No
# 資料預處理
## 讀入Training Data
- 確認讀入資料
- column數量: 17
- row數量: 17103
``` python
dataset = pd.read_csv("../input/2022-data-science-hw1/train.csv") # train data
print("Shape: "+ str(dataset.shape)) # 資料維度
dataset.head()
```
> 
- 遺失資料比例
```
print("Missing data ratio: ")
print(dataset.isnull().sum() / len(dataset)) # 遺失資料占比
```
> 
## 處理日期
- 天氣通常與年份和日期較無關聯,月份可用來判斷雨季或旱季
- 將日期從Attribute1中提取出來,只留下月份
- 將月份加入dataset成為新的Attribute
``` python
# 日期只保留月份
dataset['Attribute1'] = pd.to_datetime(dataset['Attribute1'])
month = dataset['Attribute1'].dt.month
dataset.drop(labels=['Attribute1'], axis=1, inplace=True)
dataset['month'] = month
dataset.head()
```
- 顯示各個Attribute中unique的數量
- 可以看到月份目前只剩下1~12月,共12個種類
``` python
# 各個Attribute中數值的種類
for feature in dataset.columns:
print('The feature is {} and number of categories are {}'.format(feature, len(dataset[feature].unique())))
```
> 
## 填充非數值型別資料
- 確認缺值
``` python
dataset.isnull().sum()
```
> 
- 當初怕後面還會用到原始dataset,因此拷貝了一份設為dataset_以防萬一,也方便後續檢查資料是否有壞掉
- 將非數值的空資料填充為 0
- Attribute8: 最強陣風方向
- Attribute10: 下午三點的風向
- Attribute16: 今天有沒有下雨
- 有試過填0跟眾數,反覆測試後發現填0準確率較高
``` python
dataset_ = dataset.copy()
# 填充非數值資料為0
fill_list = ['Attribute8', 'Attribute10', 'Attribute16']
for i in fill_list:
dataset_[i].fillna('0', inplace=True)
# dataset_[i].fillna(dataset_[i].mode()[0], inplace=True)
dataset_.isnull().sum()
```
> 
## 填充數值型別資料
- 將數值型別的空資料都填為median(中位數)
- 有嘗試改填mean(平均數),但效果不佳
- 可能是因為平均數較容易受極端值影響
``` python
# 填充數字型別資料為中位數
numerical = [_ for _ in dataset_.columns if dataset[_].dtype != 'O']
print(numerical)
for i in numerical:
dataset_[i].fillna(dataset_[i].median(), inplace=True)
dataset_.isnull().sum()
```
> 
- 確認資料是否有誤
``` python
numerical_features = dataset_.loc[:, numerical]
numerical_features.describe()
```
> 
## 離群值處理
- 顯示各個資料的分布範圍,方便後續分析
- Attribute5, 6, 11, 15的max和99%相差很大
- 在upper fence和lower fence之外還有數值
- 需要對以上四種Attribute的離群值進行處理
``` python
dataset_.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T
```
> 
### Box Plot
``` python
figure, axes = plt.subplots(2, 2, figsize=(30, 15))
sns.boxplot(
x='Attribute17', y='Attribute5', # 降雨量
data=dataset_, ax=axes[0, 0], palette="Set3"
)
sns.boxplot(
x='Attribute17', y='Attribute6', # 蒸發量
data=dataset_, ax=axes[0, 1], palette="Set3"
)
sns.boxplot(
x='Attribute17', y='Attribute9', # 最強陣風速度
data=dataset_, ax=axes[1, 0], palette="Set3"
)
sns.boxplot(
x='Attribute17', y='Attribute11', # 下午三點前的平均風速
data=dataset_, ax=axes[1, 1], palette="Set3"
)
plt.show()
```
> 
### Continuous Distribution Plot
``` python
figure, axes = plt.subplots(2, 2, figsize=(30, 15))
sns.distplot(
a=dataset_['Attribute5'].dropna(), # 降雨量
ax=axes[0, 0]
)
sns.distplot(
a=dataset_['Attribute6'].dropna(), # 蒸發量
ax=axes[0, 1]
)
sns.distplot(
a=dataset_['Attribute9'].dropna(), # 最強陣風速度
ax=axes[1, 0]
)
sns.distplot(
a=dataset_['Attribute11'].dropna(), # 下午三點前的平均風速
ax=axes[1, 1]
)
plt.show()
```
> 
- 計算以上四種Attribute的IQR(四分位距),lower fence和 upper fence
- IQR = Q3 - Q1
- lower fence = Q1 – (1.5 * IQR)
- upper fence = Q3 + (1.5 * IQR)
``` python
_list = ['Attribute5', 'Attribute6', 'Attribute9', 'Attribute11']
# 降雨量, 蒸發量, 最強陣風速度, 下午三點前的平均風速
def find_outliers(df, feature):
IQR = df[feature].quantile(0.75) - df[feature].quantile(0.25)
Lower_fence = df[feature].quantile(0.25) - (IQR * 3)
Upper_fence = df[feature].quantile(0.75) + (IQR * 3)
print('{feature} outliers are values < {lowerboundary} or > {upperboundary}'\
.format(feature=feature, lowerboundary=Lower_fence, upperboundary=Upper_fence))
out_of_middan = (df[feature] < Lower_fence).sum()
out_of_top = (df[feature] > Upper_fence).sum()
print(f'the number of upper outlier {out_of_top}')
print(f'the number of lower outlier {out_of_middan}')
for feature in _list:
find_outliers(dataset_, feature)
print()
```
> 
## 分割Train和Test
- x為dataset_刪除Attribute17(明天會不會下雨)的結果
- y為Attribute17
- 將x分割為X_train和X_test,y分割為y_train和y_test
- X_train在之前分析其他模型時有用到,最後的版本沒有用到
- 切割比例為train: test = 0.8: 0.2
- 防止overfitting或underfitting
``` python
from sklearn.model_selection import train_test_split
x = dataset_.drop(columns = ['Attribute17'])
y = dataset_['Attribute17']
X_train, X_test, y_train, y_test = train_test_split(x, y, test_size = 0.2)
print(X_train.shape, X_test.shape)
print(y_train.shape, y_test.shape)
```
> 
## 替換離群值
- 大於upper fence的離群值換成upper fence
- 小於lower fence的離群值換成lower fence
- 因為小於lower fence的情況指出現在Attribute6中,因此小於的部分只處理一項
``` python
def process_outliers_top(df3, Top, feature_):
return np.where(df3[feature_] > Top, Top, df3[feature_])
threashold_dict = {'Attribute5': 2.4, 'Attribute6': 9, 'Attribute9': 91.0, 'Attribute11': 57.0}
_list = ['Attribute5', 'Attribute6', 'Attribute9', 'Attribute11']
for df3 in (x, X_test):
for feature in _list:
top = threashold_dict.get(feature)
df3[feature] = process_outliers_top(df3, top, feature)
print(x[_list].max())
print(X_test[_list].max())
```
``` python
def process_outliers_bottom(df3, Bottom, feature_):
return np.where(df3[feature_] < Bottom, Bottom, df3[feature_])
_list = ['Attribute6']
threashold_dict = {'Attribute6': 0.5999999999999996}
for df3 in (x, X_test):
for feature in _list:
bottom = threashold_dict.get(feature)
df3[feature] = process_outliers_bottom(df3, bottom, feature)
print(x['Attribute6'].min())
print(X_test['Attribute6'].min())
```
## 將離散數值進行 encode
- 將 No/Yes 轉為 0/1
``` python
# 將 No/Yes 轉為0/1
x = x.replace({'No':0, 'Yes':1})
X_test = X_test.replace({'No':0, 'Yes':1})
y = y.replace({'No':0, 'Yes':1})
y_test = y_test.replace({'No':0, 'Yes':1})
```
- Attribute8和10的資料形式(e.g. NNE)會導致後續處理資料時出現問題
- 透過target encoding將兩個Attribute的資料轉為浮點數
- target encoding適合處理地區這類數量多,且沒有順序大小之分的資料
- 如果用one hot encoding,不只占空間又會生成更多的特徵;如果用label encoding,又會暗示類別之間的遠近
``` python
from category_encoders import TargetEncoder
# 最強陣風方向, 下午三點的風向
targetencoder = TargetEncoder(cols=['Attribute8', 'Attribute10'])
x = targetencoder.fit_transform(x, y)
x.head()
```
> 
- 也對X_test也進行target encoding
``` python
X_test = targetencoder.transform(X_test)
X_test.head()
```
> 
## Sampling
- train.csv的不下雨和下雨比例大概是4.4:1,不下雨的比例高很多
- 可能導致模型偏向猜「不下雨」,需要將樣本調至盡量1:1
- 以下是原始資料的不下雨/下雨圓餅圖
``` python
plt.figure( figsize=(10,5) )
y.value_counts().plot( kind='pie', colors=['lightcoral','skyblue'], autopct='%1.2f%%' )
plt.title( 'Yes/No' )
plt.ylabel( '' )
plt.show()
```
> 
- OverSampling
- 透過SMOTE合成出一些樣本,將Yes的資料量提升
- Undersampling
- 透過TomekLinks找出邊界鑑別度不高的樣本,將這些作為雜訊剔除
```python
print("Original: " + str(x.shape))
## OverSampling
from imblearn.over_sampling import SMOTE
x, y = SMOTE().fit_resample(x, y)
print("After oversampling: " + str(x.shape))
## UnderSampling
from imblearn.under_sampling import TomekLinks
x, y = TomekLinks().fit_resample(x, y)
print("After oversampling and undersampling: " + str(x.shape))
```
> 
- 透過OverSampling + Undersampling的組合,現在No/Yes的比例已經接近1:1
``` python
plt.figure( figsize=(10,5) )
y.value_counts().plot( kind='pie', colors=['lightcoral','skyblue'], autopct='%1.2f%%' )
plt.title( 'No/Yes' ) # 圖標題
plt.ylabel( '' )
plt.show()
```
> 
# 分析模型
## Logic Regression
- 概念: 找出一條直線將所有數據分類
- 沒有調整參數
- train_test_split的test準確率大概0.77 - 0.79間
- 最初都是用邏輯迴歸的版本submit,submit的準確度大概都在0.79 - 0.80間
``` python
from sklearn.model_selection cross_val_score
from sklearn.linear_model import LogisticRegression
LR = LogisticRegression(n_jobs = -1)
LR.fit(x, y)
print('Test LR score: ' + str(LR.score(X_test, y_test)))
```
> 
## KNN (K Nearest Neighbor)
- 概念: 找K個最近的點(Neighbor),依據K個點中何種類型較多,就將測試點設為此類
- 若n_neighbor=5,其中3個是No,則此測試點的結果為No
- Neighbor的數量很大程度地影響判斷結果
- n_neighbor值大概是資料數量 ^ 0.5,效果會比較好
- 原始資料量是17103,17103 ^ 0.5 ≈ 130.77843
- n_neighbor = 131
``` python
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
knn = KNeighborsClassifier(n_neighbors = 131)
knn.fit(x, y)
```
- n_neighbor值太小,雖然訓練資料的準確率比較高,但是會overfitting
> 下面的code是做圖用,會跑很久,所以檔案中我設成Markdown
``` python
knn_df = pd.DataFrame()
for i in range (1, 1000, 1):
knn = KNeighborsClassifier(n_neighbors = i)
knn.fit(x, y)
pred = knn.predict(X_test)
print("n: {}, accuracy, {}".format(i, accuracy_score(y_test, pred) * 10))
knn_df = knn_df.append({"neighbors": i, "accuracy": accuracy_score(y_test, pred) * 10}, ignore_index=True)
plt.plot(knn_df['neighbors'], knn_df['accuracy'])
plt.xlabel('Accuracy')
plt.ylabel('Neighbors')
plt.show()
```
> 
# 資料輸出
## 讀入&處理test.csv
- 確認讀入資料
- column數量: 17
- row數量: 806
```python
test_data = pd.read_csv("../input/2022-data-science-hw1/test.csv")
print(test.isnull.sum)
test_data.head()
```
> 
- 將test_data轉成和訓練資料相同格式
- test沒有缺值,不需補值
- 日期只留下月份
- No/Yes轉為0/1
- 非數值型別資料做target encoding
``` python
test_data['Attribute1'] = pd.to_datetime(test_data['Attribute1'])
month = test_data['Attribute1'].dt.month
test_data['month'] = month
test_data.drop(labels=['Attribute1'], axis=1, inplace=True)
test_data = test_data.replace({'No':0, 'Yes':1})
test_data = targetencoder.transform(test_data)
test_data.head()
```
> 
- 查看test_data的資料分布情況
- 和train差異不大
``` python
test_data.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T
```
> 
- 將預測結果轉為輸出格式
- 顯示預測結果No/Yes的數量
- test的正確答案No/Yes似乎接近1:1,不同於train的4.4:1
``` python
prediction = knn.predict(test_data)
print(type(prediction))
submit = pd.DataFrame()
for i in range(len(prediction)):
submit = submit.append({'id':i, 'ans':prediction[i]}, ignore_index=True)
submit['id'] = submit.id.map(float)
submit = submit.replace({'No':0, 'Yes':1})
submit['ans'] = submit.ans.map(int)
print(submit.dtypes)
submit.to_csv('submission.csv', index=False)
print(submit)
print(submit.loc[:,"ans"].value_counts())
```
> 
# 結語
這是第一次嘗試資料分析的題目,對於套件的使用都還不太熟悉,嘗試了XGBoost, LogicRegression, KNN三種模型,XGBoost可能是因為不會調參數導致結果遠低預期,LogicRegression則是準確率一直上不去,最後改用KNN才讓準確率來到0.83。
過程中用try and error的方式不斷改進,曾遇到過one hot encoding造成feature過多的問題、scalar導致資料失準等等奇葩問題,花了大量的時間去查資料和debug,雖然犧牲了很多睡眠時間,但一切都很值得,希望下個作業能有更好的成績。
# 結果
- Public
- Leaderboard
> 
- Submission
> 
- Private
- Leaderboard
> 
- Submission
> 