Giao diện
Trong phân tích dữ liệu, việc tính toán các đại lượng thống kê trên toàn bộ tập dữ liệu (chẳng hạn giá trị trung bình hay độ lệch chuẩn của toàn bộ bảng) thường chỉ cung cấp một góc nhìn rất khái quát. Những hiểu biết sâu sắc nhất về mặt nghiệp vụ chỉ xuất hiện khi ta bóc tách dữ liệu thành các phân khúc nhỏ hơn: So sánh năng suất theo từng phòng ban, đo lường mức chi tiêu theo từng nhóm khách hàng, hay theo dõi biến động giá theo từng vùng địa lý.
Để thực hiện các thao tác này một cách khoa học, nhà thống kê học Hadley Wickham (2011) đã chuẩn hóa mô hình Tách - Áp dụng - Gộp (Split-Apply-Combine). Thay vì viết các vòng lặp thủ công chậm chạp, thư viện pandas cung cấp công cụ groupby để thực thi mô hình này với hiệu năng cao. Nội dung bài học tập trung vào cơ chế gom nhóm, tổng hợp nhiều chỉ số với agg, gán kết quả nhóm về từng dòng với transform, và lọc phân khúc với filter. Đồng thời, bài giảng hướng dẫn chi tiết phương thức apply cùng hai công cụ tổng hợp đa chiều là bảng pivot và bảng chéo.
1. Tư duy về các thao tác nhóm (Split-Apply-Combine)
Mô hình Tách - Áp dụng - Gộp phân rã bài toán xử lý nhóm thành ba pha toán học rạch ròi:
- Tách (Split): Bảng dữ liệu được chia cắt thành các nhóm con (groups) độc lập dựa trên một hoặc nhiều khóa phân loại.
- Áp dụng (Apply): Một hàm xử lý (tính toán thống kê, chuẩn hóa, hoặc lọc dòng) được áp dụng độc lập trên từng nhóm con.
- Gộp (Combine): Toàn bộ các kết quả riêng lẻ từ từng nhóm được thu gom và ghép nối lại thành một cấu trúc dữ liệu mới duy nhất.
1.1. Khởi tạo groupby và cơ chế đánh giá trì hoãn (Lazy Evaluation)
Trong pandas, việc gọi df.groupby("khoa") chưa thực thi bất kỳ phép tính số học nào ngay lập tức. Đây là một đối tượng trì hoãn (Lazy Evaluation): Pandas chỉ quét qua các cột khóa để phân nhóm các hàng. Phép tính chỉ thực sự diễn ra khi bạn gọi một hàm tổng hợp cụ thể như .mean(), .sum() hay .agg().
python
import numpy as np
import pandas as pd
bang_nhan_su = pd.DataFrame({
"phong_ban": ["Kỹ thuật", "Kỹ thuật", "Kinh doanh", "Kinh doanh", "Kỹ thuật"],
"chuc_danh": ["Lập trình viên", "Kiểm thử", "Chuyên viên", "Trưởng nhóm", "Lập trình viên"],
"luong": [2000, 1500, 1800, 2500, 2200],
"thuong": [300, 150, 400, 600, 350]
})
# Khởi tạo đối tượng gom nhóm theo phòng ban
nhom_pb = bang_nhan_su.groupby("phong_ban")
print("Đối tượng gom nhóm:", nhom_pb)
# Phép tính trung bình chỉ thực thi tại bước này:
print(nhom_pb["luong"].mean())1.2. Duyệt qua từng nhóm
Đối tượng groupby hỗ trợ giao thức lặp (iteration protocol), trả về các cặp gồm (ten_nhom, bang_nhom). Mỗi bang_nhom là một hoàn chỉnh chứa toàn bộ các dòng thuộc về nhóm đó:
python
for ten, bang in bang_nhan_su.groupby("phong_ban"):
print(f"=== Nhóm: {ten} ===")
print(bang[["chuc_danh", "luong"]])1.3. Chọn cột và tập con các cột
- Khi chọn một cột bằng cú pháp ngoặc vuông đơn
df.groupby("phong_ban")["luong"], kết quả là một đối tượngSeriesGroupBy. Khi tính tổng hợp, nó trả về một Series. - Khi truyền một danh sách các cột
df.groupby("phong_ban")[["luong", "thuong"]], kết quả là mộtDataFrameGroupBy. Khi tính tổng hợp, nó trả về một .
1.4. Gom nhóm bằng từ điển, Series, hàm và tầng chỉ mục
Khóa gom nhóm không nhất thiết phải là một cột có sẵn trong bảng. Pandas cho phép gom nhóm bằng nhiều phương tiện linh hoạt:
- Gom nhóm bằng từ điển: Ánh xạ từng nhãn sang một nhóm mới:
python
anh_xa = {"Kỹ thuật": "Khối Sản phẩm", "Kinh doanh": "Khối Doanh thu"}
print(bang_nhan_su.set_index("phong_ban").groupby(anh_xa)["luong"].mean())- Gom nhóm bằng hàm Python: Bất kỳ hàm số nào nhận giá trị của hàng và trả về tên nhóm đều có thể làm khóa. Chẳng hạn nhóm các từ theo độ dài chuỗi bằng hàm
len. - Gom nhóm theo tầng (Index Levels): Với các có chỉ mục phân cấp
MultiIndex, ta chỉ định tham sốlevel=0hoặclevel="ten_tang"để gom nhóm theo tầng mong muốn.
2. Tổng hợp dữ liệu (Data Aggregation)
Tổng hợp (Aggregation) là quá trình rút gọn một mảng nhiều giá trị số học của một nhóm thành một giá trị vô hướng duy nhất (như tổng, trung bình, hay độ lệch chuẩn).
2.1. Các hàm tổng hợp tích hợp sẵn
Pandas tối ưu hóa bằng mã C/Cython cho các hàm tổng hợp phổ biến: Các hàm count() (đếm giá trị khác NA), sum(), mean(), median(), std(), var(), min(), max(), prod(), first(), last().
2.2. Phương thức đa năng agg và đặt tên cột tổng hợp
Phương thức agg() (hoặc aggregate()) cho phép thực thi linh hoạt nhiều phép toán:
- Áp dụng danh sách các hàm: Dùng cú pháp
nhom_pb["luong"].agg(["mean", "std", "count"]). - Áp dụng các hàm khác nhau cho từng cột khác nhau bằng từ điển:
python
nhom_pb.agg({
"luong": ["min", "max"],
"thuong": "sum"
})- Cú pháp đặt tên cột tổng hợp (Named Aggregation): Cú pháp hiện đại giúp chỉ định tường minh tên cột đầu ra kèm theo cột gốc và hàm tương ứng, loại bỏ hoàn toàn các tiêu đề cột phân cấp phức tạp:
python
ket_qua_tong_hop = bang_nhan_su.groupby("phong_ban").agg(
luong_trung_binh=("luong", "mean"),
luong_cao_nhat=("luong", "max"),
tong_tien_thuong=("thuong", "sum"),
so_nhan_su=("luong", "count")
)
print("Bảng tổng hợp với Named Aggregation:")
print(ket_qua_tong_hop)
assert ket_qua_tong_hop.loc["Kinh doanh", "tong_tien_thuong"] == 10002.3. Trả về kết quả không có chỉ mục nhóm với as_index=False
Mặc định, các khóa nhóm sẽ trở thành hàng của bảng kết quả. Nếu muốn các khóa nhóm tiếp tục nằm ở dạng cột thông thường giống như kết quả của câu lệnh GROUP BY trong SQL, ta chỉ định tham số as_index=False:
python
df_sql_style = bang_nhan_su.groupby("phong_ban", as_index=False)["luong"].mean()
print(df_sql_style)
assert "phong_ban" in df_sql_style.columns3. Phép biến đổi nhóm và lọc nhóm (Transforms and Filters)
Trong khi các hàm tổng hợp agg() rút gọn mỗi nhóm thành một dòng duy nhất, nhiều bài toán phân tích đòi hỏi phải giữ nguyên toàn bộ số lượng dòng ban đầu.
3.1. Phương thức transform và phân bổ kết quả nhóm về từng dòng
Phương thức transform() áp dụng một hàm lên từng nhóm nhưng trả về một đối tượng có kích thước và hoàn toàn trùng khớp với ban đầu. Kết quả tính toán của từng nhóm được gán cùng giá trị cho toàn bộ các hàng thuộc về nhóm đó.
Ứng dụng kinh điển nhất của transform là tính toán độ lệch so với trung bình nhóm hoặc chuẩn hóa Z-score nội bộ nhóm:
python
# Tính mức chênh lệch lương của từng nhân viên so với mức trung bình của phòng ban mình
luong_tb_nhom = bang_nhan_su.groupby("phong_ban")["luong"].transform("mean")
bang_nhan_su["chenh_lech_luong"] = bang_nhan_su["luong"] - luong_tb_nhom
print("Bảng sau khi bổ sung chênh lệch lương nội bộ nhóm:")
print(bang_nhan_su[["phong_ban", "chuc_danh", "luong", "chenh_lech_luong"]])
# Tổng chênh lệch trong mỗi nhóm luôn bằng 0
np.testing.assert_allclose(bang_nhan_su.groupby("phong_ban")["chenh_lech_luong"].sum(), 0.0)3.2. Lọc nhóm với filter
Phương thức filter() nhận vào một hàm trả về giá trị logic True hoặc False. Hàm này được gọi trên từng nhóm. Nếu nhóm thỏa mãn điều kiện, toàn bộ các hàng của nhóm đó sẽ được giữ lại, còn ngược lại thì toàn bộ nhóm bị loại bỏ khỏi bảng dữ liệu:
python
# Chỉ giữ lại các phòng ban có từ 3 nhân sự trở lên:
phong_dong_nguoi = bang_nhan_su.groupby("phong_ban").filter(lambda g: len(g) >= 3)
print("Các phòng ban có từ 3 nhân sự trở lên:")
print(phong_dong_nguoi)
assert set(phong_dong_nguoi["phong_ban"].unique()) == {"Kỹ thuật"}4. Phương thức Apply tổng quát: Tách - Áp dụng - Gộp linh hoạt
Phương thức apply() là công cụ tổng quát nhất của đối tượng groupby. Trong khi agg bắt buộc phải trả về giá trị vô hướng và transform bắt buộc phải trả về mảng cùng kích thước, apply() cho phép hàm tự do trả về một giá trị vô hướng, một Series, hoặc thậm chí một con có kích thước tùy ý.
Pandas sẽ tự động ghép nối các mảnh kết quả này lại với nhau dọc theo trục tương ứng.
python
def lay_top_nhan_vien(df_nhom, n=1, cot="luong"):
"""Trả về top n nhân viên có lương cao nhất trong nhóm."""
return df_nhom.sort_values(by=cot, ascending=False).head(n)
top_1_moi_phong = bang_nhan_su.groupby("phong_ban", group_keys=False).apply(
lay_top_nhan_vien, n=1, include_groups=False
)
print("Nhân viên có lương cao nhất mỗi phòng ban:")
print(top_1_moi_phong[["chuc_danh", "luong"]])
assert len(top_1_moi_phong) == 24.1. Khử khóa nhóm với group_keys=False
Mặc định, apply() sẽ gắn thêm tên nhóm vào để tạo thành chỉ mục phân cấp MultiIndex. Khi muốn giữ nguyên chỉ mục hàng nguyên bản của bảng dữ liệu, ta thiết lập tham số group_keys=False.
4.2. Phân tích theo khoảng và tứ phân vị (Bucket Analysis)
Bằng cách kết hợp pd.qcut hoặc pd.cut với groupby, ta có thể phân tích dữ liệu theo các phân khúc thu nhập hoặc nhóm tuổi:
python
du_lieu_khach = pd.DataFrame({
"chi_tieu": np.random.uniform(10, 500, 100),
"so_lan_mua": np.random.randint(1, 20, 100)
})
# Chia chi tiêu thành 4 khoảng tứ phân vị có số lượng khách bằng nhau:
phan_vi_chi_tieu = pd.qcut(du_lieu_khach["chi_tieu"], 4)
thong_ke_phan_vi = du_lieu_khach.groupby(phan_vi_chi_tieu, observed=False)["so_lan_mua"].agg(["count", "mean"])
print(thong_ke_phan_vi)
assert len(thong_ke_phan_vi) == 44.3. Bốn ví dụ kỹ thuật chuyên sâu với apply
Ví dụ 1: Điền giá trị khuyết thiếu bằng giá trị đặc thù của từng nhóm
Thay vì điền giá trị trung bình của toàn bộ bảng, ta điền giá trị trung bình riêng của từng bang hoặc từng nhóm sản phẩm:
python
bang_thieu = pd.DataFrame({
"khu": ["A", "A", "A", "B", "B"],
"gia": [100.0, 120.0, np.nan, 200.0, np.nan]
})
dien_theo_khu = bang_thieu.groupby("khu")["gia"].transform(lambda x: x.fillna(x.mean()))
assert dien_theo_khu.iloc[2] == 110.0
assert dien_theo_khu.iloc[4] == 200.0Ví dụ 2: Lấy mẫu ngẫu nhiên độc lập theo từng nhóm
Trích xuất ngẫu nhiên đúng
python
mau_nhom = bang_nhan_su.groupby("phong_ban", group_keys=False).apply(lambda g: g.sample(n=1, random_state=42))
assert len(mau_nhom) == 2Ví dụ 3: Giá trị trung bình gia quyền theo nhóm (Group Weighted Average)
Muốn tính doanh thu bình quân trên mỗi đơn vị hàng đã bán, cần dùng số lượng bán làm trọng số của đơn giá. Với n dòng dữ liệu, gọi giá và số lượng ở dòng i là
Viết gọn,
Điều kiện xác định của mẫu số: Phép tính trung bình gia quyền chỉ có nghĩa khi tổng trọng số dương ZeroDivisionError hoặc NaN). Do đó, hàm tính toán cần kiểm tra điều kiện mẫu số trước khi chia:
python
df_ban_hang = pd.DataFrame({
"loai": ["Điện tử", "Điện tử", "Gia dụng", "Gia dụng"],
"don_gia": [100, 200, 50, 80],
"so_luong": [10, 2, 20, 5]
})
def tinh_tb_gia_quyen(g):
tong_luong = g["so_luong"].sum()
if tong_luong <= 0 or pd.isna(tong_luong):
return np.nan
return (g["don_gia"] * g["so_luong"]).sum() / tong_luong
tb_gia_quyen = df_ban_hang.groupby("loai").apply(tinh_tb_gia_quyen, include_groups=False)
print("Đơn giá trung bình gia quyền theo loại sản phẩm:")
print(tb_gia_quyen)
assert np.isclose(tb_gia_quyen["Điện tử"], (100*10 + 200*2) / 12)
assert np.isclose(tb_gia_quyen["Gia dụng"], (50*20 + 80*5) / 25)Ví dụ 4: Ước lượng mô hình hồi quy tuyến tính theo từng nhóm
Áp dụng phép hồi quy tuyến tính
python
df_kinh_te = pd.DataFrame({
"vung": ["A", "A", "A", "B", "B", "B"],
"quang_cao": [1, 2, 3, 1, 2, 3],
"doanh_thu": [2.1, 4.2, 5.9, 1.5, 3.1, 4.4]
})
def hoi_quy_nhom(g):
# Ước lượng hệ số góc và hệ số tự do: y = a*x + b
a, b = np.polyfit(g["quang_cao"], g["doanh_thu"], deg=1)
return pd.Series({"he_so_goc": a, "he_so_tu_do": b})
ket_qua_hoi_quy = df_kinh_te.groupby("vung").apply(hoi_quy_nhom, include_groups=False)
print("Hệ số hồi quy theo từng vùng:")
print(ket_qua_hoi_quy)
assert ket_qua_hoi_quy.loc["A", "he_so_goc"] > ket_qua_hoi_quy.loc["B", "he_so_goc"]5. Bảng Pivot và Bảng chéo (Pivot Tables and Cross-Tabulation)
Bảng Pivot và Bảng chéo là hai công cụ tổng hợp đa chiều cao cấp, thường xuyên xuất hiện trong các báo cáo kinh doanh.
5.1. Bảng Pivot với pd.pivot_table
Khác với hàm pivot cơ bản vốn bị lỗi khi gặp dữ liệu trùng lặp khóa, hàm pivot_table cho phép gom nhóm và áp dụng hàm tổng hợp số học:
values: Cột dữ liệu số học cần tổng hợp.index: Danh sách các cột sẽ làm hàng.columns: Danh sách các cột sẽ làm tiêu đề cột mới.aggfunc: Hàm tổng hợp (mặc định là"mean").fill_value: Giá trị thay thế cho các ô khuyết thiếu phát sinh (thường là 0).margins=True: Tự động tính toán thêm dòng và cột tổng cộng (All):
python
bang_pivot = pd.pivot_table(
bang_nhan_su,
values="luong",
index="phong_ban",
columns="chuc_danh",
aggfunc="mean",
fill_value=0,
margins=True
)
print("Bảng Pivot lương trung bình:")
print(bang_pivot)
assert bang_pivot.loc["Kỹ thuật", "Lập trình viên"] == 21005.2. Bảng chéo với pd.crosstab
Hàm pd.crosstab() là trường hợp đặc biệt của bảng Pivot, được thiết kế chuyên biệt để đếm tần số xuất hiện đồng thời (Frequency Counts) giữa hai hoặc nhiều biến phân loại:
margins=True: Thêm dòng và cột tổng số lượng quan sát.normalize: Chuẩn hóa tần số thành tỷ lệ phần trăm:normalize="all": Chia cho tổng số quan sát của toàn bộ bảng.normalize="index": Chia theo tổng số của từng hàng (tổng mỗi hàng bằng 100%).normalize="columns": Chia theo tổng số của từng cột (tổng mỗi cột bằng 100%).
python
df_khao_sat_xe = pd.DataFrame({
"quoc_gia": ["Mỹ", "Mỹ", "Nhật", "Nhật", "Đức", "Nhật"],
"loai_xe": ["Xăng", "Điện", "Điện", "Xăng", "Xăng", "Điện"]
})
bang_tan_so = pd.crosstab(
df_khao_sat_xe["quoc_gia"],
df_khao_sat_xe["loai_xe"],
margins=True
)
print("Bảng chéo tần số loại xe theo quốc gia:")
print(bang_tan_so)
assert bang_tan_so.loc["Nhật", "Điện"] == 2
assert bang_tan_so.loc["All", "All"] == 67. Tổng kết
- Hiểu rõ ba pha Split-Apply-Combine: Phân biệt rạch ròi giữa việc tách nhóm, áp dụng hàm xử lý và gộp kết quả. Đối tượng
groupbyhoạt động theo cơ chế đánh giá trì hoãn và chỉ tính toán khi được gọi hàm cụ thể. - Khai thác Named Aggregation: Sử dụng cú pháp
agg(ten_moi=('cot', 'ham'))để tạo ra các bảng tổng hợp sạch sẽ, có tên cột tường minh và không bị phân cấp phức tạp. - Phân biệt
aggvàtransform: Sử dụngaggkhi cần rút gọn mỗi nhóm thành một dòng giá trị thống kê, còn sử dụngtransformkhi cần gán kết quả của nhóm về từng dòng để chuẩn hóa hoặc tính chênh lệch nội bộ nhóm. - Lọc nhóm với
filter: Loại trừ toàn bộ các nhóm không thỏa mãn điều kiện logic dựa trên đặc tính tổng thể của nhóm. - Linh hoạt xử lý với
apply: Áp dụng các hàm tùy biến tự do để giải quyết các bài toán phức tạp như lấy mẫu ngẫu nhiên theo nhóm, tính trung bình gia quyền hay ước lượng mô hình hồi quy. - Tổng hợp đa chiều với Pivot và Crosstab: Sử dụng
pivot_tableđể tổng hợp số học trên ma trận hai chiều, đồng thời sử dụngcrosstabđể đo lường tần số xuất hiện đồng thời và phân tích bảng tiếp liên.
Tài liệu tham khảo
Wes McKinney, Python for Data Analysis (tái bản lần 3): Chương 10: Data Aggregation and Group Operations.
Tài liệu chính thức pandas: Tài liệu hướng dẫn Group by: Split-apply-combine.
Hadley Wickham, The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software, 2011.
Hệ thống bài tập được chuyển thể từ học liệu mở của: Khóa học Lập trình xử lý dữ liệu (UET) theo giấy phép Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
IAI, Lập trình xử lý dữ liệu, học liệu và notebook. Các đề thực hành đã được biên tập cách diễn đạt và bổ sung lời giải, giữ CC BY-NC-SA 4.0. Giấy phép học liệu.
6. Bài tập rèn luyện
Bài tập 1: Tính giá trung bình chung từ thống kê nhóm
Một báo cáo thị trường lưu trú cung cấp dữ liệu giá phòng đã biết (USD/đêm) của hai khu vực A và B như sau:
| Khu vực | Giá trung bình ( | Số lượng giá đã biết ( |
|---|---|---|
| Khu A | 100 | 2 |
| Khu B | 200 | 8 |
Từ bảng tổng hợp trên, hãy tính giá trung bình của toàn bộ
Lời giải
1. Phân tích bản chất toán học
Nếu một người lấy trung bình cộng đơn giản của hai số trung bình khu:
Phép tính này cho hai trung bình nhóm trọng số bằng nhau, trong khi đề yêu cầu trung bình trên từng giá quan sát được. Hai câu hỏi chỉ cho cùng kết quả trong những trường hợp thích hợp, chẳng hạn khi hai nhóm có cùng số giá hợp lệ. Ở đây cần dùng số quan sát của mỗi nhóm làm trọng số.
Trong thực tế:
- Tổng giá trị của 2 phòng ở khu A là:
. - Tổng giá trị của 8 phòng ở khu B là:
. - Tổng giá trị của toàn bộ 10 phòng là:
.
Giá trị trung bình chuẩn xác của toàn bộ 10 quan sát phải là giá trị trung bình gia quyền (Weighted Average) theo quy mô mẫu:
Điều kiện xác định của công thức là tổng quy mô mẫu ở mẫu số phải dương (
2. Kiểm chứng bằng mã nguồn Python
python
bang_khu = pd.DataFrame({
"khu": ["A", "B"],
"gia_tb": [100.0, 200.0],
"so_luong": [2, 8]
})
# Tính tổng tiền và tổng số lượng
tong_tien = (bang_khu["gia_tb"] * bang_khu["so_luong"]).sum()
tong_so_luong = bang_khu["so_luong"].sum()
assert tong_so_luong > 0, "Mẫu số phải lớn hơn 0"
gia_tb_chung = tong_tien / tong_so_luong
print("Tổng giá trị toàn thị trường:", tong_tien)
print("Tổng số lượng phòng:", tong_so_luong)
print("Giá trung bình thực tế toàn thị trường:", gia_tb_chung)
assert gia_tb_chung == 180.0Con số
Phân tích nhóm với groupby, transform, pivot table và mở rộng cấp quận
Sau khi phân loại dải giá, ta cần đếm chỗ ở, tính trung vị giá và tổng hợp lượt đánh giá theo nhóm. Những bài sau giúp chọn giữa agg, transform và pivot_table, đồng thời giữ rõ mỗi kết quả đại diện cho chỗ ở, nhóm chủ nhà hay quận.
1. Thiết lập môi trường và dữ liệu thực hành
Để bảo đảm toàn bộ quy trình phân tích nhóm có thể thực thi độc lập, môi trường thực hành được khởi tạo với bảng dữ liệu mẫu gồm 12 dòng (DEMO, đơn vị giá: CLP/đêm). Bảng này bao gồm định danh phòng, tên quận, giá niêm yết, số lượt đánh giá trong 12 tháng qua (number_of_reviews_ltm) và tổng số lượng niêm yết của chủ nhà (calculated_host_listings_count).
python
import numpy as np
import pandas as pd
# 12 dòng dữ liệu mẫu giả lập (đơn vị: CLP/đêm)
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
# Hàm phân loại dải giá hỗ trợ tạo cột phân khúc
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
# Bổ sung cột phân khúc giá cho bảng dữ liệu mẫu
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Địa chỉ lưu trữ snapshot thực tế Inside Airbnb
SNAPSHOT_URL = (
"https://data.insideairbnb.com/chile/rm/santiago/"
"2026-06-29/visualisations/listings.csv"
)NOTE
Đối chiếu thực tế: Trên snapshot thực tế toàn thành phố Santiago gồm 18.534 niêm yết, phân khúc trung bình có 12.325 phòng với trung bình cột số đánh giá 12 tháng gần nhất xấp xỉ 15,4 lượt, trong khi phân khúc cao có mức giá trung vị đạt 145.318 CLP. Tỷ lệ chỗ ở do nhóm chủ nhà có nhãn chuyên nghiệp đăng chiếm 35,8% toàn thành phố với giá trung vị 65.078 CLP, trong khi nhóm chủ nhà cá nhân ghi nhận 12,7 lượt đánh giá trung bình. Toàn thành phố có 16.850 chỗ ở tập trung tại 8 quận lớn đạt quy mô từ 300 phòng trở lên.
2. Nội dung chi tiết các bài tập thực hành
Bài tập 4: Thống kê đặc trưng các phân khúc giá bằng agg đặt tên
Bài tập 4: Thống kê ba phân khúc giá
Cho một df chứa ít nhất bốn cột: id, phan_khuc (chuỗi hoặc giá trị khuyết), price và number_of_reviews_ltm.
Hãy viết hàm segment_stats(df: pd.DataFrame) -> pd.DataFrame nhóm bảng dữ liệu theo phan_khuc và áp dụng phương thức .agg() với đúng ba cột được đặt tên tường minh theo thứ tự sau:
so_phong: Số lượng phòng trong phân khúc, tính bằng("id", "size").gia_trung_vi: Mức giá trung vị của phân khúc, tính bằng("price", "median").review_nam_tb: Số lượt đánh giá trung bình trong năm qua, tính bằng("number_of_reviews_ltm", "mean").
Yêu cầu kỹ thuật: của kết quả là các phân khúc giá hiện diện trong bảng (các dòng khuyết phân khúc tự động bị loại bỏ theo mặc định của phép nhóm). Không làm tròn số liệu trong hàm và không làm biến đổi DataFrame df đầu vào.
Lời giải
Cách 1: Nhóm theo phân khúc và đặt tên từng cột tổng hợp
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def segment_stats(df: pd.DataFrame) -> pd.DataFrame:
# Nhóm theo phân khúc và tổng hợp đồng thời ba chỉ số với tên cột tùy chỉnh
ket_qua = df.groupby("phan_khuc", observed=True).agg(
so_phong=("id", "size"),
gia_trung_vi=("price", "median"),
review_nam_tb=("number_of_reviews_ltm", "mean"),
)
return ket_qua
# Gọi thử nghiệm hàm trên dữ liệu mẫu DEMO:
bang_thong_ke_c1 = segment_stats(DEMO)
print("Kết quả thống kê Cách 1:")
print(bang_thong_ke_c1)
# Kết thúc lời giảiGiải thích Cách 1
df.groupby("phan_khuc", observed=True) xác định các nhóm hiện có. Các dòng khuyết khóa nhóm bị bỏ qua theo mặc định, nên dòng id 204 không được đưa vào ba nhóm giá. observed=True còn giúp chỉ giữ các nhóm có quan sát khi cột đã có kiểu category.
Trong .agg, mỗi tên bên trái là một cột kết quả. Tuple ("id", "size") chọn cột id và đếm số dòng, kể cả khi một ô id khuyết. Đổi size thành count sẽ đổi quy tắc thành đếm ô không khuyết. median và mean tổng hợp cột số và bỏ qua giá trị khuyết theo quy tắc của pandas.
Ở phân khúc cao, bốn giá là 100000, 120000, 150000, 210000. Trung vị là trung bình hai giá ở giữa, bằng 135000. Tổng số đánh giá của nhóm là 14, chia cho 4 được 3.5. Bảng kết quả chỉ có một dòng cho mỗi phân khúc, không còn một dòng cho mỗi chỗ ở.
Output Cách 1
phan_khuc | so_phong | gia_trung_vi | review_nam_tb |
|---|---|---|---|
cao | 4 | 135000.0 | 3.5 |
re | 2 | 21500.0 | 6.0 |
trung | 5 | 55000.0 | 17.0 |
Cách 2: Đặt thứ tự phân khúc trước khi tổng hợp
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def segment_stats(df: pd.DataFrame) -> pd.DataFrame:
# Tạo bản sao cục bộ để thiết lập thứ tự phân loại có trật tự
df_loc = df.dropna(subset=["phan_khuc"]).copy()
thu_tu = ["re", "trung", "cao"]
df_loc["phan_khuc"] = pd.Categorical(
df_loc["phan_khuc"], categories=thu_tu, ordered=True
)
ket_qua = df_loc.groupby("phan_khuc", observed=True).agg(
so_phong=("id", "size"),
gia_trung_vi=("price", "median"),
review_nam_tb=("number_of_reviews_ltm", "mean"),
)
return ket_qua
# Gọi thử nghiệm hàm trên dữ liệu mẫu DEMO:
bang_thong_ke_c2 = segment_stats(DEMO)
print("Kết quả thống kê Cách 2:")
print(bang_thong_ke_c2)
# Kết thúc lời giảiGiải thích Cách 2
dropna(subset=["phan_khuc"]) chỉ loại dòng thiếu khóa nhóm. .copy() tạo bảng làm việc để phép gán sau đó không sửa df. pd.Categorical khai báo rõ ba nhãn hợp lệ và thứ tự re, trung, cao. Với ordered=True, thứ tự này có ý nghĩa khi sắp xếp hoặc so sánh các nhãn phân loại.
Ba phép tổng hợp trong .agg giữ nguyên ý nghĩa của Cách 1. Điểm thêm là bảng được trình bày theo thứ tự dải giá thay vì thứ tự chữ cái. Yêu cầu của đề chỉ nhận các nhóm có mặt trong dữ liệu. Vì vậy với đầu vào không có đủ ba dải, cần dùng chế độ nhóm chỉ lấy các giá trị đã quan sát, thay vì tạo thêm hàng rỗng cho danh mục vắng mặt.
DEMO có đủ cả ba phân khúc nên bảng dưới đây chứa đủ ba hàng. Việc đổi kiểu phân loại đòi hỏi đầu vào thật sự chỉ có các nhãn đã khai báo. Một nhãn nằm ngoài danh mục sẽ thành giá trị khuyết và cần được phát hiện trước khi phân tích.
Output Cách 2
phan_khuc | so_phong | gia_trung_vi | review_nam_tb |
|---|---|---|---|
re | 2 | 21500.0 | 6.0 |
trung | 5 | 55000.0 | 17.0 |
cao | 4 | 135000.0 | 3.5 |
So sánh hai cách
Cách 1 đáp ứng trực tiếp nhu cầu tổng hợp. Cách 2 phù hợp khi cần một thứ tự phân khúc nhất quán trong báo cáo, nhưng phát sinh bước sao chép và đổi kiểu. Lợi ích chính là cấu trúc và thứ tự đầu ra, không phải một bảo đảm về tốc độ. Cả hai phải giữ nguyên số tính được trong hàm, chỉ làm tròn khi trình bày.
Bài tập 5: Phân nhóm theo số niêm yết và so sánh giá cùng lượt đánh giá
Bài tập 5: Phân loại host và so sánh nhóm
Một host được quy ước là "chuyên nghiệp" nếu có số lượng niêm yết quản lý từ ngưỡng nguong trở lên (mặc định là 5 phòng).
Đây là nhãn phân nhóm của bài tập, không phải bằng chứng về nghề nghiệp hay chất lượng dịch vụ của chủ nhà. Tỷ lệ được tính trên các dòng chỗ ở, không phải trên số chủ nhà phân biệt. Số đánh giá cũng chưa phải số đặt phòng hay doanh thu.
Hãy viết hàm pro_host_summary(df: pd.DataFrame, nguong: int = 5) -> dict nhận vào bảng dữ liệu chứa ít nhất các cột calculated_host_listings_count, price và number_of_reviews_ltm. Hàm trả về một từ điển gồm đúng ba khóa sau:
chuyen: Đối tượng Series kiểu boolean biểu diễn điều kiệncalculated_host_listings_count >= nguong, có cùng và thứ tự với bảngdf.ty_le_chuyen: Một số thực (float) thể hiện tỷ lệ số dòng thuộc về chủ nhà chuyên nghiệp trên tổng số dòng của bảng.so_sanh: phân nhóm theo biến booleanchuyenvới hai cột được đặt tên:gia_trung_vi = ("price", "median")review_nam_tb = ("number_of_reviews_ltm", "mean")
Yêu cầu kỹ thuật: Tuyệt đối không được thêm cột mới vào df truyền vào. Phải thực hiện phép nhóm trực tiếp thông qua đối tượng Series bên ngoài, ví dụ df.groupby(chuyen).
Lời giải
Cách 1: Tạo mặt nạ đạt ngưỡng rồi nhóm bằng Series ngoài
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def pro_host_summary(df: pd.DataFrame, nguong: int = 5) -> dict:
# 1. Tạo Series boolean độc lập từ cột số lượng niêm yết
chuyen = df["calculated_host_listings_count"] >= nguong
# 2. Tính tỷ lệ dòng thuộc chủ nhà chuyên nghiệp
ty_le_chuyen = float(chuyen.mean())
# 3. Nhóm trực tiếp bằng Series rời bên ngoài để bảo vệ bất biến của bảng gốc
so_sanh = df.groupby(chuyen, observed=True).agg(
gia_trung_vi=("price", "median"),
review_nam_tb=("number_of_reviews_ltm", "mean"),
)
return {
"chuyen": chuyen,
"ty_le_chuyen": ty_le_chuyen,
"so_sanh": so_sanh,
}
# Gọi thử nghiệm hàm trên dữ liệu mẫu DEMO:
kq_host_c1 = pro_host_summary(DEMO, 5)
print("Tỷ lệ host chuyên nghiệp Cách 1:", kq_host_c1["ty_le_chuyen"])
print("Bảng so sánh Cách 1:")
print(kq_host_c1["so_sanh"])
# Kết thúc lời giảiGiải thích Cách 1
Phép so sánh >= nguong tạo một Series boolean có cùng với df. Mỗi True chỉ cho biết dòng chỗ ở thuộc chủ nhà có số niêm yết đạt ngưỡng quy ước. Nó không đếm số chủ nhà phân biệt và không xác nhận nghề nghiệp của họ.
chuyen.mean() lấy trung bình các cờ 0 và 1. Trên DEMO, sáu cờ đúng trong 12 dòng cho 0.5. float chuyển kết quả sang số thực Python. df.groupby(chuyen) dùng Series ấy làm khóa ngoài, nên không cần thêm cột vào bảng nguồn. Nếu tạo khóa nhóm từ nơi khác, phải bảo đảm của nó khớp với df.
Trong nhóm False, có sáu dòng nhưng một giá bị khuyết. median dùng năm giá còn lại 18000, 25000, 30000, 99999, 150000, nên trung vị bằng 30000. Trung bình số đánh giá dùng cả sáu giá trị đã có, cho 5.5. Nhóm True có tổng 78 lượt đánh giá trên sáu dòng, cho 13.0.
Output Cách 1
| Khóa nhóm | gia_trung_vi | review_nam_tb |
|---|---|---|
False | 30000.0 | 5.5 |
True | 80000.0 | 13.0 |
ty_le_chuyen = 0.5, tương ứng 6 trong 12 dòng đạt ngưỡng 5.
Cách 2: Đặt tên rõ ràng cho Series khóa nhóm
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def pro_host_summary(df: pd.DataFrame, nguong: int = 5) -> dict:
# Tạo Series boolean và đặt tên tường minh cho trục phân nhóm
chuyen = df["calculated_host_listings_count"].ge(nguong)
chuyen.name = "chuyen"
ty_le_chuyen = float(chuyen.mean())
# Nhóm qua Series có tên giúp chỉ mục của bảng so sánh rõ ràng
so_sanh = df.groupby(chuyen, observed=False).agg(
gia_trung_vi=("price", "median"),
review_nam_tb=("number_of_reviews_ltm", "mean"),
)
return {
"chuyen": chuyen,
"ty_le_chuyen": ty_le_chuyen,
"so_sanh": so_sanh,
}
# Gọi thử nghiệm hàm trên dữ liệu mẫu DEMO:
kq_host_c2 = pro_host_summary(DEMO, 5)
print("Tỷ lệ host chuyên nghiệp Cách 2:", kq_host_c2["ty_le_chuyen"])
print("Bảng so sánh Cách 2:")
print(kq_host_c2["so_sanh"])
# Kết thúc lời giảiGiải thích Cách 2
.ge(nguong) là dạng gọi phương thức của phép so sánh lớn hơn hoặc bằng. chuyen.name = "chuyen" đặt tên cho Series vừa tạo, không đổi tên hoặc dữ liệu của cột calculated_host_listings_count trong df.
Khi pandas dùng Series có tên làm khóa nhóm, của bảng tổng hợp mang tên chuyen. Điều này giúp người đọc phân biệt khóa phân loại với cột số niêm yết ban đầu. observed không tạo khác biệt trên Series boolean thông thường của ví dụ, vì đây không phải cột category có các mức chưa xuất hiện.
Từ điển đầu ra giữ cả mặt nạ từng dòng, tỷ lệ và bảng so sánh. Người gọi có thể dùng mặt nạ để lọc cùng bảng ban đầu hoặc dùng so_sanh để báo cáo mà không phải tính lại tiêu chí.
Output Cách 2
| Khóa nhóm | gia_trung_vi | review_nam_tb |
|---|---|---|
False | 30000.0 | 5.5 |
True | 80000.0 | 13.0 |
ty_le_chuyen = 0.5, tương ứng 6 trong 12 dòng đạt ngưỡng 5.
So sánh hai cách
Hai cách dùng cùng tiêu chí và cùng phép tổng hợp. Cách 2 làm tên trục nhóm dễ đọc hơn, còn lượng công việc tính toán gần tương tự Cách 1. Các chênh lệch ở bảng chỉ mô tả DEMO theo nhãn quy ước. Số đánh giá chưa phải số đặt phòng, doanh thu hoặc thước đo đầy đủ về hiệu quả vận hành.
Bài tập 6: Gắn quy mô quận vào từng dòng bằng transform và lọc quận lớn
Bài tập 6: Gắn quy mô quận và lọc bảng
Khác với phương thức agg làm thu gọn số dòng về số lượng nhóm, phương thức transform tính toán trên từng nhóm nhưng trả về kết quả có độ dài và hoàn toàn khớp với ban đầu.
Hãy viết hàm big_districts(df: pd.DataFrame, nguong: int = 300) -> dict nhận vào bảng dữ liệu chứa ít nhất cột neighbourhood (tên quận) và cột id. Hàm trả về một từ điển gồm đúng hai khóa sau:
n_quan: Đối tượng Series chứa quy mô số lượng phòng của quận tương ứng với mỗi dòng, tính toán bằngdf.groupby("neighbourhood")["id"].transform("size"), có cùng và thứ tự vớidf.df_dong: Đối tượng gồm các dòng thuộc về những quận có tổng số phòng đạt từnguongtrở lên. Giữ nguyên đúng các cột, và thứ tự dòng ban đầu củadf.
Yêu cầu kỹ thuật: Không được thêm cột n_quan vào df cũng như vào df_dong.
Lời giải
Cách 1: Dùng transform để đưa số dòng của nhóm về từng hàng
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def big_districts(df: pd.DataFrame, nguong: int = 300) -> dict:
# 1. Tính quy mô quận cho từng dòng bằng transform("size")
n_quan = df.groupby("neighbourhood")["id"].transform("size")
# 2. Tạo mặt nạ lọc các dòng thuộc quận đạt quy mô yêu cầu
mask = n_quan >= nguong
# 3. Trích xuất bảng con và giữ nguyên cấu trúc cột ban đầu
df_dong = df[mask].copy()
return {
"n_quan": n_quan,
"df_dong": df_dong,
}
# Gọi thử nghiệm hàm với ngưỡng 4 phòng trên dữ liệu DEMO:
kq_quan_c1 = big_districts(DEMO, 4)
print("Độ dài Series n_quan:", len(kq_quan_c1["n_quan"]))
print("Các giá trị n_quan mẫu:", list(kq_quan_c1["n_quan"]))
print("Số dòng thỏa mãn quy mô >= 4:", len(kq_quan_c1["df_dong"]))
# Kết thúc lời giảiGiải thích Cách 1
groupby("neighbourhood")["id"] chọn cột để tổng hợp trong từng quận. transform("size") đếm số dòng của nhóm rồi đặt số đếm ấy tại mọi vị trí thuộc nhóm. Kết quả vẫn dài 12 và đi cùng của DEMO. size đếm dòng, nên không phụ thuộc việc giá hay một ô id có bị khuyết hay không.
Mặt nạ n_quan >= nguong vì thế có thể dùng ngay để lọc df. Ở ngưỡng 4, cả năm dòng Santiago và bốn dòng Providencia được giữ. Hai dòng Ñuñoa và một dòng Vitacura không đạt ngưỡng. .copy() trả một bảng kết quả riêng với đúng các cột ban đầu.
Đừng nhầm transform("size") với groupby.size(). Lệnh thứ hai chỉ tạo một số đếm cho mỗi quận, nên không thể dùng trực tiếp như mặt nạ từng dòng của bảng 12 dòng.
Output Cách 1
| Các vị trí dòng | n_quan | Giữ với ngưỡng 4 |
|---|---|---|
| 0–4 | 5 | Có |
| 5–8 | 4 | Có |
| 9–10 | 2 | Không |
| 11 | 1 | Không |
Series n_quan là [5, 5, 5, 5, 5, 4, 4, 4, 4, 2, 2, 1]. df_dong có 9 dòng.
Cách 2: Chọn cột nhóm và lọc bằng loc
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def big_districts(df: pd.DataFrame, nguong: int = 300) -> dict:
# Sử dụng chính cột phân nhóm để tính quy mô giúp tiết kiệm vùng nhớ
n_quan = df.groupby("neighbourhood")["neighbourhood"].transform("size")
# Lọc các dòng thỏa mãn ngưỡng mà không sinh thêm cột trong lược đồ bảng
df_dong = df.loc[n_quan.ge(nguong)].copy()
return {
"n_quan": n_quan,
"df_dong": df_dong,
}
# Gọi thử nghiệm hàm với ngưỡng 4 phòng trên dữ liệu DEMO:
kq_quan_c2 = big_districts(DEMO, 4)
print("Số quận lớn thỏa mãn:", kq_quan_c2["df_dong"]["neighbourhood"].nunique())
# Kết thúc lời giảiGiải thích Cách 2
Trong phép size, dùng cột neighbourhood thay cột id vẫn đếm số dòng của cùng các nhóm. Cách viết này không cần cột id để thực hiện phép đếm, nhưng không tự chứng minh tiết kiệm bộ nhớ hơn.
.ge(nguong) tạo mặt nạ như toán tử >=. df.loc[mặt_nạ] chọn các dòng có cờ đúng và giữ nguyên cột, nhãn cùng thứ tự của chúng. Không có phép gán n_quan vào bảng nên lược đồ đầu ra không xuất hiện cột phụ.
Lời gọi .nunique() trong ví dụ đếm số tên quận phân biệt trong bảng đã lọc. Nó trả 2, khác với len(df_dong) là 9. Một số đếm đại diện cho nhóm, số kia đại diện cho dòng chỗ ở.
Output Cách 2
| Các vị trí dòng | n_quan | Giữ với ngưỡng 4 |
|---|---|---|
| 0–4 | 5 | Có |
| 5–8 | 4 | Có |
| 9–10 | 2 | Không |
| 11 | 1 | Không |
Lệnh in ở cuối Cách 2 trả Số quận lớn thỏa mãn: 2. Bảng con vẫn có 9 dòng.
So sánh hai cách
Hai cách đều dùng transform để tránh phải ghép một bảng số đếm trở lại bảng gốc. Cách 1 mở rõ biến mặt nạ, Cách 2 dùng .loc và phương thức so sánh. Ngưỡng 4 chỉ phục vụ DEMO. Ngưỡng 300 của bài thực tế là tiêu chí chọn nhóm đủ lớn, không phải bằng chứng rằng các quận nhỏ chứa dữ liệu sai.
Bài tập 7: Bảng chéo tương tác phân khúc giá và kiểu host bằng pivot table
Bài tập 7: Bảng chéo hai chiều pivot table
Để kiểm tra xem ưu thế về lượt đánh giá của chủ nhà chuyên nghiệp có đồng nhất trên mọi phân khúc giá hay không, chuyên viên cần xây dựng bảng tổng hợp chéo hai chiều.
Hãy viết hàm segment_host_pivot(df: pd.DataFrame) -> pd.DataFrame nhận vào bảng dữ liệu chứa ít nhất ba cột: phan_khuc (chuỗi hoặc khuyết), chuyen (boolean) và number_of_reviews_ltm. Hàm trả về một bảng xoay chiều (pivot_table) thỏa mãn các đặc tả:
index="phan_khuc": Các hàng đại diện cho các phân khúc giá.columns="chuyen": Các cột đại diện cho trạng thái chủ nhà chuyên nghiệp (FalsevàTrue).values="number_of_reviews_ltm": Cột số liệu cần tổng hợp.aggfunc="mean": Hàm tính giá trị trung bình cộng.
Yêu cầu kỹ thuật: Tổ hợp hàng và cột nào không có dữ liệu thực tế sẽ nhận giá trị NaN theo hành vi mặc định. Không làm tròn số liệu trong hàm và không làm biến đổi df đầu vào.
Lời giải
Cách 1: Lập bảng chéo trực tiếp bằng pivot_table
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def segment_host_pivot(df: pd.DataFrame) -> pd.DataFrame:
# Xây dựng bảng chéo hai chiều thông qua pivot_table
bang_cheo = df.pivot_table(
index="phan_khuc",
columns="chuyen",
values="number_of_reviews_ltm",
aggfunc="mean",
)
return bang_cheo
# Chuẩn bị dữ liệu thử nghiệm có đủ cột phan_khuc và chuyen:
demo_pv = DEMO.copy()
demo_pv["chuyen"] = demo_pv["calculated_host_listings_count"] >= 5
# Gọi thử nghiệm hàm trên dữ liệu mẫu:
pv_c1 = segment_host_pivot(demo_pv)
print("Kết quả bảng chéo Cách 1:")
print(pv_c1.round(2))
# Kết thúc lời giảiGiải thích Cách 1
index="phan_khuc" đặt phân khúc trên hàng và columns="chuyen" đặt hai cờ chủ nhà trên cột. values chỉ cột số đánh giá cần tính. aggfunc="mean" tính trung bình cho mỗi cặp phân khúc và cờ, nên nhiều chỗ ở có cùng cặp khóa được thu thành một ô.
Để thử hàm, demo_pv = DEMO.copy() tạo bảng riêng rồi mới bổ sung cột chuyen. Bản thân segment_host_pivot không sửa bảng nhận vào. Ở ô trung × True, ba số đánh giá là 20, 15 và 30, có trung bình 65 / 3, xấp xỉ 21.67 khi hiển thị.
Ô re × True là NaN vì DEMO không có dòng nào thuộc tổ hợp ấy. Điền 0 vào đây sẽ mang nghĩa có quan sát và trung bình bằng 0, khác với không có quan sát. .round(2) chỉ tạo bản hiển thị làm tròn, không đổi phép tính trong hàm.
Output Cách 1
phan_khuc | False | True |
|---|---|---|
cao | 1.00 | 4.33 |
re | 6.00 | NaN |
trung | 10.00 | 21.67 |
Cách 2: Nhóm hai khóa rồi đưa khóa thứ hai lên cột
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
demo_pv = DEMO.copy()
demo_pv["chuyen"] = demo_pv["calculated_host_listings_count"] >= 5
# Bắt đầu lời giải
import pandas as pd
def segment_host_pivot(df: pd.DataFrame) -> pd.DataFrame:
# Thực hiện groupby trên hai khóa phân loại rồi chuyển trục bằng unstack
bang_cheo = (
df.groupby(["phan_khuc", "chuyen"], observed=False)["number_of_reviews_ltm"]
.mean()
.unstack(level="chuyen")
)
return bang_cheo
# Gọi thử nghiệm hàm trên dữ liệu mẫu:
pv_c2 = segment_host_pivot(demo_pv)
print("Kết quả bảng chéo Cách 2:")
print(pv_c2.round(2))
# Kết thúc lời giảiGiải thích Cách 2
groupby(["phan_khuc", "chuyen"])["number_of_reviews_ltm"].mean() tạo một Series có hai cấp. Mỗi khóa chỉ mục lúc này là một cặp, chẳng hạn ("trung", True).
.unstack(level="chuyen") chuyển cấp chuyen từ lên cột. Các cặp chưa có quan sát trở thành ô khuyết trong bảng chữ nhật. Đây là bước thay đổi cách trình bày kết quả, không phải tính một trung bình khác.
Trong DEMO, phan_khuc là chuỗi nên tùy chọn observed không thêm các danh mục phân loại vắng mặt. Nếu chuyển khóa sang category, cần đặt rõ quy tắc giữ nhóm và xử lý hàng toàn khuyết trước khi đối chiếu với pivot_table.
Output Cách 2
phan_khuc | False | True |
|---|---|---|
cao | 1.00 | 4.33 |
re | 6.00 | NaN |
trung | 10.00 | 21.67 |
So sánh hai cách
pivot_table diễn đạt ngắn gọn mục tiêu tạo bảng chéo. groupby rồi unstack mở rõ hai bước tổng hợp và đổi trục, hữu ích khi cần nhiều phép tổng hợp khác nhau. Cả hai bảng mô tả lượt đánh giá trong DEMO. Chênh lệch giữa hai cột không xác lập quan hệ nhân quả hay chất lượng dịch vụ.
Bài tập 8: Tìm phòng có giá niêm yết cao nhất trong từng quận bằng transform
Bài tập 8: Tìm phòng đắt nhất theo quận
Trong các bài toán định vị phân khúc bất động sản cao cấp, chuyên viên cần lọc ra các phòng giữ mức giá niêm yết kỷ lục trong từng quận lớn (df_dong từ Bài tập 6).
Hãy viết hàm top_price_by_district(df: pd.DataFrame) -> pd.DataFrame nhận vào bảng dữ liệu chứa ít nhất các cột id, name, neighbourhood và price. Hàm trả về một chứa các dòng có giá phòng đạt mức tối đa của quận tương ứng.
Yêu cầu kỹ thuật:
- Sử dụng phương thức
transform("max")trên cộtpricetheo nhómneighbourhoodđể tìm mức giá trần của quận. - Nếu trong một quận có nhiều phòng cùng đạt mức giá tối đa ngang nhau, giữ lại toàn bộ các phòng đó. Bỏ qua các dòng có giá trị khuyết
NaN.
Lời giải
Cách 1: So sánh từng giá với giá lớn nhất của quận
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
# Bắt đầu lời giải
import pandas as pd
def top_price_by_district(df: pd.DataFrame) -> pd.DataFrame:
# 1. Tính giá tối đa của từng quận và phát tán về từng dòng
gia_max_quan = df.groupby("neighbourhood")["price"].transform("max")
# 2. Tạo mặt nạ boolean xác định các phòng đạt mức giá kỷ lục
mask = df["price"] == gia_max_quan
# 3. Lọc và trả về các dòng thỏa mãn
return df[mask].copy()
# Chuẩn bị dữ liệu hai quận lớn từ bảng DEMO:
df_quan_lon = DEMO[DEMO["neighbourhood"].isin(["Santiago", "Providencia"])].copy()
# Gọi thử nghiệm hàm:
kq_top_c1 = top_price_by_district(df_quan_lon)
print("Kết quả phòng đắt nhất Cách 1:")
print(kq_top_c1[["id", "name", "neighbourhood", "price"]])
# Kết thúc lời giảiGiải thích Cách 1
transform("max") tính giá lớn nhất theo quận rồi đưa giá ấy về từng dòng. Trong Santiago, mọi dòng được so với 120000. Trong Providencia, mọi dòng được so với 100000. Phép == vì thế trả một mặt nạ đúng tại các chỗ ở đạt mức lớn nhất của chính quận mình.
Phép so sánh được thực hiện trên từng dòng, nên nếu hai chỗ ở cùng đạt mức lớn nhất thì cả hai cờ đều đúng. Dùng idxmax chỉ lấy một vị trí sẽ không đáp ứng yêu cầu giữ tất cả trường hợp hòa giá.
Các phép cực đại bỏ qua giá khuyết khi nhóm còn giá hợp lệ. Dòng có giá NaN không bằng mức cực đại, nên bị loại. Nếu toàn bộ giá của một quận đều khuyết, cách này không trả chỗ ở nào của quận đó. Kết quả .copy() giữ các cột và của những dòng được chọn.
Output Cách 1
| gốc | id | neighbourhood | price |
|---|---|---|---|
| 2 | 203 | Santiago | 120000.0 |
| 7 | 208 | Providencia | 100000.0 |
Cách 1 còn in cột name, lần lượt là Loft Parque Forestal và Loft Pedro de Valdivia.
Cách 2: Loại giá khuyết trước rồi lọc bằng eq
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
df_quan_lon = DEMO.loc[DEMO["neighbourhood"].isin(["Santiago", "Providencia"])].copy()
# Bắt đầu lời giải
import pandas as pd
def top_price_by_district(df: pd.DataFrame) -> pd.DataFrame:
# Lọc bỏ giá trị khuyết trước khi xác định mức giá cao nhất
df_hop_le = df.dropna(subset=["price"])
gia_max = df_hop_le.groupby("neighbourhood")["price"].transform("max")
return df_hop_le.loc[df_hop_le["price"].eq(gia_max)].copy()
# Gọi thử nghiệm hàm:
kq_top_c2 = top_price_by_district(df_quan_lon)
print("Kết quả phòng đắt nhất Cách 2:")
print(kq_top_c2[["id", "neighbourhood", "price"]])
# Kết thúc lời giảiGiải thích Cách 2
dropna(subset=["price"]) tạo bảng chỉ gồm các dòng có giá. Chỉ cột price tham gia điều kiện loại dòng, nên một ô khuyết ở cột khác không khiến chỗ ở bị loại khỏi phép tìm giá lớn nhất.
Sau bước đó, phép nhóm và transform("max") giữ cùng cơ chế với Cách 1. .eq(gia_max) là dạng phương thức của phép so sánh bằng và vẫn căn chỉnh theo . .loc lấy mọi dòng thỏa điều kiện, bao gồm tất cả trường hợp hòa giá.
Bản in ở cuối chọn ba cột id, neighbourhood, price để dễ xem. Hàm thật sự vẫn trả toàn bộ các cột của các dòng được chọn, nên việc không in tên ở ví dụ này không làm mất cột name trong giá trị trả về.
Output Cách 2
| gốc | id | neighbourhood | price |
|---|---|---|---|
| 2 | 203 | Santiago | 120000.0 |
| 7 | 208 | Providencia | 100000.0 |
So sánh hai cách
Cách 1 ngắn và đủ cho quy tắc giá khuyết của đề. Cách 2 thể hiện rõ việc loại dòng chưa có giá, nhưng phải tạo thêm bảng trung gian. Tỷ lệ khuyết có thể ảnh hưởng hiệu năng, nên cần đo trước khi gọi một cách là nhanh hơn. Hai cách cùng giữ được các chỗ ở đồng giá cao nhất.
Bài tập 9: Phân tích cơ cấu phân khúc giá theo từng quận lớn
Bài tập 9: Cơ cấu phân khúc giá theo quận
Để đánh giá định vị thị trường của từng quận, ban quản lý cần nắm rõ tỷ trọng phần trăm của ba phân khúc giá (re, trung, cao) trong các chỗ ở đã xác định được phân khúc của từng quận lớn.
Hãy viết hàm segment_share_by_district(df: pd.DataFrame) -> pd.DataFrame nhận vào bảng dữ liệu chứa ít nhất các cột neighbourhood và phan_khuc. Hàm trả về một biểu diễn tỷ trọng cơ cấu:
- dòng: Tên các quận lớn (
neighbourhood). - Các cột: Đầy đủ ba phân khúc theo đúng thứ tự
["re", "trung", "cao"]. - Giá trị ô: Tỷ trọng số phòng thuộc phân khúc đó trong các dòng của quận đã xác định được phân khúc (tổng mỗi hàng bằng 1,0 tương đương 100%). Nếu một phân khúc không có phòng nào trong quận, giá trị ô nhận 0,0. Những dòng khuyết phân khúc không tham gia mẫu số này. Khi diễn giải cơ cấu toàn bộ nguồn cung, cần báo riêng tỷ lệ khuyết phân khúc.
Lời giải
Cách 1: Tạo bảng tần suất và chuẩn hóa theo hàng bằng crosstab
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
df_quan_lon = DEMO.loc[DEMO["neighbourhood"].isin(["Santiago", "Providencia"])].copy()
# Bắt đầu lời giải
import pandas as pd
def segment_share_by_district(df: pd.DataFrame) -> pd.DataFrame:
# 1. Tính bảng tần suất chéo và chuẩn hóa theo hàng
bang_ty_trong = pd.crosstab(
df["neighbourhood"],
df["phan_khuc"],
normalize="index",
)
# 2. Bảo đảm đủ ba cột phân khúc ngay cả khi có phân khúc vắng mặt
for col in ["re", "trung", "cao"]:
if col not in bang_ty_trong.columns:
bang_ty_trong[col] = 0.0
# 3. Sắp xếp lại thứ tự cột theo đúng chuẩn kinh tế
return bang_ty_trong[["re", "trung", "cao"]]
# Gọi thử nghiệm hàm trên tập các quận lớn:
share_c1 = segment_share_by_district(df_quan_lon)
print("Cơ cấu phân khúc Cách 1:")
print(share_c1.round(3))
# Kết thúc lời giảiGiải thích Cách 1
pd.crosstab đếm các cặp quận và phân khúc. normalize="index" chia từng ô cho tổng số đếm của hàng, nên kết quả là tỷ lệ từ 0 đến 1. Trong Providencia, bốn dòng chia thành 1 rẻ, 2 trung, 1 cao, cho hàng 0.25, 0.50, 0.25.
Vòng lặp chỉ bổ sung một cột phân khúc nếu cột ấy chưa tồn tại trong toàn bộ bảng chéo. Gán 0.0 biểu thị không có quan sát thuộc phân khúc đó. Việc chọn lại danh sách cột ở dòng return bảo đảm thứ tự luôn là re, trung, cao.
Cần đọc đúng mẫu số khi có phân khúc khuyết. crosstab trong đoạn mã này bỏ dòng thiếu phân khúc. Santiago có năm dòng nhưng chỉ bốn dòng xác định được dải giá, nên hàng dưới đây đang chuẩn hóa trên bốn dòng đó. Nếu yêu cầu tỷ trọng trên toàn bộ năm dòng, ba tỷ trọng là 0.2, 0.4, 0.2 và còn 0.2 khuyết phân khúc. Không được diễn giải hai mẫu số này như nhau.
Output Cách 1
neighbourhood | re | trung | cao |
|---|---|---|---|
| Providencia | 0.25 | 0.50 | 0.25 |
| Santiago | 0.25 | 0.50 | 0.25 |
Cách 2: Đếm từng cặp khóa rồi chia theo tổng của hàng
python
# Dữ liệu mẫu và các hàm hỗ trợ
import numpy as np
import pandas as pd
DEMO = pd.DataFrame(
{
"id": list(range(201, 213)),
"name": [
"Depto Central",
"Pieza Bellas Artes",
"Loft Parque Forestal",
"Estudio sin precio",
"Habitacion Santa Lucia",
"Depto Providencia",
"Suite Costanera",
"Loft Pedro de Valdivia",
"Habitacion Manuel Montt",
"Casa Nunoa",
"Loft Plaza Egaña",
"Villa Vitacura",
],
"neighbourhood": ["Santiago"] * 5
+ ["Providencia"] * 4
+ ["Ñuñoa"] * 2
+ ["Vitacura"],
"price": [
25000.0,
45000.0,
120000.0,
np.nan,
60000.0,
30000.0,
99999.0,
100000.0,
18000.0,
55000.0,
150000.0,
210000.0,
],
"number_of_reviews_ltm": [10, 20, 3, 0, 15, 8, 12, 6, 2, 30, 1, 4],
"calculated_host_listings_count": [1, 6, 12, 2, 5, 1, 1, 7, 3, 9, 1, 20],
}
)
def phan_khuc_gia(gia):
if pd.isna(gia):
return None
if gia < 30000.0:
return "re"
elif gia < 100000.0:
return "trung"
return "cao"
DEMO["phan_khuc"] = DEMO["price"].map(phan_khuc_gia)
df_quan_lon = DEMO.loc[DEMO["neighbourhood"].isin(["Santiago", "Providencia"])].copy()
# Bắt đầu lời giải
import pandas as pd
def segment_share_by_district(df: pd.DataFrame) -> pd.DataFrame:
# Đếm số lượng phòng theo từng cặp quận và phân khúc
dem = (
df.groupby(["neighbourhood", "phan_khuc"], observed=False)
.size()
.unstack(fill_value=0)
)
# Chuẩn hóa tỷ trọng bằng cách chia cho tổng số dòng của từng quận
tong_hang = dem.sum(axis=1)
bang_ty_trong = dem.div(tong_hang, axis=0)
# Bổ sung các cột phân khúc còn thiếu nếu có
for col in ["re", "trung", "cao"]:
if col not in bang_ty_trong.columns:
bang_ty_trong[col] = 0.0
return bang_ty_trong[["re", "trung", "cao"]]
# Gọi thử nghiệm hàm trên tập các quận lớn:
share_c2 = segment_share_by_district(df_quan_lon)
print("Cơ cấu phân khúc Cách 2:")
print(share_c2.round(3))
# Kết thúc lời giảiGiải thích Cách 2
.groupby(["neighbourhood", "phan_khuc"]).size() tạo số đếm cho từng cặp khóa. .unstack(fill_value=0) đưa phân khúc lên cột và điền 0 cho tổ hợp không có dòng. fill_value=0 ở đây điền số đếm vắng mặt, chưa phải điền mức giá hoặc số đánh giá bị thiếu.
dem.sum(axis=1) cộng theo chiều ngang để lấy tổng số đếm của từng quận. dem.div(tong_hang, axis=0) căn chỉnh mẫu số theo quận rồi chia mọi ô trong cùng hàng cho mẫu số tương ứng. Nếu bỏ axis=0, pandas có thể căn Series mẫu số theo cột, không đúng mục đích.
Phép nhóm mặc định cũng bỏ khóa phân khúc bị khuyết. Vì vậy bảng dưới đây có cùng mẫu số đã biết như Cách 1. Với dữ liệu ngoài DEMO, phải quyết định giữ một nhóm khuyết, loại khỏi mẫu số hay báo tỷ lệ khuyết riêng trước khi khẳng định tổng mỗi hàng bằng 1.
Output Cách 2
neighbourhood | re | trung | cao |
|---|---|---|---|
| Providencia | 0.25 | 0.50 | 0.25 |
| Santiago | 0.25 | 0.50 | 0.25 |
So sánh hai cách
Cách 1 chuyên dùng cho bảng chéo tỷ trọng. Cách 2 cho thấy rõ số đếm, tổng hàng và phép chia, thuận tiện để kiểm tra mẫu số hoặc bổ sung trọng số. Hai cách hiện cùng xử lý dòng khuyết bằng cách loại khỏi nhóm. Cần đối chiếu yêu cầu về mẫu số toàn quận trước khi dùng bảng trong báo cáo.
Tài liệu tham khảo
Wes McKinney, Python for Data Analysis (tái bản lần 3): Chương 10: Data Aggregation and Group Operations.
Tài liệu chính thức pandas: Tài liệu hướng dẫn Group by: Split-apply-combine.
Hadley Wickham, The Split-Apply-Combine Strategy for Data Analysis, Journal of Statistical Software, 2011.
Hệ thống bài tập được chuyển thể từ học liệu mở của: Khóa học Lập trình xử lý dữ liệu (UET) theo giấy phép Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
IAI, Lập trình xử lý dữ liệu, học liệu và notebook. Các đề thực hành đã được biên tập cách diễn đạt và bổ sung lời giải, giữ CC BY-NC-SA 4.0. Giấy phép học liệu.
IAI. Notebook thực hành và học liệu thực hành 2627-1. Các yêu cầu kỹ thuật được đối chiếu, biên tập và bổ sung lời giải cùng dữ liệu minh họa. Phần thích nghi từ học liệu IAI áp dụng giấy phép CC BY-NC-SA 4.0.