1 Tổng quan nội dung bài học

Bài học này cung cấp kiến thức về các tính huống phổ biến khi thực hiện phân tích khám phá dữ liệu (data exploratory analysis - EDA) đối với dữ liệu có cấu trúc. Phân tích khám phá là hoạt động rất quan trọng, giúp ta hiểu rõ hơn về tập dữ liệu đang sử dụng và phát hiện ra các pattern trong dữ liệu, từ đó giúp data BI/Data Analyst tìm ra insight từ dữ liệu, giúp Data Scientist hiểu rõ dữ liệu trước khi xây dựng các mô hình dự báo, giúp Data Engineer hiểu và phát hiện ra các vấn đề liên quan tới dữ liệu trước khi xây dựng các data pipeline, v.v.

Trong khuôn khổ bài học này này, rất khó để đề cập chi tiết đến tất cả các kiến thức từ cơ bản cho tới nâng cao cần thiết cho việc phân tích, đặc biệt là về xác suất thống kê và data visualisation. Tuy nhiên, tác giả sẽ cố gắng đề cập đến các khái niệm và kiến thức căn bản cần thiết cho từng nội dung của bài học.

Trong quá trình học và sau khi học xong bài học này, học viên có thể tham khảo các tài liệu để bổ túc ở mức độ nền tảng về các kiến thức về xác suất thống kê, data visualisation và về kĩ năng phân tích dữ liệu. Tôi gợi ý một số tài liệu sau cho người mới bắt đầu:

  • Naked statistics (Charles Wheelan)

  • Story telling with data (Cole Nussbaumer Knaflic)

Sau khi đã nắm được nền tảng, đã xây dựng được cách tư duy về các vấn đề trên, học viên sẽ dễ dàng hơn khi tự tìm kiếm tài liệu và đào sâu về những chủ đề cụ thể tuỳ theo nhu cầu.

2 Hướng dẫn thực hành

Để thực hành viết code theo tài liệu này cũng như để làm các bài tập, sử dụng Google Colab notebook sau đây:

https://colab.research.google.com/drive/12hs5byyg4tYJgTwXPKYFCmH5plXhkuxw?usp=sharing

3 Lưu ý về vẽ biểu đồ

Các biểu đồ trong bài học này sẽ được vẽ bằng Python, sử dụng 2 thư viện phổ biến là seaborn và matplotlib. Bạn sẽ thấy rằng vẽ biểu đồ bằng Python hay bằng R (với thư viện ggplot2) tuy cho ta nhiều tuỳ chỉnh để thực hiện vẽ biểu đồ chính xác đúng ý muốn và giúp ta vẽ được một số biểu đồ có độ phức tạp cao, các thư viện này đòi hỏi ta phải viết code khá nhiều (với ggplot2 trong R thì số dòng code bạn phải viết sẽ ít hơn trong Python một chút) khi so sánh với các công cụ vẽ biểu đồ khác đơn giản, trực quan và nhanh chóng hơn như Excel, PowerBI, Tableau, … Vì vậy, hãy suy nghĩ và linh hoạt trong việc lựa chọn công cụ để vẽ biểu đồ sao cho hợp lí. Bạn có thể tham khảo các gợi ý sau để lựa chọn công cụ vẽ biểu đồ cho phù hợp với từng hoàn cảnh, và dù bạn có chọn công cụ nào, thì các kiến thức về phân tích dữ liệu trong bài học này cũng vẫn sẽ không thay đổi:

  • Chọn công cụ vẽ biểu đồ nào mà bạn cảm thấy tự tin sử dụng nhất. Dù công cụ có phức tạp nhưng nếu bạn nắm rõ về nó và thường xuyên sử dụng thì vẫn sẽ cho ra kết quả nhanh hơn các công cụ đơn giản khác.

  • Nếu bạn biết dùng nhiều công cụ, hãy chọn những công cụ ít đòi hỏi phải coding (ví dụ như Excel, PowerBI, Tableau, …) để phân tích nhanh một tập dữ liệu nếu lượng dữ liệu không quá lớn và đặc biệt là nếu bạn có kiến thức nghiệp vụ liên quan tới tập dữ liệu đó khá vững và trước đây đã từng làm việc với nguồn dữ liệu tương tự (như thế nào là “dữ liệu không quá lớn” là một chủ đề kĩ thuật và để giải đáp một cách hoàn chỉnh, chính xác thì xin được phép hẹn bạn đọc trong một bài giảng hoặc một blog post khác).

  • Nếu bạn ít khi làm việc với tập dữ liệu đó, có kiến thức nghiệp vụ chưa quá vững, hay vì lí do nào khác mà chưa cảm thấy tự tin khi sử dụng tập dữ liệu cần phân tích, và cần phải kiểm tra thật kĩ tập dữ liệu trước khi dùng, thì hãy dùng những công cụ cho phép coding (ví dụ như Python, R, SQL). Sau khi đã hiểu rõ về tập dữ liệu, bạn có thể dùng chính Python, R để vẽ biểu đồ hoặc xuất dữ liệu ra rồi import vào Excel, PowerBI, … để vẽ.

  • Cân nhắc chọn những công cụ vẽ biểu đồ bằng coding như Python, R khi bạn muốn tạo những báo cáo phân tích bằng markdown (là dạng file mà bạn có thể đưa cả text, code, hình ảnh, video, … vào, và sau đó export ra dưới dạng html hay pdf rất tiện lợi cho việc chia sẻ). Ví dụ tình huống thường gặp là làm báo cáo bằng Python, trình bày trong Jupyter Notebook (có thể xuất ra dưới dạng html, pdf hoặc gửi trực tiếp notebook); hoặc làm báo cáo bằng R Markdown, xuất ra dưới dạng html hoặc pdf.

  • Cân nhắc chọn những BI tool (PowerBI, Tableau, …) khi bạn muốn làm các báo cáo, dashboard mà người dùng có thể tương tác được dễ dàng, có giao diện thân thiện, dễ sử dụng, dữ liệu có thể được cập nhật định kì, thậm chí là ’in real time” hoặc “near real time”, và cần có các tính năng liên quan tới bảo mật và cấp quyền truy cập báo cáo.

  • Cân nhắc sử dụng Excel nếu đơn vị của bạn chưa quen hoặc chưa đầu tư (kinh phí) để sử dụng các công cụ BI tool mới như PowerBI hay Tableau; nếu việc bảo mật thông tin trong báo cáo không quá quan trọng; hoặc nhu cầu phân tích chưa rõ ràng, còn cần nhiều thay đổi trong tương lai. Excel vẫn là công cụ đa năng, linh hoạt, phổ biến và ở một số khía cạnh là thân thiện nhất với người dùng doanh nghiệp nói chung.

4 Phân loại dữ liệu

Ta cần hiểu cách phân loại dữ liệu vì những loại dữ liệu khác nhau sẽ có cách phân tích và trình bày kết quả phân tích khác nhau, và cách chúng được dùng trong những mô hình xác suất thống kê/ML cũng khác nhau.

Ta có thể phân chia dữ liệu thành 3 loại lớn là dữ liệu có cấu trúc (structured data), dữ liệu phi cấu trúc (unstructured data) và dữ liệu bán cấu trúc (semi-structured data). Đọc mục 2 bài viết tổng quan về ngành dữ liệu để xem thêm chi tiết về 3 loại dữ liệu này:

