Trong lĩnh vực phân tích dữ liệu kinh tế và kiểm toán chuỗi cung ứng, thông thầu (Bid Rigging) là hành vi thỏa thuận ngầm giữa các nhà thầu nhằm thao túng giá đấu thầu hoặc chia phần thị trường.
Để phát hiện hành vi này, kỹ năng quan trọng nhất của một Data Analyst / AI Engineer không phải là vội vã dựng các mô hình học máy phức tạp, mà là EDA (Exploratory Data Analysis - Phân tích khám phá dữ liệu) chuẩn xác. Dưới đây là hướng dẫn làm Project từng bước cụ thể.
BƯỚC 1: CHUẨN BỊ MÔI TRƯỜNG & KHAI BÁO THƯ VIỆN
Đầu tiên, bạn khởi tạo tệp Jupyter Notebook hoặc file Python và nạp các thư viện xử lý dữ liệu cơ bản:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Cấu hình phong cách hiển thị biểu đồ
sns.set_theme(style="whitegrid")
plt.rcParams['font.size'] = 10
# Đọc bộ dữ liệu đấu thầu thô (chuyển ô có ký tự '.' thành dạng NaN)
df = pd.read_csv("bidding_dataset.csv", na_values=['.'])
print("Kích thước bộ dữ liệu gốc:", df.shape)
print(df.head())
BƯỚC 2: TIỀN XỬ LÝ & LÀM SẠCH DỮ LIỆU (DATA PREPROCESSING)
2.1. Xử lý giá trị khuyết thiếu (Missing Values Imputation)
Trong dữ liệu đấu thầu thô, các ô thiếu thông tin thường bị chèn ký tự dấu chấm. Sau khi chuyển thành NaN, ta thực hiện gán giá trị trung bình (Mean Imputation) cho các cột định lượng:
# Kiểm tra số lượng ô khuyết thiếu ở mỗi cột
print("Thống kê ô thiếu:\n", df.isnull().sum())
# Danh sách các cột định lượng cần xử lý khuyết thiếu
numeric_cols = ['WBID', 'LFWBID', 'LFCBID', 'LFWQTI', 'LFCQTI', 'RAW_PRICE', 'DISTANCE']
# Gán giá trị trung bình (Mean) cho các cột thiếu
for col in numeric_cols:
if col in df.columns and df[col].isnull().sum() > 0:
mean_val = df[col].mean()
df[col] = df[col].fillna(mean_val)
print(f"Đã gán giá trị trung bình {mean_val:.4f} cho cột {col}")
2.2. Tách trường thời gian & Rút mẫu kiểm chứng (Sampling)
Ta tách trường Ngày và Tháng từ cột ngày tháng (Leddit date) để phục vụ phân tích chu kỳ mùa vụ. Đồng thời áp dụng lấy mẫu phân tầng (Stratified Sampling) để kiểm tra tính đại diện của mẫu:
# 1. Tách Ngày và Tháng từ cột 'Leddit'
if 'Leddit' in df.columns:
df['Leddit'] = pd.to_datetime(df['Leddit'])
df['Day'] = df['Leddit'].dt.day
df['Month'] = df['Leddit'].dt.month
# 2. Lấy mẫu phân tầng theo trường Thị trường (Market)
sample_stratified = df.groupby('Market', group_keys=False).apply(lambda x: x.sample(frac=0.3, random_state=42))
# 3. Vẽ biểu đồ KDE so sánh đường phân phối giá giữa tập gốc và tập mẫu
plt.figure(figsize=(9, 4))
sns.kdeplot(df['WBID'], label='Dữ liệu gốc', color='blue')
sns.kdeplot(sample_stratified['WBID'], label='Mẫu phân tầng', color='orange', linestyle='--')
plt.title('So sánh Phân phối Giá thầu WBID giữa Tập gốc và Tập mẫu')
plt.xlabel('Giá thầu WBID')
plt.ylabel('Mật độ phân phối (Density)')
plt.legend()
plt.tight_layout()
plt.show()
BƯỚC 3: BÓC TÁCH DẤU HIỆU THÔNG THẦU QUA EDA
3.1. So sánh giá trúng thầu giữa hai thị trường đối chứng
Bằng cách so sánh giá trúng thầu trung bình giữa thị trường nghi vấn (Tri-County) và thị trường đối chứng cạnh tranh tự do (Showroom/Surround) qua các năm:
# Tính giá trúng thầu trung bình (WBID) theo từng năm và thị trường
avg_price_year = df.groupby(['Year', 'Market'])['WBID'].mean().reset_index()
# Vẽ biểu đồ đường
plt.figure(figsize=(10, 5))
sns.lineplot(data=avg_price_year, x='Year', y='WBID', hue='Market', marker='o', linewidth=2.5)
plt.title('Duy biến Giá trúng thầu Trung bình giai đoạn 1983 - 1991')
plt.xlabel('Năm')
plt.ylabel('Giá trúng thầu trung bình (WBID)')
plt.tight_layout()
plt.show()
Phân tích Insight: Biểu đồ sẽ cho thấy giá thầu tại Tri-County liên tục duy trì ở mức cao vượt trội so với Showroom. Do không phải chịu áp lực đè giá từ đối thủ, liên minh thông thầu tại Tri-County tự do ấn định mức giá trúng thầu cao để chia nhau lợi nhuận.
3.2. Kiểm tra mức độ tập trung thị phần trúng thầu (Market Allocation)
Thống kê số lượng gói thầu trúng của từng nhà thầu ở từng khu vực:
plt.figure(figsize=(12, 5))
sns.countplot(data=df, x='Vendor', hue='Market', palette='Set2')
plt.title('Số lượng Gói thầu Trúng của các Nhà thầu theo Thị trường')
plt.xlabel('Nhà thầu (Vendor)')
plt.ylabel('Số lượng hợp đồng trúng')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Phân tích Insight: Hầu hết hợp đồng trúng thầu tại Tri-County tập trung tuyệt đối vào tay hai nhà thầu lớn là Trauth và Meyer. Ngược lại ở thị trường Showroom, các gói thầu trúng được phân bổ đồng đều cho nhiều bên. Đây là bằng chứng rõ ràng cho việc chia phần thị trường (Market Allocation).
BƯỚC 4: LOẠI TRỪ CÁC YẾU TỐ NHIỄU (KHOẢNG CÁCH & GIÁ NGUYÊN LIỆU)
Để khẳng định kết luận thông thầu là chính xác, ta tiến hành kiểm tra xem mức giá cao tại Tri-County có phải do chi phí vận chuyển hay giá nguyên liệu đắt đỏ gây ra hay không:
# 1. Vẽ biểu đồ phân tán Scatter Plot giữa Khoảng cách và Giá thầu
plt.figure(figsize=(8, 4))
sns.scatterplot(data=df, x='DISTANCE', y='WBID', hue='Market', alpha=0.7)
plt.title('Biểu đồ Scatter: Khoảng cách Vận chuyển vs Giá thầu WBID')
plt.xlabel('Khoảng cách (Miles)')
plt.ylabel('Giá thầu WBID')
plt.tight_layout()
plt.show()
# 2. Tính ma trận tương quan Pearson
corr_cols = ['WBID', 'RAW_PRICE', 'DISTANCE', 'LFWQTI']
corr_matrix = df[corr_cols].corr()
plt.figure(figsize=(6, 5))
sns.heatmap(corr_matrix, annot=True, cmap='Blues', fmt='.2f')
plt.title('Ma trận Tương quan Pearson giữa các Biến')
plt.tight_layout()
plt.show()
Kết quả phân tích định lượng:
Khoảng cách vận chuyển (Distance): Biểu đồ rải rác không tạo ra xu hướng tuyến tính rõ ràng. Khoảng cách xa hay gần không quyết định việc giá thầu bị đẩy lên cao.
Giá nguyên liệu sữa (Raw Price): Hệ số tương quan Pearson giữa giá nguyên liệu và giá thầu chỉ đạt mức trung bình từ 0.35 đến 0.38. Giá nguyên liệu tăng là biến động chung của toàn bộ ngành, không thể giải thích cho việc giá thầu tại Tri-County cao hơn bất thường so với Showroom.
BƯỚC 5: TỔNG KẾT VỚI BIỂU ĐỒ HỘP (BOXPLOT ANALYSIS)
Cuối cùng, sử dụng biểu đồ Hộp (Boxplot) để chứng minh sự khác biệt về Trung vị (Median) và Độ lệch chuẩn (Standard Deviation) giữa hai thị trường:
plt.figure(figsize=(8, 5))
sns.boxplot(data=df, x='Market', y='WBID', palette='Pastel1')
plt.title('Biểu đồ Hộp (Boxplot) So sánh Giá thầu theo Thị trường')
plt.xlabel('Thị trường')
plt.ylabel('Giá thầu WBID')
plt.tight_layout()
plt.show()
| Tiêu chí phân tích Boxplot | Thị trường Cạnh tranh (Showroom) | Thị trường Thông thầu (Tri-County) |
|---|---|---|
| Giá trị trung vị (Median) | Thấp, dao động trong khoảng từ 0.12 đến 0.13. | Cao vượt trội, nằm ở mức từ 0.15 trở lên. |
| Độ lệch chuẩn & Biên độ | Thể hiện sự biến động giá rộng, phản ánh cạnh tranh gay gắt. | Dải hộp hẹp ở khoảng trúng thầu, xuất hiện các điểm dị biệt cao. |
KẾT LUẬN
Qua 5 bước thực hành EDA chi tiết trên Python, bạn đã có thể chứng minh được giả thuyết thông thầu bằng các số liệu và biểu đồ định lượng hoàn toàn thuyết phục. Đây chính là nền tảng vững chắc cho những bạn nào muốn theo đuổi học nghề AI Engineer, trước khi xây dựng các mô hình Machine Learning phát hiện bất thường tự động trong thực tế.