1. TỔNG QUAN DỰ ÁN

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.

2. KHỞI TẠO TẬP DỮ LIỆU THÔ (MESSY DATA)

Để 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ý")
Bảng 1: Dữ liệu CRM thô chưa qua xử lý
CustomerID FullName Phone Email Raw_Address
C001 Nguyễn Văn A 0901234567 Q1, TPHCM
C002 Trần Thị B 84987654321 Ba Đình, HN
C002 Trần Thị B 84987654321 Ba Đình, HN
C003 Lê Văn C 091-234-5678 lvc@.com Đà Nẵng
C004 phạm thị d sai_so_dien_thoai quận 1, hồ chí minh
C005 Hoàng E 0933334444 Cần Thơ

3. LÀM SẠCH VÀ XÁC THỰC DỮ LIỆU (DATA CLEANING)

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")
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 Q1, TPHCM
C002 Trần Thị B +84987654321 Ba Đình, HN
C003 Lê Văn C +84912345678 NA Đà Nẵng
C004 Phạm Thị D Invalid_Phone quận 1, hồ chí minh
C005 Hoàng E +84933334444 NA Cần Thơ

4. HỢP NHẤT DỮ LIỆU BẢO ĐẢM TOÀN VẸN (SECURE DATA MERGING)

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)")
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 Q1, TPHCM 1500000 Sản phẩm tốt, giao nhanh
C002 Trần Thị B +84987654321 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 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

5. LÀM GIÀU DỮ LIỆU BẰNG AI (DATA ENRICHMENT & LABELING)

Để 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")
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

6. BÁO CÁO CHẤT LƯỢNG & TRỰC QUAN HÓA (DATA QUALITY & VISUALIZATION)

Đá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.

# Báo cáo Profiling Nhanh
cat("--- DATA QUALITY REPORT ---\n")
## --- 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
cat("---------------------------\n")
## ---------------------------
# 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")

7. TỔNG KẾT

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.