Chuyển đến nội dung chính

Web Scraping và Phân tích Cú pháp Dữ liệu trong R | Khám phá dữ liệu H-1b Phần 1

Tìm hiểu cách thu thập dữ liệu từ web, tiền xử lý và thực hiện phân tích khám phá dữ liệu cơ bản với R
· 15 phút đọc

Khám phá cùng AI

Mục tiêu của hướng dẫn này là chỉ cho bạn cách thu thập dữ liệu về thị thực H1B thông qua web scraping với R. Tiếp theo, bạn cũng sẽ học cách phân tích cú pháp các đối tượng JSON, và cách lưu trữ cũng như thao tác dữ liệu để có thể thực hiện phân tích khám phá dữ liệu (EDA) cơ bản trên tập dữ liệu lớn về hồ sơ H1B.

Có thể bạn sẽ học được cách định vị bản thân tốt hơn với tư cách ứng viên hoặc một số đoạn mã R mới!

Giới thiệu

Tuần trước, blog của DataCamp "Can Data Help Your H-1B Visa Application" đã giới thiệu với bạn một số kết quả phân tích dữ liệu H-1B qua các năm. Giờ là lúc bắt tay vào phân tích dữ liệu cho chính mình và xem bạn còn khám phá được gì nữa! Ted Kwartler sẽ hướng dẫn bạn qua loạt bài hướng dẫn R này.

Tôi có một người bạn làm tại một hãng luật ở Texas chuyên nộp hồ sơ H1B. H1B là thị thực không định cư tại Hoa Kỳ cho phép các nhà tuyển dụng Mỹ tạm thời sử dụng lao động nước ngoài trong các ngành nghề chuyên môn. Rõ ràng, việc được chấp thuận là cực kỳ khó vì số lượng thị thực có hạn so với hàng nghìn người nộp đơn. Dù đây chỉ là giai thoại, tôi quyết định tự mình khám phá dữ liệu với hy vọng giúp các ứng viên đủ điều kiện biết rằng nước Mỹ là nơi chào đón!

Lấy Dữ liệu: Web Scraping và Phân tích Cú pháp

Đồng nghiệp tại DataCamp giới thiệu tôi đến trang này, một website đơn giản chứa dữ liệu H1B từ 2012 đến 2016. Trang cho biết có 2 triệu hồ sơ H1B được tổ chức trong một bảng duy nhất.Tôi quyết định thu thập dữ liệu này một cách lập trình (đọc: web scrape) vì tôi không định copy/paste đến hết đời! Như bạn thấy, hình dưới hiển thị một phần dữ liệu H1B của Boston trên trang:

Cơ sở dữ liệu Lương H1B

Các thư viện được sử dụng trong hướng dẫn này gồm jsonlite để phân tích cú pháp JSON, rvest để “thu hoạch” HTML, pbapply, mà tôi rất thích vì nó thêm thanh tiến trình cho các hàm apply cơ bản, và data.table, giúp cải thiện hiệu năng của R trên các data frame lớn.

library(jsonlite)
library(rvest)
library(pbapply)
library(data.table)

Khám phá Cấu trúc Trang

Khi khám phá trang, bạn sẽ nhận ra biểu mẫu tìm kiếm gợi ý các tùy chọn điền sẵn. Ví dụ, gõ “B” vào trường thành phố sẽ hiện một cửa sổ gợi ý như dưới đây. Hình bên dưới cho thấy các tùy chọn điền sẵn khi tôi gõ “B”:

Tùy chọn điền sẵn khi tìm bản ghi lương

Điều này có nghĩa bạn có thể dùng tính năng điền sẵn như một cách hiệu quả để truy vấn trang. Dùng Chrome, bạn có thể tải lại rồi nhấp chuột phải để “Inspect” trang, sau đó chuyển đến “Network” trong bảng dành cho nhà phát triển và cuối cùng gõ “B” trên trang để tải cửa sổ gợi ý. Khám phá các liên kết trong bảng Network, bạn sẽ thấy một truy vấn PHP trả về một đối tượng JSON các thành phố như thế này. Mục tiêu đầu tiên là thu thập tất cả các thành phố được gợi ý rồi dùng danh sách đó để quét nhiều trang chứa dữ liệu H1B. Khi bạn xem URL trước đó, bạn sẽ thấy nó kết thúc bằng một chữ cái. Vì vậy, bạn có thể dùng paste0() với phần gốc URL, http://h1bdata.info/cities.php?term=, và letters. Phần gốc sẽ được tái sử dụng cho từng giá trị trong letters. Đối tượng letters là một vector tích hợp sẵn trong R từ “a” đến “z”. Đối tượng json.cities là một vector các URL, từ a đến z, chứa mọi gợi ý điền sẵn dưới dạng JSON.

