Thuyết minh BCTC ngân hàng, phân nhóm nợ bằng Python
Dùng finlens lấy thuyết minh BCTC của 30 tổ chức tín dụng Việt Nam về pandas DataFrame: dư nợ theo năm nhóm chất lượng nợ, cho vay theo ngành, cơ cấu tiền gửi.
Bốn báo cáo chính nói ngân hàng cho vay bao nhiêu. Thuyết minh nói cho vay cho ai, ngành nào, và bao nhiêu trong đó đang có vấn đề.
Hai phương thức, cùng nằm trong client.financials:
| Phương thức | Trả về |
|---|---|
notes(symbols, section=…) | Từng dòng thuyết minh theo kỳ, dạng long |
note_sections() | Danh mục 22 nhóm, kèm độ phủ đo lúc gọi |
Năm nhóm nợ — thứ hầu hết người dùng tìm
import finlens
client = finlens.client()
df = client.financials.notes("VCB", section="loan_quality", start_year=2025)
# Bỏ dòng gốc, giữ năm nhóm con
la = df[df["parent_code"].notna() & (df["quarter"] == 2)]
print(la[["code", "line_item", "value"]]) code line_item value
4.1 4.1. Nợ đủ tiêu chuẩn 1.536317e+15
4.2 4.2. Nợ cần chú ý 3.876597e+12
4.3 4.3. Nợ dưới tiêu chuẩn 2.106631e+12
4.4 4.4. Nợ nghi ngờ 2.013210e+12
4.5 4.5. Nợ xấu có khả năng mất vốn 1.145609e+13Đây là phân nhóm theo Thông tư 11. Nhóm 3, 4 và 5 hợp thành nợ xấu:
goc = df[df["parent_code"].isna() & (df["quarter"] == 2)]["value"].iloc[0]
xau = la[la["code"].isin(["4.3", "4.4", "4.5"])]["value"].sum()
print(f"Tỷ lệ nợ xấu: {xau / goc:.2%}")Ba thứ sẽ làm bạn đọc sai số nếu bỏ qua
quarter = 0nghĩa là CẢ NĂM, không phải "thiếu quý". Lọcdf[df["quarter"] > 0]là cách nhanh nhất để mất sạch số liệu năm, im lặng.- Dòng cả năm bằng đúng quý 4 với số dư cân đối kế toán. Cộng bốn quý rồi cộng thêm dòng cả năm là cộng quý 4 hai lần.
- Đừng tự cắt chuỗi
codeđể suy ra cha. Dùng cộtparent_code— server chịu trách nhiệm cho quan hệ đó.
Dựng lại cây bằng một groupby
Mỗi dòng mang parent_code, level và order_index, nên cấu trúc thuyết minh
dựng lại được ngay trên frame — không phải gọi thêm một endpoint thứ hai:
df = client.financials.notes("VCB", section="loan_by_industry", start_year=2026)
ky = df[df["quarter"] == 2]
for cha, con in ky[ky["parent_code"].notna()].groupby("parent_code"):
print(cha)
for _, r in con.sort_values("order_index").iterrows():
print(f" {r['code']:<6} {r['line_item'][:44]:<44} {r['value']:,.0f}")code là số thứ tự in trong chính bản BCTC (4.1, 4.2), không phải một id
nội bộ — nên nó khớp đúng dòng trong bản PDF bạn mở cạnh.
22 nhóm, và độ phủ của từng nhóm
ds = client.financials.note_sections()
print(ds[["code", "section", "node_count", "organization_count", "last_year"]])Bảng này tồn tại thay cho một danh sách tĩnh trong tài liệu, vì
organization_count và last_year được đo lại mỗi lần gọi. Đọc chúng trước
khi kết luận một nhóm là "không có dữ liệu".
Bảy nhóm hay dùng nhất:
| Token | Nội dung |
|---|---|
loan_quality | Cho vay theo chất lượng nợ — năm nhóm Thông tư 11 |
loan_by_industry | Cho vay theo ngành kinh tế |
loan_by_customer_type | Cho vay theo đối tượng khách hàng |
loan_provision | Dự phòng cho vay khách hàng |
deposit_by_type | Tiền gửi theo loại (không kỳ hạn, có kỳ hạn…) |
service_income | Lãi thuần từ hoạt động dịch vụ, chi tiết từng khoản |
operating_expense | Chi phí hoạt động |
Bỏ trống section thì trả cả 22 nhóm. Tham số nhận một giá trị; muốn hai
nhóm thì bỏ trống rồi lọc trên frame:
df = client.financials.notes("VCB", start_year=2026)
df = df[df["section"].isin(["loan_quality", "loan_provision"])]Ba nhóm của nguồn cố ý không có token
Nguồn có 25 nhóm; thư viện phơi ra 22. Ba nhóm bị loại vì gần như không có dữ liệu: cho vay theo vị trí địa lý (ngừng cập nhật từ 2017, chỉ 9 tổ chức), tài sản sinh lãi (5 dòng trên 20 năm) và công nợ phải trả lãi (4 dòng).
Cấp token cho chúng là mời bạn gõ một thứ luôn trả gần như rỗng — mà một bảng rỗng thì không phân biệt được "tổ chức này không có khoản đó" với "mình gõ sai". Gõ một token không tồn tại là lỗi có thông báo, kèm danh sách token hợp lệ.
Ai có dữ liệu, và từ năm nào
30 tổ chức tín dụng, từ 2006 tới nay. Nhưng độ phủ không đều: năm 2006 chỉ có 9 tổ chức, và chỉ bão hoà 28–30 từ 2016.
30 mã, nhưng 28 ngân hàng
EVF (ICB 8773) và TIN (ICB 8771) là công ty tài chính, không phải ngân
hàng. Chúng có mặt vì nguồn danh mục xếp chúng vào loại doanh nghiệp NH.
Cần đúng nhóm ngân hàng thì lọc trước bằng
client.meta.symbols(icb="8355")["symbol"].
Mã không phải tổ chức tín dụng đi vào phần lỗi theo từng mã và không làm hỏng phần còn lại của lời gọi:
df = client.financials.notes("VCB,HPG", section="loan_quality", start_year=2026)
# HPG vào df.attrs["finlens"]["failed"]; VCB vẫn trả về bình thườngĐơn vị là VND thô
Cột value tính bằng đồng Việt Nam nguyên, khác kVND của cột giá cổ phiếu.
Đừng giả định — đọc thẳng:
print(df.attrs["finlens"]["units"]["value"]) # 'VND'value là <NA> khi nguồn không có ô đó — không phải 0.
Chỉ có báo cáo hợp nhất
Nguồn không có báo cáo riêng lẻ cho nhóm này, nên không có tham số nào để chọn. Mọi con số là số hợp nhất.
Không có cột tỷ lệ nào
Nhóm này trả con số tuyệt đối, không phát thêm một cột npl_ratio. Lý do:
indicators() đã có chỉ tiêu đó, và hai đường đã được đối chiếu — NPL suy từ
thuyết minh cho 1,0012% với VCB quý 2/2025 so với 0,01001 của indicators(),
lệch dưới 3e-05.
Cần tỷ lệ thì dùng indicators(); cần chính các con số của từng nhóm thì dùng
notes().
Bản async
import asyncio
import finlens
async def main():
async with finlens.async_client() as client:
df = await client.financials.notes("VCB", section="loan_quality")
print(len(df))
asyncio.run(main())Xem thêm
Báo cáo tài chính & chỉ tiêu
Bốn báo cáo chính và chỉ tiêu tính sẵn, gồm npl_ratio.
Tra cứu danh mục
Lọc đúng 28 mã ngân hàng bằng icb="8355".
Kiểu dữ liệu
Quy ước đơn vị và cách đọc df.attrs["finlens"].
Xử lý lỗi
on_error, ValidationError và cây ngoại lệ FL_*.
Câu hỏi thường gặp
Làm sao lấy dư nợ chia theo năm nhóm chất lượng nợ của một ngân hàng?
Gọi `client.financials.notes("VCB", section="loan_quality")` của thư viện Python finlens. Nhóm `loan_quality` là thuyết minh cho vay theo chất lượng nợ theo Thông tư 11, gồm năm dòng con: nợ đủ tiêu chuẩn, nợ cần chú ý, nợ dưới tiêu chuẩn, nợ nghi ngờ và nợ xấu có khả năng mất vốn. Frame còn có một dòng gốc mang tổng dư nợ, phân biệt bằng cột `parent_code` rỗng. Dữ liệu phủ 30 tổ chức tín dụng từ năm 2006 tới nay, và đơn vị là đồng Việt Nam thô.
Cột quarter bằng 0 nghĩa là gì, có phải dữ liệu bị thiếu không?
Không, `0` nghĩa là CẢ NĂM. Đây là quy ước chung của thư viện Python finlens ở mọi endpoint báo cáo tài chính: `quarter` mang `0` cho số liệu năm và `1` tới `4` cho từng quý. Lọc `df[df["quarter"] > 0]` là cách nhanh nhất để mất sạch số liệu năm mà không có lỗi nào báo. Bảng dữ liệu gốc dùng số `5` cho cả năm, nhưng máy chủ đã quy đổi nên `5` không bao giờ xuất hiện trong frame; nếu bạn truyền `quarters=5` thì thư viện từ chối kèm chỉ dẫn dùng `0`.
Cộng bốn quý lại có ra số cả năm không?
Không, và với số dư cân đối kế toán thì cộng như vậy là nhân bốn lần sai. Dư nợ theo nhóm là số dư tại một thời điểm, nên dòng cả năm bằng đúng dòng quý 4 — đo trên dữ liệu thật của thư viện Python finlens, cả hai đều cho cùng một con số. Nếu bạn lấy cả năm dòng của một năm rồi cộng lại, bạn cộng bốn quý cộng thêm một bản sao của quý 4. Hãy lọc `quarter` trước khi tổng hợp.
Vì sao có 30 mã nhưng tài liệu nói chỉ 28 ngân hàng?
Vì hai mã trong đó là công ty tài chính chứ không phải ngân hàng. Trong 30 tổ chức tín dụng mà thư viện Python finlens trả về ở nhóm này, 28 mã thuộc ngành ICB 8355 Ngân hàng, còn `EVF` thuộc ICB 8773 và `TIN` thuộc ICB 8771 — hai mã tài chính tổng hợp. Chúng có mặt vì nguồn danh mục xếp chúng vào loại doanh nghiệp `NH`, tức mâu thuẫn nằm giữa hai cột của chính nguồn ấy. Nếu bạn cần đúng nhóm ngân hàng, hãy lọc bằng `client.meta.symbols(icb="8355")`.
Tỷ lệ nợ xấu tính từ thuyết minh có khớp với chỉ tiêu npl_ratio có sẵn không?
Có, và hai đường đã được đối chiếu. Lấy tổng ba nhóm cuối chia cho tổng năm nhóm của thư viện Python finlens cho ra 1,0012 phần trăm với VCB quý 2 năm 2025, so với 0,01001 mà `client.financials.indicators()` trả cho chỉ tiêu `npl_ratio` — lệch dưới ba phần trăm nghìn. Vì hai con số đã khớp, nhóm thuyết minh không phát thêm một cột tỷ lệ nào: dùng `indicators()` nếu bạn chỉ cần tỷ lệ, và dùng `notes()` khi bạn cần chính các con số tuyệt đối của từng nhóm.
Cập nhật lần cuối