# 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() ``` > ![](https://i.imgur.com/zFMV93v.png) - 遺失資料比例 ``` print("Missing data ratio: ") print(dataset.isnull().sum() / len(dataset)) # 遺失資料占比 ``` > ![](https://i.imgur.com/EaRMbSd.png) ## 處理日期 - 天氣通常與年份和日期較無關聯,月份可用來判斷雨季或旱季 - 將日期從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()))) ``` > ![](https://i.imgur.com/ZrrLOov.png) ## 填充非數值型別資料 - 確認缺值 ``` python dataset.isnull().sum() ``` > ![](https://i.imgur.com/Mk83xqd.png) - 當初怕後面還會用到原始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() ``` > ![](https://i.imgur.com/XZqYwUV.png) ## 填充數值型別資料 - 將數值型別的空資料都填為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() ``` > ![](https://i.imgur.com/QvmpD6k.png) - 確認資料是否有誤 ``` python numerical_features = dataset_.loc[:, numerical] numerical_features.describe() ``` > ![](https://i.imgur.com/53w8kzq.png) ## 離群值處理 - 顯示各個資料的分布範圍,方便後續分析 - 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 ``` > ![](https://i.imgur.com/p1TTVbo.png) ### 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() ``` > ![](https://i.imgur.com/kNjJZ6T.png) ### 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() ``` > ![](https://i.imgur.com/JloqbUZ.png) - 計算以上四種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() ``` > ![](https://i.imgur.com/nBtNvuR.png) ## 分割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) ``` > ![](https://i.imgur.com/A5FnINM.png) ## 替換離群值 - 大於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() ``` > ![](https://i.imgur.com/GpTTcek.png) - 也對X_test也進行target encoding ``` python X_test = targetencoder.transform(X_test) X_test.head() ``` > ![](https://i.imgur.com/IsJ9CEg.png) ## 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() ``` > ![](https://i.imgur.com/6xZNrGq.png) - 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)) ``` > ![](https://i.imgur.com/s2VSAMe.png) - 透過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() ``` > ![](https://i.imgur.com/DbeHEDQ.png) # 分析模型 ## 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))) ``` > ![](https://i.imgur.com/riJFzzu.png) ## 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() ``` > ![](https://i.imgur.com/VjXWKG3.png) # 資料輸出 ## 讀入&處理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() ``` > ![](https://i.imgur.com/b5gtGPc.png) - 將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() ``` > ![](https://i.imgur.com/G1j1lfW.png) - 查看test_data的資料分布情況 - 和train差異不大 ``` python test_data.describe([0.01,0.05,0.1,0.25,0.5,0.75,0.9,0.99]).T ``` > ![](https://i.imgur.com/q2Y3MF1.png) - 將預測結果轉為輸出格式 - 顯示預測結果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()) ``` > ![](https://i.imgur.com/jNXpR9A.png) # 結語 這是第一次嘗試資料分析的題目,對於套件的使用都還不太熟悉,嘗試了XGBoost, LogicRegression, KNN三種模型,XGBoost可能是因為不會調參數導致結果遠低預期,LogicRegression則是準確率一直上不去,最後改用KNN才讓準確率來到0.83。 過程中用try and error的方式不斷改進,曾遇到過one hot encoding造成feature過多的問題、scalar導致資料失準等等奇葩問題,花了大量的時間去查資料和debug,雖然犧牲了很多睡眠時間,但一切都很值得,希望下個作業能有更好的成績。 # 結果 - Public - Leaderboard > ![](https://i.imgur.com/zGyJWxr.png) - Submission > ![](https://i.imgur.com/DSuCM06.png) - Private - Leaderboard > ![](https://i.imgur.com/Gt5GG4l.png) - Submission > ![](https://i.imgur.com/2rlvcTy.png)