https://datatute.vn/wp-content/uploads/2023/10/data_industry_overview.html

Trong bài học này, chúng ta sẽ cùng tìm hiểu về các cách thức phân tích và trình bày kết quả phân tích cho dữ liệu có cấu trúc (structured data) và cụ thể là dữ liệu dạng bảng (rectangular data hay tabular data) vì đây là dạng dữ liệu thường gặp nhất. Ví dụ của dạng dữ liệu này là các bảng trong excel, relational database, hay các dataframe trong R hoặc Python.

Bên cạnh dữ liệu dạng bảng thì ta cũng sẽ có thể bắt gặp và sử dụng dữ liệu time series (1 chuỗi các giá trị liên tiếp của 1 biến nhất định) hay dữ liệu dạng graph (hay còn gọi là network). Tuy nhiên, những loại dữ liệu này sẽ không được đề cập đến trong bài học này.

Trong phân tích dữ liệu có cấu trúc và đặc biệt là dữ liệu dạng bảng, có 2 nhóm dữ liệu chính mà ta cần lưu ý và nắm rõ sự khác biệt giữa chúng, đó là numeric data và categorical data:

  • Numeric data (dữ liệu dạng số):

    • Continuous data: ví dụ như thời gian tính bằng giây, hay doanh thu tính bằng VND, …

    • Discrete data: ví dụ như các số đếm như số lượng khách hàng, số lượng đơn hàng, …

  • Categorical data (dữ liệu dạng phân nhóm/phân loại): loại dữ liệu này chỉ có một số lượng nhất định các giá trị, ví dụ như giới tính (Male, Female, Other), nhóm khách hàng (normal, VIP, …).

    • Binary data: đây là 1 trường hợp đặc biệt của categorical data, chỉ bao gồm 2 giá trị là 1/0 (hoặc là Yes/No hay True/False)

    • Ordinal data: đây cũng là 1 dạng categorical data. Các giá trị của loại dữ liệu này có xếp thứ tự. Ví dụ: đánh giá chất lượng sản phẩm của khách hàng (1 sao, 2 sao, 3 sao, 4 sao, 5 sao).

5 Phân tích riêng categorical data

Các bài toán phân tích trong thực tế thường bao hàm cả categorical data và numerical data. Tuy nhiên cũng có trường hợp ta chỉ quan tâm tới 1 biến categorical data nhất định. Thường khi chỉ xét tới 1 biến categorical data, ta sẽ quan tâm tới tần suất xuất hiện của từng giá trị trong biến categorical data đó (frequency distribution of categorical data) và giá trị nào xuất hiện nhiều nhất (mode). Tần suất cũng thường được thể hiện dưới dạng phần trăm (%).

Để phân tích các nội dung trên, ta thường sử dụng một công cụ đơn giản nhưng rất mạnh mẽ, đó là biểu đồ cột (bar chart).

5.1 Dùng bar chart để thể hiện tần suất xuất hiện/tỉ lệ của các giá trị trong categorical data

Đọc hiểu các yêu cầu cơ bản của bar chart tại đây: https://xdgov.github.io/data-design-standards/visualizations/bar-chart

Có 1 lưu ý quan trọng ta cần nhớ là trục thể hiện giá trị của các bar trong bar chart (trục y trong vertical bar chart và trục x trong horizontal bar chart) luôn phải bắt đầu bằng giá trị 0, nếu không thì người đọc biểu đồ sẽ có cảm giác sai khi so sánh mức độ lớn, bé giữa các bar với nhau.

Đề bài: ta có dữ liệu về khách hàng của một doanh nghiệp như sau.

import pandas as pd
url_customer_full = 'https://raw.githubusercontent.com/tuanphan92/datatute-datasets/main/data-analytics-booster/customer_full.csv'
customer_full_df = pd.read_csv(url_customer_full, encoding = "utf-8", sep="\t")
CustomerID Name Age DateOfBirth Country City
1 Nguyen Van A 19 1/11/2004 VN Hanoi
2 Le Van D 20 7/15/2003 VN Hanoi
10 Nguyen Van C 20 3/18/2003 VN Da Nang
34 Dinh Van O 26 12/12/1996 VN Ho Chi Minh City
39 Hoang Thi E 37 12/14/1985 VN Hanoi
42 Le Thuy F 41 10/14/1981 VN Vinh
44 Dinh Van R 123 1/1/1900 VN Hanoi
56 Le Thuy H 23 10/9/1999 VN Vinh
65 Le Thuy I 23 10/9/1999 VN Hanoi
70 Hoang Van J 23 10/9/1999 VN Hanoi
74 Pham Thanh K 23 10/9/1999 VN Hanoi
78 Dinh Van P 23 10/9/1999 VN Ho Chi Minh City
79 Hoang Van M 23 12/23/1999 VN Hanoi
83 Hoang Van N 34 1/29/1989 VN Ho Chi Minh City
92 Huynh Thi O 22 2/9/2001 VN Ho Chi Minh City
98 Huynh Thi P 30 8/9/1992 VN Hai Phong
102 Ly Thi U 33 11/4/1989 VN Da Nang
108 Huynh Thi R 37 5/24/1986 VN Hai Phong
111 Huynh Thi S 32 3/31/1991 VN Hai Phong
115 Huynh Thi Q 123 1/1/1900 VN Hanoi
118 Ly Thi V 23 10/9/1999 VN Hai Phong
119 Pham Thi I 23 10/9/1999 VN Ho Chi Minh City
121 Ly Thi B 23 10/9/1999 VN Ho Chi Minh City
131 Le Van C 23 10/9/1999 VN Da Nang
132 Nguyen Van B 23 10/9/1999 VN Ho Chi Minh City
135 Le Van E 23 10/9/1999 VN Hanoi
146 Le Van F 23 10/9/1999 VN Hanoi
147 Pham Thi G 23 10/9/1999 VN Da Nang
148 Pham Thi H 23 10/9/1999 VN Hanoi
150 Ly Thi A 23 10/9/1999 VN Hanoi
169 Hoang Van K 23 10/9/1999 VN Ho Chi Minh City
173 Pham Thanh L 23 10/9/1999 VN Hai Phong
177 Pham Thanh M 23 10/9/1999 VN Hanoi
180 Dinh Van N 25 9/2/1997 VN Da Nang
181 Hoang Thi D 24 12/12/1998 VN Hai Phong
184 Hoang Van L 22 11/3/2000 VN Ho Chi Minh City
189 Dinh Van Q 123 1/1/1900 VN Vinh
193 Le Thuy G 35 12/30/1987 VN Da Nang
199 Dinh Van S 34 12/21/1988 VN Da Nang

Hãy cho biết tổng quan về tình hình phân bố khách hàng theo vị trí địa lý và thành phố nào có nhiều khách hàng nhất

5.1.1 Nhóm dữ liệu và đếm số lượng khách hàng theo từng “City”

# Nhóm dữ liệu và đếm số lượng khách hàng theo từng "City"
cust_by_city = customer_full_df.groupby(
    "City",
    as_index=False
).agg(
    count_customers=("CustomerID", "nunique")
).sort_values(
  by="count_customers", 
  ascending=False
)

