Bối cảnh: Trong lĩnh vực E-commerce và FMCG, dữ liệu khách hàng thường bị phân mảnh ở nhiều nguồn (CRM, Website, Mạng xã hội) và chứa nhiều lỗi định dạng do thao tác nhập liệu thủ công.
Mục tiêu: Ứng dụng tư duy thuật toán bằng R và Kỹ thuật Prompt Engineering để tự động hóa luồng công việc: Thu thập \(\rightarrow\) Làm sạch \(\rightarrow\) Hợp nhất \(\rightarrow\) Làm giàu bằng AI \(\rightarrow\) Trực quan hóa.
Để minh họa rủi ro thực tế, dự án khởi tạo hai tập dữ liệu chứa các lỗi kinh điển: khoảng trắng thừa, định dạng khóa chính không đồng nhất (chữ hoa/thường) và lỗi nhập liệu
# 2.1. Dữ liệu CRM nội bộ (Chứa nhiều lỗi định dạng và khoảng trắng ẩn)
crm_data <- data.frame(
CustomerID = c("C001", "C002", "C002", "C003", "C004", "C005"), # Có trùng lặp C002
FullName = c("Nguyễn Văn A", "Trần Thị B", "Trần Thị B", "Lê Văn C", "phạm thị d", "Hoàng E"),
Phone = c("0901234567", "84987654321", "84987654321", "091-234-5678", "sai_so_dien_thoai", "0933334444"),
Email = c(" nva@gmail.com ", "ttb@yahoo.com", "ttb@yahoo.com", "lvc@.com", "ptd@company.vn", "hoange@gmail"),
Raw_Address = c("Q1, TPHCM", "Ba Đình, HN", "Ba Đình, HN", "Đà Nẵng", "quận 1, hồ chí minh", "Cần Thơ")
)
# 2.2. Dữ liệu Hành vi mua sắm & Phản hồi (Khóa chính bị lệch chuẩn)
behavior_data <- data.frame(
CustomerID = c("c001", " C002 ", "C003", "C004", "C099"), # c001 viết thường, C002 dư khoảng trắng, C099 là khách vãng lai
Total_Spend = c(1500000, 3200000, 500000, 7800000, 120000),
Raw_Feedback = c("Sản phẩm tốt, giao nhanh", "Hàng bị lỗi vỏ hộp", "Bình thường", "Rất hài lòng, sẽ mua lại", "Giao hàng quá chậm")
)
kable(head(crm_data), caption = "Bảng 1: Dữ liệu CRM thô chưa qua xử lý")| CustomerID | FullName | Phone | Raw_Address | |
|---|---|---|---|---|
| C001 | Nguyễn Văn A | 0901234567 | nva@gmail.com | Q1, TPHCM |
| C002 | Trần Thị B | 84987654321 | ttb@yahoo.com | Ba Đình, HN |
| C002 | Trần Thị B | 84987654321 | ttb@yahoo.com | Ba Đình, HN |
| C003 | Lê Văn C | 091-234-5678 | lvc@.com | Đà Nẵng |
| C004 | phạm thị d | sai_so_dien_thoai | ptd@company.vn | quận 1, hồ chí minh |
| C005 | Hoàng E | 0933334444 | hoange@gmail | Cần Thơ |
Vận dụng các biểu thức chính quy (Regex) và logic toán học để loại bỏ các điểm dị biệt (outliers), chuẩn hóa chuỗi và làm sạch dữ liệu tự động.
clean_crm <- crm_data %>%
# Bước 0: Pre-trimming - Xóa khoảng trắng thừa ở đầu/cuối của TẤT CẢ các cột
mutate(across(everything(), trimws)) %>%
# Bước 1: Loại bỏ dữ liệu trùng lặp (Deduplication)
distinct() %>%
# Bước 2: Chuẩn hóa Tên (Viết hoa chữ cái đầu)
mutate(FullName = str_to_title(FullName)) %>%
# Bước 3: Làm sạch và định dạng lại Số điện thoại (Chuyển về chuẩn +84)
mutate(
Clean_Phone = str_remove_all(Phone, "[^0-9]"),
Clean_Phone = case_when(
str_starts(Clean_Phone, "0") & nchar(Clean_Phone) == 10 ~ str_replace(Clean_Phone, "^0", "+84"),
str_starts(Clean_Phone, "84") & nchar(Clean_Phone) == 11 ~ paste0("+", Clean_Phone),
TRUE ~ "Invalid_Phone"
)
) %>%
# Bước 4: Xác thực cấu trúc Email bằng Regex
mutate(
Is_Valid_Email = str_detect(Email, "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"),
Clean_Email = ifelse(Is_Valid_Email, Email, NA) # Chuyển email sai thành Missing Value (NA)
) %>%
select(CustomerID, FullName, Clean_Phone, Clean_Email, Raw_Address)
kable(clean_crm, caption = "Bảng 2: Dữ liệu CRM sau khi làm sạch bằng Regex")| CustomerID | FullName | Clean_Phone | Clean_Email | Raw_Address |
|---|---|---|---|---|
| C001 | Nguyễn Văn A | +84901234567 | nva@gmail.com | Q1, TPHCM |
| C002 | Trần Thị B | +84987654321 | ttb@yahoo.com | Ba Đình, HN |
| C003 | Lê Văn C | +84912345678 | NA | Đà Nẵng |
| C004 | Phạm Thị D | Invalid_Phone | ptd@company.vn | quận 1, hồ chí minh |
| C005 | Hoàng E | +84933334444 | NA | Cần Thơ |
Sử dụng left_join để kết nối dữ liệu định danh khách hàng với dữ liệu hành vi mua sắm, tạo ra một góc nhìn toàn diện (360-degree view) trên một bảng dữ liệu duy nhất.
# Chuẩn hóa Khóa chính cho cả 2 bảng: Xóa khoảng trắng & Viết hoa toàn bộ
clean_crm <- clean_crm %>% mutate(CustomerID = toupper(trimws(CustomerID)))
clean_behavior <- behavior_data %>% mutate(CustomerID = toupper(trimws(CustomerID)))
# Kiểm tra dữ liệu mồ côi (Khách hàng có giao dịch nhưng không có trong CRM)
orphan_records <- anti_join(clean_behavior, clean_crm, by = "CustomerID")
print(paste("Phát hiện", nrow(orphan_records), "bản ghi mồ côi (Orphan Records) cần đối soát lại với hệ thống."))## [1] "Phát hiện 1 bản ghi mồ côi (Orphan Records) cần đối soát lại với hệ thống."
# Tiến hành hợp nhất tạo Customer 360 View
customer_360 <- clean_crm %>%
left_join(clean_behavior, by = "CustomerID") %>%
replace_na(list(Total_Spend = 0, Raw_Feedback = "No comment"))
kable(customer_360, caption = "Bảng 3: Hồ sơ khách hàng 360 độ (Đã hợp nhất)")| CustomerID | FullName | Clean_Phone | Clean_Email | Raw_Address | Total_Spend | Raw_Feedback |
|---|---|---|---|---|---|---|
| C001 | Nguyễn Văn A | +84901234567 | nva@gmail.com | Q1, TPHCM | 1500000 | Sản phẩm tốt, giao nhanh |
| C002 | Trần Thị B | +84987654321 | ttb@yahoo.com | Ba Đình, HN | 3200000 | Hàng bị lỗi vỏ hộp |
| C003 | Lê Văn C | +84912345678 | NA | Đà Nẵng | 500000 | Bình thường |
| C004 | Phạm Thị D | Invalid_Phone | ptd@company.vn | quận 1, hồ chí minh | 7800000 | Rất hài lòng, sẽ mua lại |
| C005 | Hoàng E | +84933334444 | NA | Cần Thơ | 0 | No comment |
Trong thực tế, các trường văn bản tự do (Free-text) như Địa chỉ hay Phản hồi rất khó phân loại bằng code thuần. Tại đây, tôi mô phỏng tư duy thiết kế luồng tự động hóa bằng cách viết hàm gọi API của LLM (Mô phỏng) để bóc tách và gắn nhãn (Labeling) dữ liệu.
# ==============================================================================
# [CODE SNIPPET] - TÍCH HỢP LLM API THỰC TẾ (OpenAI / Anthropic)
# Sử dụng thư viện httr & jsonlite để đẩy chuỗi text lên API và nhận JSON trả về
# ==============================================================================
# library(httr)
# library(jsonlite)
#
# ai_sentiment_labeler <- function(feedback_text, api_key) {
# response <- POST(
# url = "[https://api.openai.com/v1/chat/completions](https://api.openai.com/v1/chat/completions)",
# add_headers(Authorization = paste("Bearer", api_key)),
# content_type_json(),
# body = toJSON(list(
# model = "gpt-3.5-turbo",
# messages = list(
# list(role = "system", content = "Gắn nhãn phản hồi sau thành: Positive, Negative, hoặc Neutral. Chỉ trả về 1 từ duy nhất."),
# list(role = "user", content = feedback_text)
# ),
# temperature = 0
# ), auto_unbox = TRUE)
# )
# return(content(response)$choices[[1]]$message$content)
# }
# ==============================================================================
# Hàm mô phỏng (Mock-up function) dựa trên rule-based để xuất dữ liệu minh họa
extract_region_ai <- function(address) {
address_lower <- tolower(address)
if (str_detect(address_lower, "hcm|hồ chí minh")) return("Miền Nam")
if (str_detect(address_lower, "hn|hà nội")) return("Miền Bắc")
if (str_detect(address_lower, "đà nẵng|cần thơ")) return("Miền Trung/Tây Nam")
return("Khác")
}
analyze_sentiment_ai <- function(feedback) {
feedback_lower <- tolower(feedback)
if (str_detect(feedback_lower, "tốt|hài lòng")) return("Positive")
if (str_detect(feedback_lower, "lỗi|chậm")) return("Negative")
return("Neutral")
}
# Thực thi làm giàu dữ liệu (Data Enrichment)
enriched_data <- customer_360 %>%
rowwise() %>%
mutate(
Region = extract_region_ai(Raw_Address),
Sentiment_Label = analyze_sentiment_ai(Raw_Feedback),
# Phân hạng khách hàng (Customer Tiering) dựa trên chi tiêu
Customer_Tier = case_when(
Total_Spend >= 5000000 ~ "VIP",
Total_Spend >= 1000000 ~ "Standard",
TRUE ~ "Basic"
)
) %>%
ungroup()
kable(enriched_data %>% select(CustomerID, Region, Sentiment_Label, Customer_Tier),
caption = "Bảng 4: Kết quả bóc tách và dán nhãn dữ liệu")| CustomerID | Region | Sentiment_Label | Customer_Tier |
|---|---|---|---|
| C001 | Miền Nam | Positive | Standard |
| C002 | Miền Bắc | Negative | Standard |
| C003 | Miền Trung/Tây Nam | Neutral | Basic |
| C004 | Miền Nam | Positive | VIP |
| C005 | Miền Trung/Tây Nam | Neutral | Basic |
Trực quan hóa kết quả phân loại để đánh giá bức tranh tổng thể về khách hàng trước khi bàn giao dữ liệu cho đội ngũ Phân tích nghiệp vụ (Business Intelligence) hoặc Marketing.
## --- DATA QUALITY REPORT ---
cat("Tỷ lệ làm sạch Email thành công:", sum(!is.na(enriched_data$Clean_Email)) / nrow(enriched_data) * 100, "%\n")## Tỷ lệ làm sạch Email thành công: 60 %
cat("Số lượng khách hàng VIP cần chăm sóc đặc biệt:", sum(enriched_data$Customer_Tier == "VIP"), "\n")## Số lượng khách hàng VIP cần chăm sóc đặc biệt: 1
## ---------------------------
# Trực quan hóa phân khúc và cảm xúc
ggplot(enriched_data, aes(x = Customer_Tier, fill = Sentiment_Label)) +
geom_bar(position = "dodge", color = "black", alpha = 0.85) +
scale_fill_manual(values = c("Negative" = "#e74c3c",
"Neutral" = "#bdc3c7",
"Positive" = "#2ecc71")) +
theme_minimal() +
labs(
title = "Phân bổ Cảm xúc Khách hàng theo Phân khúc Chi tiêu",
subtitle = "Dữ liệu đã được làm sạch, xác thực và dán nhãn hoàn thiện",
x = "Phân khúc Khách hàng (Tier)",
y = "Số lượng",
fill = "Nhãn Cảm xúc (AI Labeled)"
) +
theme(text = element_text(size = 12),
legend.position = "bottom")Toàn bộ luồng mã lệnh trên đảm bảo tính toàn vẹn của tập dữ liệu từ đầu vào phân mảnh đến đầu ra chuẩn hóa. Việc kết hợp linh hoạt giữa các thuật toán Regex và tư duy ứng dụng Prompt AI cho phép hệ thống mở rộng (Scale-up) để xử lý hàng triệu dòng dữ liệu một cách tối ưu và tự động.