Cleaning Data in Python 如何簡單上手資料清洗

學.誌|Chris Kang
19 min readNov 29, 2019

--

From Unsplash

之前曾經在如何高效進行數據分析文章中裡提過,在處理資料前都必須對資料進行簡單的探索。目的其實很簡單:

寧可朝大致而正確的方向前進,也不要向精確卻錯誤的道路邁進。

基礎資料檢視

這個步驟,是資料處理最重要的步驟。

通常這個步驟執行的是否徹底,會決定最終分析時資料的質量,以及假設是否能被順利檢驗,畢竟如果本來的數據就有問題,分析出來的結果就必然導致錯誤的結論。

通常,最常用的有這幾個方式:

  • df.head()|檢視前五筆的資料。
  • df.tail()|檢視最後五筆的資料。
  • df.columns|檢視數據的欄位名稱。
  • df.shape|檢視數據欄、列的數量,會以 (欄, 列) 方式呈現。
  • df.info()|檢視各個欄位的資料型態與該欄位的資料數量。
  • df.describe()|檢視整體資料的標準差、平均值等基本統計資料。

通常發現某些欄位的數據有異,通常也代表可能有輸入錯誤(typo),或是該數據有異質點能加以分析。

檢視資料組成

再來,我們通常會對各個欄位進行進一步的資料檢視。這時 value_counts 的公式就派上用場了!

df.value_counts(dropna=False)df.Country.value_counts(dropna=False).head()OUTPUT:
USA 5
GER 7
.
.
.

value_counts 的公式主要是用來計算在該欄位中,不同的資料個別有多少數量。所以,輸出的結果會如上,為每個不同的數據進行計數。

# 用於列出所有相異的數據
unique()
# 用於列出相異數據的筆數,亦即 number of unique
nunique()

最後,要注意要把數據調整成恰當的資料型態,例如 object 調整成 int32 等(後面的數字代表變數大小),讓資料的大小更輕便,可以利用 astype() 的方法來達到目標。

df['Country'] = df.Country.astype('int32')

上述欄位在調整後,就會正確顯示該欄位為 integer 32 了。

探索性分析(Exploring Data Analysis)– 資料視覺化

光是探索性分析就能寫一系列的文章來解說,這裡從最實用的幾個工具來進行說明與演示。通常我們會使用下列這些工具,判斷資料是否合理。

  • Hist 直方圖|用於瞭解資料數值分布。
  • Box plot 箱型圖|用於瞭解四分位圖、中位數和極值。
  • ECDF (Empirical Cumulative Distribution Function)|能取代掉 Hist 的分群偏誤(bin bias),亦即因為分群而使資料產生偏誤。

A. Histgram 直方圖繪製

只要簡單的使用 pandas.DataFrame.hist() 的套件,就能快速地看出資料如何分布。下方直接使用 df.hist() 的快速繪圖方式來進行檢視。

# Import necessary modules
import pandas as pd
import matplotlib.pyplot as plt
# Create the hist
df.hist(column='initial_cost')
# Display the plot
plt.show()

B. Boxplot 箱型圖繪製

可以看見下圖有個明顯的離群值,使整個箱型圖明顯變形。

其中,by 能夠快速使用 .groupby() 的效果,依照 ‘Borough’ 的欄位資料群組化後,觀看 ‘inital_cost’ column= 資料;rot 則可旋轉下方的文字標籤。

PS:Pandas — Boxplot 官方文件說明

# Import necessary modules
import pandas as pd
import matplotlib.pyplot as plt
# Create the boxplot
df.boxplot(column='initial_cost', by='Borough', rot=90)
# Display the plot
plt.show()

The principle of Tidy Data 整理數據的原則

  1. 每一個變數(variable)形成一欄
  2. 每一觀察值(observation)形成一列
  3. 每一種觀測單位形成一個表格

Tidy Data 的整理方式:

How to convert to tidy data

I. Melt DataFrame