cust_by_city
##                City  count_customers
## 2             Hanoi               14
## 3  Ho Chi Minh City                9
## 0           Da Nang                7
## 1         Hai Phong                6
## 4              Vinh                3

Lưu ý: Khi “groupby” thì các giá trị NULL/None/NA sẽ bị bỏ qua, nên các dòng có category NULL sẽ không được xét đến. Ta cần lưu ý điểm này để tránh sai sót trong phân tích. Tốt nhất những category NULL này nên được xử lý để nhận giá trị text nào đó, ví dụ như “Unknown”.

5.1.2 Vẽ bar chart để thể hiện phân bố khách hàng theo tỉnh thành

# Import các thư viện cần thiết
import matplotlib.pyplot as plt 
import seaborn as sns

ax1 = sns.barplot(
  data = cust_by_city,
  x = "City", 
  y = "count_customers"  
)

plt.show(ax1)

5.1.3 Vẽ bar chart với đầy đủ các yếu tố cho một biểu đồ chuyên nghiệp

- Tên các cột rõ ràng

- Có các giá trị tuyệt đối và giá trị tương đối (tỉ lệ %)

- Có tên biểu đồ và tên đơn vị/cá nhân thực hiện biểu đồ

- Highlight yếu tố cần làm nổi bật (tuỳ vào yêu cầu đề bài mà có hay không, ở đây đề bài yêu cầu nêu ra tỉnh/TP có nhiều khách hàng nhất)

5.1.3.1 Chuẩn bị dữ liệu để tính toán cả giá trị tuyệt đối và giá trị tương đối (%) trước khi vẽ biểu đồ

# Bước 0: Chuẩn bị dữ liệu để tính toán cả giá trị tuyệt đối và giá trị tương đối (%) trước khi vẽ biểu đồ
cust_by_city = customer_full_df.groupby(
    "City",
    as_index=False
).agg(
    count_customers=("CustomerID", "nunique")
).sort_values(
  by="count_customers", 
  ascending=False
)

# Tạo thêm cột thể hiện tỉ lệ phần trăm của từng tỉnh thành so với tổng số khách hàng
cust_by_city["percentage"] = (
    round(
      100*(
        cust_by_city["count_customers"]/sum(cust_by_city["count_customers"])
      ),
      2
    )
)

# Tạo cột thể hiện cả số lượng khách hàng lẫn tỉ lệ %
cust_by_city["ct_and_pct"] = (
    cust_by_city["count_customers"].astype("string") + " (" + cust_by_city["percentage"].astype("string") + "%)"
)

# reset index của bảng dữ liệu để việc add số vào biểu đồ sau này sẽ dễ dàng hơn
data = cust_by_city.reset_index(drop=True)
data
##                City  count_customers  percentage  ct_and_pct
## 0             Hanoi               14       35.90  14 (35.9%)
## 1  Ho Chi Minh City                9       23.08  9 (23.08%)
## 2           Da Nang                7       17.95  7 (17.95%)
## 3         Hai Phong                6       15.38  6 (15.38%)
## 4              Vinh                3        7.69   3 (7.69%)

5.1.3.2 Vẽ biểu đồ

# Bước 1: Vẽ biểu đồ cơ bản. Kết quả trả ra sẽ là một `matplotlib.axes` object. Sử dụng các methods của object này ta dễ dàng tiếp tục tuỳ biến biểu đồ
ax2 = sns.barplot(
  data = data,
  x = "City", 
  y = "count_customers",
  color = "#83C5BE",
  saturation = 1
)

# Bước 2: Chèn số liệu vào biểu đồ
i = 0
for bar in ax2.patches:
  ax2.annotate(
      data["ct_and_pct"][i],
      xy = (bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5), # số 0.5 là để chữ hiện cách đỉnh của mỗi bar 0.5 đơn vị. Tuỳ scale của trục y mà ta điều chỉnh cho phù hợp
      horizontalalignment = "center"
  )
  i+=1

# Bước 3: Đổi lại tên các trục trên biểu đồ thành chữ viết thông thường (thay vì tên cột mặc định ở dataframe với dấu gạch dưới)
# Tuy nhiên trong biểu đồ này, thực chất ta không cần tên của trục x và y vì nó sẽ được thể hiện ở tiêu đề. Vì vậy ta bỏ tên của 2 trục này
ax2.set_ylabel("")
ax2.set_xlabel("")

# ax2.set_ylabel('No. customers', fontweight='normal', color = 'black', fontsize='10', horizontalalignment='center')
# g2.set_xlabel('title of the xlabel', fontweight='bold', color = 'red', fontsize='10', horizontalalignment='center')

# Bước 4: Bỏ các giá trị ở trục y để tránh làm rối biểu đồ vì mỗi bar đã có hiện số liệu rồi
ax2.set_yticks([])
## []
# Bước 5 (optional): Bỏ các dấu tick ở trục x để biểu đồ đẹp hơn
ax2.tick_params(bottom=False)

# Bước 6: Thêm tiêu đề (title)
ax2.set_title('Number of customers by city')

# Bước 7: Thêm chú thích vào biểu đồ
ax2.annotate(
    'Source: Data Analytics Department - ABC Company', 
    xy = (1.0, -0.12), 
    xycoords='axes fraction',
    horizontalalignment='right',
    fontstyle='italic'
)

# Bước 8: bỏ khung viền bao quanh biểu đồ để biểu đồ gọn gàng hơn
ax2.set_frame_on(False)
# Nếu chỉ muốn bỏ khung viền bên trên và bên phải, ta làm như sau:
# ax2.spines["top"].set_visible(False)
# ax2.spines["right"].set_visible(False)

# Bước 9: Highlight tỉnh/thành phố có nhiều khách hàng nhất (bar đầu tiên)
ax2.patches[0].set_facecolor('#006D77')

# Bước 10: Hiện biểu đồ hoàn chỉnh
plt.show(ax2)

6 Phân tích riêng numerical data

6.1 Phân phối của dữ liệu

Để hiểu được các giá trị của numerical data phân phối như thế nào, ta cần nắm rõ các khái niệm về xác suất thống kê mô tả sau đây:

6.1.1 Median:

Median (số trung vị) là giá trị giữa trong một phân bố, mà số các quan sát nằm trên hay dưới giá trị đó là bằng nhau.

Nguồn: Wikipedia

6.1.2 Quartiles:

Quartiles (tứ phân vị) là đại lượng mô tả sự phân bố và sự phân tán của tập dữ liệu. Tứ phân vị có 3 giá trị, đó là tứ phân vị thứ nhất (Q1), thứ nhì (Q2), và thứ ba (Q3). Ba giá trị này chia một tập hợp dữ liệu (đã sắp xếp dữ liệu theo trật từ từ bé đến lớn) thành 4 phần có số lượng quan sát đều nhau.

Giá trị tứ phân vị thứ hai Q2 chính bằng giá trị trung vị

Giá trị tứ phân vị thứ nhất Q1 bằng trung vị phần dưới

Giá trị tứ phân vị thứ ba Q3 bằng trung vị phần trên

Nguồn: Wikipedia

Xem ảnh minh hoạ dưới đây để hiểu trực quan hơn về các tứ phân vị.