json.cities<-paste0('http://h1bdata.info/cities.php?term=', letters)
Phân tích Cú pháp các Đối tượng JSONĐối tượng json.cities là một vector gồm 26 liên kết cần được R đọc vào. Dùng lapply() hoặc pblapply() cùng với fromJSON, R sẽ phân tích từng đối tượng JSON để tạo all.cities. Bạn lồng kết quả trong unlist để đầu ra là một vector chuỗi đơn giản. Với đoạn mã này, bạn đã có toàn bộ các thành phố điền sẵn được sắp vào một vector có thể dùng để dựng các trang web thực sự chứa dữ liệu.
all.cities<-unlist(pblapply(json.cities,fromJSON))
Để giảm thời gian tải từng trang, bạn có thể quyết định truyền hai tham số, city và year, vào mỗi truy vấn trang. Ví dụ, dữ liệu H1B của Boston năm 2012, rồi Boston 2013, v.v. Một hàm tuyệt vời khi tạo kết hợp các nhân tố là expand.grid().Trong mã dưới đây, bạn thấy thông tin thành phố được truyền vào, all.cities, và sau đó là năm dùng seq() từ 2012 đến 2016. Hàm này tạo hơn 5000 kết hợp thành phố-năm. expand.grid() sẽ tự động tạo Boston 2012, Boston 2013, Boston 2014, v.v. vì mỗi thành phố và mỗi năm là một kết hợp nhân tố riêng.
city.year<-expand.grid(city=all.cities,yr=seq(2012,2016))
Một số thành phố như Los Angeles gồm hai từ nên phải được mã hóa cho URL. Hàm url_encode() chuyển “Los Angeles” thành Los%20Angeles để địa chỉ hợp lệ. Bạn truyền toàn bộ vector vào và url_encode() sẽ hoạt động theo từng hàng:
city.year$city<-urltools::url_encode(as.character(city.year$city))
Cuối cùng, bạn lại dùng hàm paste0() để nối phần gốc URL với các kết hợp thành phố và bang trong city.year. Xem một liên kết ví dụ tại đây.
all.urls<-paste0('http://h1bdata.info/index.php?em=&job=&city=', city.year[,1],'&year=', city.year[,2])

Trích xuất Thông tin Từ Trang

Sau khi hoàn thành các bước trước, bạn có thể tạo một hàm tùy chỉnh tên là main để thu thập dữ liệu từ mỗi trang. Đây là một quy trình đơn giản dùng các hàm từ rvest. Đầu tiên, một URL được nhận và read_html() phân tích nội dung trang. Tiếp đó, html_table duy nhất của trang được chọn từ tất cả thông tin HTML khác. Hàm main chuyển đối tượng x thành data.table để lưu trữ hiệu quả trong bộ nhớ. Cuối cùng, trước khi kết thúc main, bạn có thể thêm Sys.sleep để không bị coi là tấn công DDOS.

main<-function(url.x){
  x<-read_html(url.x)
  x<-html_table(x)
  x<-data.table(x[[1]])
  return(x)
  Sys.sleep(5)
}

Hãy đi lấy dữ liệu thôi! Tôi thích có thanh tiến trình pblapply() để theo dõi tiến độ scraping.
Bạn chỉ cần truyền all.urls và hàm main vào hàm pblapply(). Ngay lập tức, R bắt đầu tải một trang, thu thập bảng và giữ một data.table trong bộ nhớ cho trang đó. Mỗi URL lần lượt được thu thập và giữ trong bộ nhớ.

all.h1b<-pblapply(all.urls, main)

Kết hợp Dữ liệu Thành Một Data Table

Phù! Mất hàng giờ liền! Tại thời điểm này all.h1b là một danh sách các data table, mỗi trang một bảng. Để hợp nhất danh sách thành một data table duy nhất, bạn có thể dùng rbindlist. Cách này giống do.call(rbind, all.h1b) nhưng nhanh hơn nhiều.