在整理資料時,最重要的便是調整資料的形式,至符合我們要分析的格式;但有時資料的型態並不符合預期,這時就可以靠 melt 來達成我們的願望,簡直就像變魔術xD

# Original Data
Ozone Solar.R Wind Temp Month Day
0 41.0 190.0 7.4 67 5 1
1 36.0 118.0 8.0 72 5 2
2 12.0 149.0 12.6 74 5 3
3 18.0 313.0 11.5 62 5 4
4 NaN NaN 14.3 56 5 5
# Melt tb: tb_melt
airquality_melt = pd.melt(airquality, id_vars=['Month', 'Day'])
# Melted Data
Month Day variable value
0 5 1 Ozone 41.0
1 5 2 Ozone 36.0
2 5 3 Ozone 12.0
3 5 4 Ozone 18.0
4 5 5 Ozone NaN
.
.
.
607 9 26 Temp 70.0
608 9 27 Temp 77.0
609 9 28 Temp 75.0
610 9 29 Temp 76.0
611 9 30 Temp 68.0

上方的案例,利用 id_vars 固定住「Month」和「Day」這兩個欄位,並溶解其他欄位。當然,還有更多的參數能夠控制溶解的方式,可以參考下面寫的文章來操作。

下面再舉一個更詳細的使用過程,melt() 的公式說明:

  1. id_vars: 固定欄位
  2. var_name: 顯示分拆後的欄位名
  3. value_name: 顯示分拆後值的欄位名

因此除了 MonthDay 沒有被溶解外,其他的變數都被轉換到 measurement 的欄位。

# Original Data
Ozone Solar.R Wind Temp Month Day
0 41.0 190.0 7.4 67 5 1
1 36.0 118.0 8.0 72 5 2
2 12.0 149.0 12.6 74 5 3
3 18.0 313.0 11.5 62 5 4
4 NaN NaN 14.3 56 5 5
#melting Data
airquality_melt = pd.melt(airquality, id_vars=['Month', 'Day'], var_name='measurement', value_name='reading')
# Melted Data
Month Day measurement reading
0 5 1 Ozone 41.0
1 5 2 Ozone 36.0
2 5 3 Ozone 12.0
3 5 4 Ozone 18.0
4 5 5 Ozone NaN
.
.
.
607 9 26 Temp 70.0
608 9 27 Temp 77.0
609 9 28 Temp 75.0
610 9 29 Temp 76.0
611 9 30 Temp 68.0

II. Pivot Table Analysis

Pivot_table() 的功能與 melt() 相反,透過交叉檢視資料,從中發現原先資料編排上沒有挖掘到的資訊。詳細的方式可以參考下文,會了這個方式,就能夠挖掘出比一般人更多的洞見。

調整與分割欄位

有時欄位的資料型態,並非我們想要格式。這時透過 strsplit() 就能按格式切開資料,並整理至另一個新的表格。

Splitting a column with .str

可以發現 variable 的欄位,被分成 genderage_group 兩個欄位。

# Create the 'gender' column
tb_melt['gender'] = tb_melt.variable.str[0]
# Create the 'age_group' column
tb_melt['age_group'] = tb_melt.variable.str[1:]
# Print the head of tb_melt
print(tb_melt.head())
country year variable value gender age_group
0 AD 2000 m014 0.0 m 014
1 AE 2000 m014 2.0 m 014
2 AF 2000 m014 52.0 m 014
3 AG 2000 m014 0.0 m 014
4 AL 2000 m014 2.0 m 014

Splitting a column with .split() and .get()

然而有時,資料的分隔並不會都是等距,這時就需要利用 split() 來分拆資料。下列範例利用 .str.split(‘_’) 來分拆資料。

  1. 透過 .str.split(‘_’) 來分拆資料,並放進去 str_split 的欄位
  2. 透過 str.get() 來獲取該欄位 list 中的特定資料
  3. 分別放至我們想要的欄位裡