Lời xin lỗi: Các ảnh dưới đây được tác giả thu thập từ nhiều nguồn tài liệu khác nhau từ lâu và nay đã quên mất nguồn, mong bạn đọc thông cảm.

6.1.3 Các khái niệm khác thường gặp:

6.1.4 Diễn giải ý nghĩa của median và các quartiles:

Giả sử ta có tập dữ liệu về giá trị hợp đồng của các khách hàng của một công ty như sau. Khoá chính là ContractID, nghĩa là mỗi dòng thể hiện 1 hợp đồng khác nhau. Đơn vị của cột Amount là USD

url = 'https://raw.githubusercontent.com/tuanphan92/datatute-datasets/main/JobSeeker/Lesson1/CONTRACT.txt'
df = pd.read_csv(url, encoding = "utf-8", sep="\t")
df = df[["ContractID", "Amount"]]
df
##     ContractID  Amount
## 0          245      49
## 1          246      22
## 2           38      23
## 3           39     199
## 4          882     299
## 5          883      38
## 6          884      33
## 7          885      45
## 8          886      66
## 9          887     147
## 10         888     325
## 11         889     235
## 12         921      88
## 13         247      14
## 14         248     225
## 15         249     199
## 16         250     199
## 17         251      78
## 18         252      79
## 19         253      99
## 20         254     199
## 21         255     150
## 22         256    4999
## 23         923     255
## 24         258     350
## 25         259     199
## 26        1002     199
## 27        1003       0
## 28        1004      79
## 29        1005      99
## 30        1006     199
## 31        1007     200
## 32        1008     240
## 33        1009     235
## 34          48     228
## 35          49     133
## 36          50      48
## 37          51      44
## 38          52      45

Các giá trị tứ phân vị của biến “Amount” như sau:

df["Amount"].quantile([0.25, 0.5, 0.75])
## 0.25     48.5
## 0.50    147.0
## 0.75    212.5
## Name: Amount, dtype: float64

Ta đọc hiểu ý nghĩa các giá trị này như sau:

  • Q1 = 48.5 nên có 25% số hợp đồng có giá trị hợp đồng <= 48.5 USD. Hoặc ta cũng có thể nói rằng có 75% số hợp đồng có giá trị hợp đồng >= 48.5 USD.

  • Q3 = 212.5 nên có 75% số hợp đồng có giá trị hợp đồng <= 212.5 USD. Hoặc ta cũng có thể nói rằng có 25% số hợp đồng có giá trị hợp đồng >= 212.5 USD.

  • Q2 = 147.0 nên ta nói rằng giá trị hợp đồng có trung vị bằng 147 USD. Hoặc ta cũng có thể nói rằng có 50% số hợp đồng có giá trị hợp đồng <= 147 USD. Hoặc ta cũng có thể nói rằng có 50% số hợp đồng có giá trị hợp đồng >= 147 USD.

Lưu ý:

  • Tuỳ vào mục tiêu phân tích và thông điệp muốn truyền tải tới người nghe/đọc mà ta diễn giải các giá trị của tứ phân vị theo cách phù hợp. Ví dụ: Giả sử hoạt động kinh doanh đang tốt và ta muốn nhấn mạnh điều đó thì hãy diễn giải Q1 là có 2/3 (75%) số hợp đồng có giá trị hợp đồng >= xyz USD (ý là hầu hết hợp đồng ký được với khách hàng đều có giá trị lớn) thay vì nói rằng có 25% số hợp đồng có giá trị <= xyz USD.

  • Khi dùng hàm pd.series.quantile() để tính toán các quantiles, các giá trị NULL/NaN/NA sẽ bị bỏ qua. Ví dụ ta muốn xem phân phối dữ liệu giá trị hợp đồng của 1 tập dữ liệu có 100 quan sát ứng với 100 hợp đồng nhưng 20 hợp đồng bị NULL giá trị thì hàm quantile() sẽ chỉ tính toán các quantiles trên 80 quan sát mà thôi. Vì vậy ta cần lưu ý để xử lý dữ liệu cho phù hợp. Ví dụ nếu 20 hợp đồng NULL giá trị kia thực chất là 1 loại hợp đồng đặc biệt mà công ty không thu về tiền hoặc thu về rất ít không đáng kể chứ không phải bị thiếu dữ liệu, thì ta có thể chuyển các giá trị hợp đồng đó về = 0 và hàm quantiles() sẽ tính toán trên toàn bộ 100 quan sát/hợp đồng. Tuy nhiên, kể cả trong trường hợp này, việc chỉ tính toán quantiles trên các hợp đồng thường và loại bỏ những hợp đồng đặc biệt này ra có lẽ sẽ đem đến nhiều insights hữu ích hơn.

6.1.5 Histogram:

Histogram là loaị biểu đồ được dùng phổ biến để thể hiện phân phối của dữ liệu continuous (Numeric data có thể được chia thành 2 loại là continuous và discrete, và histogram chỉ được dùng để thể hiện phân phối của dữ liệu continuous).

Ưu điểm:

  • Dễ đọc, dễ hiểu.

  • Giúp ta hanh chóng nắm bắt được hình dạng của phân phối dữ liệu (normal distribution hay skewed right, skewed left, …), thấy được dữ liệu phân tán hay tập trung ở những khoảng nào, và thấy được các outliers.

Nhược điểm:

  • Hình dạng của histogram phụ thuộc vào bin size (hay bin width) mà ta lựa chọn khi vẽ biểu đồ. Bin size là kích cỡ của từng cột trong histogram. Việc lựa chọn bin size sẽ quyết định có bao nhiêu bin (cột chữ nhật) xuất hiện trong histogram và sẽ làm thay đổi hình dạng của histogram. Tuỳ vào tập dữ liệu mà khi thay đổi bin size, histogram sẽ thay đổi nhiều hay ít và ảnh hưởng nhiều hay ít tới nhận định của ta về phân phối dữ liệu. Dù có những quy tắc để giúp chọn ra bin size tối ưu cho histogram, việc lựa chọn bin size hoặc lựa chọn số lượng bin thường mang tính chủ quan của người vẽ biểu đồ, và ngay cả khi dùng các quy tắc lựa chọn bin size thì việc chọn quy tắc nào cũng là một quyết định chủ quan.

  • Không phù hợp với những bộ dữ liệu mà có quá ít quan sát, vì đơn giản là không đủ dữ liệu để giúp histogram thể hiện ra được một hình mẫu phân phối dữ liệu nào cả.

Các hình dạng của phân phối dữ liệu:

Lưu ý: Ta thấy rằng histogram và bar chart rất giống nhau, đặc biệt là khi số lượng bin của histogram nhỏ thì histogram lại càng giống một bar chart thông thường, nhưng khác là với histogram thì các nhóm (categories / bins) ở trục x thực ra chính là dùng numeric data để tạo thành (biến numeric data thành categorical data) và các nhóm được xếp theo thứ tự tăng dần. Việc này giúp giảm bớt mức độ phức tạp của dữ liệu, giúp dễ phát hiện ra các điểm đặc biệt trong phân phối dữ liệu hơn. Điều này đặc biệt quan trọng trong phân tích khám phá dữ liệu (EDA).

6.1.6 Box and whisker plot:

