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ơ |
Thực hiện “rửa thô” (Pre-trimming) toàn bộ khoảng trắng trước khi đưa vào các thuật toán Regex để tránh sai số khi xác thực.
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ơ |
Chuẩn hóa Khóa chính (Primary Key) trước khi hợp nhất và sử dụng anti_join để kiểm soát các bản ghi mồ côi (Orphan records) - nguyên nhân hàng đầu gây thất thoát dữ liệu.
# 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 |
Để phân loại các trường văn bản tự do (Free-text) phức tạp, hệ thống tích hợp năng lực xử lý ngôn ngữ tự nhiên của LLMs.
Dưới đây bao gồm 2 phần: Đoạn mã tích hợp API thực tế (được comment để bảo mật Key) và Hàm mô phỏng (Mock-up) để chạy trực quan kết quả.
# ==============================================================================
# [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 |
Đánh giá chất lượng dữ liệu cuối cùng và xuất biểu đồ cung cấp Insight cho phòng 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")Kiến trúc pipeline trên minh chứng năng lực kết hợp giữa thao tác làm sạch dữ liệu lớn (Bulk Regex), kiểm soát toàn vẹn dữ liệu (Anti-join PK) và khả năng mở rộng để tích hợp các mô hình LLMs (API Calls). Luồng xử lý này có thể ứng dụng trực tiếp để tự động hóa hoạt động Data Appending và Labeling cho mọi hệ thống CRM/ERP.