all.h1b<-rbindlist(all.h1b)

Cuối cùng hãy lưu dữ liệu để bạn không phải làm lại. May cho bạn, tôi đã lưu một bản ở đây.

write.csv(all.h1b,'h1b_data.csv', row.names=F)

Làm sạch Dữ liệu

Dù bạn đã scrape dữ liệu, vẫn cần thêm vài bước để đưa nó về định dạng dễ xử lý.

Bạn dùng lubridate để tổ chức ngày tháng. Bạn cũng sử dụng stringr, cung cấp các hàm thao tác chuỗi.

library(lubridate)
library(stringr)

Dù là sở thích cá nhân, tôi thích dùng scipen=999. Không bắt buộc nhưng nó loại bỏ ký hiệu khoa học.

options(scipen=999)

Kết quả là việc scrape đã thu được 1,8 triệu trong số 2 triệu bản ghi H1B. Theo tôi, 1,8 triệu là đủ tốt. Vậy hãy nạp dữ liệu bằng fread(): hàm này giống read.csv nhưng là “trình đọc tệp nhanh & thân thiện”.

h1b.data<-fread('h1b_data.csv')

Các tên cột dữ liệu scrape được đều là chữ hoa và có khoảng trắng.

Vì vậy, tham chiếu theo tên khá bất tiện nên việc đầu tiên là đổi tên chúng.

Đổi tên cột cần các hàm ở cả hai phía của toán tử gán (<-). Ở bên trái dùng colnames() và truyền vào data frame. Ở bên phải, bạn truyền vào một vector chuỗi.

Trong ví dụ này, trước tiên bạn lấy tên gốc và chuyển thành chữ thường bằng tolower(). Ở dòng thứ hai bạn áp dụng gsub(), một hàm thay thế toàn cục.

Khi gsub() nhận ra một mẫu, trong trường hợp này là khoảng trắng, nó sẽ thay mọi lần xuất hiện bằng tham số thứ hai, dấu gạch dưới. Cuối cùng, bạn cần nói với gsub() thực hiện thay thế trong names(h1b.data) là các cột hiện đã ở dạng chữ thường.

colnames(h1b.data)<-tolower(names(h1b.data))
colnames(h1b.data)<-gsub(' ', '_', names(h1b.data))  

Một trong những hàm đầu tiên tôi dùng khi khám phá dữ liệu là tail(). Hàm này sẽ trả về các hàng cuối. Ở đây, tail() sẽ trả về 8 hàng cuối.

Điều này giúp bạn nhanh chóng thấy hình dạng dữ liệu và các vector trông thế nào.

tail(h1b.data, 8)

Tiếp theo, tôi luôn kiểm tra lớp (class) của các vector. Với dữ liệu scrape từ web, các giá trị số hoặc nhân tố có thể trở thành văn bản. Bạn sẽ thấy rằng sửa class ngay bây giờ sẽ tránh bực bội về sau!

Dùng hàm apply(), bạn truyền h1b.data, sau đó là 2 và hàm class. Vì bạn chọn 2, R sẽ kiểm tra class của từng cột và trả về console. Bạn có thể dùng apply() với 1 để áp dụng hàm theo hàng nhưng trong trường hợp này không hữu ích.

apply(h1b.data,2,class)

Ôi không!

Tất cả các cột đều là “character” và phải được sửa. Tôi sẽ chỉ bạn cách đổi một trong các cột ngày và phần còn lại để bạn tự thực hành. Dùng tail(), bạn có thể xem 6 hàng cuối của các ngày bị phân lớp sai.

Để sửa ngày tháng, các dấu gạch chéo / cần được đổi thành -. Một lần nữa, dùng gsub() để tìm /, và thay bằng -.

tail(h1b.data$submit_date)
h1b.data$submit_date<-gsub('/', '-', h1b.data$submit_date)

Với các dấu gạch ngang đã có, bạn áp dụng mdy() viết tắt của "month, day, year". Đó là vì ngày tháng được khai báo theo thứ tự này. Nếu thứ tự khác, bạn sẽ sắp xếp lại các chữ cái của mdy tương ứng.