Bên cạnh histogram thì box and whisker plot (nói gọn là boxplot) cũng là loaị biểu đồ được dùng rất phổ biến để thể hiện phân phối của dữ liệu continuous.

Ta có boxplot sau đây. Hãy thử xem bạn có thể đọc ra tất cả những điểm quan trọng trên boxplot này không?

Đáp án:

6.2 Correlation

Khi thực hiện phân tích khám phá dữ liệu (EDA), đặc biệt là để chuẩn bị cho việc xây dựng các mô hình dự báo, ta thường phải kiểm tra mối tương quan giữa các biến dự báo (predictor) và giữa các biến dự báo với biến mục tiêu (target variable).

Khi nói về correlation thì thuật ngữ quan trọng nhất ta cần nhớ đến đó là “correlation coefficient” (hệ số tương quan). Đây là một chỉ số đo lường mức độ liên quan đến nhau của các biến dạng số (numerical variables).

Có nhiều loại hệ số tương quan nhưng phổ biến nhất là Pearson Correlation Coefficient. Đây là hệ số tương quan đo lường mối tương quan tuyến tính (linear correlation) giữa 2 biến numeric. Để sử dụng Pearson Correlation Coefficient thì ta cần đảm bảo dữ liệu thoả mãn một số điều kiện nhất định (assumptions). Xem các assumption và ví dụ để giải thích về chúng tại đây: https://www.statology.org/pearson-correlation-assumptions/

Hệ số tương quan nhận các giá trị từ -1.0 đến 1.0. Nếu hệ số tương quan có giá trị âm nghĩa là 2 biến có mối tương quan ngược chiều (biến này tăng thì biến kia giảm và ngược lại). Nếu hệ số tương quan bằng -1.0 thì 2 biến có mối tương quan là “perfect negative correlation”. Ngược lại, nếu hệ số tương quan có giá trị dương nghĩa là 2 biến có mối tương quan cùng chiều (cùng tăng hoặc cùng giảm). Nếu hệ số tương quan bằng 1.0 thì 2 biến có mối tương quan là “perfect positive correlation”. Nếu hệ số tương quan bằng 0 thì nghĩa là không có mối tương quan tuyến tính nào giữa giá trị của 2 biến đó cả.

Khi xây dựng các mô hình dự báo, thường ta sẽ cần biết được các biến nào (variable/feature) có mức độ tương quan cao với nhau và với biến target. Tuỳ thuộc vào bối cảnh và vào thuật toán được lựa chọn mà ta sẽ cần phải xử lý các biến có tương quan cao với nhau. Ví dụ, giả sử ta có 2 biến có tương quan cao với nhau trong mô hình, nếu mô hình của ta sử dụng thuật toán mà bản thân thuật toán không xử lý được multicollinearity thì ta cần phải loại bỏ 1 trong 2 biến đó.

Lưu ý quan trọng: correlation (mối quan hệ tương quan) không đồng nghĩa với causation (mối quan hệ nguyên nhân - kết quả). 2 biến có tương quan với nhau thì chưa chắc là 1 biến thay đổi sẽ khiến cho biến kia thay đổi. Ví dụ: Giả sử ta phân tích lợi nhuận của một công ty và ta thấy lợi nhuận có tương quan cùng chiều (positive correlation) với số lượng nhân viên trong công ty (có những giai đoạn lợi nhuận tăng và số lượng nhân viên cũng tăng). Tuy nhiên, chỉ dựa vào đây thì ta không thể kết luận rằng việc tuyển dụng thêm nhân viên sẽ làm tăng lợi nhuận cho công ty. Lí do là vì cả việc công ty tăng lợi nhuận lẫn tăng số lượng nhân viên có thể đều là do 1 yếu tố thứ 3, ví dụ như do thị trường đang vào xu hướng tăng và nhu cầu mua mặt hàng đó của người tiêu dùng tăng lên, hoặc do công ty ra mắt một sản phẩm mới giúp thu hút nhiều khách hàng hơn. Trong những giai đoạn mà nhu cầu của khách hàng tăng cao, công ty phải tuyển dụng thêm nhân viên để đáp ứng nhu cầu này. Vì vậy, tuy rằng lợi nhuận và số lượng nhân viên của công ty có mối quan hệ tương quan cùng chiều, chúng lại không có mối quan hệ nhân-quả với nhau. Khi phân tích, ta cần hết sức lưu ý vấn đề này và tìm hiểu kĩ các yếu tố ẩn phía sau các sự việc, hiện tượng để đưa ra được những nhận định chính xác nhất.

Đề bài:

Ta có một pandas dataframe gồm 2 cột là Height và Weight như sau:

import pandas as pd
data = {
    'Height': [170, 168, 177, 181, 172, 171, 169, 175, 174, 178],
    'Weight': [65, 59, 77, 84, 69, 72, 63, 75, 70, 80]
}

df = pd.DataFrame(data)

df
##    Height  Weight
## 0     170      65
## 1     168      59
## 2     177      77
## 3     181      84
## 4     172      69
## 5     171      72
## 6     169      63
## 7     175      75
## 8     174      70
## 9     178      80

Sử dụng method corr() ta có thể tính toán correlation giữa 2 biến chiều cao và cân nặng như sau:

correlation = df['Weight'].corr(df['Height'])
print("Correlation between Height and Weight is:", correlation)
## Correlation between Height and Weight is: 0.9630006771100893

Kết quả cho ta thấy rằng 2 biến chiều cao và cân nặng có mối tương quan cùng chiều rất mạnh với nhau. Tuy nhiên, như đã lưu ý ở trên, điều này không có nghĩa là chúng có mối quan hệ nhân-quả với nhau.

6.2.1 Thực hành

Có pandas dataframe như sau, hãy tạo pearson correlation matrix thể hiện mối tương quan của từng cột với tất cả các cột còn lại.

data = {
    'Height': [170, 168, 177, 181, 172, 171, 169, 175, 174, 178],
    'Weight': [65, 59, 77, 84, 69, 72, 63, 75, 70, 80],
    'Age': [23, 21, 25, 27, 22, 24, 23, 26, 24, 25]
}

df = pd.DataFrame(data)

df
##    Height  Weight  Age
## 0     170      65   23
## 1     168      59   21
## 2     177      77   25
## 3     181      84   27
## 4     172      69   22
## 5     171      72   24
## 6     169      63   23
## 7     175      75   26
## 8     174      70   24
## 9     178      80   25

Đáp án:

correlation_matrix = df.corr(method ='pearson')
Height Weight Age
Height 1.0000000 0.9630007 0.8736601
Weight 0.9630007 1.0000000 0.9027306
Age 0.8736601 0.9027306 1.0000000

7 Phân tích categorical data và numerical data

7.1 Tính toán giá trị kì vọng (expected value)

Đề bài: công ty ABC cung cấp dịch vụ quản lý page facebook cho các cá nhân và hộ kinh doanh nhỏ lẻ, với 3 gói dịch vụ và các mức phí như sau:

  • Gói tiết kiệm (g1): 5,000,000 VND / tháng

  • Gói tiêu chuẩn (g2): 7,000,000 VND / tháng

  • Gói cao cấp (g3): 9,000,000 VND / tháng Công ty mở một hội thảo về xây dựng và quản lý fanpage với sự tham gia của nhiều KOL, KOC nhằm mục đích thu thập thêm thông tin của các khách hàng tiềm năng (kiếm thêm “lead”). Công ty dự kiến sau buổi hội thảo:

  • 5% số người tham gia hội thảo sẽ mua gói dịch vụ cao cấp

  • 10% số người tham gia hội thảo sẽ mua gói dịch vụ tiêu chuẩn

  • 15% số người tham gia hội thảo sẽ mua gói dịch vụ tiết kiệm