# Melt ebola: ebola_melt
ebola_melt = pd.melt(ebola, id_vars=['Date', 'Day'], var_name='type_country', value_name='counts')
# Create the 'str_split' column
ebola_melt['str_split'] = ebola_melt['type_country'].str.split('_')
# Create the 'type' column
ebola_melt['type'] = ebola_melt.str_split.str.get(0)
# Create the 'country' column
ebola_melt['country'] = ebola_melt.str_split.str.get(1)
# Print the head of ebola_melt
print(ebola_melt.head())
OUTPUT:
Date type_country counts str_split type country
0 289 Cases_Guinea 2776.0 [Cases, Guinea] Cases Guinea
1 288 Cases_Guinea 2775.0 [Cases, Guinea] Cases Guinea
2 287 Cases_Guinea 2769.0 [Cases, Guinea] Cases Guinea
3 286 Cases_Guinea NaN [Cases, Guinea] Cases Guinea
4 284 Cases_Guinea 2730.0 [Cases, Guinea] Cases Guinea

有效搜尋檔案與資料名稱

如果有很多個檔案可以使用,此時就可以利用套件 glob 來抓取特定的檔案。

  1. 引進 globpandas 套件
  2. 定義 glob 的搜尋條件,「」代表任意的字元並以 .csv 結尾
# Import necessary modules
import glob
import pandas as pd
# Write the pattern: pattern
pattern = '*.csv'
# Save all file matches: csv_files
csv_files = glob.glob(pattern)
# Print the file names
print(csv_files)
# Load the second file into a DataFrame: csv2
csv2 = pd.read_csv(csv_files[1])
# Print the head of csv2
print(csv2.head())

Regular expression in cleaning data

有時候最悲慘的是,資料格式並沒有特定的規律,無法使用上述的方式處理。這時強大的正規表達式就出場了!

  1. 引入 Regular expression 套件 re
  2. 利用 compile 來搜尋 d (digit) 和 {3} 三個數字(記得索引每個字的時候都要有 \ 來進行定義)。
  3. 利用 match() 來找出相對應的資料。
# Import the regular expression module
import re
# Compile the pattern: prog
prog = re.compile('\d{3}-\d{3}-\d{4}')
# See if the pattern matches
result = prog.match('123-456-7890')
print(bool(result))
# See if the pattern matches
result2 = prog.match('1123-456-7890')
print(bool(result2))

如果要找到特定的數字

透過 \d+ 來搜尋一位數(含)以上的數字,可以看到結果正確搜尋出 [‘10’, ‘1’]

# Import the regular expression module
import re
# Find the numeric values: matches
matches = re.findall('\d+', 'the recipe calls for 10 strawberries and 1 banana')
# Print the matches
print(matches)
OUTPUT:
['10', '1']

正規表達式的練習,可以參考:W3Cshool|30分钟内让你明白正则表达式入門教程

Checking Data Quality

用 function 來加速清理數據

下面的範例,是為了示範如何把 malefemale 轉換成 0, 1,並為空缺值替換成 NaN。

另外,使用 apply() 可以放入我們的公式,直接對特定欄位進行操作。

# Define recode_gender()
def recode_gender(gender):
# Return 0 if gender is 'Female'
if gender == 'Female':
return 0

# Return 1 if gender is 'Male'
elif gender == 'Male':
return 1

# Return np.nan
else:
return np.nan
# Apply the function to the sex column
tips['recode'] = tips.sex.apply(recode_gender)
# Print the first five rows of tips
print(tips.head())

另一個方式,是用lambda 來清理數據:

其中 apply 所應用的方式,就是前面提到過的正規表達式啦!

# Write the lambda function using replace
tips['total_dollar_replace'] = tips.total_dollar.apply(lambda x: x.replace('$', ''))
# Write the lambda function using regular expressions
tips['total_dollar_re'] = tips.total_dollar.apply(lambda x: re.findall('\d+\.\d+', x)[0])
# Print the head of tips
print(tips.head())

