1. Bảng có nhãnBiểu diễn dữ liệu bằng DataFrame và Series.
Ví dụ về bảng Dữ liệu giả lập; mỗi hàng là một chỗ ở, giá tính bằng USD/đêm.
chỉ mục loại giá a Nguyên căn 100 b Phòng riêng 50 c Nguyên căn chưa biết
Ta cần giữ cả nhãn hàng , tên cột và kiểu dữ liệu của từng cột.
Nối NumPy: một ndarray có một dtype, còn bảng này có cả chữ và số. Pandas cung cấp DataFrame để làm việc với bảng có nhãn. Chỉ mục a/b/c đánh dấu hàng; không phải cột dữ liệu thứ ba. Không khẳng định NumPy không thể chứa dữ liệu hỗn hợp; trọng tâm là cấu trúc bảng phù hợp hơn.
Tạo bảng với DataFrame DataFrame là bảng hai chiều. Mỗi khoá của dict dưới đây tạo một cột.
import pandas as pd
import numpy as np
df = pd.DataFrame({
"loai": ["Nguyên căn", "Phòng riêng", "Nguyên căn"],
"gia": [100., 50., np.nan],
}, index=["a", "b", "c"])
print(df)
loai gia
a Nguyên căn 100.0
b Phòng riêng 50.0
c Nguyên căn NaN
NaN đánh dấu giá trị thiếu; chưa biết giá khác với giá bằng 0.
Chỉ từ dict trong code sang hai cột trong bảng. index cung cấp nhãn hàng. Nếu bỏ index khi tạo từ các list này, pandas dùng nhãn 0,1,2. NaN biểu diễn thiếu trong cột float ở đây; chưa mở các dtype nullable.
Nhãn và kích thước của bảng df.index giữ nhãn hàng, df.columns giữ tên cột; cả hai là Index.
print(df.index)
Index(['a', 'b', 'c'], dtype='str')
print(df.columns)
Index(['loai', 'gia'], dtype='str')
Chỉ mục không tính vào số cột: bảng có 2 cột.
Index lưu nhãn kèm dtype của nhãn; ở đây nhãn là chuỗi nên dtype là str. Dùng repr thật để sinh viên nhận ra output khi tự chạy. shape là tuple (số hàng, số cột), đọc như shape của mảng NumPy. Chỉ mục có thể là số hoặc chữ, không mặc định là vị trí; phần loc/iloc sau sẽ làm rõ. Index là kiểu riêng của pandas dùng để lưu nhãn.
Mỗi cột là một Series Lấy cột gia: các giá trị vẫn đi cùng nhãn hàng a, b, c.
gia = df["gia"]
print(gia)
a 100.0
b 50.0
c NaN
Name: gia, dtype: float64
Series là dữ liệu một chiều có chỉ mục; gia là tên của Series này.
Name trong output là tên Series, dtype là kiểu dữ liệu của các giá trị. Series cũng có thể tạo độc lập với pd.Series; không phải chỉ tồn tại bên trong DataFrame.
Tính toán trên Series Nhân các giá trị với 2, như phép tính trên mảng NumPy:
print(gia * 2)
a 200.0
b 100.0
c NaN
Name: gia, dtype: float64
Nhãn của hàng được giữ lại. Giá trị thiếu vẫn là NaN.
Phép toán tạo kết quả mới, gia và df vẫn giữ nguyên. Sinh viên nhìn nhãn để biết kết quả ứng với chỗ ở nào. Khớp nhãn giữa hai Series khác thứ tự để Bài 5.
Mỗi cột có một dtype Cột loại chứa chữ; cột giá chứa số thực và giá trị thiếu.
print(df.dtypes)
loai str
gia float64
dtype: object
Hai dòng đầu là dtype của hai cột. Dòng cuối mô tả Series kết quả của df.dtypes.
Không hiểu dtype: object là dtype chung của DataFrame. df.dtypes trả một Series chứa các đối tượng dtype. Trong pandas 3.0.3 cột chuỗi được suy luận là str. Dtype không tự nói ý nghĩa nghiệp vụ: mã số có thể lưu bằng int nhưng không có nghĩa nên tính trung bình mã.
Bài tập 1: Chọn chỗ ở Giá giả lập (USD). Phí dọn dẹp chỉ thu một lần cho mỗi lượt ở.
Chỗ ở Giá mỗi đêm Phí dọn dẹp a 100 0 b 80 100 c 60 300
Tạo DataFrame và tính tổng tiền cho 2 đêm, 7 đêm bằng phép toán trên Series. Mỗi trường hợp chọn chỗ nào rẻ nhất? Từ bao nhiêu đêm thì c rẻ hơn cả a và b? Kiểm tra lại bằng code. Mục đích: dùng các cột có ý nghĩa khác nhau để lập phép tính, chọn phương án theo tổng tiền và kiểm chứng ngưỡng thay đổi. Dữ liệu giả lập, tổng tiền trong bài chỉ gồm giá đêm và phí dọn dẹp đã nêu; số đêm là số nguyên dương. Cách tạo: cho_o = pd.DataFrame({"gia_dem": [100, 80, 60], "phi_don": [0, 100, 300]}, index=["a", "b", "c"]). Tổng cho n đêm: cho_o["gia_dem"] * n + cho_o["phi_don"]. Hai Series lấy từ cùng bảng có cùng chỉ mục; chưa yêu cầu cơ chế khớp nhãn khác thứ tự. Với 2 đêm: a=200, b=260, c=420, chọn a. Với 7 đêm: a=700, b=660, c=720, chọn b. Để c rẻ hơn a: 60n+300 < 100n nên n > 7,5. Để c rẻ hơn b: 60n+300 < 80n+100 nên n > 10. Số đêm tối thiểu là 11. Kiểm tra n=10: [1000, 900, 900], b và c bằng nhau; n=11: [1100, 980, 960], c rẻ nhất. Chấp nhận cách lập luận bằng bất đẳng thức hoặc thử các số đêm và giải thích ngưỡng; không bắt buộc idxmin, apply, groupby hoặc thêm cột chưa học. Chỉ đọc nhãn từ Series kết quả là đủ. Không suy rộng thành báo giá Airbnb thật.
2. Đọc và hiểu dữ liệuĐọc bảng chỗ ở, kiểm tra giá rồi nhận xét mức giá.
Đọc bảng Airbnb Santiago Tìm hiểu mức giá chỗ ở tại Santiago từ snapshot ngày 29/06/2026.
url = ("https://data.insideairbnb.com/chile/rm/santiago/"
"2026-06-29/visualisations/listings.csv")
listings = pd.read_csv(url)
print(listings.shape)
(18534, 19)
read_csv tạo DataFrame: 18.534 hàng, 19 cột .
Bản rút gọn visualisations/listings.csv, không phải bản chi tiết data/listings.csv.gz. Chuyển từ tạo DataFrame bằng dict sang đọc từ file. Mỗi hàng là một chỗ ở trong snapshot, không phải lượt đặt phòng. Không gọi dữ liệu này là toàn bộ thị trường cho thuê.
Danh sách các cột Một hàng là một chỗ ở; mỗi cột là một thông tin về nó.
print(listings.columns)
Index(['id', 'name', 'host_id', 'host_profile_id', 'host_name',
'neighbourhood_group', 'neighbourhood', 'latitude', 'longitude',
'room_type', 'price', 'minimum_nights', 'number_of_reviews',
'last_review', 'reviews_per_month', 'calculated_host_listings_count',
'availability_365', 'number_of_reviews_ltm', 'license'],
dtype='str')
Khoá: id, host_id. Số đo: price, minimum_nights. Nhãn phân loại: room_type, neighbourhood.
Hoàn thiện slide GV thêm. columns trả tên cột, chưa cho biết đơn vị hoặc ý nghĩa đầy đủ. Không cần đọc thuộc 19 tên. Phân loại theo vai trò để biết cột nào tính được, cột nào chỉ để tra cứu: cộng trung bình một cột khoá như host_id là vô nghĩa. latitude/longitude là số nhưng là toạ độ, không phải số đo để cộng trung bình. Giá cao/thấp có thể liên quan loại chỗ ở nên giữ room_type cạnh price. Khoá dùng để tra lại bản ghi. Bài này bắt đầu với id, room_type và price.
Xem trước vài dòng Mỗi hàng là một chỗ ở; price là giá một đêm, tính bằng peso Chile (CLP).
print(listings[["id", "room_type", "price"]].head(3))
id room_type price
0 978070332077815549 Private room 45647.0
1 1069858035768058539 Private room 19856.0
2 37181369 Entire home/apt 46776.0
[[...]] chọn các cột theo tên; head(3) lấy 3 hàng đầu.
Các số 0, 1, 2 bên trái là chỉ mục; id là mã chỗ ở từ file.
Đây là lần đầu dùng nhiều tên cột: giải thích đủ để đọc ví dụ, phần 3 mới so Series/DataFrame và chọn chi tiết. head cho xem nhanh, không đại diện toàn bảng. read_csv tạo chỉ mục số mặc định; id vẫn là cột dữ liệu, không tự trở thành index. room_type là loại chỗ ở. Đơn vị CLP của bảng Santiago khác USD trong ví dụ giả lập. Ý nghĩa và đơn vị phải đối chiếu mô tả nguồn: https://insideairbnb.com/get-the-data/ .
Đếm giá trị thiếu Thử trên cột giá của bảng nhỏ trước:
Nhãn giagia.isna()a 100 False b 50 False c NaN True
print(gia.isna().sum())
1
isna() đánh dấu ô thiếu. sum() đếm các giá trị True.
Đếm bool: True đóng góp 1, False đóng góp 0. Không dùng gia == np.nan để phát hiện thiếu. Cách biểu diễn thiếu có thể khác theo dtype, dùng isna thay vì so một ký hiệu cụ thể.
Bao nhiêu chỗ ở chưa có giá?
print(listings["price"].isna().sum())
846
print(listings["price"].count())
17688
Trong 18.534 hàng: 846 thiếu giá , 17.688 có giá .count() chỉ đếm giá trị không thiếu.
Nối chính xác cơ chế trên cột nhỏ sang cột thật. Số liệu kiểm bằng snapshot. Không cần cho dtypes ở đây vì không phục vụ câu hỏi đếm thiếu; dtype trong phần cấu trúc và notebook tự học.
Giá điển hình là bao nhiêu? Tính trên các giá được ghi nhận, đơn vị CLP/đêm.
thong_so = ["mean", "median", "max"]
print(listings["price"].agg(thong_so).round())
mean 118200.0
median 59000.0
max 97000045.0
Name: price, dtype: float64
Trung bình gấp đôi trung vị vì vài giá rất lớn kéo lên: bỏ ba giá cao nhất, trung bình còn khoảng 105.303; trung vị không đổi.
Số đã kiểm trên snapshot 2026-06-29: mean 118.200, median 59.000, mean sau khi bỏ 3 giá cao nhất là 105.303, bản ghi lớn nhất chiếm 4,6395% tổng giá. Trung vị chỉ phụ thuộc thứ hạng nên vài giá lớn không kéo được nó. Các thống kê bỏ qua giá thiếu. Max 97000045 cần kiểm nguồn, đơn vị và bản ghi, chưa chứng minh là sai; mục 5 sẽ xem các hàng đó. Mức giá phổ biến cần xem phân bố, không tự đồng nhất với mean hay median. agg chạy các phép tổng hợp đã chọn; round() làm tròn để hiển thị. agg chạy các phép tổng hợp đã chọn; round() làm tròn để hiển thị. Riêng bản ghi 97 triệu chiếm khoảng 4,6% tổng giá.
Bài tập 2: Giá có giảm không? Giá của ba chỗ ở qua hai lần thu thập (giả lập, USD/đêm).
Chỗ ở Lần 1 Lần 2 a 100 110 b 200 210 c 300 chưa có giá
Tính trung bình các giá đã biết ở mỗi lần thu thập. Tính lại hai trung bình khi chỉ xét a và b ở cả hai lần. Vì sao cách thứ nhất cho thấy giá trung bình giảm, còn cách thứ hai lại tăng? Mục đích: phân biệt thay đổi giá với thay đổi tập chỗ ở có dữ liệu. Bài độc lập với bài tập 1. Trung bình các giá đã biết: lần 1 là 200 từ a,b,c; lần 2 là 160 từ a,b. Trung bình giảm vì hai lần tính không cùng tập chỗ ở: giá 300 của c có trong lần 1, giá c thiếu ở lần 2. Chỉ xét a,b ở cả hai lần: trung bình từ 150 lên 160; từng chỗ đều tăng 10. Chưa biết giá lần 2 của c nên chưa kết luận cho cả ba chỗ ở. Cả hai phép tính trung bình đều đúng về số học, nhưng trả lời hai câu hỏi khác nhau. Không được coi NaN là 0 hoặc mặc định c giữ nguyên giá. Sinh viên có thể tính tay rồi tạo DataFrame với hai cột lan_1, lan_2 để kiểm chứng từng cột bằng mean(). Chưa yêu cầu cú pháp lọc pandas: câu 2 có thể tạo bảng riêng gồm a,b. Chấp nhận cách lấy phần đầu mỗi Series nếu sinh viên biết; không biến bài thành kiểm tra API chưa học.
3. Chọn hàng, chọn cộtChọn theo nhãn, vị trí hoặc điều kiện.
Chọn một ô
Lấy giá của chỗ ở b trong df đầu bài.
Vị trí hàng Chỉ mục loai gia 0 a Nguyên căn 100 1 b Phòng riêng 50 2 c Nguyên căn NaN
print(df.loc["b", "gia"])
50.0
print(df.iloc[1, 1])
50.0
loc dùng nhãn; iloc dùng vị trí hàng và cột (từ 0).
Cột loai ở vị trí 0, gia ở vị trí 1; chỉ mục a,b,c không phải một cột dữ liệu. Hai lệnh cùng lấy giá 50 nhưng diễn đạt hai cách xác định ô. Không giới thiệu chỉ mục số ở đây để tránh lẫn với vị trí.
Chọn nhiều hàng và cột
Lấy loại và giá của hai chỗ ở b, a, theo đúng thứ tự đó.
print(df.loc[["b", "a"], ["loai", "gia"]])
loai gia
b Phòng riêng 50.0
a Nguyên căn 100.0
Danh sách trước dấu phẩy chọn hàng; danh sách sau chọn cột.
Danh sách nhãn vừa chọn vừa quy định thứ tự. Kết quả có 2 hàng, 2 cột. Không phải phép nối, không sửa thứ tự df. Có thể liên hệ chọn vùng bằng np.ix_ trong NumPy, nhưng không cần lôi cơ chế đó vào slide.
Giữ kết quả dạng bảng
print(df.loc[["a", "b"], "gia"])
a 100.0
b 50.0
Name: gia, dtype: float64
Tên cột "gia" → Series, shape (2,).
print(df.loc[["a", "b"], ["gia"]])
gia
a 100.0
b 50.0
Danh sách ["gia"] → DataFrame, shape (2, 1).
Đọc output: Series có Name/dtype, DataFrame có tiêu đề cột. Muốn giữ dạng bảng để xuất file hoặc bổ sung cột thì dùng danh sách tên cột. df["gia"] và df[["gia"]] cũng khác nhau theo nguyên tắc này. Không cần nói một dạng tốt hơn dạng kia.
Chọn các hàng liên tiếp
📖 Tự học
Lấy giá từ hàng a đến hết hàng b:
print(df.loc["a":"b", "gia"])
a 100.0
b 50.0
Name: gia, dtype: float64
loc lấy cả nhãn cuối b.
print(df.iloc[0:2, 1])
a 100.0
b 50.0
Name: gia, dtype: float64
iloc bỏ vị trí cuối 2, như lát cắt NumPy.
Cả hai lệnh cho cùng kết quả vì đề cùng một yêu cầu. df có nhãn a,b,c duy nhất và đúng thứ tự đang thấy. Không giới thiệu các ngoại lệ lát cắt khi nhãn trùng hay mốc không tồn tại. Anchor slice-positions trước đây được gộp vào slide này.
Tạo điều kiện chọn hàng
Chỉ lấy những chỗ ở nguyên căn.
mask = df["loai"] == "Nguyên căn"
print(mask)
a True
b False
c True
Name: loai, dtype: bool
Phép so sánh chạy trên từng hàng, trả về một Series bool cùng chỉ mục a, b, c.
Name: loai vì mask sinh ra từ cột loai; dtype: bool cho biết các giá trị là True/False. Đây là điều kiện về loại, không phải về giá, nên c vẫn được đánh dấu True dù thiếu giá. Nối bool mask đã học ở NumPy. Mask lấy trực tiếp từ df nên cùng chỉ mục. Việc khớp nhãn giữa các Series khác thứ tự để Bài 5.
Dùng mask để lấy hàng
nguyen_can = df.loc[mask, ["loai", "gia"]]
print(nguyen_can)
loai gia
a Nguyên căn 100.0
c Nguyên căn NaN
loc giữ các hàng có mask bằng True: a và c.
Chỉ vào a/c trong bảng mask ở slide trước và kết quả ở slide này. Nhãn c vẫn là c, không tự đánh lại thành 1. Ô thiếu giá không bị loại bởi điều kiện về loại. df không bị thay đổi khi lấy kết quả.
Ngoặc vuông và .loc
Tên cột trong ngoặc cho ra cột; Series bool trong ngoặc cho ra hàng.
Cách viết Kết quả df["gia"]Một cột, trả về Series df[["loai", "gia"]]Nhiều cột, trả về bảng df[mask]Các hàng có True, giữ đủ mọi cột df.loc[mask, "gia"]Chọn hàng và cột trong một lệnh
Cần nói rõ cả hàng lẫn cột thì dùng .loc: hàng trước, cột sau.
Cùng một cặp ngoặc vuông nhưng ý nghĩa đổi theo thứ đặt bên trong: tên cột chọn cột, Series bool chọn hàng. df[mask] và df.loc[mask] cho cùng kết quả; df[mask] không cho chọn cột trong cùng lệnh; muốn lấy cột phải mở thêm một cặp ngoặc nữa, và dạng hai lần ngoặc đó sẽ hỏng khi dùng để gán (mục 4). Không nói dạng nào tốt hơn tuyệt đối; điều cần là đọc được cả hai. Nối slide AI cuối bài: nhầm nhãn với vị trí là lỗi hay gặp khi đọc code có sẵn. Code trên mạng hoặc do AI sinh ra hay dùng df[mask]; sinh viên cần đọc được dạng này.
Kết hợp nhiều điều kiện
Trong bảng Santiago, lấy nguyên căn có giá dưới 50.000 CLP/đêm . Chỉ giữ mã và giá.
mask = ((listings["room_type"] == "Entire home/apt")
& (listings["price"] < 50_000))
chon = listings.loc[mask, ["id", "price"]]
print(chon.shape)
(4273, 2)
&: thoả mãn cả hai; |: thoả mãn ít nhất một điều kiện. Đặt mỗi điều kiện trong ngoặc.
Snapshot Santiago 2026-06-29 có 4273 hàng thoả, kết quả 2 cột. Đây là lọc theo điều kiện đã nêu, không phải kiểm tra đầy đủ chất lượng giá. So sánh giá NaN trong cột float trả False; chưa biết giá không có nghĩa giá cao. Không dùng and/or Python cho Series.
Bài tập 3: Lọc theo ngân sách
Bảng lua_chon (giả lập, USD/đêm). Ngân sách: nguyên căn ≤ 100, phòng riêng ≤ 60.
Chỉ mục loai gia a Nguyên căn 90 b Phòng riêng 70 c Nguyên căn 120 d Phòng riêng 60
Dự đoán các hàng được chọn. Viết mask để lấy hai cột loai, gia của các hàng đó. Kết quả a, d; d nằm đúng biên 60 nên được chọn. b có giá 70 dưới ngưỡng nguyên căn nhưng vượt ngưỡng phòng riêng; c là nguyên căn nhưng vượt ngưỡng 100. mask = ((lua_chon["loai"] == "Nguyên căn") & (lua_chon["gia"] <= 100)) | ((lua_chon["loai"] == "Phòng riêng") & (lua_chon["gia"] <= 60)); lua_chon.loc[mask, ["loai", "gia"]]. Hai loại loại trừ nhau theo dữ liệu đề. Phân biệt OR giữa hai nhóm điều kiện với AND trong mỗi nhóm. Chạy đáp án kiểm bảng đúng a,d và không sửa lua_chon.
4. Tính toán và sửa bảngTạo bảng giá mới, sửa giá rồi tính lại các khoản tiền.
Tạo cột tính giá tiền cho ba đêm
Thêm vào df cột tiền ba đêm, tính từ giá mỗi đêm (USD).
df["ba_dem"] = df["gia"] * 3
print(df[["gia", "ba_dem"]])
gia ba_dem
a 100.0 300.0
b 50.0 150.0
c NaN NaN
Gán vào tên cột mới sẽ thêm cột vào bảng.
Thêm ba_dem trực tiếp vào df; cột gia vẫn giữ 100, 50, NaN. Phép nhân trả Series rồi phép gán tạo cột trong df. Với NaN, tiền cũng thiếu. Ví dụ ba đêm ở đây chỉ tính giá đêm; không dùng bảng phí dọn dẹp riêng của bài tập 1.
Sửa giá của một chỗ ở
Muốn thử giá mới cho b mà vẫn giữ giá trong df, tạo bản sao rồi sửa:
bang_gia = df.copy()
bang_gia.loc["b", "gia"] = 60
print(bang_gia["gia"])
a 100.0
b 60.0
c NaN
Name: gia, dtype: float64
copy() tạo bảng riêng. Giá của b trong bang_gia là 60; trong df vẫn là 50.
copy() được giới thiệu khi cần giữ bảng giá cũ và thử giá mới riêng. Bản sao mang cả cột ba_dem vừa thêm. Nếu chỉ viết bang_gia = df thì hai tên cùng trỏ tới một bảng; sửa qua bang_gia cũng đổi df. Phép gán qua loc xác định ô cần sửa. Giá b trong bang_gia là 60, trong df vẫn là 50. Cột ba_dem chưa tính lại; sẽ xem rõ sau khi hoàn tất sửa giá.
Sửa Series lấy ra từ bảng
pandas 3 dùng Copy-on-Write: Series chỉ tách khỏi bảng khi bị ghi, nên sửa nó không đổi bảng.
cot_gia = bang_gia["gia"]
cot_gia.loc["a"] = 80
Chỉ mục cot_gia bang_gia["gia"] a 80 100 b 60 60 c NaN NaN
Muốn sửa bảng, gán vào bang_gia.loc[hàng, cột]. pandas 2 hành xử khác; notebook đã ghim pandas 3.
Copy-on-Write có từ pandas 1.5 dưới dạng tuỳ chọn; từ pandas 3.0 trở thành mặc định và là chế độ duy nhất. Hành vi pandas 3.x Copy-on-Write: chọn Series rồi sửa không làm thay đổi bảng nguồn. Có thể chia sẻ bộ nhớ trước khi ghi; không khẳng định thao tác đọc luôn sao chép vật lý. Khác với sửa view NumPy ở Bài 3. Chained assignment bang_gia["gia"]["a"]=80 cũng không phải cách sửa bảng trong pandas 3. Nguồn https://pandas.pydata.org/docs/user_guide/copy_on_write.html .
Hai lần ngoặc thì bảng không đổi
Dạng viết hay gặp trong code cũ và code do AI sinh ra:
bang_gia["gia"]["a"] = 80
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. […] Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
print(bang_gia.loc["a", "gia"])
100.0
Ngoặc thứ nhất lấy ra một Series, ngoặc thứ hai ghi vào Series đó: bảng không đổi. Viết một lệnh: bang_gia.loc["a", "gia"] = 80.
Đây là chained assignment. ChainedAssignmentError là một Warning chứ không phải exception: code vẫn chạy tiếp, chỉ in cảnh báo, nên lỗi này dễ bị bỏ qua và làm sai kết quả về sau. Output trên slide đã rút gọn phần giữa của thông báo; chạy notebook sẽ thấy đầy đủ kèm tên file và số dòng. Cùng một nguyên nhân với slide trước: Copy-on-Write. Sinh viên sẽ gặp dạng này khi chép code viết cho pandas 1.x. Không dạy cách tắt cảnh báo.
Giảm giá cho chỗ ở nguyên căn
Giảm 10% giá nguyên căn trong bang_gia; giữ giá phòng riêng.
mask = bang_gia["loai"] == "Nguyên căn"
bang_gia.loc[mask, "gia"] = bang_gia.loc[mask, "gia"] * 0.9
print(bang_gia[["loai", "gia"]])
loai gia
a Nguyên căn 90.0
b Phòng riêng 60.0
c Nguyên căn NaN
Nối thao tác đọc ở mục 3 với phép gán: bên phải lấy giá nguyên căn và nhân 0,9; bên trái ghi kết quả vào đúng hàng và cột của bảng. Giá a từ 100 thành 90, b giữ 60 từ lần cập nhật trước, c vẫn NaN. df ban đầu không đổi. Nhân 0,9 là giảm 10%, không phải trừ 10 USD.
Tính lại tiền sau khi sửa giá
print(bang_gia[["gia", "ba_dem"]])
gia ba_dem
a 90.0 300.0
b 60.0 150.0
c NaN NaN
ba_dem vẫn giữ kết quả cũ. Chạy lại phép tính:
bang_gia["ba_dem"] = bang_gia["gia"] * 3
print(bang_gia[["gia", "ba_dem"]])
gia ba_dem
a 90.0 270.0
b 60.0 180.0
c NaN NaN
Cột lưu giá trị đã tính, không lưu công thức như ô Excel.
Trước tính lại a90/300,b60/150; sau tính lại a90/270,b60/180. Thứ tự đúng cho yêu cầu này: thay đổi giá trước, tính tiền sau. Đừng giải thích là lỗi Copy-on-Write: ngay cả gán trực tiếp thành công cũng không tự tính lại cột khác. Sửa giá xong, cột phụ thuộc vẫn giữ số cũ cho tới khi tính lại.
Bài tập 4: Báo giá ba đêm
Dùng df đầu bài. Giảm 10% giá nguyên căn, rồi tính tiền ba đêm. Giữ nguyên df.
bao_gia = df.copy()
bao_gia["ba_dem"] = bao_gia["gia"] * 3
mask = bao_gia["loai"] == "Nguyên căn"
gia_nguyen_can = bao_gia.loc[mask, "gia"]
gia_nguyen_can *= 0.9
Code trên cho tiền ba đêm của a là bao nhiêu? Theo yêu cầu, phải là bao nhiêu? Sửa code và kiểm tra cả ba hàng. Trong pandas 3, code cho tiền ba đêm của a là 300, trong khi yêu cầu là 270; b là 150, c là NaN. Có hai lỗi: chỉ sửa Series đã lấy ra và tính ba_dem trước khi sửa giá. Đáp án: bao_gia = df.copy(); mask = bao_gia["loai"] == "Nguyên căn"; bao_gia.loc[mask, "gia"] = bao_gia.loc[mask, "gia"] * 0.9; bao_gia["ba_dem"] = bao_gia["gia"] * 3. Kiểm tra: giá trong df vẫn là 100, 50, NaN; giá trong bao_gia là 90, 50, NaN và tiền ba đêm là 270, 150, NaN. Chỉ sửa phép gán nhưng để phép tính ba_dem ở đầu vẫn chưa đúng. Giá c thiếu nên tiền ba đêm cũng thiếu. Chấp nhận cách khác nếu giữ nguyên df và cho kết quả đúng.
5. Kiểm tra và lưuKiểm tra các giá trị cần chú ý và thông tin được giữ trong file.
Xem những chỗ ở giá cao nhất
Ở mục 2, giá lớn nhất gần 97 triệu CLP/đêm . Xem các hàng có giá cao nhất:
print(listings.nlargest(3, "price")[["id", "price"]])
id price
4485 651018255986860283 97000045.0
513 704362156276774524 82566932.0
18477 1699499358806776408 48863551.0
nlargest(3, "price") lấy ba hàng có giá cao nhất. Dùng id để tra lại chỗ ở trong nguồn dữ liệu.
Số liệu từ snapshot 2026-06-29, giá lớn nhất là 97.000.045 CLP. Cần đối chiếu bản ghi nguồn, đơn vị và loại chỗ ở trước khi sửa hoặc xoá. Giá cao chưa chứng minh dữ liệu sai. Ba hàng này cũng chưa bao quát mọi trường hợp cần kiểm tra. Sinh viên đã học chọn cột nên có thể đọc được đoạn lấy id và price.
Tỷ lệ giá dưới 80 USD/đêm
Với Series bool, mean() là tỷ lệ giá trị True.
Chỉ mục gia gia < 80 a 100 False b 50 True c NaN False
print((df["gia"] < 80).mean())
0.3333333333333333
Trên cả bảng: 1/3. Chỉ xét hàng có giá bằng notna():
co_gia = df["gia"].notna()
print((df.loc[co_gia, "gia"] < 80).mean())
0.5
Kết quả là 1/3 khi tính trên toàn bảng; 1/2 khi chỉ xét hai giá đã biết. Mean của Series bool là số giá trị True chia cho số giá trị bool không thiếu. Bảng dùng float64 chứa np.nan: NaN < 80 trả False, nhưng không có nghĩa giá c được xác nhận là cao. Với dtype nullable, phép so sánh có thể giữ NA. Vì vậy, cần xác định tập hàng trước khi tính. notna là phủ định của isna đã học: True khi có giá. Liên hệ bài tập 2: tập chỗ ở được đưa vào phép tính ảnh hưởng đến kết luận.
Cùng câu hỏi trên bảng Santiago
Tỷ lệ giá dưới 50.000 CLP/đêm trên toàn bảng:
print((listings["price"] < 50_000).mean())
0.3600949606129276
Chỉ trên 17.688 hàng có giá:
co_gia = listings["price"].notna()
print((listings.loc[co_gia, "price"] < 50_000).mean())
0.3773179556761646
Mẫu số đổi thì tỷ lệ đổi: 36,0% và 37,7% trả lời hai câu hỏi khác nhau.
Số đã kiểm trên snapshot 2026-06-29: 0,3601 trên 18.534 hàng và 0,3773 trên 17.688 hàng có giá. 846 hàng thiếu giá bị tính là False ở phép thứ nhất, nên tỷ lệ thấp hơn. Chênh lệch nhỏ nhưng đủ để đổi kết luận nếu báo cáo làm tròn tới phần trăm. Câu hỏi thứ nhất là trong toàn bộ chỗ ở của snapshot; câu thứ hai là trong các chỗ ở đã biết giá. Không nói phép nào đúng hơn: phải nói rõ mẫu số. Ngưỡng 50.000 CLP lấy lại từ slide lọc ở mục 3. Xác định tập hàng trước, rồi mới tính.
Lưu bảng giá để dùng tiếp
Chỉ giữ các hàng có giá và ba cột id, room_type, price.
cot = ["id", "room_type", "price"]
co_gia = listings["price"].notna()
ket_qua = listings.loc[co_gia, cot]
ket_qua.to_csv("gia_co_thong_tin.csv", index=False)
index=False bỏ chỉ mục 0, 1, … khi ghi; mã chỗ ở vẫn nằm trong cột id.
Bảng chưa kiểm tra xong: các giá rất lớn vẫn còn. Lưu để làm tiếp.
File có 17.688 hàng và 3 cột, không thiếu price. Chỉ mục số được tạo khi đọc CSV nên không cần xuất trong ví dụ listings này; mã chỗ ở đã nằm trong cột id. Đây chưa phải bảng đã kiểm tra xong chất lượng: vẫn còn những giá cần xác minh. Giới thiệu to_csv đủ để sử dụng kết quả; định dạng và encoding sẽ học ở Bài 6. Không kết luận rằng luôn phải dùng index=False: bài tập sau dùng chỉ mục chứa mã chỗ ở.
Đọc lại file vừa lưu
doc_lai = pd.read_csv("gia_co_thong_tin.csv")
print(doc_lai.shape)
(17688, 3)
print(doc_lai["price"].isna().sum())
0
print(doc_lai["price"].sum() == ket_qua["price"].sum())
True
Đối chiếu: 17.688 hàng, 3 cột, không thiếu giá, tổng giá không đổi.
shape chỉ kiểm kích thước, chưa xác nhận tên cột hay nội dung từng ô. Notebook có thêm print(doc_lai.columns) để đối chiếu. Giảng viên có thể kiểm toàn bộ dữ liệu bằng pd.testing.assert_frame_equal(doc_lai, ket_qua.reset_index(drop=True), check_exact=False); chưa yêu cầu sinh viên học API kiểm thử này. Những kiểm tra trên giúp phát hiện lỗi ghi và đọc file, chưa xác nhận dữ liệu nguồn là đúng.
Bài tập 5: Giữ mã chỗ ở trong file
Bảng df đầu bài dùng a, b, c làm chỉ mục. Một bạn gửi bảng giá bằng cách:
bang_gui = df[["gia"]]
bang_gui.to_csv("bang_gui.csv", index=False)
Người nhận chỉ có file CSV. Họ có biết mỗi giá thuộc chỗ ở nào không? Sửa cách lưu để giữ cả mã chỗ ở và giá. Đọc lại file để kiểm tra. File ban đầu có đúng bốn dòng: gia / 100.0 / 50.0 / "" — ô thiếu được ghi thành hai dấu nháy, vì bảng chỉ còn một cột nên một dòng rỗng sẽ không phân biệt được với dòng trống; mất các mã a, b, c. Số hàng và số giá thiếu vẫn đúng, nhưng không còn biết giá thuộc chỗ ở nào. Có hai cách hợp lệ: giữ chỉ mục khi xuất (index=True hoặc bỏ tham số index; có thể thêm index_label="ma"), hoặc đưa mã vào một cột rồi dùng index=False. Cách dùng kiến thức đã học: bang_gui = df[["gia"]].copy(); bang_gui["ma"] = bang_gui.index; bang_gui.to_csv("bang_gui.csv", index=False). Đọc lại phải giữ được các cặp a–100, b–50, c–NaN. Không bắt buộc thứ tự cột hay tên ma. Người nhận không có bảng gốc nên không thể dựa vào vị trí hàng để đoán mã. Ví dụ listings trước đó dùng index=False vẫn giữ được mã vì id là một cột dữ liệu.