Từ những dự đoán trên, bộ phận Marketing của công ty tính toán giá trị hợp đồng kì vọng của 1 người tham gia hội thảo (expected value of each participant) theo công thức như sau:

  • Nhân giá trị của từng gói dịch vụ với khả năng xảy ra của nó (tức là với tỉ lệ % dự kiến số người tham gia hội thảo sẽ mua gói dịch vụ đó)

  • Cộng tổng các kết quả thu được lại với nhau

EV = (5,000,000 * 0.15) + (7,000,000 * 0.1) + (9,000,000 * 0.05) = 1,900,000 VND

Ta thấy rằng expected value về bản chất chính là giá trị “bình quân gia quyền” (weighted mean hay weighted average). Expected value là một khái niệm rất phổ biến trong kinh doanh, đặc biệt là trong việc định giá, tính toán chi phí, hoạch định ngân sách. Trong ví dụ này, expected value của mỗi khách hàng tham gia hội thảo cho ta biết doanh thu tiềm năng có thể thu được về từ hội thảo trên mỗi khách hàng, từ đó kết hợp với việc tính toán chi phí bỏ ra cho hội thảo mà công ty có thể đưa ra quyết định nên tổ chức hội thảo hay không, và nếu tổ chức thì doanh thu, lợi nhuận dự kiến thu về là bao nhiêu.

Lưu ý: Categorical data ở đây là gói dịch vụ (có các giá trị là “Gói tiết kiệm”, “Gói tiêu chuẩn”, “Gói cao cấp”) và để tính ra expected value thì ta cần kết hợp dùng numerical data là giá của từng gói và tỉ lệ % số người dự kiến mua của từng gói.

7.2 Phân phối các giá trị của numerical data trong từng nhóm (category) của categorical data

Đề bài: Công ty XYZ muốn phân tích số tiền mà các khách hàng của họ chi tiêu cho từng dòng sản phẩm có điểm gì khác nhau. Các chiều phân tích bao gồm loại hình khách hàng và dòng sản phẩm.

Dữ liệu lấy từ hệ thống của công ty XYZ như sau:

url_cust_spending = 'https://raw.githubusercontent.com/tuanphan92/datatute-datasets/main/data-analytics-booster/customer_spending_12m.csv'
cust_spending_df = pd.read_csv(url_cust_spending, encoding = "utf-8", sep=",")

cust_spending_df
##      customer_id customer_category product_category  spending_12m
## 0              1                 A            cat_3        225.52
## 1              2                 A            cat_3        177.00
## 2              3                 A            cat_1        339.17
## 3              4                 A            cat_3        111.16
## 4              5                 A            cat_1        113.75
## ..           ...               ...              ...           ...
## 175          176                 B            cat_2       1391.28
## 176          177                 B            cat_2       1243.25
## 177          178                 B            cat_2       1303.03
## 178          179                 B            cat_2       1393.61
## 179          180                 B            cat_2        851.56
## 
## [180 rows x 4 columns]

7.2.1 Phân tích theo chiều khách hàng:

# Tính toán các giá trị min, max và các quartiles để phục vụ cho việc vẽ biểu đồ
quantiles_by_category = [
    (
      categ,
      cust_spending_df.loc[cust_spending_df["customer_category"]==categ, "spending_12m"].quantile([0, 0.25, 0.5, 0.75, 1])
    )
    for categ in cust_spending_df["customer_category"].unique()
]

quantiles_by_category
## [('A', 0.00     12.7100
## 0.25    121.2725
## 0.50    218.9500
## 0.75    365.0700
## 1.00    497.9000
## Name: spending_12m, dtype: float64), ('B', 0.00     201.140
## 0.25     518.325
## 0.50     728.570
## 0.75     976.910
## 1.00    2850.610
## Name: spending_12m, dtype: float64)]
# Vẽ biểu đồ boxplot để dễ hình dung phân phối của dữ liệu cho từng nhóm khách hàng
import matplotlib.pyplot as plt
import seaborn as sns

# Bước 1: Vẽ biểu đồ cơ bản. Kết quả trả ra sẽ là một `matplotlib.axes` object. Sử dụng các methods của object này ta dễ dàng tiếp tục tuỳ biến biểu đồ
ax2 = sns.boxplot(
  x="customer_category",
  y="spending_12m",
  data=cust_spending_df,
  width=0.7 # làm cho các box hẹp lại để có thêm khoảng trống để thể hiện dữ liệu (bước 2)
)

# Bước 2: Chèn số liệu vào biểu đồ
for i in range(len(quantiles_by_category)):
  for stat in quantiles_by_category[i][1]:
    ax2.annotate(
        round(stat,1),
        xy = (i, stat), xycoords = 'data',
        xytext=(75, 0), textcoords='offset points' # chọn offset point (ở đâu là 75) để label cách box ra 1 đoạn 
    )

# Bước 3: Đổi lại tên các trục trên biểu đồ thành chữ viết thông thường (thay vì tên cột mặc định ở dataframe với dấu gạch dưới)
# Tuy nhiên trong biểu đồ này, thực chất ta không cần tên của trục x và y vì nó sẽ được thể hiện ở tiêu đề. Vì vậy ta bỏ tên của 2 trục này
ax2.set_ylabel("")
ax2.set_xlabel("")

# Bước 4: Bỏ các giá trị ở trục y để tránh làm rối biểu đồ vì mỗi bar đã có hiện số liệu rồi
ax2.set_yticks([])
## []
# Bước 5 (optional): Bỏ các dấu tick ở trục x để biểu đồ đẹp hơn
ax2.tick_params(bottom=False)

# Bước 6: Thêm tiêu đề (title)
ax2.set_title('Distribution of spending in the last 12 months by customer category')

# Bước 7: Thêm chú thích vào biểu đồ
ax2.annotate(
    'Source: Data Analytics Department - ABC Company',
    xy = (1.0, -0.12),
    xycoords='axes fraction',
    horizontalalignment='right',
    fontstyle='italic'
)

# Bước 8: bỏ khung viền bao quanh biểu đồ để biểu đồ gọn gàng hơn
ax2.set_frame_on(False)
# Nếu chỉ muốn bỏ khung viền bên trên và bên phải, ta làm như sau:
# ax2.spines["top"].set_visible(False)
# ax2.spines["right"].set_visible(False)

# Bước 9: Hiện biểu đồ hoàn chỉnh
plt.show()

Nhận xét: Ta thấy các khách hàng thuộc nhóm B có các giá trị min, Q1, median, Q3 đều lớn hơn các giá trị tương ứng của khách hàng nhóm A. Nhóm B còn có 1 khách hàng cá biệt chi tiêu tới 2850 USD. Trong thực tế, rất có thể nhóm B là nhóm khách hàng có thu nhập cao, được công ty định danh là nhóm khách hàng VIP.

