1. Đọc và tính toán với ngày giờ
Xác định cách ghi ngày trước khi đổi kiểu và tính toán.
Đổi chuỗi thành ngày giờ
Cần tính số ngày từ 28/02 đến 03/03/2026. Trừ trực tiếp hai chuỗi này được không?
chuoi = pd.Series(["28/02/2026", "03/03/2026"])
ngay = pd.to_datetime(chuoi, format="%d/%m/%Y")
print(ngay)
0 2026-02-28
1 2026-03-03
dtype: datetime64[us]
to_datetime đổi chuỗi thành ngày giờ; format mô tả cách ghi: %d ngày, %m tháng, %Y năm bốn chữ số.
Hỏi lớp: trừ hai chuỗi được không? (đáp: không; Python báo TypeError vì chuỗi chỉ là văn bản. Sắp xếp theo chuỗi còn đặt 03/03/2026 trước 28/02/2026.) pandas hiển thị ngày theo năm-tháng-ngày sau chuyển đổi; đây là cách hiển thị, không làm đổi thời điểm. datetime64[us]: pandas 3 lưu ngày giờ với đơn vị micro giây.
Ngày và tháng có thể bị đảo
print(pd.Series({
"ngày/tháng/năm": pd.to_datetime(
"03/07/2026", format="%d/%m/%Y"),
"tháng/ngày/năm": pd.to_datetime(
"03/07/2026", format="%m/%d/%Y"),
}))
ngày/tháng/năm 2026-07-03
tháng/ngày/năm 2026-03-07
dtype: datetime64[us]
Cả hai cách đọc đều hợp lệ. Cần dựa vào quy ước của nguồn dữ liệu để chọn format.
Hỏi lớp trước khi hiện output: hai dòng có ra cùng một ngày không? (đáp: không; 3/7 và 7/3 cách nhau gần 4 tháng, và không dòng nào báo lỗi.) Đọc nhầm là lỗi ngữ nghĩa: không có exception nhưng sai ngày. Bài tập 1 cho thấy hệ quả khi không khai báo format. dayfirst=True chỉ ưu tiên ngày trước, không thay cho một format đã biết.
Ngày không hợp lệ và ngày bị thiếu
goc = pd.Series(["29/06/2026", "31/06/2026", None])
doc = pd.to_datetime(
goc, format="%d/%m/%Y", errors="coerce")
print(pd.DataFrame({"goc": goc, "ngay": doc}))
goc ngay
0 29/06/2026 2026-06-29
1 31/06/2026 NaT
2 NaN NaT
errors="coerce" đổi ngày không đọc được thành NaT (giá trị thiếu của ngày giờ) thay vì báo lỗi. Hàng 1 là lỗi chuyển đổi, hàng 2 thiếu từ nguồn; giữ cột gốc để phân biệt.
31/6 không tồn tại. Bỏ errors="coerce" thì to_datetime báo ValueError "day is out of range for month". coerce chỉ đổi giá trị lỗi thành NaT: không sửa ngày sai, không xoá hàng. Mask lỗi chuyển đổi goc.notna() & doc.isna() chỉ chọn hàng 1.
Lấy tháng và thứ trong tuần
Giống .str ở Bài 7, .dt lấy thành phần ngày giờ của cả cột.
print(pd.DataFrame({
"ngay": ngay,
"thang": ngay.dt.month,
"thu": ngay.dt.dayofweek,
}))
ngay thang thu
0 2026-02-28 2 5
1 2026-03-03 3 1
dayofweek: thứ Hai = 0, …, Chủ nhật = 6. Có thể lấy thêm year, quarter.
Đối chiếu 28/2/2026 là thứ Bảy (5), 3/3 là thứ Ba (1). Có thể dùng các cột này với mask hoặc groupby như Bài 4–5. Trích tháng không giữ năm; chỉ dùng tháng khi có mục đích gộp nhiều năm.
Tính khoảng cách giữa hai ngày
khoang = ngay.iloc[1] - ngay.iloc[0]
print(khoang)
3 days 00:00:00
Mỗi giá trị ngày giờ là một Timestamp; trừ hai Timestamp tạo Timedelta (khoảng thời gian). .days lấy số ngày; với một Series dùng .dt.days.
Tính qua ranh giới tháng, không lấy ngày trong tháng trừ nhau. Nếu có giờ, days là số ngày trọn; không phải tổng số giây hoặc ngày dạng số thực.
Bài tập 1: Tính số đêm lưu trú
Mỗi hàng là một lượt đặt. Ngày được ghi theo DD/MM/YYYY (giả lập).
dat = pd.DataFrame({
"vao": ["03/07/2026", "10/07/2026"],
"ra": ["06/07/2026", "12/07/2026"],
})
vao = pd.to_datetime(dat["vao"])
ra = pd.to_datetime(dat["ra"])
so_dem = (ra - vao).dt.days
print(so_dem.tolist())
Sửa code để tính đúng số đêm của từng lượt đặt.
Đáp án: [3, 2] đêm. Code gốc chạy không lỗi, không cảnh báo, nhưng pandas đọc theo thứ tự tháng/ngày/năm (03/07 thành ngày 7 tháng 3) nên cho [92, 61]. Hỏi lớp: dấu hiệu nào cho thấy [92, 61] sai? (đáp: lượt 03/07–06/07 không thể dài 92 đêm.) Không có format, pandas đoán định dạng từ giá trị đầu rồi áp cho cả cột, nên kết quả phụ thuộc thứ tự dòng (cột bắt đầu bằng 28/02/2026 thì pandas đoán đúng ngày trước); format="mixed" vẫn cho [92, 61]. Sinh viên phải viện dẫn quy ước DD/MM/YYYY, không sửa ngày trong bảng để hợp kết quả. Chấp nhận dayfirst=True trên bộ ví dụ; format đã biết vẫn chắc hơn, vì dayfirst=True đọc chuỗi 2026-03-07 thành ngày 3 tháng 7 mà không báo gì.
Đổi múi giờ
📖 Tự học
Nhật ký (log) của phần mềm hoặc API có thể ghi thời điểm theo UTC (giờ phối hợp quốc tế).
utc = pd.Timestamp("2026-06-29 22:30", tz="UTC")
print(utc.tz_convert("Asia/Ho_Chi_Minh"))
2026-06-30 05:30:00+07:00
tz_convert đổi cách hiển thị sang múi giờ khác. Cùng một thời điểm có thể thuộc hai ngày khác nhau, tuỳ múi giờ.
Khác với tz_convert, tz_localize gắn múi giờ cho ngày giờ đang chưa có múi giờ; chỉ gắn khi biết quy ước của nguồn. Không phải chuỗi thời gian nào từ API cũng ở UTC. Cột date của Inside Airbnb chỉ có ngày, không có giờ, nên không gắn múi giờ.
2. Tổng hợp theo thời gian
Đếm theo ngày, tháng và làm mượt các dao động.
Dữ liệu đánh giá tại Santiago
rv_raw là bảng đọc từ file reviews.csv: mỗi hàng là một đánh giá; date là ngày ghi nhận, viết theo năm-tháng-ngày.
rv_raw["date"] = pd.to_datetime(
rv_raw["date"], format="%Y-%m-%d")
print(rv_raw.head(3))
listing_id date
0 52811 2010-11-13
1 52811 2011-03-23
2 52811 2012-01-23
Nguồn: https://data.insideairbnb.com/chile/rm/santiago/2026-06-29/visualisations/reviews.csv. Có 690.112 hàng, 15.243 chỗ ở, 4.697 ngày khác nhau: một ngày thường có nhiều đánh giá. listing_id là mã chỗ ở, không phải mã riêng của từng đánh giá. read_csv(parse_dates=["date"]) ở Bài 6 đổi kiểu ngay khi đọc.
Kiểm tra ngày vượt mốc chụp
pd.Timestamp tạo mốc chụp 29/06/2026 để so với cột date.
moc = pd.Timestamp("2026-06-29")
ngoai_moc = rv_raw.loc[rv_raw["date"] > moc, "date"]
print(ngoai_moc.value_counts().sort_index())
date
2026-06-30 155
2026-07-01 49
Name: count, dtype: int64
File có 204 hàng ghi ngày sau mốc chụp. Ta sẽ loại chúng khỏi phạm vi phân tích này.
Vì sao có ngày sau 29/06? Bản chụp được thu trong bốn ngày: cột last_scraped của listings.csv.gz (bản 90 cột) có 1.004 chỗ ở thu ngày 29/06, 3.097 ngày 30/06, 7.398 ngày 01/07, 7.035 ngày 02/07. Cả 204 đánh giá sau mốc chụp đều thuộc chỗ ở thu muộn; không đánh giá nào muộn hơn ngày thu của chính chỗ ở đó. Chỗ ở nào cũng được thu từ 29/06 trở đi, nên đánh giá đến 29/06 có đủ ở mọi chỗ ở. Công khai số hàng bị loại, không im lặng lọc rồi trình bày như file gốc. Ngày bị loại khác với NaT; cột date trong snapshot này không có lỗi chuyển đổi.
Chọn dữ liệu theo năm và tháng
Đặt ngày làm chỉ mục (DatetimeIndex); sort_index() sắp theo thời gian để cắt được theo khoảng ngày.
rv = rv_raw.loc[rv_raw["date"] <= moc].copy()
r = rv.set_index("date").sort_index()
print(pd.Series({
"năm 2026": len(r.loc["2026"]),
"tháng 3/2026": len(r.loc["2026-03"]),
}))
năm 2026 128459
tháng 3/2026 25108
dtype: int64
.loc["2026-03"] lấy mọi hàng trong tháng 3/2026. Chỉ mục có thể có nhiều hàng cùng một ngày.
Dòng đầu loại 204 hàng sau mốc chụp; còn 689.908 đánh giá. Không xoá các hàng trùng chỉ mục: mỗi hàng là một đánh giá. Hỏi lớp: bỏ sort_index() thì r.loc["2026-03"] còn chạy không? r.loc["2026-01":"2026-05"] thì sao? (đáp: cái đầu vẫn ra 25.108; cái sau báo KeyError vì các đánh giá trong file xếp chủ yếu theo từng chỗ ở, không theo ngày. Slide 30 cắt r.loc["2022":"2025"] nên cần sắp trước.) Số 25.108 dùng để đối chiếu với kết quả resample sắp học.
Gộp đánh giá theo tháng
Ví dụ giả lập có ba đánh giá: hai trong tháng 1, một trong tháng 3.
vi_du = pd.Series([1, 1, 1], index=pd.to_datetime([
"2026-01-03", "2026-01-10", "2026-03-02",
]))
print(vi_du.resample("ME").size())
2026-01-31 2
2026-02-28 0
2026-03-31 1
Freq: ME, dtype: int64
resample("ME") gộp theo tháng, gắn nhãn cuối tháng; size() đếm hàng. Tháng 2 có 0 đánh giá.
Liên hệ groupby của Bài 5: resample cũng chia nhóm rồi tổng hợp, nhưng nhóm được xác định bằng lịch. 0 là không có hàng đánh giá nào trong ví dụ; một tháng chưa thu thập dữ liệu thì không được coi là có 0 đánh giá.
Đếm số đánh giá từng tháng
theo_thang = r.resample("ME").size()
print(theo_thang.tail(3))
date
2026-04-30 21656
2026-05-31 22296
2026-06-30 16112
Freq: ME, dtype: int64
Có thể đổi ME thành D (ngày), W-SUN (tuần kết thúc Chủ nhật), QE (quý).
Nhấn mạnh 16.112 là đến 29/6, dù nhãn của resample là 30/6: nhãn cuối tháng không có nghĩa tháng đó đã trọn. Không dùng count trên cột có giá trị thiếu nếu mục tiêu là đếm tất cả hàng.
Tháng cuối chưa trọn
Mốc chụp là 29/06 nên tháng 6 chưa trọn, chưa so được với tháng 5.
dau_thang = moc.replace(day=1)
thang_day_du = theo_thang.loc[
theo_thang.index < dau_thang]
print(thang_day_du.tail(2))
date
2026-04-30 21656
2026-05-31 22296
Freq: ME, dtype: int64
replace(day=1) lấy đầu tháng chứa mốc chụp. Chỉ giữ các tháng có nhãn cuối tháng trước ngày 01/06.
Hỏi lớp: tháng 6 thấp hơn tháng 5 tới 27,7% (16.112 so với 22.296); thiếu riêng ngày 30/6 giải thích được bao nhiêu? (đáp: khoảng 3%: tháng 6 trung bình 555,6 đánh giá/ngày, thêm một ngày như vậy chỉ được khoảng 16.670. So cùng khoảng 1–29/6 với 1–29/5 vẫn thấp hơn khoảng 20%.) Đây là quan sát, chưa kết luận nguyên nhân. Nếu nghiên cứu cần tháng đang diễn ra, vẫn có thể giữ nhưng phải so cùng khoảng quan sát và ghi rõ kỳ chưa trọn. Không mặc định mọi kỳ cuối đều thiếu; cần đối chiếu mốc chụp.
Bài tập 2: Trung bình mỗi ngày
Hệ thống đã thu đủ dữ liệu từ 01–07/06/2026. Bảng chỉ liệt kê ngày có đánh giá (giả lập).
Ngày Số đánh giá 01/06 2 03/06 1 07/06 4
Tính trung bình số đánh giá mỗi ngày trong tuần này.
Đáp án: 7 đánh giá / 7 ngày = 1 đánh giá/ngày. dem_ngay.mean() cho 7/3, chỉ là trung bình trên ngày có đánh giá. Bảng đã tổng hợp số đếm: resample("D").sum() tạo [2, 0, 1, 0, 0, 0, 4], không dùng size() vì nó đếm dòng. Chấp nhận tính tổng/7 trực tiếp, không ép một API. Nếu chưa thu dữ liệu ngày vắng, không có căn cứ điền 0.
Tính trung bình theo cửa sổ trượt
Để bớt dao động, mỗi tháng lấy trung bình của tháng đó và hai tháng trước.
s = pd.Series([10, 50, 30, 40], index=["T1", "T2", "T3", "T4"])
tb = s.rolling(3, min_periods=3).mean()
print(pd.DataFrame({"so_danh_gia": s, "tb_3_thang": tb}))
so_danh_gia tb_3_thang
T1 10 NaN
T2 50 NaN
T3 30 30.0
T4 40 40.0
Cửa sổ chuyển từ T1–T3 sang T2–T4. 3 là số hàng; min_periods=3 yêu cầu đủ 3 giá trị không thiếu.
Hỏi lớp dự đoán tb_3_thang ở T3 và T4 trước khi hiện output (đáp: (10 + 50 + 30) / 3 = 30 và (50 + 30 + 40) / 3 = 40; T4 khác T3 vì cửa sổ đã trượt sang T2–T4). Kết quả ở mỗi tháng chỉ dùng tháng đó và các tháng trước. Với cửa sổ theo số hàng, min_periods mặc định bằng kích thước cửa sổ, nên bỏ min_periods=3 vẫn ra hai NaN đầu.
Trung bình của sáu tháng gần nhất
muot = thang_day_du.rolling(6, min_periods=6).mean()
Mỗi điểm trên đường xanh là trung bình của tháng đó và 5 tháng liền trước.
Đường xám là số đánh giá mỗi tháng; đường xanh là trung bình trượt 6 hàng, gắn vào hàng cuối của cửa sổ. Chuỗi do resample tạo nên mỗi hàng là một tháng. Hỏi lớp: tháng 4/2020 chỉ có 321 đánh giá, sao đường xanh vẫn ở khoảng 1.650? (đáp: cửa sổ còn gồm 5 tháng trước 1.808, 1.937, 2.324, 1.996, 1.541; trung bình 1.654,5. Trong 2020–2021, đường xám thấp nhất ở 4/2020, đường xanh ở 9/2020 (535,5), trễ 5 tháng.) Đường xanh chỉ làm mượt, không phải dự báo và không giải thích nguyên nhân giảm; mức giảm 2020–2021 là quan sát từ biểu đồ.
3. So sánh giữa các kỳ
So tháng này với tháng trước hoặc với cùng tháng năm trước.
Đặt giá trị kỳ trước cạnh kỳ này
mau = pd.Series([10, 12, 15], index=["T1", "T2", "T3"])
print(pd.DataFrame({
"ky_nay": mau, "ky_truoc": mau.shift(1),
}))
ky_nay ky_truoc
T1 10 NaN
T2 12 10.0
T3 15 12.0
shift(1) dịch giá trị xuống một hàng; chỉ mục giữ nguyên. T1 chưa có kỳ trước nên nhận NaN.
Đây là dịch giá trị theo hàng, không dịch ngày trên lịch; slide sau cho thấy hệ quả khi thiếu một tháng.
Thiếu một tháng thì shift bị lệch
thieu = pd.Series([10, 15, 18], index=pd.to_datetime([
"2026-01-31", "2026-03-31", "2026-04-30",
]))
print(pd.DataFrame({"gia_tri": thieu, "shift_1": thieu.shift(1)}))
gia_tri shift_1
2026-01-31 10 NaN
2026-03-31 15 10.0
2026-04-30 18 15.0
Tháng 3 nhận giá trị tháng 1 vì tháng 2 không có hàng. Với chuỗi do resample tạo, mỗi tháng đã có một hàng.
Hỏi lớp trước khi hiện output: shift_1 của tháng 3 bằng bao nhiêu? (đáp: 10, giá trị của tháng 1, vì tháng 2 không có hàng.) Không ép tháng 2 thành 0 vì chưa biết vì sao nó vắng. thieu.asfreq("ME") tạo hàng tháng 2 với NaN; sau đó shift(1) tại tháng 3 là NaN. Khác với chuỗi từ resample("ME").size(): ở đó tháng không có hàng nào nhận 0, vì 0 là số hàng ghi nhận trong tháng.
Tính tỷ lệ thay đổi
Kỳ gốc là kỳ dùng làm mẫu số, ở đây là kỳ liền trước: tỷ lệ thay đổi = (kỳ này − kỳ gốc) / kỳ gốc.
print((mau.pct_change() * 100).round(1))
T1 NaN
T2 20.0
T3 25.0
dtype: float64
pct_change() trả về tỷ lệ: T2 là (12 − 10) / 10 = 0,2; nhân 100 mới ra phần trăm: 20,0%.
Hỏi lớp: kỳ gốc của T3 là 12 hay 10, và T3 tăng bao nhiêu phần trăm? (đáp: 12; (15 − 12) / 12 = 25%. Mức tăng 3 là số đơn vị, không phải 3%.) Kết quả đầu tiên NaN vì không có kỳ gốc; NaN khác với không tăng. pct_change() là cách viết gọn của mau / mau.shift(1) - 1.
So với tháng liền trước
Tháng Số đánh giá 04/2026 21.656 05/2026 22.296
doi_thang = thang_day_du.pct_change()
print(f"Tháng 5/2026: {doi_thang.loc['2026-05-31']:.1%}")
Tháng 5/2026: 3.0%
Kỳ gốc là tháng 4/2026.
Đối chiếu tay 22296 / 21656 - 1 = 0,029553…, tức 3,0% sau làm tròn. Nhãn đủ ngày "2026-05-31" trả về một số; nhãn rút gọn "2026-05" (như slide 16) trả về Series một phần tử nên f-string với :.1% báo TypeError.
So với cùng tháng năm trước
Tháng Số đánh giá 05/2025 14.509 05/2026 22.296
doi_nam = thang_day_du / thang_day_du.shift(12) - 1
print(f"Tháng 5/2026: {doi_nam.loc['2026-05-31']:.1%}")
Tháng 5/2026: 53.7%
shift(12) lấy giá trị 12 hàng trước. Với chuỗi tháng liên tiếp, đó là cùng tháng năm trước.
Đối chiếu 22296 / 14509 - 1 = 0,536701… Chỉ dùng tháng trọn. Cùng kỳ và kỳ liền trước trả lời hai câu hỏi khác nhau; không nói một cách luôn tốt hơn cách kia.
Chọn kỳ gốc để so sánh
Tháng 5/2026 có 22.296 đánh giá.
Kỳ gốc Thay đổi Ý nghĩa Tháng 4/2026 +3,0% Biến động tháng gần nhất Tháng 5/2025 +53,7% Biến động qua một năm
Hai kết quả dùng hai kỳ gốc khác nhau. So cùng tháng giúp giảm ảnh hưởng của những khác biệt theo mùa.
Hai tỷ lệ +3,0% và +53,7% đều đúng với kỳ gốc đã nêu. So cùng tháng vẫn chưa loại ảnh hưởng xu hướng, thay đổi số chỗ ở hoặc tỷ lệ viết đánh giá.
Tổng hợp theo tháng qua nhiều năm
Gộp bốn năm trọn gần nhất: 2022–2025 .
tron = r.loc["2022":"2025"]
dem_thang = tron.groupby(tron.index.month).size()
chi_so = dem_thang / dem_thang.mean() * 100
print(chi_so.round().astype(int).tolist())
[81, 67, 90, 84, 81, 85, 119, 125, 104, 118, 133, 112]
tron.index.month là tháng của từng hàng, lấy từ chỉ mục nên không cần .dt. Chia số đánh giá của mỗi tháng (gộp bốn năm) cho trung bình của 12 tháng: 100 là mức trung bình.
.index.month lấy tháng từ DatetimeIndex; .dt.month dùng cho cột hoặc Series ngày giờ (r.index.dt.month báo AttributeError). Không dùng 2026 vì mới có tháng 1–6: thêm vào thì chỉ số trung bình của tháng 1–6 từ 81 lên 107, của tháng 7–12 từ 119 xuống 93. Hỏi lớp: nếu không có mùa vụ, số đánh giá chỉ tăng đều khoảng 1,6 lần mỗi năm, thì tháng 12 so với tháng 1 thế nào? (đáp: vẫn cao hơn gần 1,6 lần; chỉ do xu hướng, chỉ số đi từ 79 ở tháng 1 lên 124 ở tháng 12.) Chỉ số này không phải phân rã mùa vụ thống kê.
Phân bố đánh giá theo tháng
Số đánh giá tăng khoảng 1,6 lần mỗi năm nên các tháng cuối năm cao hơn một phần chỉ vì xu hướng. Chỉ số này chưa tách xu hướng và chưa điều chỉnh số ngày từng tháng.
Tổng đánh giá cả năm: 48.387 (2022), 75.190 (2023), 131.267 (2024), 211.432 (2025), trung bình tăng khoảng 1,6 lần mỗi năm. Nếu chia mỗi tháng cho trung bình trượt 12 tháng có tâm để tách xu hướng, chỉ số thành [105, 84, 103, 95, 88, 86, 115, 115, 99, 104, 112, 93]: tháng 11 chỉ còn khoảng +12%, tháng 7–8 khoảng +15%, tháng 2 khoảng −16%. Hình này không đủ để kết luận về kỳ nghỉ hay lễ hội. Nếu câu hỏi là số đánh giá trung bình mỗi ngày, phải đổi phép tính và mẫu số.
Bài tập 3: Tìm tháng tăng mạnh
Số đánh giá từng tháng năm 2025 (giả lập, các tháng đã trọn):
Tháng 1 2 3 4 5 6 Số đánh giá 90 120 90 150 120 240
Viết code tìm các tháng có số đánh giá cao hơn ít nhất 50% so với trung bình của 3 tháng liền trước.
Chỉ xét tháng có đủ 3 tháng trước để so sánh.
Đáp án: tháng 4 và tháng 6. Tháng 4 so với trung bình tháng 1, 2, 3: (90 + 120 + 90) / 3 = 100; 150 cao hơn đúng 50% nên được chọn. Tháng 5 so với trung bình 120 nên không tăng. Tháng 6 so với trung bình tháng 3, 4, 5: (90 + 150 + 120) / 3 = 120; 240 cao hơn 100%. Với Series dem có chỉ mục là ngày cuối tháng liên tiếp, tính tb_truoc = dem.rolling(3, min_periods=3).mean().shift(1), rồi chọn dem[dem >= 1.5 * tb_truoc]. rolling(3) mặc định tính cả tháng hiện tại; shift(1) đưa trung bình của ba tháng trước sang hàng đang xét. Ba tháng đầu chưa có đủ kỳ gốc nên tb_truoc là NaN. Nếu quên shift, không tháng nào được chọn trên dữ liệu này. Chấp nhận dịch dem trước rồi mới rolling, hoặc cách khác chọn đúng cửa sổ.