Để đảm bảo cột đã được đổi đúng, bạn xem lại tail và kiểm tra class. tail() sẽ in các ngày trông như “2016-03-11 UTC” và class của vector nên là “POSIXct” thay vì “character”.

h1b.data$submit_date<-mdy(h1b.data$submit_date)
tail(h1b.data$submit_date)
class(h1b.data$submit_date)

Với loại phân tích này, việc trích riêng tháng và năm từ ngày tháng vào các cột mới là ý tưởng hay. Trong mã dưới đây bạn thấy hai cột mới $submit_month và $submit_yr được khai báo.

Trong lubridate, hàm month() có thể áp dụng lên cả cột để trích giá trị tháng từ một ngày. Hàm year() tương tự nhận cột ngày để tạo h1b.data$submit_yr. Khi bạn dùng hàm head(), giờ bạn sẽ thấy hai cột mới đã được tạo.

h1b.data$submit_month<-month(h1b.data$submit_date, label=T)
h1b.data$submit_yr<-year(h1b.data$submit_date)
head(h1b.data)

Tiếp theo, hãy xem cột $base_salary. Nó có dấu phẩy ở hàng nghìn và R coi là kiểu ký tự, nên phải đổi lại. Lại một lần nữa gsub() ra tay để loại dấu phẩy và thay bằng ký tự rỗng. Sau đó as.numeric() được áp dụng cho h1b.data$base_salary để chính thức đổi các giá trị thành số.

Bạn có thể xem một phần vector mới với head() ở dòng thứ ba.

h1b.data$base_salary<-gsub(',','',h1b.data$base_salary)
h1b.data$base_salary<-as.numeric(h1b.data$base_salary)
head(h1b.data$base_salary)

Một cách khác để chia tách dữ liệu này là theo bang. Khi bạn xem cột h1b.data$location, bạn thấy thành phố và bang được phân tách bằng dấu phẩy. Mã dưới đây dùng str_split_fixed() để tách thông tin địa điểm tại dấu phẩy đầu tiên. Chỉ cần truyền vào cột, ký tự phân tách và số cột muốn trả về. Đối tượng state thu được là một ma trận lớn có cùng số hàng như h1b.data và 2 cột.

state<-str_split_fixed(h1b.data$location,', ', 2)

Hai dòng mã tiếp theo gắn các vector bang riêng lẻ dưới dạng $city và $state vào h1b.data. Các vector này không hoàn hảo vì có thể khác nhau về chính tả như “Winston Salem” so với “Winston-Salem”. Nhìn chung, phương pháp này đủ tốt cho EDA đơn giản nhưng lưu ý có thể cần gộp thuật ngữ trong các phân tích khác.

h1b.data$city<-state[,1]
h1b.data$state<-state[,2] 

Khám phá Dữ liệu: Bước đầu

Nếu bạn đang nộp hồ sơ H1B, bạn sẽ muốn biết những bang nào có nhiều hồ sơ nhất để tăng cơ hội được chấp thuận. Hàm table() được dùng để đếm các biến phân loại và dễ dàng áp dụng cho h1b.data$state. Ở dòng thứ hai, bạn có thể tạo một data frame nhỏ để lưu tên bang và số lượng H1B tương ứng.

state.tally<-table(h1b.data$state)
state.tally<-data.frame(state=names(state.tally), h1b=as.vector(state.tally))

Dùng state.tally và barplot() sẽ tạo một biểu đồ cột cơ bản về giá trị H1B theo bang. Tham số thứ hai names.arg khai báo nhãn cột và las=3 bảo R đặt nhãn theo chiều dọc. Bạn có thể thấy vài địa điểm hơi lộn xộn do tách bằng dấu phẩy nhưng ý chính vẫn rõ ràng… một ứng viên H1B nhiều khả năng ở CA, NJ, NY hoặc TX.

barplot(state.tally$h1b,
        names.arg = names(table(h1b.data$state)),
        las=3)

Thống kê thị thực H1B

Bạn thấy số lượng thị thực H1B theo bang từ 2012 đến 2016.

Tiếp theo, hãy thử hiểu mối tương quan giữa thị thực H1B và một dữ kiện bên ngoài. Đơn giản, R có sẵn một tập dữ liệu tên state.x77. Đây là ma trận với 50 hàng, mỗi hàng là một bang, và các thông tin như dân số và tuổi thọ trong Tổng điều tra Hoa Kỳ năm 1977.