7.2.2 Phân tích theo chiều sản phẩm:

# Tính toán các giá trị min, max và các quartiles để phục vụ cho việc vẽ biểu đồ
quantiles_by_category = [
    (
      categ,
      cust_spending_df.loc[cust_spending_df["product_category"]==categ, "spending_12m"].quantile([0, 0.25, 0.5, 0.75, 1])
    )
    for categ in cust_spending_df["product_category"].unique()
]

quantiles_by_category
## [('cat_3', 0.00     12.71
## 0.25    161.92
## 0.50    312.70
## 0.75    451.90
## 1.00    951.09
## Name: spending_12m, dtype: float64), ('cat_1', 0.00      15.84
## 0.25     162.33
## 0.50     340.12
## 0.75     514.44
## 1.00    2850.61
## Name: spending_12m, dtype: float64), ('cat_2', 0.00     543.1300
## 0.25     805.5100
## 0.50    1160.7800
## 0.75    1393.0275
## 1.00    1487.0700
## Name: spending_12m, dtype: float64)]
# Vẽ biểu đồ boxplot để dễ hình dung phân phối của dữ liệu cho từng nhóm sản phẩm
import matplotlib.pyplot as plt
import seaborn as sns

# Bước 1: Vẽ biểu đồ cơ bản. Kết quả trả ra sẽ là một `matplotlib.axes` object. Sử dụng các methods của object này ta dễ dàng tiếp tục tuỳ biến biểu đồ
ax2 = sns.boxplot(
  x="product_category",
  y="spending_12m",
  data=cust_spending_df,
  width=0.7 # làm cho các box hẹp lại để có thêm khoảng trống để thể hiện dữ liệu (bước 2)
)

# Bước 2: Chèn số liệu vào biểu đồ
for i in range(len(quantiles_by_category)):
  for stat in quantiles_by_category[i][1]:
    ax2.annotate(
        round(stat,1),
        xy = (i, stat), xycoords = 'data',
        xytext=(45, 0), textcoords='offset points' # chọn offset point (ở đâu là 75) để label cách box ra 1 đoạn 
    )

# Bước 3: Đổi lại tên các trục trên biểu đồ thành chữ viết thông thường (thay vì tên cột mặc định ở dataframe với dấu gạch dưới)
# Tuy nhiên trong biểu đồ này, thực chất ta không cần tên của trục x và y vì nó sẽ được thể hiện ở tiêu đề. Vì vậy ta bỏ tên của 2 trục này
ax2.set_ylabel("")
ax2.set_xlabel("")

# Bước 4: Bỏ các giá trị ở trục y để tránh làm rối biểu đồ vì mỗi bar đã có hiện số liệu rồi
ax2.set_yticks([])
## []
# Bước 5 (optional): Bỏ các dấu tick ở trục x để biểu đồ đẹp hơn
ax2.tick_params(bottom=False)

# Bước 6: Thêm tiêu đề (title)
ax2.set_title('Distribution of spending in the last 12 months by product category')

# Bước 7: Thêm chú thích vào biểu đồ
ax2.annotate(
    'Source: Data Analytics Department - ABC Company',
    xy = (1.0, -0.12),
    xycoords='axes fraction',
    horizontalalignment='right',
    fontstyle='italic'
)

# Bước 8: bỏ khung viền bao quanh biểu đồ để biểu đồ gọn gàng hơn
ax2.set_frame_on(False)
# Nếu chỉ muốn bỏ khung viền bên trên và bên phải, ta làm như sau:
# ax2.spines["top"].set_visible(False)
# ax2.spines["right"].set_visible(False)

# Bước 9: Hiện biểu đồ hoàn chỉnh
plt.show()

Nhận xét: Ta thấy nhìn chung mức độ chi tiêu của khách hàng vào nhóm sản phẩm “cat_1” và “cat_3” là tương đối giống nhau (vì các giá trị Q1, median, Q3 gần như bằng nhau). Tuy nhiên, chi tiêu vào nhóm sản phẩm “cat_2” có giá trị cao hơn hẳn 2 nhóm còn lại. Nếu cần đưa ra 1 con số là cao hơn bao nhiêu, ta có thể lấy median để so sánh. Trong trường hợp này, median của nhóm “cat_2” (1160.8 USD) cao hơn ~3.7 lần so với median của nhóm “cat_3” (312.7 USD), nên ta có thể kết luận như sau:

Chi tiêu vào nhóm sản phẩm “cat_2” trung bình cao gấp 3.7 lần chi tiêu vào nhóm sản phẩm “cat_1”.

Lưu ý:

  • cách dùng từ “trung bình” trong câu kết luận trên không mang hàm ý là so sánh giá trị trung bình (mean / average) giữa chi tiêu vào 2 nhóm sản phẩm, mà là 1 từ mang ý nghĩa tổng quát, giúp việc truyền tải thông tin tới những người không có nền tảng về xác suất thống kê được dễ hơn. Ta có thể hiểu từ “trung bình” ở đây theo nghĩa là “nhìn chung thì …”, “tổng quan thì …”. Tuy nhiên nếu có ai hỏi về cách tính ra số 3.7 hoặc nghi vấn rằng bạn thuần tuý chia 2 giá trị trung bình (mean) cho nhau để tính toán, thì bạn cần giải thích rõ cho họ hiểu là bạn dùng số trung vị (median) và cho họ xem phân phối dữ liệu qua biểu đồ boxplot.

  • Bộ dữ liệu này chỉ có 1 outlier (người khách hàng chi tiêu 2850.6 USD) và không ảnh hưởng tới những kết luận mà ta đã đưa ra. Tuy nhiên, trong thực tế, nếu bộ dữ liệu có nhiều outlier và mức chi tiêu của các nhóm khách hàng, nhóm sản phẩm không có sự khác biệt quá lớn, thì ta nên thực hiện các bước phân tích như sau: 1) tính toán các quantiles và visualise dữ liệu bằng boxplot để có cái nhìn tổng quan và phát hiện outlier, 2) tìm hiểu lí do vì sao có các outlier đó và xử lý outlier, 3) tính toán lại các quantiles, visualise lại dữ liệu, và đưa ra nhận xét / kết luận.

7.2.3 Phân tích theo chiều khách hàng và sản phẩm:

# Tính toán các giá trị min, max và các quartiles để phục vụ cho việc vẽ biểu đồ
quantiles_by_category = []

for pdt_categ in cust_spending_df["product_category"].unique():
  for cust_categ in cust_spending_df["customer_category"].unique():
    quantiles_by_category.append(
        (
          pdt_categ, 
          cust_categ,
          cust_spending_df.loc[
              (cust_spending_df["product_category"]==pdt_categ) & (cust_spending_df["customer_category"]==cust_categ), 
              "spending_12m"
          ].quantile([0, 0.25, 0.5, 0.75, 1]) 
        )
    )

