好多同學把統計和資料清洗搞混,直接把原始資料發給我,做個統計吧,這個時候其實很大的工作量是在資料清洗和處理上,如果資料很雜亂,清洗起來是很費工夫的,反而清洗好的資料做統計分析常常就是一行程式碼的事情。
Data scientists only spend 20% of their time creating insights, the rest wrangling data.
想想今天就給大家寫一篇資料處理的常用函式介紹吧。全是自己的一丟丟經驗,肯定不會是最優的,僅僅是給個參考,因為在R中同一個目的的實現方法太多了,找到適合自己的才是最好的。我爭取儘量清晰地一步一步給大家展示一下整個清洗資料的流程。
在R語言中我們會用一系列的方法把我們的資料清洗過程連起來,整個的思路就是從原始資料開始,一步一步形成我們的最終可以用來做統計的資料。
整體上我們資料處理的步驟可以包含下面5個部分,也是有順序的5步:
- Importing of data(資料匯入)
- Column names cleaned or changed(列名的清洗轉換)
- De-duplication(去重)
- Column creation and transformation (e.g. re-coding or standardising values)(生成新變數)
- Rows filtered or added(資料選擇)
本文就帶著大家一步一步走一遍,中間會詳細說明一些核心函式的用法,希望對大家有點幫助。當然了,以下內容預設您已經理解dplyr包的基礎,比如Pipes%>%符號。
資料匯入
資料匯入大家應該都沒有問題,就算有問題網上搜搜一般都可以解決,匯入資料的方法有很多種,這兒會推薦大家直接用右上角的選單import data,或者使用rio包的import函式。我現在有一個已經匯入的原始資料raw如下圖:
資料匯入進來之後我們首先應該整體上看看變數型別,變數名稱都是如何的,以此決定我們是不是將變數名初步改改,或者變數型別也得改改,就是首先得有個整體把握,此時推薦大家執行一下skim函式,這個是幫助我們瞭解資料整體形式的十分方便的函式,請大家把summary忘掉,直接執行skim:
skimr::skim(raw)
函式的輸出包括整體的資料有多少行多少列,列的不同型別(數值,字元,時間)有幾種,然後不同的列的型別還會輸出我們關心的指標,比如字元型的列都會有每一列的缺失比例,極值,非重複值,空白等:
比如對於每一個數值型的列都會有缺失數量,均值標準差百分位數,直方圖等等我們關心的東西:
反正就是大家用skim函式就可以從整體上把握住我們的資料的樣子了。
另外還會推薦大家用names函式看看資料的列名,有了對資料集的整體把握和全部的變數名,我們可以緊接著進行下一步:變數名的轉換。
列名的清洗轉換
轉化的目的就是使得之後的操作呼叫變數可以更加的清晰和方便,我可以瞅瞅我的原始資料的列名:
names(raw)
在檢視列名時我們需要關注一下列名是不是符合以下要求:
- 在可讀性高的情況下儘可能短
- 沒空格
- 沒有特殊字元(&, #, <, >, …)
- 樣式統一(e.g. all date columns named like date_onset, date_report, date_death…)
對照上面的標準我們其實就可以知道目前我的原始資料raw的列名中,infection date中出現了空格,date onset . infection date這兩個列名的也有空格而且形式應該統一以下比較好;然後…28這個列名有特殊符號,這些最好都先得改改。
當我們的資料集非常大的時候,比如有好幾千個變數的時候,改列名或者列名統一也是一件比較繁瑣的事情,這個時候會推薦大家用clean_names()函式自動修改一下,之後再寫程式碼微調,這樣對大型資料庫處理起來可以節省大量時間,對我們的資料raw我們可以寫出如下程式碼:
data <- raw %>%
janitor::clean_names()
執行上面的程式碼後我們再看data的列名就可以發現至少特殊字元和空格的問題統統都沒有了:
上面屬於粗獷的處理,但是還有其它的問題,反正實際情況中大家也免不了需要改列名的,此時可以用rename函式進行列名的手動修改,基本格式是新名=舊名,如下:
raw %>%
janitor::clean_names() %>%
rename(date_infection = infection_date,
date_hospitalisation = hosp_date,
date_outcome = date_of_outcome)
執行程式碼後我們就可以看到所有設定的列名都被改好了。
很多時候我們是從一個很大的微觀資料庫中摘變數來分析的,就是常常我們只需要那些我們用得著的變數,這時可以用select函式,這個大家用的比較多,這兒分享幾個在使用select的時候的輔助函式,將這些輔助函式和select結合起來會使得效率更高,這些函式有一個統一的名字叫做“tidyselect” helper functions,常見的如下圖:
比如我就想選擇所有的數值型變數來分析我就可以寫出如下程式碼:
select(where(is.numeric))
比如我就想找變數名中包含某個字元的變數,我就可以用contain函式,比如我現在手裡是一個母子配對資料集,變數既有母親的也有孩子的,我就可以用contain方便滴篩選出來母子的年齡:
select(contains("age"))
上面的程式碼會將所有包含“age”這個字串的變數都篩出來;同樣的道理我們還可以用ends_with() and starts_with()篩出來大型資料集中以某個字元開頭和結尾的列,比如一個縱向隨訪資料集每一波的cesd都是以cesd開頭為列名的我們就可以用starts_with()將所有隨訪的cesd都篩出來。還有matches()函式也可以幫助實現列名的匹配篩選,比如在raw資料中,用如下程式碼就可以篩選出所有列名中含有“onset”,“hosp”,“fev”的列
raw %>%
select(matches("onset|hosp|fev")) %>%
names()
上面一步就實現了將fever的發病時間,入院時間,住院時長這些變數都篩出來,指導這些操作在處理大型資料庫的時候就會省事很多。
反正,整體的操作都是非常靈活的,會有很多細節需要學習,但是大家要掌握的是我知道有這麼一個函式可以解決這個問題,我就先記住函式名,具體細節可以邊用邊查,整體的學習過程就是這樣。
新變數生成和變數轉換
在資料處理中我們還會涉及到變數的改變和根據原有變數生成新變數,變數生成和轉換都可以用mutate來實現,具體規則就是:
mutate(new_column_name = value or transformation)
就上面這個式子,用起來可就是包羅永珍,比如在你的資料中有身高體重,我想計算一個新的變數叫做bmi,則可以寫出程式碼如下:
mutate(bmi = wt_kg / (ht_cm/100)^2)
還有很多的新變數生成和轉換的應用場景,比如完全複製一個變數,新列全是7,用另外的變數計算,兩個變數的值貼一起形成新變數:
raw%>%
mutate(
new_var_dup = case_id, # 完全複製
new_var_static = 7, # 新列全是7
new_var_static = new_var_static + 5, #用另外的變數計算新變數
new_var_paste = stringr::str_glue("{hospital} on ({date_hospitalisation})") # 兩個變數的值貼一起形成新變數
)
還有很多很多的操作都是在mutate中完成的。
我們常常還會有的需求是一次性處理好多個變數,比如一次性將所有的變數都轉換為字元型別,這個時候為了程式碼的整潔統一我們依然可以用mutate和across,結合.cols和.fns引數就行,比如下的程式碼就是將3個列全部轉換為字串,大家不用特意再去用lapply或者寫迴圈什麼的:
raw %>%
mutate(across(.cols = c(temp, ht_cm, wt_kg), .fns = as.character))
還有幾個小技巧,比如我想將資料庫的所有列都進行某一個操作,我不用將所有的列名都敲出來,只需要用everything函式就可以,比如用下面的程式碼就實現將資料的所有列轉換為字元型:
raw %>%
mutate(across(.cols = everything(), .fns = as.character))
大家把握住一個原則就是列的生成轉換就是用mutate就行,然後涉及到選擇的時候我們一定記得要結合輔助函式“tidyselect” helper functions。要有這個意識。
還有一個函式要給大家介紹一下就是coalesce()
很多時候我們一個變數有兩種測量方式,比如有自我報告的體重,還有物理測量的體重,我們通常的想法是以物理測量的為準,當物理測量有缺失我們用自我報告的資料來填補,這麼一個過程我們就可以用coalesce函式一步搞定,如下:
所以說我們在使用mutate的時候我們可以根據需要結合coalesce函式,比如下面的程式碼就實現了在raw資料集中當village_detection缺失時用village_residence的值填補:
raw %>%
mutate(village = coalesce(village_detection, village_residence))
- 變數重新編碼
變數重新編碼也是常見的操作,它也是屬於變數轉換的大框框裡面的,所以我們依然是用mutate,比如在我們的raw資料中,我們有個變數hospital,這個變數有很多的水平,其實好多水平是一樣的:
table(raw$hospital, useNA = "always")
這種情況在我們自己錄入的資料庫中經常會出現
就是"Mitylira Hopital"和"Mitylira Hospital",和"Military Hopital"其實都可以看成是錄入的時候錄錯了,其實他們都是"Military Hospital",這個時候我們要做的就是重新編碼變數,可以用mutate和recode實現我們的需求:
raw%>%
mutate(hospital = recode(hospital,
# for reference: OLD = NEW
"Mitylira Hopital" = "Military Hospital",
"Mitylira Hospital" = "Military Hospital",
"Military Hopital" = "Military Hospital",
"Port Hopital" = "Port Hospital",
"Central Hopital" = "Central Hospital",
"other" = "Other",
"St. Marks Maternity Hopital (SMMH)" = "St. Mark's Maternity Hospital (SMMH)"
))
上面的程式碼執行完,我們再看相應的錯誤的錄入都正確地歸為相應水平了
大家還應該掌握的使用邏輯判斷進行變數重新編碼的方法,這個時候需要用到replace(),ifelse()andif_else()和case_when(),給大家寫一個case_when的例子,這個函式就是在我們需要根據某個變數的值生成新變數的時候使用,比如我們根據age_unit的不同取值,生成新變數age_years,我們就可以用case_when():
raw %>%
mutate(age_years = case_when(
age_unit == "years" ~ age,
age_unit == "months" ~ age/12, # 年齡單位為月,age_years就等於年齡/12
is.na(age_unit) ~ age, # 年齡單位缺失的話,預設成“年”,age_years就等於age
TRUE ~ NA_real_)) #其餘所有情況都歸為age_years缺失
在使用case_when的時候我們可以將想設定的都設定好,餘下的情形都可以用關鍵字TRUE代表,就想上面程式碼的最後一行那樣,對於age_unit這個變數的其餘的所有情況我們都認為age_years為缺失。
- 缺失值替換
缺失值轉換依然可以在mutate中完成,因為它依然是在變數轉化的框架裡:
因為我們的hospital這個變數其實是有很多的缺失值的,我們希望將相應的缺失值都替換成missing,我們就可以寫出如下程式碼:
raw %>%
mutate(hospital = replace_na(hospital, "Missing"))
有一種情況需要注意,就是因子變數中有NA,我們如果用replace_na會報錯,上面程式碼中hospital變數是字元型的,所以沒有問題。就是對於一個因子來講,它本身水平就是固定的,有了NA,我們將NA進行替代,比如替代成missing,其實missing它並不是因子原來本身的一個水平,所以會報錯,這個時候我們可以用fct_explicit_na()函式。
fct_explicit_na()函式會直接將因子變數中的NA進行相應的替換,替換的值也自動成為該因子的一個水平。
- 數值變數轉分類變數
就是說我們有時候想將連續變數轉成分類變數分析,這個時候常常會用到的函式有age_categories(),cut(),quantile(),ntile()
看一個age_categories()的例子:
raw%>%
mutate(
age_cat = age_categories(
age_years,
breakers = c(0, 5, 10, 15, 20,
30, 40, 50, 60, 70)))
上面的程式碼就將age_years這個連續變數化成了分類變數,分的節點就是breakers引數的向量中,quantile(),ntile()則可以幫助我們快速地劃分節點。分類過後就可以用table函式檢視每個類別的數量,上面的程式碼就是將連續變數age_years用breakers引數中的點進行了劃分,劃分後形成了分類變數,結果如下:
有時候我們對劃分的結果會不放心,比如這個類別到底是開區間還是閉區間,當然這些都是有引數可以調的,為了確認我們也可以做交叉表格,我麼可以把原來的連續變數和生成的分類變數進行交叉:
table("Numeric Values" = raw$age_years,
"Categories" = raw$age_cat,
useNA = "always")
通過這麼樣一個操作我們就可以判斷是不是相應的連續變數都被正確地劃分到了相應的類別中:
還有一種比較特別的需求,我們雖然想按連續變數分組,但是我想每個組的人數相同,這個時候結合ntile()就可以實現,比如我想把age_years化成分類變數,且規定每一類人數相同,我就可以寫出如下程式碼:
ntile_data <- raw %>%
mutate(even_groups = ntile(age_years, 10))
去重
去重大家就去研究一個函式,叫distinct就行。
行的過濾和新增
給資料庫的行進行改變大家都會,但是要在原先的資料框中間插入一行怎麼辦呢?
可以用addrow,比如我想在原來的資料集raw的第二行之前插入一行,我可以用如下程式碼:
raw %>%
add_row(row_num = 666,
case_id = "abc",
generation = 4,
`infection date` = as.Date("2020-10-10"),
.before = 2)
該行的每一個變數都需要規定一下,沒設定的都會空著,.before = 2的意思就是在原來資料框的第二行之前插入。
- 按規則進行行的選擇
選擇行也是用的比較多的,比如我就想選性別為女的行,或者我就想選擇某些變數沒有缺失的行等等,選擇行我們是用filter,但是在以是否缺失為條件的時候大家不要去用filter(!is.na(column) & !is.na(column))這個時候推薦大家用drop_na,通過drop_na就可以將某個變數有確實的行全拿掉。
- 橫向計算
正常我們計算變數都是縱向依次計算的,比如最開始寫的BMI計算的例子,有時候我們需要對一個觀測的多個變數進行計算,比如一個病人有好多症狀,我想對每個病人症狀個數求和,本質上這是一個橫向計算的問題,我就可以使用rowwise()函式,用完之後記得ungroup()一下:
row %>%
rowwise() %>%
mutate(num_symptoms = sum(c(fever, chills, cough, aches, vomit) == "yes")) %>%
ungroup() %>%
select(fever, chills, cough, aches, vomit, num_symptoms)
比如上面的程式碼就計算好了每一個病人的症狀個數。
小結
今天給大家寫了資料處理中的一些函式和處理的一般流程:匯入資料後先整體把握,第二步規範列名,列搞定之後第三步就是去重,去完重就是生成新變數,變數轉換;最後一步就是行的選擇和新增。每一個步驟中給大家寫了一點點例子,感謝大家耐心看完,自己的文章都寫的很細,重要程式碼都在原文中,希望大家都可以自己做一做,請轉發本文到朋友圈後私信回覆“資料連結”獲取所有資料和本人收集的學習資料。如果對您有用請先記得收藏,再點贊分享。
也歡迎大家的意見和建議,大家想了解什麼統計方法都可以在文章下留言,說不定我看見了就會給你寫教程哦,有疑問歡迎私信。