Mẹo: hãy dùng nguồn dữ liệu mới hơn trong phân tích của riêng bạn.

Hiện tại, dùng state.x77 là ví dụ tốt để học. Khám phá tập dữ liệu tích hợp này bằng head().

head(state.x77)

Hãy trộn thông tin này với dữ liệu state.tally vào một data frame lớn hơn để hiểu các mối quan hệ. Để làm vậy, tạo một data frame state.data chứa chữ viết tắt bang, state.abb, và dữ liệu Tổng điều tra 1977 cũ. Sau đó gọi merge, truyền vào state.tally và state.data.

Bạn có thể chỉ định rõ cột state là vector để nối. Xem một phần data frame ở dòng thứ ba bằng cách đánh chỉ số từ hàng 15 đến 20.

state.data<-data.frame(state=state.abb,state.x77)
state.data<-merge(state.tally,
                  state.data,
                  by='state')
state.data[15:20,]

Một hàm EDA cơ bản là cor: hàm này in ra hệ số tương quan giữa hai biến.

Nhớ rằng tương quan nằm trong khoảng -1 đến 1: 0 nghĩa là các biến không tương quan và có thể không liên quan. Số tiến gần 1 nghĩa là các giá trị tương quan dương (hy vọng) như lập trình R và thu nhập. Số âm nghĩa là mối quan hệ di chuyển ngược chiều, như lập trình R và đời sống xã hội!

Đoạn mã này áp dụng cor cho dân số bang năm 1977 và số lượng H1B hiện tại. Hãy nhớ, mã nhằm minh họa cách thực hiện phân tích dù thời điểm không khớp. Bạn có thể đổi $Population để tham chiếu đến vector khác trong data frame.

cor(state.data$Population, state.data$h1b)

Một cách khác để khảo sát mối quan hệ giữa các biến là dùng biểu đồ phân tán, đặc biệt là ma trận phân tán. Dùng pairs() để vẽ nhanh ma trận phân tán. Mã dưới dùng công thức để định nghĩa mối quan hệ. Mỗi cột được khai báo riêng lẻ với dấu cộng ở giữa. Tham số data nhận data frame và main là tiêu đề đồ thị.

pairs(~ h1b + Population + Income,
      data = state.data,
      main='h1b relationships')

Ma trận phân tán H1B

Bạn thấy ma trận phân tán này trực quan hóa số lượng H1B so với Population và Income của bang.

Bạn có thể quan sát mối quan hệ giữa Population và h1b. Điều này hợp lý vì bang đông dân hơn sẽ có nhiều cơ hội việc làm hơn cần thị thực H1B.

Để “phóng to” vào một biểu đồ đơn từ ma trận, chỉ cần gọi plot() và truyền vào hai biến.

plot(state.data$Income,state.data$h1b,
     main = 'Income to H1B')

Bước Tiếp Theo với Dữ liệu Thị thực H1B

Bạn mới chỉ bắt đầu hành trình khám phá H1B! Bộ dữ liệu này rất phong phú và trong bài tiếp theo bạn sẽ khám phá dữ liệu lương, loại bỏ ngoại lệ và tạo các biểu đồ ggplot2 thuyết phục hơn.

Bạn cũng sẽ phát triển thêm các khái niệm EDA bằng cách khám phá trạng thái H1B theo thời gian và các nhà tuyển dụng hàng đầu. Một trong những biểu đồ thú vị mà tôi sẽ cho bạn xem trước là biểu đồ hộp thể hiện phân phối lương theo trạng thái thị thực H1B bằng đoạn mã sau:

ggplot(h1b.data) +
geom_boxplot(aes(factor(case_status),base_salary,fill=as.factor(case_status))) +
  ylim(0,100000) +
  theme_gdocs() +
  scale_fill_gdocs() +
  theme(axis.text.x=element_blank())

Biểu đồ hộp theo trạng thái thị thực H1B

Hãy đón xem phần tiếp theo trong chuỗi hướng dẫn "Khám phá dữ liệu H-1B với R"! Trong lúc chờ đợi, hãy cân nhắc xem hướng dẫn data frame trong R, khóa học Nhập dữ liệu trong R hoặc Xử lý dữ liệu trong R với dplyr của chúng tôi. 

Chủ đề

Liên quan