n1 = cust_spending_df["product_category"].nunique()
n2 = cust_spending_df["customer_category"].nunique()
stat_tuples = [tuple(quantiles_by_category[i:i+n2]) for i in range(0, len(quantiles_by_category), n2)]
stat_tuples
## [(('cat_3', 'A', 0.00     12.7100
## 0.25    109.6375
## 0.50    232.5550
## 0.75    380.9825
## 1.00    494.6900
## Name: spending_12m, dtype: float64), ('cat_3', 'B', 0.00    201.14
## 0.25    354.96
## 0.50    528.47
## 0.75    690.83
## 1.00    951.09
## Name: spending_12m, dtype: float64)), (('cat_1', 'A', 0.00     15.8400
## 0.25    126.9975
## 0.50    190.3700
## 0.75    344.9325
## 1.00    497.9000
## Name: spending_12m, dtype: float64), ('cat_1', 'B', 0.00     243.95
## 0.25     475.06
## 0.50     602.20
## 0.75     913.28
## 1.00    2850.61
## Name: spending_12m, dtype: float64)), (('cat_2', 'A', 0.00   NaN
## 0.25   NaN
## 0.50   NaN
## 0.75   NaN
## 1.00   NaN
## Name: spending_12m, dtype: float64), ('cat_2', 'B', 0.00     543.1300
## 0.25     805.5100
## 0.50    1160.7800
## 0.75    1393.0275
## 1.00    1487.0700
## Name: spending_12m, dtype: float64))]
# Vẽ biểu đồ boxplot để dễ hình dung phân phối của dữ liệu cho từng nhóm sản phẩm
import matplotlib.pyplot as plt
import seaborn as sns

# Bước 1: Vẽ biểu đồ cơ bản. Kết quả trả ra sẽ là một `matplotlib.axes` object. Sử dụng các methods của object này ta dễ dàng tiếp tục tuỳ biến biểu đồ
g = sns.catplot(
  x="customer_category",
  y="spending_12m",
  data=cust_spending_df,
  col='product_category',
  kind='box',
  width=0.7 # làm cho các box hẹp lại để có thêm khoảng trống để thể hiện dữ liệu (bước 2)
)

# Bước 2: Chèn số liệu vào biểu đồ
# Truy cập các axes
axes = g.axes.flatten()

# Chèn số liệu vào từng subplot
for i in range(n1):
  ax = axes[i]
  for j in range(n2):
    for stat in stat_tuples[i][j][2]:
      ax.annotate(
        round(stat,1),
        xy = (j, stat), xycoords = 'data',
        xytext=(60, 0), textcoords='offset points' # chọn offset point (ở đâu là 75) để label cách box ra 1 đoạn 
      )

# Bước 3: Đổi lại tên các trục trên biểu đồ thành chữ viết thông thường (thay vì tên cột mặc định ở dataframe với dấu gạch dưới)
# Tuy nhiên trong biểu đồ này, thực chất ta không cần tên của trục x và y vì nó sẽ được thể hiện ở tiêu đề. Vì vậy ta bỏ tên của 2 trục này
g.set_xlabels("")

g.set_ylabels("")

# Bước 4 (optional): Bỏ các dấu tick ở trục x để biểu đồ đẹp hơn
g.tick_params(bottom=False)

# # Bước 5: Thêm tiêu đề (title)
g.fig.subplots_adjust(top=0.85) # đưa các chart xuống thấp 1 chút để lấy chỗ điền title
g.fig.suptitle('Distribution of spending in the last 12 months by product category and customer category', fontsize = 16)

# Bước 6: Thêm chú thích vào biểu đồ
g.fig.text(
    x = 1.0,
    y = 0,
    s = 'Source: Data Analytics Department - ABC Company',
    horizontalalignment='right',
    fontstyle='italic'
)

# Bước 7: Hiện biểu đồ hoàn chỉnh
plt.show()

Trong thực tế, đôi khi ta sẽ cần phân tích kĩ hơn về phân phối dữ liệu bằng cách so sánh nhiều quantiles hơn giữa các nhóm thay vì chỉ so sánh min, max và các quartiles (tứ phân vị - Q1, Q2, Q3). Ta có thể tạo 1 bảng quantiles như sau để phân tích

def calculate_quantile(series, q):
    return series.quantile(q)

quantile_table = cust_spending_df.groupby(["customer_category", "product_category"]).agg(
    min=("spending_12m", "min"),
    q10=("spending_12m", lambda x: calculate_quantile(x, 0.1)),
    q20=("spending_12m", lambda x: calculate_quantile(x, 0.2)),
    q30=("spending_12m", lambda x: calculate_quantile(x, 0.3)),
    q40=("spending_12m", lambda x: calculate_quantile(x, 0.4)),
    q50=("spending_12m", lambda x: calculate_quantile(x, 0.5)),
    q60=("spending_12m", lambda x: calculate_quantile(x, 0.6)),
    q70=("spending_12m", lambda x: calculate_quantile(x, 0.7)),
    q80=("spending_12m", lambda x: calculate_quantile(x, 0.8)),
    q90=("spending_12m", lambda x: calculate_quantile(x, 0.9)),
    max=("spending_12m", "max")
).reset_index()
customer_category product_category min q10 q20 q30 q40 q50 q60 q70 q80 q90 max
A cat_1 15.84 62.755 117.762 133.029 160.640 190.370 226.650 310.522 392.04 481.996 497.90
A cat_3 12.71 73.771 94.774 145.944 174.696 232.555 297.464 347.068 419.13 462.286 494.69
B cat_1 243.95 361.592 424.344 513.384 526.772 602.200 727.608 867.632 932.86 962.592 2850.61
B cat_2 543.13 604.603 748.662 850.003 949.558 1160.780 1260.738 1329.505 1405.52 1428.779 1487.07
B cat_3 201.14 243.770 312.700 370.310 382.800 528.470 587.560 664.070 721.52 782.020 951.09

Nhận xét:

  • Chi tiêu của nhóm khách hàng B cao hơn so với nhóm khách hàng A ở mọi nhóm sản phẩm, trung bình cao hơn từ ~2.7 - 3 lần

  • Chi tiêu của nhóm khách hàng A vào nhóm sản phẩm “cat_1” và “cat_3” chênh lệch nhau không đáng kể. Nhóm khách hàng A không chi tiêu vào sản phẩm “cat_2” (trong thực tế, có thể nhóm sản phẩm “cat_2” không được bán cho nhóm khách hàng A).

  • Chi tiêu của nhóm khách hàng B vào nhóm sản phẩm “cat_2” cao hơn trung bình ~2 lần so với chi tiêu của họ vào các nhóm sản phẩm còn lại (trong thực tế, có thể nhóm “cat_2” là nhóm sản phẩm có giá trị hợp đồng lớn).

7.3 Aggregate các giá trị của numerical data trong từng nhóm (category) của categorical data

Tương tự như phần Dùng bar chart để thể hiện tần suất xuất hiện/tỉ lệ của các giá trị trong categorical data phía trên, nhưng nếu đề bài yêu cầu so sánh tổng giá trị hợp đồng giữa các khu vực địa lý chẳng hạn, thì ta sẽ vẫn “groupby” theo “City”, nhưng sau đó thay vì đếm số dòng hay số giá trị của “CustomerID”, ta tính tổng (hàm “sum”) giá trị hợp đồng (giả sử bộ dữ liệu phía trên có cột “contract_value”). Như vậy thì đây có thể coi là phân tích có bao gồm cả categorical data (“City”) và numerical data (“contract_value”).

Cách vẽ biểu đồ cột như đã trình bày bên trên.