如果數據重複,就可以用 drop_duplicate()

# Create the new DataFrame: tracks
tracks = billboard[['year', 'artist', 'track', 'time']]
# Print info of tracks
print(tracks.info())
# Drop the duplicates: tracks_no_duplicates
tracks_no_duplicates = tracks.drop_duplicates()

如果想要用 mean / median 等數字填充:

可以使用 fillna() ,後面加上特定的公式帶入。

# Calculate the mean of the Ozone column: oz_mean
oz_mean = airquality.Ozone.mean()
# Replace all the missing values in the Ozone column with the mean
airquality['Ozone'] = airquality.Ozone.fillna(oz_mean)
# Print the info of airquality
print(airquality.info())

用 assert 來驗證

如果 assert 後面的函示為 false,則會顯示 error。

assert 主要是用來避免當特定的數值可能 Error,導致後面的欄位不會執行。只要 assert 該欄位出現 Error,系統就會報錯,並繼續執行下去。

# Assert that there are no missing values
assert pd.notnull(ebola).all().all()
# Assert that all values are >= 0
assert (ebola >= 0).all().all()

Use the pd.notnull() function on ebola (or the .notnull()method of ebola) and chain two .all() methods (that is, .all().all()). The first .all() method will return a True or False for each column, while the second .all() method will return a single True or False.

轉換資料型態

有時會遇到資料型態被預設成 object,這時就能使用 astype() 來轉換資料型態。

# Convert the sex column to type 'category'
tips.sex = tips.sex.astype('category')
# Convert the smoker column to type 'category'
tips.smoker = tips.smoker.astype('category')
# Print the info of tips
print(tips.info())

df.dtype 檢驗資料型態

# Convert the year column to numeric
gapminder.year = pd.to_numeric(gapminder.year)
# Test if country is of type object
assert gapminder.country.dtypes == np.object
# Test if year is of type int64
assert gapminder.year.dtypes == np.int64
# Test if life_expectancy is of type float64
assert gapminder.life_expectancy.dtypes == np.float64

綜合範例:

  1. 設定 function 來檢驗資料是否正確。
  2. 利用 columns[0] 來確認資料欄位是否正確。
  3. 利用 value_counts()[0] 來檢驗資料是否有不正確地重複。
def check_null_or_valid(row_data):
"""Function that takes a row of data,
drops all missing values,
and checks if all remaining values are greater than or equal to 0
"""
no_na = row_data.dropna()
numeric = pd.to_numeric(no_na)
ge0 = numeric >= 0
return ge0
# Check whether the first column is 'Life expectancy'
assert g1800s.columns[0] == 'Life expectancy'
# Check whether the values in the row are valid
assert g1800s.iloc[:, 1:].apply(check_null_or_valid, axis=1).all().all()
# Check that there is only one instance of each country
assert g1800s['Life expectancy'].value_counts()[0] == 1

大部分的人可能會想像,處理數據分析時會花大部分的時間拉圖表,但很多時候資料的來源是混亂且未經整理的。因此在大部分的時候,可能會花高達 60%~80% 的時間在處理數據,以求資料的精確與清晰。

這篇文章主要是透過 DataCamp 的 Cleaning Data in Python 課程,來紀錄在清洗資料時,可能會遇到的問題,以及可以如何解決它。

如果文中有任何不清楚或是筆誤,都歡迎直接留言跟我說,也歡迎一起討論數據分析的過程!

謝謝你/妳,願意把我的文章閱讀完

如果你喜歡筆者在 Medium 的文章,可以拍個手(Claps),最多可以按五個喔!也歡迎你分享給你覺得有需要的朋友們。

--

--

學.誌|Chris Kang

嗨!我是 Chris,一位擁有技術背景的獵頭,熱愛解決生活與職涯上的挑戰。專注於產品管理/資料科學/前端開發 / 人生成長,在這條路上,歡迎你找我一起聊聊。歡迎來信合作和交流: chriskang0917@gmail.com