DHDataHọc
Trắc nghiệmBài 24/305 phút đọc+10 XP

Bài tập: hàm thống kê

Bạn sẽ học được

  • Ghép hàm, tham số, giá trị trả về và module thành một bài hoàn chỉnh.
  • Thiết kế hàm thống kê trả dict có cấu trúc.
  • Xử lý list rỗng mà không chia cho 0.
  • Tách tính toán khỏi định dạng báo cáo.
  • Tự kiểm tra bằng bộ ca thử.

Đề bài

Viết hai hàm:

  1. thong_ke(nums) nhận list số, trả dict gồm count, sum, mean, min, max. Trả None nếu list rỗng.
  2. bao_cao(rows) nhận list (tên, điểm), in từng người và một dòng tổng kết: trung bình, thấp nhất, cao nhất. Nếu không có dữ liệu, trả chuỗi "Chưa có dữ liệu".

Không in trong thong_ke — hàm này chỉ tính và trả kết quả để dùng lại được.

Khái niệm cốt lõi

Bắt đầu bằng hàm thống kê:

code
def thong_ke(nums):
    if not nums:
        return None
    return {
        "count": len(nums),
        "sum": sum(nums),
        "mean": sum(nums) / len(nums),
        "min": min(nums),
        "max": max(nums),
    }

Ba quyết định thiết kế đáng chú ý:

  • Trả None khi rỗng thay vì 0 hay dict toàn số 0. Người gọi phân biệt được "không có dữ liệu" với "dữ liệu toàn số 0".
  • Trả dict thay vì tuple: người gọi đọc stats["mean"] rõ hơn stats[2], và thêm chỉ số mới không phá vỡ thứ tự.
  • Tính sum(nums) hai lần trong ví dụ cho dễ đọc. Với list rất lớn, gán total = sum(nums) một lần rồi dùng lại — tối ưu nhỏ nhưng miễn phí.

Chạy thử:

code
print(thong_ke([4, 9, 1, 7]))
print(thong_ke([]))
code
{'count': 4, 'sum': 21, 'mean': 5.25, 'min': 1, 'max': 9}
None

Kiểm tra bằng tay: tổng 4 + 9 + 1 + 7 = 21, chia 4 được 5.25. min là 1, max là 9. Kết quả khớp.

Tách báo cáo khỏi tính toán

bao_cao chỉ lo định dạng, gọi thong_ke cho phần số:

code
def bao_cao(rows):
    scores = [score for _, score in rows]
    stats = thong_ke(scores)
    if stats is None:
        return "Chưa có dữ liệu"
    lines = [f"{name}: {score}" for name, score in rows]
    lines.append(
        f"TB {stats['mean']:.2f} | thấp nhất {stats['min']} | cao nhất {stats['max']}"
    )
    return "\n".join(lines)

Điểm quan trọng: bao_cao trả chuỗi, không print. Nhờ vậy có thể kiểm thử bằng cách so sánh chuỗi, ghi ra file, hoặc gửi qua API — người gọi quyết định hiển thị thế nào.

code
rows = [("An", 85), ("Bình", 92), ("Chi", 78), ("Dũng", 60)]
print(bao_cao(rows))
print(bao_cao([]))
code
An: 85
Bình: 92
Chi: 78
Dũng: 60
TB 78.75 | thấp nhất 60 | cao nhất 92
Chưa có dữ liệu

78.75 là trung bình của 85, 92, 78, 60: tổng 315, chia 4. Định dạng .2f giữ hai chữ số thập phân; nếu dùng :g sẽ ra 78.75 nhưng với số tròn như 80.0 lại thành 80, không nhất quán trong báo cáo.

Vì sao dùng _ trong comprehension for _, score in rows: tên _ là quy ước cho "không dùng giá trị này". Nó vẫn là biến hợp lệ, nhưng báo cho người đọc biết tên người học không cần cho phép tính.

Thêm median bằng thư viện chuẩn

Trung bình dễ bị lệch bởi giá trị ngoại lai. Median — giá trị giữa — phản ánh phân bố tốt hơn:

code
import statistics

print(statistics.median([4, 9, 1, 7]))
print(statistics.median([4, 9, 1, 7, 10]))
code
5.5
7

Với bốn số, median là trung bình của hai số giữa 4 và 7 → 5.5. Với năm số, median là 7. Đây là lý do báo cáo thực tế thường in cả mean và median: nếu hai số chênh nhiều, dữ liệu có ngoại lai.

Có thể thêm vào dict:

code
"median": statistics.median(nums)

Nhưng hãy cân nhắc: mỗi chỉ số thêm vào là một dòng code và một khái niệm người đọc phải hiểu. Chỉ thêm khi báo cáo thực sự dùng.

Tự kiểm tra

  1. thong_ke([]) trả gì? Vì sao không trả dict với mean = 0?
  2. Vì sao thong_ke không nên print kết quả?
  3. bao_cao trả chuỗi thay vì in — lợi ích gì khi kiểm thử?
  4. Với [85, 92, 78, 60], trung bình là bao nhiêu? Median là bao nhiêu?
  5. Vì sao dùng {name: score for name, score in rows} có thể mất dữ liệu?
  6. Nếu list có một phần tử, min, max, mean bằng gì?

Đáp án:

  1. None; 0 sẽ bị nhầm với dữ liệu thật bằng 0.
  2. Vì hàm chỉ nên tính và trả; in làm kết quả không dùng lại được.
  3. So sánh chuỗi trực tiếp, không phải bắt stdout; dùng được trong API/file.
  4. Mean 78.75; median (78 + 85) / 2 = 81.5.
  5. Nếu trùng tên, khóa sau ghi đè khóa trước — dùng list hoặc cộng dồn thay vì dict.
  6. Cả ba đều bằng chính phần tử đó.

Thực hành thêm

  1. Thêm median và stdev vào thong_ke bằng statistics.
  2. Viết loc_dat(rows, nguong=50) trả list người có điểm từ ngưỡng trở lên.
  3. Viết xep_hang(rows) trả list đã sắp theo điểm giảm dần, dùng sorted(key=...).
  4. Đổi bao_cao để nhận thêm tham số *, ví dụ don_vi="điểm".
  5. Thêm kiểm tra điểm ngoài 0–100 và bỏ qua kèm cảnh báo.
  6. Đặt hai hàm vào helpers.py, viết main.py import và chạy.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
ZeroDivisionErrorChia cho len của list rỗngKiểm tra rỗng trước
Hàm trả None ngoài ý muốnNhánh if thiếu returnRà mọi nhánh
Báo cáo in ra Noneprint(bao_cao(...)) khi hàm đã inChọn một: trả hoặc in
Trung bình saiCộng nhầm cột hoặc chia sai lenKiểm tra bằng tay một ca nhỏ
Mất người trùng tênGom vào dict theo tênGiữ list các dòng
KeyError khi đọc statsĐổi tên khóaThống nhất tên khóa
Số lẻ thập phân dàiKhông định dạng:.2f khi hiển thị

Tóm tắt

  • thong_ke trả dict hoặc None; không in, không chia khi rỗng.
  • bao_cao chỉ định dạng, tách khỏi phép tính.
  • Trả chuỗi giúp kiểm thử và tái sử dụng ở CLI/API/file.
  • Mean và median cùng in để phát hiện ngoại lai.
  • Kiểm tra một ca nhỏ bằng tay trước khi tin kết quả.

Dùng ← → để chuyển bài