47  Mạng lưới và đồ thị trong tài chính

Thị trường tài chính là những mạng lưới. Lợi nhuận của mỗi cổ phiếu đều chịu ảnh hưởng từ các mối liên kết: các doanh nghiệp có chung chuỗi cung ứng, thành viên hội đồng quản trị, kiểm toán viên, nhà đầu tư, bên cho vay và cơ quan quản lý. Các cú sốc lan truyền qua những liên kết này (chẳng hạn, một vụ phá sản gây hiệu ứng lan tỏa trong mạng lưới nhà cung cấp, một cú siết thanh khoản của ngân hàng trung ương lan ra qua các khoản dư nợ liên ngân hàng, còn thông tin khuếch tán qua các thành viên hội đồng quản trị kiêm nhiệm và qua việc các tổ chức cùng nắm giữ cổ phiếu). Tuy nhiên, mô thức thực nghiệm chiếm ưu thế trong tài chính lại coi các doanh nghiệp là những quan sát độc lập được lập chỉ số theo \((i, t)\), chỉ liên hệ với nhau thông qua mức độ phơi nhiễm chung với các nhân tố chung. Chương này giới thiệu các công cụ để mô hình hóa, đo lường và khai thác cấu trúc mạng lưới mà các hồi quy dữ liệu bảng (panel) tiêu chuẩn bỏ qua.

Hệ thống tài chính Việt Nam có mật độ mạng lưới đặc biệt cao. Sở hữu nhà nước tạo ra một mạng lưới các doanh nghiệp đan xen: cùng một bộ chủ quản có thể giám sát cả bên đi vay, bên cho vay và bên bảo hiểm. Các tập đoàn kim tự tháp (như Vingroup, Masan và FPT) liên kết hàng chục thực thể niêm yết và chưa niêm yết thông qua các chuỗi sở hữu vốn cổ phần. Hệ thống ngân hàng có quy mô nhỏ và tập trung, với một số ít ngân hàng thương mại nhà nước nắm giữ phần lớn tài sản, từ đó hình thành các khoản dư nợ liên ngân hàng dày đặc. Việc thành viên hội đồng quản trị kiêm nhiệm (chẳng hạn, những thành viên đồng thời tham gia nhiều hội đồng quản trị) rất phổ biến và thường đi theo các quan hệ sở hữu. Bản thân thị trường cổ phiếu cũng thể hiện các mẫu hình đồng biến động lợi nhuận mà khi được biểu diễn dưới dạng mạng lưới tương quan, sẽ bộc lộ các cụm theo ngành và theo hình thức sở hữu mà phân tích nhân tố tiêu chuẩn không nhìn thấy.

Chương này trình bày toàn bộ các phương pháp mạng lưới được sử dụng trong kinh tế học tài chính, từ lý thuyết đồ thị cổ điển đến các mạng nơ-ron đồ thị (GNN) hiện đại. Chúng ta tổ chức nội dung thành tám phần. Thứ nhất, các nền tảng của lý thuyết đồ thị và cách biểu diễn dữ liệu tài chính dưới dạng mạng lưới. Thứ hai, các mạng lưới sở hữu và kiểm soát, là cấu trúc mạng lưới đặc trưng nhất của thị trường Việt Nam. Thứ ba, các mạng lưới thành viên hội đồng quản trị kiêm nhiệm và quản trị công ty. Thứ tư, các mạng lưới chuỗi cung ứng và thương mại. Thứ năm, các mạng lưới tương quan và đồng biến động phục vụ xây dựng danh mục đầu tư và giám sát rủi ro hệ thống. Thứ sáu, các mạng lưới liên ngân hàng và lây lan. Thứ bảy, các mạng lưới xã hội giữa các địa phương, được đo bằng Chỉ số Kết nối Xã hội (Social Connectedness Index), và vai trò của chúng như kênh truyền dẫn thông tin tài chính (L. Allen, Peng, và Shan 2026). Thứ tám, các mạng nơ-ron đồ thị và học máy dựa trên đồ thị cho định giá tài sản, rủi ro tín dụng và phát hiện bất thường.

Hiện mã
import pandas as pd
import numpy as np
from pathlib import Path
import warnings
warnings.filterwarnings("ignore")

# Graph libraries
import networkx as nx
from scipy import sparse
from scipy.spatial.distance import squareform

# Statistical and econometric
from scipy import stats
import statsmodels.api as sm
from linearmodels.panel import PanelOLS

# Visualization
import plotnine as p9
from mizani.formatters import percent_format, comma_format
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

# Deep learning (for GNNs later)
import torch
import torch.nn as nn
import torch.nn.functional as F
Hiện mã
# DataCore.vn API
from datacore import DataCore
dc = DataCore()

47.1 Nền tảng lý thuyết đồ thị cho tài chính

47.1.1 Biểu diễn dữ liệu tài chính dưới dạng đồ thị

Một đồ thị \(G = (V, E)\) gồm một tập đỉnh (nút) \(V\) và một tập cạnh (liên kết) \(E \subseteq V \times V\). Trong các mạng lưới tài chính, các nút đại diện cho các tác nhân kinh tế, như doanh nghiệp, ngân hàng, nhà đầu tư, thành viên hội đồng quản trị, còn các cạnh đại diện cho các mối quan hệ, bao gồm tỷ lệ sở hữu, cho vay, ghế trong hội đồng quản trị, hợp đồng cung ứng và đồng biến động lợi nhuận.

Các đồ thị tài chính có nhiều dạng khác nhau:

Có hướng so với vô hướng. Quan hệ sở hữu là có hướng: doanh nghiệp \(A\) sở hữu một tỷ lệ cổ phần trong doanh nghiệp \(B\), nhưng không nhất thiết có chiều ngược lại. Quan hệ kiêm nhiệm hội đồng quản trị là vô hướng: nếu thành viên \(d\) ngồi trong hội đồng quản trị của cả doanh nghiệp \(A\) và doanh nghiệp \(B\), thì mối liên kết là đối xứng. Chuỗi cung ứng là có hướng: \(A\) cung cấp cho \(B\).

Có trọng số so với không trọng số. Các mạng lưới sở hữu tự nhiên có trọng số theo tỷ lệ sở hữu. Các mạng lưới tương quan có trọng số theo hệ số tương quan từng cặp. Quan hệ kiêm nhiệm hội đồng quản trị có thể được gán trọng số theo số thành viên dùng chung.

Tĩnh so với động. Hầu hết các mạng lưới tài chính tiến triển theo thời gian khi quan hệ sở hữu thay đổi, các thành viên hội đồng quản trị luân chuyển và các tương quan dịch chuyển. Một đồ thị theo thời gian \(G_t = (V_t, E_t)\) nắm bắt sự tiến triển này.

Hai phía so với một phía. Dữ liệu thô của nhiều mạng lưới tài chính là đồ thị hai phía (bipartite): thành viên hội đồng quản trị \(\times\) doanh nghiệp, nhà đầu tư \(\times\) cổ phiếu, ngân hàng \(\times\) bên đi vay. Phép chiếu một phía (one-mode projection) chuyển đồ thị này thành đồ thị một phía: hai doanh nghiệp được nối với nhau nếu có chung một thành viên hội đồng quản trị, hai cổ phiếu được nối với nhau nếu có chung một nhà đầu tư tổ chức, v.v.

47.1.2 Các thước đo chính của đồ thị

Với một đồ thị \(G\) có \(n = |V|\) nút và \(m = |E|\) cạnh, chúng ta định nghĩa các thước đo sau, mỗi thước đo có một cách diễn giải tài chính riêng.

Độ trung tâm bậc (degree centrality). Bậc \(k_i\) của nút \(i\) là số cạnh liên thuộc với \(i\). Trong đồ thị có hướng, chúng ta phân biệt bậc vào \(k_i^{\text{in}}\) (các cạnh hướng tới \(i\)) và bậc ra \(k_i^{\text{out}}\) (các cạnh xuất phát từ \(i\)). Độ trung tâm bậc chuẩn hóa là:

\[ C_D(i) = \frac{k_i}{n - 1} \tag{47.1}\]

Trong một mạng lưới sở hữu, bậc ra cao có nghĩa là doanh nghiệp sở hữu cổ phần trong nhiều doanh nghiệp khác (tập đoàn đa ngành); bậc vào cao có nghĩa là nhiều thực thể sở hữu cổ phần trong doanh nghiệp đó (sở hữu phân tán).

Độ trung tâm trung gian (betweenness centrality). Tỷ lệ các đường đi ngắn nhất giữa mọi cặp nút đi qua nút \(i\):

\[ C_B(i) = \sum_{s \neq i \neq t} \frac{\sigma_{st}(i)}{\sigma_{st}} \tag{47.2}\]

trong đó \(\sigma_{st}\) là tổng số đường đi ngắn nhất từ \(s\) đến \(t\) và \(\sigma_{st}(i)\) là số đường đi đi qua \(i\). Độ trung gian cao xác định các nút “cầu nối” (tức là các doanh nghiệp hoặc ngân hàng nối những bộ phận vốn tách rời của hệ thống tài chính). Sự sụp đổ của một ngân hàng có độ trung gian cao có thể làm phân mảnh mạng lưới liên ngân hàng.

Độ trung tâm vector riêng (eigenvector centrality). Một nút là trung tâm nếu nó được nối với các nút trung tâm khác. Độ trung tâm vector riêng là nghiệm của:

\[ \lambda \mathbf{c} = A \mathbf{c} \tag{47.3}\]

trong đó \(A\) là ma trận kề và \(\lambda\) là trị riêng lớn nhất. PageRank của Google là một biến thể được chính quy hóa, thiết kế cho đồ thị có hướng. Trong các mạng lưới tài chính, độ trung tâm vector riêng xác định các tổ chức quan trọng về mặt hệ thống (tức là những tổ chức được nối với các tổ chức quan trọng khác).

Hệ số phân cụm (clustering coefficient). Tỷ lệ các nút lân cận của một nút mà bản thân chúng cũng là lân cận của nhau:

\[ C_C(i) = \frac{2 T_i}{k_i(k_i - 1)} \tag{47.4}\]

trong đó \(T_i\) là số tam giác chứa nút \(i\). Mức phân cụm cao cho thấy các nhóm gắn kết chặt chẽ (chẳng hạn, các tập đoàn, các vòng cho vay, các danh mục cùng đầu tư).

Cấu trúc cộng đồng. Nhiều mạng lưới tài chính có cấu trúc mô-đun: các nhóm nút kết nối dày đặc với nhau nhưng liên kết thưa giữa các nhóm. Các thuật toán phát hiện cộng đồng (Louvain, lan truyền nhãn, phân cụm phổ) xác định các mô-đun này, mà trong các mạng lưới tài chính thường tương ứng với các tập đoàn, các ngành hoặc các cụm cho vay.

Hiện mã
def compute_network_metrics(G):
    """
    Compute standard network metrics for a graph.

    Parameters
    ----------
    G : nx.Graph or nx.DiGraph
        Input graph.

    Returns
    -------
    dict : Node-level and graph-level metrics.
    """
    is_directed = G.is_directed()

    # Node-level metrics
    if is_directed:
        in_degree = dict(G.in_degree())
        out_degree = dict(G.out_degree())
        degree = {n: in_degree[n] + out_degree[n] for n in G.nodes()}
    else:
        degree = dict(G.degree())

    betweenness = nx.betweenness_centrality(G, weight="weight")
    eigenvector = nx.eigenvector_centrality_numpy(
        G, weight="weight"
    ) if len(G) > 0 else {}
    clustering = nx.clustering(G, weight="weight") if not is_directed else {}

    # PageRank (works for both directed and undirected)
    pagerank = nx.pagerank(G, weight="weight")

    # Graph-level metrics
    n_nodes = G.number_of_nodes()
    n_edges = G.number_of_edges()
    density = nx.density(G)

    # Connected components
    if is_directed:
        n_weak_components = nx.number_weakly_connected_components(G)
        n_strong_components = nx.number_strongly_connected_components(G)
    else:
        n_components = nx.number_connected_components(G)

    # Degree distribution statistics
    degrees = list(degree.values())
    avg_degree = np.mean(degrees) if degrees else 0
    max_degree = max(degrees) if degrees else 0

    # Assortativity
    assortativity = nx.degree_assortativity_coefficient(G)

    # Community detection (Louvain)
    if not is_directed:
        try:
            communities = nx.community.louvain_communities(G)
            modularity = nx.community.modularity(G, communities)
            n_communities = len(communities)
        except Exception:
            modularity, n_communities = np.nan, 0
    else:
        modularity, n_communities = np.nan, 0

    node_metrics = pd.DataFrame({
        "degree": degree,
        "betweenness": betweenness,
        "eigenvector": eigenvector,
        "pagerank": pagerank,
        "clustering": clustering if clustering else {n: np.nan for n in G.nodes()}
    })

    graph_metrics = {
        "n_nodes": n_nodes,
        "n_edges": n_edges,
        "density": density,
        "avg_degree": avg_degree,
        "max_degree": max_degree,
        "assortativity": assortativity,
        "modularity": modularity,
        "n_communities": n_communities
    }

    return node_metrics, graph_metrics

47.1.3 Ma trận kề và các tính chất phổ

Ma trận kề \(A \in \mathbb{R}^{n \times n}\) mã hóa cấu trúc đồ thị: \(A_{ij} = w_{ij}\) nếu có một cạnh từ \(i\) đến \(j\) với trọng số \(w_{ij}\), và \(A_{ij} = 0\) trong trường hợp ngược lại. Với đồ thị vô hướng, \(A\) là ma trận đối xứng.

Ma trận Laplace của đồ thị \(L = D - A\) (trong đó \(D\) là ma trận bậc đường chéo) có các trị riêng \(0 = \lambda_1 \leq \lambda_2 \leq \ldots \leq \lambda_n\) với những cách diễn giải cấu trúc quan trọng:

  • Bội của \(\lambda = 0\) bằng số thành phần liên thông.
  • Trị riêng thứ hai \(\lambda_2\) (độ liên thông đại số hay giá trị Fiedler) đo mức độ liên kết tốt của đồ thị. \(\lambda_2\) thấp hàm ý đồ thị có một nút thắt cổ chai, tức là một điểm yếu mà việc cắt một vài cạnh sẽ làm đứt rời những phần lớn của đồ thị.
  • Các vector riêng của \(L\) cung cấp phép nhúng phổ (spectral embedding) của đồ thị, là nền tảng cho phân cụm phổ và các mạng tích chập đồ thị.

Ma trận Laplace chuẩn hóa \(\tilde{L} = D^{-1/2} L D^{-1/2}\) được dùng trong GCN vì nó ổn định việc truyền thông điệp giữa các nút có bậc khác nhau.

Hiện mã
def spectral_graph_analysis(A, n_components=10):
    """
    Compute spectral properties of a financial network.

    Parameters
    ----------
    A : np.ndarray or sparse matrix
        Adjacency matrix.
    n_components : int
        Number of eigenvalues/vectors to compute.

    Returns
    -------
    dict : Eigenvalues, algebraic connectivity, spectral gap.
    """
    n = A.shape[0]

    if sparse.issparse(A):
        A_dense = A.toarray()
    else:
        A_dense = A

    # Degree matrix
    D = np.diag(A_dense.sum(axis=1))

    # Graph Laplacian
    L = D - A_dense

    # Normalized Laplacian
    D_inv_sqrt = np.diag(1.0 / np.sqrt(np.diag(D) + 1e-10))
    L_norm = D_inv_sqrt @ L @ D_inv_sqrt

    # Eigendecomposition (smallest eigenvalues)
    eigenvalues, eigenvectors = np.linalg.eigh(L_norm)

    # Sort by eigenvalue
    idx = np.argsort(eigenvalues)
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]

    # Algebraic connectivity (Fiedler value)
    fiedler_value = eigenvalues[1] if n > 1 else 0
    fiedler_vector = eigenvectors[:, 1] if n > 1 else np.zeros(n)

    # Spectral gap
    spectral_gap = eigenvalues[1] - eigenvalues[0] if n > 1 else 0

    return {
        "eigenvalues": eigenvalues[:n_components],
        "fiedler_value": fiedler_value,
        "fiedler_vector": fiedler_vector,
        "spectral_gap": spectral_gap,
        "spectral_embedding": eigenvectors[:, 1:n_components + 1]
    }

47.2 Mạng lưới sở hữu và kiểm soát

47.2.1 Cấu trúc sở hữu ở Việt Nam

Có thể nói các mạng lưới sở hữu là cấu trúc đồ thị có hệ quả kinh tế lớn nhất trong thị trường Việt Nam. Bức tranh doanh nghiệp Việt Nam được đặc trưng bởi ba nét riêng tạo ra các cấu trúc sở hữu phức tạp:

State ownership pyramids. The government holds equity in hundreds of firms through a hierarchy of holding entities: the State Capital Investment Corporation (SCIC), line ministries, provincial People’s Committees, and state-owned economic groups (tập đoàn kinh tế nhà nước). These chains create multi-layered pyramids where the state’s ultimate control rights may substantially exceed its cash flow rights.

Private business groups. Vietnamese conglomerates (Vingroup, Masan, FPT, Hoà Phát, Thaco) create complex webs of cross-ownership, subsidiary relationships, and associate stakes. These structures serve multiple purposes: internal capital markets, tax optimization, regulatory arbitrage, and control enhancement.

Sở hữu vòng tròn và sở hữu chéo. Các quy định của Việt Nam không ngăn chặn hiệu quả tình trạng sở hữu vòng tròn (doanh nghiệp \(A\) sở hữu doanh nghiệp \(B\), doanh nghiệp này sở hữu doanh nghiệp \(C\), và doanh nghiệp sau cùng này lại sở hữu doanh nghiệp \(A\)), tạo ra các vòng lặp trong đồ thị sở hữu làm khuếch đại quyền kiểm soát vượt quá tỷ lệ sở hữu trực tiếp và làm phình vốn chủ sở hữu kế toán (Bebchuk 1999).

Hiện mã
# Load ownership data
ownership = dc.get_ownership_network(
    date="2024-06-30",
    min_stake=1.0  # Minimum 1% ownership stake
)

# Load firm characteristics
firms = dc.get_firm_characteristics(
    start_date="2024-01-01",
    end_date="2024-06-30"
).groupby("ticker").last().reset_index()

print(f"Ownership edges: {len(ownership)}")
print(f"Unique owners: {ownership['owner_id'].nunique()}")
print(f"Unique targets: {ownership['target_ticker'].nunique()}")
Hiện mã
# Build directed ownership graph
G_own = nx.DiGraph()

# Add firm nodes with attributes
for _, row in firms.iterrows():
    G_own.add_node(
        row["ticker"],
        node_type="firm",
        market_cap=row.get("market_cap", 0),
        industry=row.get("industry", "Unknown"),
        is_soe=row.get("state_ownership_pct", 0) > 50
    )

# Add ownership edges
for _, row in ownership.iterrows():
    owner = row["owner_id"]
    target = row["target_ticker"]
    stake = row["ownership_pct"]

    # Add owner node if not present
    if owner not in G_own:
        G_own.add_node(
            owner,
            node_type=row.get("owner_type", "entity"),
            market_cap=0,
            industry="Holding"
        )

    G_own.add_edge(owner, target, weight=stake / 100)

node_metrics_own, graph_metrics_own = compute_network_metrics(G_own)

print(f"\nOwnership Network Summary:")
for k, v in graph_metrics_own.items():
    print(f"  {k}: {v}")
Bảng 47.1: Mạng lưới sở hữu: Thống kê cấp độ đồ thị
Hiện mã
own_stats = pd.DataFrame([graph_metrics_own]).T
own_stats.columns = ["Value"]
own_stats = own_stats.round(4)
own_stats

47.2.2 Sở hữu cuối cùng và các chuỗi kiểm soát

Sở hữu trực tiếp đánh giá thấp quyền kiểm soát thực sự được thực thi qua các chuỗi kim tự tháp. Tỷ lệ sở hữu cuối cùng của thực thể \(A\) trong doanh nghiệp \(Z\) qua chuỗi \(A \to B \to C \to Z\) là tích của các tỷ lệ sở hữu trung gian:

\[ \omega_{A \to Z}^{\text{ultimate}} = \prod_{(i,j) \in \text{path}(A, Z)} w_{ij} \tag{47.5}\]

trong đó \(w_{ij}\) là tỷ lệ sở hữu trực tiếp của \(i\) trong \(j\). Khi tồn tại nhiều đường đi từ \(A\) đến \(Z\), tổng sở hữu cuối cùng là tổng trên các đường đi. Tuy nhiên, quyền kiểm soát được quyết định bởi mắt xích yếu nhất trong chuỗi (tỷ lệ sở hữu nhỏ nhất dọc theo đường đi), phản ánh nguyên tắc rằng kiểm soát đòi hỏi đa số ở mỗi tầng.

Hiện mã
def compute_ultimate_ownership(G, source, target, max_depth=10):
    """
    Compute ultimate ownership of source in target through all paths.

    Parameters
    ----------
    G : nx.DiGraph
        Ownership graph with edge weights as ownership fractions.
    source : str
        Ultimate owner node.
    target : str
        Target firm node.
    max_depth : int
        Maximum chain length to consider.

    Returns
    -------
    dict : Total ownership, control rights, number of paths.
    """
    if source not in G or target not in G:
        return {"ownership": 0, "control": 0, "n_paths": 0}

    total_ownership = 0
    max_control = 0
    n_paths = 0

    # Find all simple paths from source to target
    try:
        for path in nx.all_simple_paths(G, source, target,
                                         cutoff=max_depth):
            # Cash flow rights: product of stakes
            ownership = 1.0
            min_stake = 1.0
            for i in range(len(path) - 1):
                edge_data = G[path[i]][path[i + 1]]
                stake = edge_data.get("weight", 0)
                ownership *= stake
                min_stake = min(min_stake, stake)

            total_ownership += ownership
            max_control = max(max_control, min_stake)
            n_paths += 1
    except nx.NetworkXNoPath:
        pass

    return {
        "ownership": total_ownership,
        "control": max_control,
        "n_paths": n_paths
    }


def compute_control_wedge(G, firms_list, max_depth=5):
    """
    Compute the wedge between control and cash flow rights
    for the largest shareholder of each firm.

    The wedge = control rights - cash flow rights.
    Positive wedge → potential for tunneling.
    """
    wedge_data = []

    for firm in firms_list:
        if firm not in G:
            continue

        # Find all direct owners
        predecessors = list(G.predecessors(firm))
        if not predecessors:
            continue

        # Find largest direct owner
        stakes = {p: G[p][firm]["weight"] for p in predecessors}
        largest_owner = max(stakes, key=stakes.get)
        direct_stake = stakes[largest_owner]

        # Compute ultimate ownership through all paths
        ultimate = compute_ultimate_ownership(
            G, largest_owner, firm, max_depth
        )

        wedge_data.append({
            "ticker": firm,
            "largest_owner": largest_owner,
            "direct_stake": direct_stake,
            "ultimate_ownership": ultimate["ownership"],
            "control_rights": ultimate["control"],
            "n_ownership_paths": ultimate["n_paths"],
            "wedge": ultimate["control"] - ultimate["ownership"]
        })

    return pd.DataFrame(wedge_data)


# Compute for all listed firms
listed_firms = [n for n, d in G_own.nodes(data=True)
                if d.get("node_type") == "firm"]
wedge_df = compute_control_wedge(G_own, listed_firms)
Bảng 47.2: Khoảng cách giữa quyền kiểm soát và quyền dòng tiền: Thống kê tóm tắt
Hiện mã
wedge_summary = wedge_df[
    ["direct_stake", "ultimate_ownership", "control_rights",
     "n_ownership_paths", "wedge"]
].describe(percentiles=[0.1, 0.25, 0.5, 0.75, 0.9]).T.round(4)
wedge_summary
Hiện mã
(
    p9.ggplot(wedge_df, p9.aes(x="wedge"))
    + p9.geom_histogram(bins=50, fill="#2E5090", alpha=0.7)
    + p9.geom_vline(xintercept=0, linetype="dashed", color="#C0392B")
    + p9.labs(
        x="Control Wedge (Control Rights − Cash Flow Rights)",
        y="Count",
        title="Control-Ownership Wedge: Positive Values Indicate Tunneling Risk"
    )
    + p9.theme_minimal()
    + p9.theme(figure_size=(10, 5))
)
Hình 47.1

47.2.3 Độ trung tâm sở hữu và giá trị doanh nghiệp

Chúng ta kiểm định xem vị trí của một doanh nghiệp trong mạng lưới sở hữu có dự báo được định giá thị trường của doanh nghiệp đó hay không, sau khi kiểm soát các yếu tố quyết định tiêu chuẩn:

\[ Q_{i,t} = \beta_0 + \beta_1 \text{Centrality}_{i,t} + \beta_2 \text{Wedge}_{i,t} + \boldsymbol{\gamma}' \mathbf{X}_{i,t} + \alpha_{\text{ind}} + \delta_t + \varepsilon_{i,t} \tag{47.6}\]

Hiện mã
# Merge network metrics with firm characteristics
node_metrics_own_df = node_metrics_own.reset_index().rename(
    columns={"index": "ticker"}
)

valuation_data = firms.merge(node_metrics_own_df, on="ticker", how="inner")
valuation_data = valuation_data.merge(
    wedge_df[["ticker", "wedge", "n_ownership_paths"]],
    on="ticker", how="left"
)

# Panel regression
val_clean = valuation_data.dropna(
    subset=["tobins_q", "eigenvector", "wedge",
            "log_size", "profitability", "leverage"]
)

if len(val_clean) > 50:
    model_network_val = sm.OLS(
        val_clean["tobins_q"],
        sm.add_constant(val_clean[[
            "eigenvector", "betweenness", "wedge",
            "log_size", "profitability", "leverage"
        ]])
    ).fit(cov_type="HC1")

    print("Ownership Network Position and Firm Value:")
    for var in ["eigenvector", "betweenness", "wedge"]:
        print(f"  {var}: {model_network_val.params[var]:.4f} "
              f"(t = {model_network_val.tvalues[var]:.3f})")

47.2.4 Phát hiện tập đoàn

Các tập đoàn (tức là tập hợp các doanh nghiệp độc lập về mặt pháp lý nhưng liên kết với nhau bằng quan hệ sở hữu chung) là một đặc trưng nổi bật của cấu trúc doanh nghiệp Việt Nam. Chúng ta phát hiện chúng bằng thuật toán, sử dụng phương pháp phát hiện cộng đồng trên đồ thị sở hữu.

Hiện mã
def detect_business_groups(G, min_group_size=3, ownership_threshold=0.10):
    """
    Detect business groups from ownership network.

    A business group is a connected component in the undirected
    projection of the ownership graph, filtered for economically
    meaningful ownership stakes.

    Parameters
    ----------
    G : nx.DiGraph
        Directed ownership graph.
    min_group_size : int
        Minimum firms in a group.
    ownership_threshold : float
        Minimum ownership stake to count as a link.

    Returns
    -------
    DataFrame : Group assignments with apex firm.
    """
    # Filter edges by threshold
    G_filtered = nx.DiGraph()
    for u, v, d in G.edges(data=True):
        if d.get("weight", 0) >= ownership_threshold:
            G_filtered.add_edge(u, v, **d)

    # Convert to undirected for component detection
    G_undirected = G_filtered.to_undirected()

    # Find connected components
    components = list(nx.connected_components(G_undirected))

    # Filter by size
    groups = [c for c in components if len(c) >= min_group_size]

    # For each group, identify the apex (top of pyramid)
    group_data = []
    for group_id, members in enumerate(groups):
        subgraph = G_filtered.subgraph(members)

        # Apex: node with highest out-degree and lowest in-degree
        # (controls others but is not controlled)
        scores = {}
        for node in members:
            in_deg = subgraph.in_degree(node)
            out_deg = subgraph.out_degree(node)
            scores[node] = out_deg - in_deg

        apex = max(scores, key=scores.get) if scores else list(members)[0]

        for member in members:
            node_data = G.nodes.get(member, {})
            group_data.append({
                "ticker": member,
                "group_id": group_id,
                "group_size": len(members),
                "apex": apex,
                "is_apex": member == apex,
                "node_type": node_data.get("node_type", "unknown"),
                "industry": node_data.get("industry", "Unknown"),
                "market_cap": node_data.get("market_cap", 0)
            })

    return pd.DataFrame(group_data)


groups_df = detect_business_groups(G_own)
print(f"Business groups detected: {groups_df['group_id'].nunique()}")
print(f"Firms in groups: {len(groups_df[groups_df['node_type'] == 'firm'])}")
Bảng 47.3: Các tập đoàn lớn nhất của Việt Nam theo phân tích mạng lưới sở hữu
Hiện mã
top_groups = (
    groups_df.groupby("group_id")
    .agg(
        apex=("apex", "first"),
        n_members=("ticker", "count"),
        n_listed=("node_type", lambda x: (x == "firm").sum()),
        total_mcap=("market_cap", "sum"),
        industries=("industry", lambda x: x.nunique())
    )
    .sort_values("total_mcap", ascending=False)
    .head(15)
    .reset_index()
)
top_groups["total_mcap_bn"] = top_groups["total_mcap"] / 1e9
top_groups[["apex", "n_members", "n_listed", "total_mcap_bn", "industries"]]

47.3 Mạng lưới thành viên hội đồng quản trị kiêm nhiệm

47.3.1 Xây dựng mạng lưới

Một quan hệ kiêm nhiệm hội đồng quản trị tồn tại khi một thành viên đồng thời giữ ghế trong hội đồng quản trị của hai hay nhiều doanh nghiệp. Mạng lưới kiêm nhiệm hội đồng quản trị là phép chiếu một phía của đồ thị hai phía thành viên\(\times\)doanh nghiệp: hai doanh nghiệp được nối với nhau nếu có chung ít nhất một thành viên, với trọng số là số thành viên dùng chung.

Quan hệ kiêm nhiệm là một kênh thông tin. Bizjak, Lemmon, và Naveen (2008) chỉ ra rằng các thông lệ trả thưởng lan truyền qua mạng lưới hội đồng quản trị. Cai và c.s. (2014) chứng minh rằng các doanh nghiệp được nối với nhau qua quan hệ kiêm nhiệm có chính sách đầu tư tương quan với nhau. Trong thị trường Việt Nam, quan hệ kiêm nhiệm thường đi theo các quan hệ sở hữu (chẳng hạn, công ty mẹ bổ nhiệm thành viên vào hội đồng quản trị của công ty con), tạo ra một kênh quản trị củng cố kênh sở hữu.

Hiện mã
# Load board membership data
board_data = dc.get_board_memberships(
    date="2024-06-30"
)

print(f"Director-firm pairs: {len(board_data)}")
print(f"Unique directors: {board_data['director_id'].nunique()}")
print(f"Unique firms: {board_data['ticker'].nunique()}")

# Build bipartite graph
B = nx.Graph()
for _, row in board_data.iterrows():
    B.add_node(row["director_id"], bipartite=0)
    B.add_node(row["ticker"], bipartite=1)
    B.add_edge(
        row["director_id"], row["ticker"],
        role=row.get("role", "director"),
        is_independent=row.get("is_independent", False)
    )

# Project to firm-firm interlock network
firm_nodes = [n for n, d in B.nodes(data=True) if d.get("bipartite") == 1]
G_interlock = nx.bipartite.weighted_projected_graph(B, firm_nodes)

# Edge weight = number of shared directors
interlock_metrics, interlock_graph = compute_network_metrics(G_interlock)

print(f"\nBoard Interlock Network:")
print(f"  Firms: {G_interlock.number_of_nodes()}")
print(f"  Interlocking pairs: {G_interlock.number_of_edges()}")
print(f"  Density: {nx.density(G_interlock):.4f}")
Hiện mã
# Do board interlocks follow ownership lines?
# Compare interlock edges with ownership edges
interlock_edges = set(G_interlock.edges())
ownership_edges_undirected = set()

for u, v in G_own.edges():
    if u in firm_nodes and v in firm_nodes:
        ownership_edges_undirected.add((min(u, v), max(u, v)))

interlock_edges_normalized = {
    (min(u, v), max(u, v)) for u, v in interlock_edges
}

overlap = interlock_edges_normalized & ownership_edges_undirected
n_interlock = len(interlock_edges_normalized)
n_ownership = len(ownership_edges_undirected)
n_overlap = len(overlap)

print(f"Interlock edges: {n_interlock}")
print(f"Ownership edges (firm-firm): {n_ownership}")
print(f"Overlap: {n_overlap}")
if n_interlock > 0:
    print(f"Fraction of interlocks with ownership link: "
          f"{n_overlap / n_interlock:.3f}")

47.3.2 Kiêm nhiệm và đồng biến động lợi nhuận

Nếu quan hệ kiêm nhiệm đóng vai trò kênh thông tin, các doanh nghiệp được nối với nhau qua quan hệ kiêm nhiệm sẽ thể hiện mức đồng biến động lợi nhuận vượt mức, vượt quá phần được giải thích bởi đặc điểm ngành hoặc quy mô chung. Chúng ta kiểm định điều này bằng phương pháp của Cohen và Frazzini (2008):

\[ \rho_{ij,t} = \alpha + \beta \cdot \text{Interlock}_{ij,t} + \gamma \cdot \text{SameIndustry}_{ij} + \delta \cdot \text{SizeProximity}_{ij,t} + \varepsilon_{ij,t} \tag{47.7}\]

Hiện mã
# Compute pairwise return correlations for interlocked and non-interlocked pairs
monthly_returns = dc.get_monthly_returns(
    start_date="2023-01-01",
    end_date="2024-06-30"
)

# Pivot to wide format
returns_wide = monthly_returns.pivot(
    index="date", columns="ticker", values="ret"
).dropna(axis=1, thresh=12)

# Correlation matrix
corr_matrix = returns_wide.corr()

# Compare correlations: interlocked vs non-interlocked
interlock_corrs = []
non_interlock_corrs = []

tickers_in_both = set(corr_matrix.columns) & set(G_interlock.nodes())

for i, ticker_i in enumerate(tickers_in_both):
    for ticker_j in list(tickers_in_both)[i + 1:]:
        corr = corr_matrix.loc[ticker_i, ticker_j]
        if np.isnan(corr):
            continue

        if G_interlock.has_edge(ticker_i, ticker_j):
            interlock_corrs.append(corr)
        else:
            non_interlock_corrs.append(corr)

if interlock_corrs and non_interlock_corrs:
    t_stat, p_val = stats.ttest_ind(interlock_corrs, non_interlock_corrs)
    print(f"Interlocked pairs: n={len(interlock_corrs)}, "
          f"mean corr={np.mean(interlock_corrs):.4f}")
    print(f"Non-interlocked: n={len(non_interlock_corrs)}, "
          f"mean corr={np.mean(non_interlock_corrs):.4f}")
    print(f"Difference t-stat: {t_stat:.3f}, p-value: {p_val:.4f}")

47.4 Mạng lưới chuỗi cung ứng và thương mại

47.4.1 Xây dựng đồ thị chuỗi cung ứng

Các quan hệ chuỗi cung ứng (liên kết khách hàng–nhà cung cấp) thể hiện những mối liên hệ kinh tế thực mà qua đó các cú sốc lan truyền. Acemoglu, Ozdaglar, và Tahbaz-Salehi (2015) chứng minh về mặt lý thuyết rằng khi tồn tại các liên kết chuỗi cung ứng, các cú sốc đặc thù của từng doanh nghiệp có thể tạo ra biến động tổng thể thay vì triệt tiêu lẫn nhau. Cohen và Frazzini (2008) và Menzly và Ozbas (2010) cho thấy các liên kết chuỗi cung ứng dự báo được chênh lệch lợi nhuận chéo (cross-sectional): khi cổ phiếu của một khách hàng lớn giảm giá, cổ phiếu của các nhà cung cấp giảm theo với độ trễ.

Hiện mã
# Load supply chain data
supply_chain = dc.get_supply_chain_data(
    start_date="2020-01-01",
    end_date="2024-12-31"
)

# Build directed supply chain graph
G_supply = nx.DiGraph()

for _, row in supply_chain.iterrows():
    G_supply.add_edge(
        row["supplier_ticker"],
        row["customer_ticker"],
        weight=row.get("transaction_value", 1),
        pct_of_supplier_revenue=row.get("pct_supplier_revenue", 0),
        pct_of_customer_cogs=row.get("pct_customer_cogs", 0),
        year=row.get("year", 2024)
    )

print(f"Supply chain links: {G_supply.number_of_edges()}")
print(f"Firms involved: {G_supply.number_of_nodes()}")

47.4.2 Động lượng khách hàng

Chiến lược “động lượng khách hàng” của Cohen và Frazzini (2008) khai thác sự lan truyền chậm của thông tin qua các chuỗi cung ứng. Khi cổ phiếu của một doanh nghiệp khách hàng tăng giá, cổ phiếu của các nhà cung cấp có xu hướng tăng theo trong những tháng tiếp theo:

\[ r_{i,t+1} = \alpha + \beta \cdot \text{CustomerReturn}_{i,t} + \gamma \cdot r_{i,t} + \boldsymbol{\delta}' \mathbf{X}_{i,t} + \varepsilon_{i,t} \tag{47.8}\]

trong đó \(\text{CustomerReturn}_{i,t} = \sum_{j \in \text{Customers}(i)} w_{ij} \cdot r_{j,t}\) là lợi nhuận bình quân gia quyền của các khách hàng của doanh nghiệp \(i\).

Hiện mã
# Compute customer-weighted returns for each supplier
def compute_customer_returns(supply_graph, monthly_returns):
    """
    Compute customer-weighted returns for each supplier firm.
    """
    # For each supplier, compute weighted average customer return
    results = []

    for date in monthly_returns["date"].unique():
        date_returns = monthly_returns[monthly_returns["date"] == date]
        ret_dict = dict(zip(date_returns["ticker"], date_returns["ret"]))

        for supplier in supply_graph.nodes():
            customers = list(supply_graph.successors(supplier))
            if not customers:
                continue

            customer_rets = []
            customer_weights = []
            for cust in customers:
                if cust in ret_dict:
                    edge_data = supply_graph[supplier][cust]
                    weight = edge_data.get("pct_of_supplier_revenue", 1)
                    customer_rets.append(ret_dict[cust])
                    customer_weights.append(weight)

            if customer_rets:
                weights = np.array(customer_weights)
                if weights.sum() > 0:
                    weights = weights / weights.sum()
                else:
                    weights = np.ones(len(weights)) / len(weights)

                weighted_ret = np.average(customer_rets, weights=weights)
                results.append({
                    "date": date,
                    "ticker": supplier,
                    "customer_ret": weighted_ret,
                    "n_customers": len(customer_rets)
                })

    return pd.DataFrame(results)


customer_rets = compute_customer_returns(G_supply, monthly_returns)

# Merge with own returns and lag
momentum_data = monthly_returns.merge(
    customer_rets, on=["ticker", "date"], how="inner"
)

momentum_data = momentum_data.sort_values(["ticker", "date"])
momentum_data["own_ret_lag"] = momentum_data.groupby("ticker")["ret"].shift(1)
momentum_data["customer_ret_lag"] = momentum_data.groupby(
    "ticker"
)["customer_ret"].shift(1)
momentum_data["ret_lead"] = momentum_data.groupby("ticker")["ret"].shift(-1)

# Regression
mom_clean = momentum_data.dropna(
    subset=["ret_lead", "customer_ret_lag", "own_ret_lag"]
)

model_mom = sm.OLS(
    mom_clean["ret_lead"],
    sm.add_constant(mom_clean[["customer_ret_lag", "own_ret_lag"]])
).fit(cov_type="cluster", cov_kwds={"groups": mom_clean["ticker"]})
Bảng 47.4: Động lượng khách hàng: Lợi nhuận của nhà cung cấp được dự báo bởi lợi nhuận của khách hàng
Hiện mã
mom_results = pd.DataFrame({
    "Coefficient": model_mom.params.round(4),
    "Std Error": model_mom.bse.round(4),
    "t-stat": model_mom.tvalues.round(3),
    "p-value": model_mom.pvalues.round(4)
})
mom_results

47.4.3 Lan truyền trong mạng lưới: Khuếch tán cú sốc qua các chuỗi cung ứng

Mô hình của Acemoglu, Ozdaglar, và Tahbaz-Salehi (2015) dự báo rằng tác động tổng thể của một cú sốc đặc thù phụ thuộc vào cấu trúc của mạng lưới. Trong mạng hình sao (một trung tâm duy nhất), các cú sốc tại trung tâm tạo ra biến động tổng thể. Trong mạng đối xứng, các cú sốc triệt tiêu lẫn nhau. Chúng ta triển khai một phương pháp dựa trên mô phỏng để đo lường sự lan truyền cú sốc trong chuỗi cung ứng của Việt Nam.

Hiện mã
def simulate_shock_propagation(G, shocked_node, shock_size=-0.10,
                                decay=0.5, max_steps=5):
    """
    Simulate the propagation of an idiosyncratic shock through
    a supply chain network.

    Parameters
    ----------
    G : nx.DiGraph
        Supply chain graph (supplier → customer).
    shocked_node : str
        Node receiving the initial shock.
    shock_size : float
        Initial shock magnitude (e.g., -0.10 = -10% revenue shock).
    decay : float
        Fraction of shock transmitted per link (0 < decay < 1).
    max_steps : int
        Maximum propagation steps.

    Returns
    -------
    dict : {node: cumulative shock received}.
    """
    shocks = {shocked_node: shock_size}
    frontier = {shocked_node}

    for step in range(max_steps):
        new_frontier = set()
        for node in frontier:
            # Propagate to customers (downstream)
            for customer in G.successors(node):
                edge_weight = G[node][customer].get(
                    "pct_of_customer_cogs", 0.1
                )
                transmitted = shocks[node] * decay * edge_weight
                if abs(transmitted) > 0.001:
                    shocks[customer] = shocks.get(customer, 0) + transmitted
                    new_frontier.add(customer)

            # Propagate to suppliers (upstream)
            for supplier in G.predecessors(node):
                edge_weight = G[supplier][node].get(
                    "pct_of_supplier_revenue", 0.1
                )
                transmitted = shocks[node] * decay * edge_weight
                if abs(transmitted) > 0.001:
                    shocks[supplier] = shocks.get(supplier, 0) + transmitted
                    new_frontier.add(supplier)

        frontier = new_frontier
        if not frontier:
            break

    return shocks


# Identify systemically important supply chain nodes
# (Those whose shock has largest aggregate impact)
systemic_importance = {}
listed_in_supply = [n for n in G_supply.nodes()
                    if n in set(firms["ticker"])]

for firm in listed_in_supply[:100]:  # Sample for computational feasibility
    shocks = simulate_shock_propagation(G_supply, firm, -0.10)
    aggregate_impact = sum(abs(v) for k, v in shocks.items() if k != firm)
    systemic_importance[firm] = aggregate_impact

systemic_df = pd.DataFrame(
    list(systemic_importance.items()),
    columns=["ticker", "systemic_impact"]
).sort_values("systemic_impact", ascending=False)
Bảng 47.5: Các doanh nghiệp quan trọng nhất về mặt hệ thống trong mạng lưới chuỗi cung ứng
Hiện mã
top_systemic = systemic_df.head(20).merge(
    firms[["ticker", "industry", "market_cap"]],
    on="ticker", how="left"
)
top_systemic["market_cap_bn"] = top_systemic["market_cap"] / 1e9
top_systemic[["ticker", "industry", "market_cap_bn", "systemic_impact"]].round(4)

47.5 Mạng lưới tương quan và đồng biến động

47.5.1 Xây dựng từ dữ liệu lợi nhuận

Mạng lưới tương quan nối các tài sản có lợi nhuận đồng biến động. Có lẽ đây là mạng lưới tài chính được sử dụng rộng rãi nhất vì nó chỉ cần dữ liệu lợi nhuận—không cần thông tin sở hữu hay chuỗi cung ứng độc quyền. Cách xây dựng tiêu chuẩn gồm ba bước:

  1. Tính các tương quan từng cặp. Với \(n\) tài sản trong \(T\) kỳ, ma trận tương quan mẫu \(\hat{\rho} \in \mathbb{R}^{n \times n}\) có \(n(n-1)/2\) phần tử riêng biệt.

  2. Đặt ngưỡng hoặc biến đổi. Chuyển ma trận tương quan dày thành một đồ thị thưa. Các cách tiếp cận phổ biến: ngưỡng cứng (\(\rho_{ij} > \bar{\rho}\)), cây khung nhỏ nhất (Minimum Spanning Tree, MST), hoặc đồ thị lọc cực đại phẳng (Planar Maximally Filtered Graph, PMFG).

  3. Phân tích đồ thị. Phát hiện cộng đồng cho thấy các cụm theo ngành; độ trung tâm xác định các cổ phiếu dẫn dắt thị trường; sự tiến triển theo thời gian theo dõi các thay đổi chế độ.

Hiện mã
# Compute correlation matrix from daily returns
daily_returns = dc.get_daily_returns(
    start_date="2023-01-01",
    end_date="2024-06-30"
)

daily_wide = daily_returns.pivot(
    index="date", columns="ticker", values="ret"
).dropna(axis=1, thresh=200)

corr = daily_wide.corr()

# Minimum Spanning Tree (MST)
# Convert correlation to distance: d = sqrt(2(1-ρ))
dist = np.sqrt(2 * (1 - corr.values))
np.fill_diagonal(dist, 0)

# Build complete graph with distances
G_complete = nx.Graph()
tickers = list(corr.columns)
for i in range(len(tickers)):
    for j in range(i + 1, len(tickers)):
        G_complete.add_edge(
            tickers[i], tickers[j],
            weight=dist[i, j],
            correlation=corr.iloc[i, j]
        )

# MST
G_mst = nx.minimum_spanning_tree(G_complete, weight="weight")

# Thresholded correlation network
threshold = 0.5
G_corr = nx.Graph()
for i in range(len(tickers)):
    for j in range(i + 1, len(tickers)):
        if corr.iloc[i, j] > threshold:
            G_corr.add_edge(
                tickers[i], tickers[j],
                weight=corr.iloc[i, j]
            )

print(f"MST: {G_mst.number_of_nodes()} nodes, "
      f"{G_mst.number_of_edges()} edges")
print(f"Corr network (ρ > {threshold}): "
      f"{G_corr.number_of_nodes()} nodes, "
      f"{G_corr.number_of_edges()} edges")

47.5.2 Khung kết nối Diebold–Yilmaz

Diebold và Yılmaz (2014) đề xuất đo lường mức độ kết nối tài chính bằng phân rã phương sai từ mô hình tự hồi quy vector (VAR). Tỷ lệ phương sai sai số dự báo trước \(H\) bước của biến \(i\) do các cú sốc từ biến \(j\) gây ra xác định mức kết nối có hướng theo từng cặp:

\[ C_{i \leftarrow j}(H) = \frac{\tilde{\theta}_{ij}^g(H)}{\sum_{j=1}^{n} \tilde{\theta}_{ij}^g(H)} \times 100 \tag{47.9}\]

trong đó \(\tilde{\theta}_{ij}^g(H)\) là phân rã phương sai sai số dự báo tổng quát. Chỉ số kết nối tổng (total connectedness index, TCI) tổng hợp trên mọi cặp:

\[ \text{TCI}(H) = \frac{\sum_{i \neq j} C_{i \leftarrow j}(H)}{\sum_{i,j} C_{i \leftarrow j}(H)} \times 100 \tag{47.10}\]

TCI cao cho thấy một hệ thống kết nối chặt chẽ, nơi các cú sốc lan truyền rộng; TCI thấp cho thấy mức độ cô lập tương đối.

Hiện mã
def diebold_yilmaz_connectedness(returns_df, var_order=2,
                                  forecast_horizon=10):
    """
    Compute the Diebold-Yilmaz (2014) connectedness table.

    Parameters
    ----------
    returns_df : DataFrame
        Returns with columns as assets, index as dates.
    var_order : int
        VAR lag order.
    forecast_horizon : int
        Forecast horizon for variance decomposition.

    Returns
    -------
    dict : Connectedness matrix, TCI, TO/FROM measures.
    """
    from statsmodels.tsa.api import VAR

    # Fit VAR
    model = VAR(returns_df.dropna())
    results = model.fit(var_order)

    # Generalized Forecast Error Variance Decomposition
    fevd = results.fevd(forecast_horizon)

    # Extract decomposition matrix at horizon H
    n = returns_df.shape[1]
    C = np.zeros((n, n))

    for i in range(n):
        decomp = fevd.decomp[i]  # H x n array
        C[i, :] = decomp[-1, :]  # Take horizon H values

    # Normalize rows to sum to 100
    row_sums = C.sum(axis=1, keepdims=True)
    C_norm = C / row_sums * 100

    # Connectedness measures
    # FROM: how much of i's variance comes from others
    FROM = C_norm.sum(axis=1) - np.diag(C_norm)

    # TO: how much of others' variance comes from i
    TO = C_norm.sum(axis=0) - np.diag(C_norm)

    # NET = TO - FROM
    NET = TO - FROM

    # Total Connectedness Index
    TCI = FROM.sum() / n

    names = returns_df.columns.tolist()

    connectedness_df = pd.DataFrame(C_norm, index=names, columns=names)
    connectedness_df["FROM_others"] = FROM
    connectedness_df.loc["TO_others"] = list(TO) + [TCI]

    return {
        "connectedness_matrix": connectedness_df,
        "TCI": TCI,
        "FROM": pd.Series(FROM, index=names),
        "TO": pd.Series(TO, index=names),
        "NET": pd.Series(NET, index=names)
    }


# Apply to top Vietnamese stocks by liquidity
top_stocks = (
    daily_returns.groupby("ticker")["volume"]
    .mean()
    .nlargest(20)
    .index
)

top_returns = daily_wide[
    [c for c in top_stocks if c in daily_wide.columns]
].dropna()

dy_results = diebold_yilmaz_connectedness(top_returns)
print(f"Total Connectedness Index: {dy_results['TCI']:.2f}%")
Bảng 47.6: Kết nối ròng Diebold–Yilmaz: Bên truyền dẫn ròng (+) và bên tiếp nhận ròng (−)
Hiện mã
net_df = pd.DataFrame({
    "TO_others": dy_results["TO"].round(2),
    "FROM_others": dy_results["FROM"].round(2),
    "NET": dy_results["NET"].round(2)
}).sort_values("NET", ascending=False)

net_df
Hiện mã
# Rolling TCI to track systemic risk over time
def rolling_tci(returns_wide, window=252, step=21, var_order=1,
                 forecast_horizon=10, min_stocks=10):
    """Compute rolling Total Connectedness Index."""
    dates = returns_wide.index
    tci_series = []

    for i in range(window, len(dates), step):
        window_data = returns_wide.iloc[i - window:i]

        # Keep stocks with sufficient data
        valid_cols = window_data.dropna(axis=1, thresh=int(window * 0.9))
        if valid_cols.shape[1] < min_stocks:
            continue

        # Use top stocks by variance (most informative)
        top_cols = valid_cols.var().nlargest(min_stocks).index
        subset = valid_cols[top_cols].dropna()

        if len(subset) < window * 0.8:
            continue

        try:
            result = diebold_yilmaz_connectedness(
                subset, var_order, forecast_horizon
            )
            tci_series.append({
                "date": dates[i],
                "TCI": result["TCI"],
                "n_stocks": len(top_cols)
            })
        except Exception:
            continue

    return pd.DataFrame(tci_series)


# tci_df = rolling_tci(daily_wide, window=252, step=21)

47.6 Mạng lưới liên ngân hàng và lây lan tài chính

47.6.1 Thị trường liên ngân hàng như một mạng lưới

Thị trường liên ngân hàng, nơi các ngân hàng cho nhau vay dự trữ, là mạng lưới tài chính điển hình. F. Allen và Gale (2000) chứng minh rằng cấu trúc các liên kết liên ngân hàng quyết định việc sự sụp đổ của một ngân hàng sẽ lan truyền thành khủng hoảng hệ thống hay được mạng lưới hấp thụ. Hai cấu trúc cực đoan minh họa điều này:

Mạng đầy đủ (mọi ngân hàng nối với mọi ngân hàng). Mức dư nợ của mỗi ngân hàng với bất kỳ một đối tác nào đều nhỏ. Sự sụp đổ của một ngân hàng gây ra tổn thất nhỏ cho nhiều ngân hàng, mà từng ngân hàng có thể tự hấp thụ. Mạng lưới có khả năng chống chịu.

Mạng vòng (mỗi ngân hàng nối với một ngân hàng lân cận). Mức dư nợ của mỗi ngân hàng tập trung vào một đối tác duy nhất. Sự sụp đổ của một ngân hàng có thể kéo đổ ngân hàng lân cận, kích hoạt một chuỗi vỡ nợ dây chuyền. Mạng lưới dễ đổ vỡ.

Hệ thống ngân hàng Việt Nam, với sự tập trung của các ngân hàng nhà nước và việc cơ quan quản lý nhấn mạnh cho vay liên ngân hàng để quản lý thanh khoản, nằm giữa hai thái cực này.

Hiện mã
# Load interbank exposure data
interbank = dc.get_interbank_exposures(
    date="2024-06-30"
)

# Build interbank network
G_bank = nx.DiGraph()

for _, row in interbank.iterrows():
    G_bank.add_edge(
        row["lender_bank"],
        row["borrower_bank"],
        weight=row["exposure_bn_vnd"],
        exposure_pct_equity=row.get("exposure_pct_equity", 0)
    )

# Add bank attributes
bank_info = dc.get_bank_characteristics(date="2024-06-30")
for _, row in bank_info.iterrows():
    if row["ticker"] in G_bank:
        G_bank.nodes[row["ticker"]].update({
            "total_assets": row.get("total_assets", 0),
            "equity": row.get("equity", 0),
            "is_soe": row.get("is_soe", False),
            "tier1_ratio": row.get("tier1_ratio", 0)
        })

bank_metrics, bank_graph_stats = compute_network_metrics(G_bank)

47.6.2 Mô phỏng vỡ nợ dây chuyền

Chúng ta triển khai cơ chế bù trừ của Eisenberg và Noe (2001) để mô phỏng các vụ vỡ nợ dây chuyền trong mạng lưới liên ngân hàng. Khi một ngân hàng sụp đổ, ngân hàng đó không thể thực hiện các nghĩa vụ liên ngân hàng, gây tổn thất cho các chủ nợ của nó, và đến lượt các chủ nợ này có thể sụp đổ:

\[ p_i^* = \min\left(\bar{p}_i, \; e_i + \sum_{j} \frac{L_{ji}}{\sum_k L_{jk}} p_j^*\right) \tag{47.11}\]

trong đó \(p_i^*\) là khoản thanh toán thực tế của ngân hàng \(i\), \(\bar{p}_i\) là tổng nghĩa vụ của ngân hàng đó, \(e_i\) là tài sản bên ngoài trừ nợ phải trả bên ngoài của nó, và \(L_{ji}/\sum_k L_{jk}\) là phần nghĩa vụ của ngân hàng \(j\) phải trả cho ngân hàng \(i\).

Hiện mã
def simulate_cascading_defaults(G, initial_failure, 
                                  equity_buffer=0.08,
                                  max_rounds=20):
    """
    Simulate cascading defaults in an interbank network.

    Parameters
    ----------
    G : nx.DiGraph
        Interbank exposure graph (lender → borrower, weight = exposure).
    initial_failure : str
        Bank that fails initially.
    equity_buffer : float
        Fraction of equity that absorbs losses before default.
    max_rounds : int
        Maximum cascade rounds.

    Returns
    -------
    dict : Defaulted banks, round-by-round losses, systemic loss.
    """
    defaulted = {initial_failure}
    cascade_history = [{"round": 0, "defaults": [initial_failure],
                        "total_defaults": 1}]

    for round_num in range(1, max_rounds + 1):
        new_defaults = set()

        for bank in G.nodes():
            if bank in defaulted:
                continue

            # Compute losses from defaulted counterparties
            total_loss = 0
            for defaulted_bank in defaulted:
                if G.has_edge(bank, defaulted_bank):
                    # bank lent to defaulted_bank → loss
                    exposure = G[bank][defaulted_bank]["weight"]
                    # Assume LGD = 60%
                    loss = exposure * 0.6
                    total_loss += loss

            # Check if losses exceed equity buffer
            bank_data = G.nodes.get(bank, {})
            equity = bank_data.get("equity", float("inf"))

            if total_loss > equity * equity_buffer:
                new_defaults.add(bank)

        if not new_defaults:
            break

        defaulted |= new_defaults
        cascade_history.append({
            "round": round_num,
            "defaults": list(new_defaults),
            "total_defaults": len(defaulted)
        })

    # Compute systemic loss
    total_system_equity = sum(
        G.nodes[n].get("equity", 0) for n in G.nodes()
    )
    defaulted_equity = sum(
        G.nodes[n].get("equity", 0) for n in defaulted
    )
    systemic_loss_pct = (
        defaulted_equity / total_system_equity
        if total_system_equity > 0 else 0
    )

    return {
        "defaulted_banks": list(defaulted),
        "n_defaults": len(defaulted),
        "cascade_rounds": len(cascade_history) - 1,
        "cascade_history": cascade_history,
        "systemic_loss_pct": systemic_loss_pct
    }


# Simulate cascade for each bank
cascade_results = []
for bank in G_bank.nodes():
    result = simulate_cascading_defaults(G_bank, bank)
    cascade_results.append({
        "initial_failure": bank,
        "n_cascading_defaults": result["n_defaults"],
        "systemic_loss_pct": result["systemic_loss_pct"],
        "cascade_rounds": result["cascade_rounds"]
    })

cascade_df = pd.DataFrame(cascade_results).sort_values(
    "systemic_loss_pct", ascending=False
)
Bảng 47.7: Các ngân hàng quan trọng về mặt hệ thống: Phân tích vỡ nợ dây chuyền
Hiện mã
cascade_df.head(15).round(4)

47.7 Kết nối xã hội giữa các địa phương

Mọi mạng lưới trong chương này cho đến giờ đều nối các doanh nghiệp hoặc tổ chức tài chính. Một loại mạng lưới khác nối con người, và qua con người nối các địa phương. Điều này quan trọng với tài chính vì các hộ gia đình tìm hiểu về sản phẩm tài chính từ những người họ quen biết, và vì bên cho vay và nhà đầu tư cũng một phần tìm hiểu về các bên đi vay và doanh nghiệp ở xa thông qua chính những mối quan hệ đó. Cho đến gần đây, mạng lưới này chưa thể được đo lường trên quy mô lớn. Chỉ số Kết nối Xã hội (Social Connectedness Index, SCI) của Bailey, Cao, Kuchler, Stroebel, và Wong (2018) đã thay đổi điều đó. Được xây dựng từ toàn bộ các liên kết bạn bè giữa người dùng Facebook, chỉ số này đo lường, với mỗi cặp vùng \(i\) và \(j\), xác suất tương đối để một người dùng ở \(i\) và một người dùng ở \(j\) là bạn bè:

\[ \text{SCI}_{ij} = \frac{\text{Connections}_{ij}}{\text{Users}_i \times \text{Users}_j}. \tag{47.12}\]

Mức độ kết nối giảm mạnh theo khoảng cách địa lý, nhưng cũng phản ánh lịch sử di cư, các tuyến giao thông, và các ranh giới chính trị và văn hóa (Bailey, Cao, Kuchler, Stroebel, và Wong 2018). Với một vùng cho trước, SCI xác định một tập trọng số trên tất cả các vùng còn lại. Hữu ích nhất là các tỷ trọng bạn bè, tức tỷ lệ các liên kết bạn bè của vùng \(i\) dẫn đến vùng \(j\),

\[ w_{ij} = \frac{\text{SCI}_{ij} \times \text{Users}_j}{\sum_{k} \text{SCI}_{ik} \times \text{Users}_k}, \tag{47.13}\]

biến bất kỳ biến số vùng \(X_j\) nào thành một mức phơi nhiễm xã hội:

\[ \text{SocialExp}_i(X) = \sum_{j \in \mathcal{N}(i)} w_{ij}\, X_j , \tag{47.14}\]

trong đó \(\mathcal{N}(i)\) là tập các vùng mà trên đó mức phơi nhiễm được tính: tất cả các vùng, hoặc chỉ những vùng ở xa \(i\) về mặt địa lý. Hai khuôn mẫu xây dựng trên Phương trình 47.14 làm nền cho tài liệu tài chính xã hội được Kuchler và Stroebel (2021) tổng quan. Trong Bailey, Cao, Kuchler, và Stroebel (2018), \(X_j\) là tăng trưởng giá nhà ở các vùng của bạn bè: những cá nhân có bạn bè trải qua mức tăng giá nhà lớn hơn kỳ vọng mức tăng trong tương lai lớn hơn và mua nhà lớn hơn với giá cao hơn. Trong Kuchler và c.s. (2022), \(X_j\) là vốn tổ chức đặt tại vùng \(j\): các doanh nghiệp có trụ sở ở những vùng có mức gần gũi xã hội với vốn cao thì có sở hữu tổ chức, định giá và thanh khoản cao hơn, với các tác động tập trung ở các doanh nghiệp nhỏ không được các nhà phân tích theo dõi. Các nghiên cứu trước đó đã xác lập phiên bản ở cấp độ cá nhân của ý tưởng này, từ việc tham gia thị trường cổ phiếu lan truyền qua tương tác xã hội (Hong, Kubik, và Stein 2004) đến việc những người cho vay trên một nền tảng cho vay ngang hàng coi mạng lưới bạn bè của bên đi vay là tín hiệu về chất lượng tín dụng (Lin, Prabhala, và Viswanathan 2013).

47.7.1 Mạng lưới xã hội như kênh truyền dẫn tín dụng

L. Allen, Peng, và Shan (2026) đưa bộ công cụ này vào tín dụng tiêu dùng và, một cách hiếm có, vào cả hai phía của thị trường cùng lúc. Bối cảnh của họ là LendingClub, nền tảng cho vay trực tuyến lớn nhất Hoa Kỳ, có dữ liệu công khai bao gồm cả các đơn bị từ chối lẫn được chấp thuận, nhờ đó có thể nghiên cứu các quyết định cấp vốn chứ không chỉ các khoản vay đã được cấp. Phiên bản đầu của bài báo sử dụng các đơn vay từ năm 2010 đến 2017, xác định vị trí bên đi vay theo mã ZIP ba chữ số, và ánh xạ các khu vực đó sang SCI cấp hạt (county).

Cầu. Số đơn vay trực tuyến ở một khu vực tăng lên sau khi hoạt động vay trực tuyến tăng ở những khu vực ở xa về địa lý nhưng có kết nối xã hội với khu vực đó. Giới hạn địa lý chính là yếu tố nhận diện. Bạn bè ở gần cùng chia sẻ điều kiện kinh tế địa phương, nên tương quan giữa hoạt động vay của một khu vực và của bạn bè nó có thể phản ánh một cú sốc chung chứ không phải quá trình học hỏi. Việc giới hạn \(\mathcal{N}(i)\) trong Phương trình 47.14 vào các vùng ở xa (ngoài bang, trong bản thảo đầu, theo thiết kế của Bailey, Cao, Kuchler, và Stroebel (2018)) loại bỏ thành phần chung đó. Phần còn lại phù hợp với việc nhận thức về một sản phẩm tài chính mới lan truyền qua các mối quan hệ xã hội: mọi người nghe về một nền tảng từ những người bạn đã dùng nó.

Cung. Các đơn vay từ những khu vực có mức gần gũi xã hội cao với các vùng dồi dào tiền gửi, một thước đo theo tinh thần của Kuchler và c.s. (2022) với tiền gửi thay cho vốn tổ chức, có khả năng được cấp vốn cao hơn. Riêng kết quả này có thể phản ánh một trong hai kênh mà bài báo đặt đối chọi nhau. Các mối quan hệ xã hội có thể truyền thông tin khó có được về điều kiện tín dụng của một cộng đồng đến những người có khả năng cấp vốn cho cộng đồng đó. Hoặc chúng có thể truyền sự thiên vị theo quan hệ thân quen (affinity): sự quen thuộc, và việc lặp lại các ý kiến qua một mạng lưới tạo ra thiên lệch thuyết phục, khiến bên cấp vốn ưu ái các bên đi vay có kết nối bất kể chất lượng. Cả hai kênh đều dự báo rằng các bên đi vay có kết nối nhận được nhiều tín dụng hơn. Chúng bất đồng về hiệu quả của khoản vay cận biên. Sự thiên vị theo quan hệ thân quen hạ thấp tiêu chuẩn, nên các khoản vay bổ sung sẽ vỡ nợ thường xuyên hơn; thông tin làm sắc nét việc sàng lọc, nên chúng sẽ vỡ nợ ít hơn. Kết quả sau thực tế (ex post) tốt hơn, điều này chỉ ra kênh thông tin. Lin, Prabhala, và Viswanathan (2013) đi đến kết luận tương tự từ các liên kết bạn bè của từng bên đi vay trên Prosper, và khi Duarte, Siegel, và Young (2012) áp dụng cùng logic cho ngoại hình của bên đi vay, họ thấy rằng những bên đi vay trông đáng tin mà người cho vay ưu ái thực sự vỡ nợ ít hơn.

Nhận diện. Mức độ kết nối xã hội không được phân bổ ngẫu nhiên. Các khu vực mà cư dân có mạng lưới phân tán về địa lý thường giàu hơn, học vấn cao hơn và di động hơn (Bailey, Cao, Kuchler, Stroebel, và Wong 2018), và mỗi đặc điểm đó đều ảnh hưởng trực tiếp đến kết quả tín dụng. Do đó, phiên bản đã công bố của bài báo thiết lập quan hệ nhân quả bằng các biến công cụ shift-share. Các tỷ trọng là các trọng số mạng lưới xã hội \(w_{ij}\), còn các cú dịch chuyển là những sự kiện ở các vùng có kết nối làm thay đổi cầu hoặc cung tín dụng ở đó mà không phản ánh điều kiện ở khu vực của chính bên đi vay: thiên tai ở phía cầu và hành vi sai phạm của cố vấn tài chính ở phía cung. Hành vi sai phạm của cố vấn được đo lường đặc biệt tốt ở Hoa Kỳ vì hồ sơ kỷ luật của nhà môi giới là công khai, đây là dữ liệu mà Egan, Matvos, và Seru (2019) khai thác.

Phân phối. Cả tác động lên cầu lẫn lên cung đều mạnh nhất ở các cộng đồng thiệt thòi, và không đi kèm tỷ lệ nợ quá hạn cao hơn. Các mối quan hệ xã hội với những vùng thịnh vượng hơn dường như thay thế một phần cho hạ tầng thông tin địa phương (chi nhánh ngân hàng, lịch sử tín dụng dài, bên cho vay dựa trên quan hệ) mà các khu vực thiệt thòi thiếu.

Bài báo đáng được đọc kỹ bởi bất kỳ ai xây dựng các thước đo mạng lưới, vì bốn lý do. Nó coi một mạng lưới giữa các địa phương là một đối tượng quan sát được, đo lường được ngang hàng với các liên kết sở hữu hay chuỗi cung ứng. Nó nghiên cứu cả hai phía của một thị trường thông qua cùng một mạng lưới, điều mà ít bài báo về mạng lưới làm được. Nó sử dụng kết quả sau thực tế làm phép kiểm định phân tách thông tin với thiên lệch, một phép kiểm định mà mọi khẳng định rằng mạng lưới “cải thiện” phân bổ đều nên được yêu cầu vượt qua. Và nó nêu rõ rằng biến phơi nhiễm xã hội là một cấu trúc shift-share, điều quyết định những gì có thể dùng để nhận diện nó.

47.7.2 Logic shift-share và suy luận

Phương trình 47.14 là tổng có trọng số các cú dịch chuyển theo vùng, cùng cấu trúc với các biến công cụ Bartik của kinh tế học lao động, và tài liệu kinh tế lượng về các thiết kế này nêu rất chính xác khi nào chúng nhận diện được một tác động nhân quả. Việc nhận diện có thể đến từ các tỷ trọng ngoại sinh (Goldsmith-Pinkham, Sorkin, và Swift 2020). Ở đây điều đó có nghĩa là cấu trúc các mối quan hệ xã hội của một khu vực không liên quan đến kết quả tín dụng của khu vực đó ngoại trừ thông qua các cú sốc mà các mối quan hệ truyền đi, một giả định mạnh khi các cấu trúc đó phản ánh di cư, dân tộc và cấu trúc ngành. Ngoài ra, việc nhận diện có thể đến từ các cú dịch chuyển được phân bổ giữa các vùng gần như ngẫu nhiên, có điều kiện theo các tỷ trọng (Borusyak, Hull, và Jaravel 2022). Thiên tai là những cú dịch chuyển hấp dẫn chính vì con đường thứ hai là hợp lý đối với chúng. Dù theo con đường nào, suy luận cũng đòi hỏi sự cẩn trọng. Các vùng có trọng số phơi nhiễm tương tự nhận các cú sốc tương quan với nhau, nên các sai số chuẩn thông thường và sai số chuẩn theo cụm vùng đánh giá quá cao độ chính xác. Adão, Kolesár, và Morales (2019) suy ra các sai số chuẩn vững trước tương quan do phơi nhiễm này. Một hồi quy phơi nhiễm xã hội được báo cáo mà không có chúng, và không có các kiểm định cân bằng của các cú dịch chuyển với đặc điểm vùng trước kỳ, thì chưa đủ sức thuyết phục.

47.7.3 Kết nối xã hội ở Việt Nam

Dữ liệu đã có sẵn. Bản phát hành SCI công khai của Meta bao phủ các quốc gia ngoài châu Âu ở cấp đơn vị hành chính thứ nhất, mà với Việt Nam nghĩa là 63 tỉnh theo phiên bản ranh giới được dùng trong bản phát hành. Facebook được sử dụng rộng rãi ở Việt Nam, nhưng nhiều liên lạc riêng tư lại đi qua Zalo, mà SCI không quan sát được. Do đó, mức độ kết nối giữa các cặp tỉnh từ Facebook chỉ đo lường một kênh tiếp xúc xã hội, không phải tất cả.

Hai đặc điểm của bối cảnh này cần được lưu ý trước khi chạy bất kỳ hồi quy nào. Thứ nhất, việc hợp nhất các tỉnh của Việt Nam từ 63 xuống 34 vào năm 2025 có nghĩa là các thống kê cấp tỉnh hiện hành phải được ánh xạ ngược về ranh giới cũ, hoặc SCI phải được tổng hợp xuôi theo trọng số dân số, và cả hai cách ánh xạ đều không vô hại đối với những tỉnh bị chia cho nhiều đơn vị mới. Thứ hai, di cư nội địa là yếu tố quyết định chính của mức độ kết nối, và tổng điều tra ghi nhận trực tiếp điều này. Các cuộc Tổng điều tra Dân số và Nhà ở năm 2009 và 2019 ghi nhận tỉnh cư trú năm năm trước của mỗi người trả lời, từ đó cho ra các ma trận di cư từ tỉnh này sang tỉnh khác. Một thước đo phơi nhiễm có trọng số theo di cư vừa là phép kiểm tra tính hợp lệ của SCI, vừa, do có trước hầu hết các kết quả cần quan tâm, là một ứng viên biến công cụ cho nó.

Có bốn thiết kế nghiên cứu có thể chuyển giao trực tiếp nhất.

Mức gần gũi xã hội với vốn đối với các doanh nghiệp niêm yết. Vốn đầu tư và hoạt động môi giới ở Việt Nam tập trung tại Thành phố Hồ Chí Minh và Hà Nội. Thước đo của Kuchler và c.s. (2022), được tính cho tỉnh đặt trụ sở của từng doanh nghiệp, với tiền gửi hay thu nhập cấp tỉnh thay cho vốn tổ chức, đặt câu hỏi liệu các doanh nghiệp có trụ sở ở những tỉnh gần gũi xã hội với các trung tâm này có thanh khoản, định giá và sở hữu nước ngoài cao hơn hay không. Tỉnh đặt trụ sở xuất hiện trong mọi bản cáo bạch niêm yết, và các biến kết quả là những biến tiêu chuẩn trong cuốn sách này.

Sự khuếch tán của việc tham gia thị trường của nhà đầu tư cá nhân. Sự gia tăng đột biến các tài khoản môi giới mới trong giai đoạn 2020–2022 là một sự kiện phía cầu đúng loại mà L. Allen, Peng, và Shan (2026) nghiên cứu: nhận thức về một sản phẩm tài chính lan truyền qua các mối quan hệ xã hội. Thiết kế đặt câu hỏi liệu việc mở tài khoản ở một tỉnh có tăng tốc sau khi việc mở tài khoản tăng ở các tỉnh ở xa nhưng có kết nối xã hội hay không. Ràng buộc chính là dữ liệu. Thống kê tài khoản được công bố ở cấp quốc gia, và việc phân tách theo tỉnh sẽ cần sự hợp tác của các công ty môi giới.

Cho vay trên nền tảng trong khung thử nghiệm có kiểm soát (sandbox). Nghị định 94/2025/ND-CP, có hiệu lực từ tháng 7 năm 2025, tạo ra cơ chế thử nghiệm có kiểm soát cho cho vay ngang hàng cùng với chấm điểm tín dụng và API mở. Một thị trường nền tảng trong những năm đầu là bối cảnh của kết quả về cầu trong L. Allen, Peng, và Shan (2026), và nếu dữ liệu về các bên tham gia sandbox được cung cấp cho các nhà nghiên cứu, thì sự lan rộng của các nền tảng giữa các tỉnh gần như là một cơ hội tái lập. Việt Nam cũng là nơi mà kết quả phía cung có thể quan trọng nhất. Như đã thảo luận trong Mục 2.7, độ bao phủ của trung tâm thông tin tín dụng vẫn còn mỏng, và L. Allen, Peng, và Shan (2026) gợi ý rằng các mối quan hệ xã hội với những vùng giàu hơn có thể một phần thay thế cho hạ tầng thông tin còn thiếu.

Các cú sốc cho thiết kế shift-share. Ở phía cầu, bão và lũ lụt tác động đến những tỉnh xác định được vào những thời điểm được ghi nhận rõ ràng, trong đó có bão Yagi ở các tỉnh phía Bắc vào tháng 9 năm 2024, và đây là những cú dịch chuyển tự nhiên. Ở phía cung, sự kiện tương ứng gần nhất ở Việt Nam với hành vi sai phạm của cố vấn tại Hoa Kỳ là việc bán trái phiếu doanh nghiệp cho người gửi tiền ngân hàng thông qua mạng lưới chi nhánh, bị phát hiện trong vụ Vạn Thịnh Phát và SCB năm 2022, một cú sốc đối với niềm tin vào tiết kiệm qua trung gian ngân hàng. Sử dụng nó làm cú dịch chuyển đòi hỏi phải biết các trái chủ bị ảnh hưởng sống ở đâu, thông tin này không công khai, và không nên thực hiện thiết kế này nếu thiếu thông tin đó.

Sáu quy tắc thực hành sau đây áp dụng cho nghiên cứu mạng lưới xã hội cấp tỉnh ở Việt Nam.

  1. Định nghĩa “ở xa” theo vùng, không theo sự tiếp giáp. Hình dạng dài và hẹp của Việt Nam khiến các tỉnh lân cận có thể cách xa nhau về kinh tế còn các tỉnh ở xa lại hội nhập chặt chẽ. Hãy đo phơi nhiễm trên các tỉnh nằm ngoài vùng kinh tế–xã hội của chính bên đi vay, tương tự hạn chế ngoài bang, và chứng tỏ rằng kết quả vẫn vững với các ngưỡng cắt thay thế.
  2. Cố định phiên bản ranh giới. Làm việc xuyên suốt với 63 tỉnh trước năm 2025, và ghi lại bảng chuyển đổi tương ứng cho mọi dữ liệu được báo cáo theo hệ thống 34 tỉnh.
  3. Coi SCI là ảnh chụp của một mạng lưới nội sinh. Nó được quan sát tại một vài thời điểm, sau phần lớn quá trình di cư đã tạo ra nó. Với các kết quả có trước ảnh chụp này, hãy dùng ma trận di cư của tổng điều tra năm 2009 làm thước đo phiên bản sớm hơn.
  4. Báo cáo các chẩn đoán shift-share. Cho thấy những tỷ trọng nào chi phối ước lượng (Goldsmith-Pinkham, Sorkin, và Swift 2020), kiểm định cân bằng của các cú dịch chuyển với đặc điểm cấp tỉnh trước kỳ (Borusyak, Hull, và Jaravel 2022), và dùng các sai số chuẩn vững với phơi nhiễm (Adão, Kolesár, và Morales 2019).
  5. Thực hiện phép kiểm định hiệu quả. Bất kỳ phát hiện nào cho rằng các mối quan hệ xã hội làm tăng phê duyệt, cấp vốn hay đầu tư đều phải được tiếp nối bằng một kiểm định trên các kết quả sau thực tế, vỡ nợ đối với tín dụng và lợi nhuận tiếp theo đối với cổ phiếu, phân biệt thông tin với sự thiên vị theo quan hệ thân quen, như L. Allen, Peng, và Shan (2026) đã làm.
  6. Tôn trọng số lượng đơn vị nhỏ. Với 63 tỉnh, suy luận theo cụm dựa trên ít cụm. Hãy báo cáo các giá trị p của bootstrap cụm kiểu wild (wild-cluster bootstrap) bên cạnh các giá trị thông thường.

47.8 Mạng nơ-ron đồ thị cho dự báo tài chính

47.8.1 Từ đồ thị đến dự báo

Phân tích mạng lưới cổ điển tính các đặc trưng thủ công (độ trung tâm, phân cụm, thành viên cộng đồng) rồi đưa vào các mô hình hồi quy tiêu chuẩn. Các mạng nơ-ron đồ thị (GNN) học các đặc trưng trực tiếp từ cấu trúc đồ thị và thuộc tính của nút thông qua truyền thông điệp. Ý tưởng then chốt là biểu diễn của một nút không chỉ phụ thuộc vào đặc trưng của chính nó mà còn phụ thuộc vào đặc trưng của các nút lân cận, lân cận của chúng, và cứ thế tiếp tục.

47.8.2 Mạng tích chập đồ thị (GCN)

Mạng tích chập đồ thị (Graph Convolutional Network) của Kipf và Welling (2016) thực hiện phép tích chập phổ trên đồ thị. Quy tắc lan truyền theo từng lớp là:

\[ H^{(\ell+1)} = \sigma\left(\tilde{D}^{-1/2} \tilde{A} \tilde{D}^{-1/2} H^{(\ell)} W^{(\ell)}\right) \tag{47.15}\]

trong đó \(\tilde{A} = A + I_n\) là ma trận kề có thêm các vòng tự nối, \(\tilde{D}\) là ma trận bậc tương ứng, \(H^{(\ell)}\) là ma trận đặc trưng nút ở lớp \(\ell\), \(W^{(\ell)}\) là ma trận trọng số có thể học, và \(\sigma\) là hàm phi tuyến (ReLU). Số hạng chuẩn hóa \(\tilde{D}^{-1/2} \tilde{A} \tilde{D}^{-1/2}\) lấy trung bình các đặc trưng của mỗi nút với đặc trưng của các nút lân cận, có trọng số theo bậc.

47.8.3 Mạng chú ý đồ thị (GAT)

Mạng chú ý đồ thị (Graph Attention Network) của Veličković và c.s. (2017) thay thế phép chuẩn hóa cố định trong GCN bằng các hệ số chú ý được học:

\[ h_i^{(\ell+1)} = \sigma\left(\sum_{j \in \mathcal{N}(i)} \alpha_{ij}^{(\ell)} W^{(\ell)} h_j^{(\ell)}\right) \tag{47.16}\]

trong đó hệ số chú ý \(\alpha_{ij}\) được tính như sau:

\[ \alpha_{ij} = \frac{\exp\left(\text{LeakyReLU}\left(\mathbf{a}^\top [W h_i \| W h_j]\right)\right)}{\sum_{k \in \mathcal{N}(i)} \exp\left(\text{LeakyReLU}\left(\mathbf{a}^\top [W h_i \| W h_k]\right)\right)} \tag{47.17}\]

Điều này cho phép mô hình học xem các nút lân cận nào mang nhiều thông tin nhất đối với từng nút, thay vì đối xử bình đẳng với mọi nút lân cận.

Hiện mã
class GCNLayer(nn.Module):
    """Graph Convolutional Network layer (Kipf & Welling, 2016)."""

    def __init__(self, in_features, out_features, bias=True):
        super().__init__()
        self.weight = nn.Parameter(torch.FloatTensor(in_features, out_features))
        if bias:
            self.bias = nn.Parameter(torch.FloatTensor(out_features))
        else:
            self.bias = None
        self.reset_parameters()

    def reset_parameters(self):
        nn.init.xavier_uniform_(self.weight)
        if self.bias is not None:
            nn.init.zeros_(self.bias)

    def forward(self, x, adj_norm):
        """
        Parameters
        ----------
        x : Tensor (n_nodes, in_features)
            Node feature matrix.
        adj_norm : Tensor (n_nodes, n_nodes)
            Normalized adjacency matrix (D^{-1/2} A D^{-1/2}).
        """
        support = x @ self.weight
        output = adj_norm @ support
        if self.bias is not None:
            output = output + self.bias
        return output


class GATLayer(nn.Module):
    """Graph Attention Network layer (Velickovic et al., 2017)."""

    def __init__(self, in_features, out_features, n_heads=4,
                 dropout=0.1, concat=True):
        super().__init__()
        self.n_heads = n_heads
        self.out_features = out_features
        self.concat = concat

        self.W = nn.Parameter(
            torch.FloatTensor(n_heads, in_features, out_features)
        )
        self.a = nn.Parameter(torch.FloatTensor(n_heads, 2 * out_features, 1))
        self.dropout = nn.Dropout(dropout)
        self.leaky_relu = nn.LeakyReLU(0.2)
        self.reset_parameters()

    def reset_parameters(self):
        nn.init.xavier_uniform_(self.W)
        nn.init.xavier_uniform_(self.a)

    def forward(self, x, adj):
        """
        Parameters
        ----------
        x : Tensor (n_nodes, in_features)
        adj : Tensor (n_nodes, n_nodes)
            Binary adjacency matrix (1 if edge, 0 otherwise).
        """
        n = x.size(0)

        # Linear transformation for each head
        # x: (n, in_f) -> h: (heads, n, out_f)
        h = torch.einsum("ni,hio->hno", x, self.W)

        # Attention coefficients
        # Concatenate h_i and h_j for all pairs
        h_i = h.unsqueeze(2).expand(-1, -1, n, -1)  # (heads, n, n, out_f)
        h_j = h.unsqueeze(1).expand(-1, n, -1, -1)  # (heads, n, n, out_f)
        concat_h = torch.cat([h_i, h_j], dim=-1)    # (heads, n, n, 2*out_f)

        e = self.leaky_relu(
            torch.einsum("hnmo,hoi->hnm", concat_h, self.a).squeeze(-1)
        )

        # Mask non-edges
        mask = adj.unsqueeze(0).expand(self.n_heads, -1, -1)
        e = e.masked_fill(mask == 0, float("-inf"))

        # Softmax attention
        alpha = F.softmax(e, dim=-1)
        alpha = self.dropout(alpha)

        # Weighted aggregation
        out = torch.einsum("hnm,hmo->hno", alpha, h)  # (heads, n, out_f)

        if self.concat:
            out = out.permute(1, 0, 2).reshape(n, -1)  # (n, heads * out_f)
        else:
            out = out.mean(dim=0)  # (n, out_f)

        return out, alpha


class FinancialGNN(nn.Module):
    """
    Graph Neural Network for financial node prediction.
    Supports GCN and GAT layers with flexible architecture.
    """

    def __init__(self, input_dim, hidden_dim=64, output_dim=1,
                 n_layers=2, n_heads=4, gnn_type="gat",
                 dropout=0.3):
        super().__init__()

        self.gnn_type = gnn_type
        self.dropout = nn.Dropout(dropout)

        if gnn_type == "gcn":
            self.layers = nn.ModuleList()
            self.layers.append(GCNLayer(input_dim, hidden_dim))
            for _ in range(n_layers - 1):
                self.layers.append(GCNLayer(hidden_dim, hidden_dim))

        elif gnn_type == "gat":
            self.layers = nn.ModuleList()
            self.layers.append(
                GATLayer(input_dim, hidden_dim // n_heads,
                         n_heads=n_heads, concat=True)
            )
            for _ in range(n_layers - 2):
                self.layers.append(
                    GATLayer(hidden_dim, hidden_dim // n_heads,
                             n_heads=n_heads, concat=True)
                )
            # Last layer: single head, no concat
            self.layers.append(
                GATLayer(hidden_dim, hidden_dim,
                         n_heads=1, concat=False)
            )

        # Prediction head
        self.head = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(hidden_dim // 2, output_dim)
        )

    def forward(self, x, adj):
        """
        Parameters
        ----------
        x : Tensor (n_nodes, input_dim)
            Node features.
        adj : Tensor (n_nodes, n_nodes)
            Adjacency matrix.
        """
        h = x
        attention_weights = []

        for i, layer in enumerate(self.layers):
            if self.gnn_type == "gcn":
                h = layer(h, adj)
                h = F.relu(h) if i < len(self.layers) - 1 else h
            elif self.gnn_type == "gat":
                h, alpha = layer(h, adj)
                attention_weights.append(alpha)
                if i < len(self.layers) - 1:
                    h = F.elu(h)

            h = self.dropout(h)

        predictions = self.head(h).squeeze(-1)
        return predictions, attention_weights

47.8.4 GNN cho dự báo lợi nhuận chéo (cross-sectional)

Chúng ta áp dụng GNN để dự báo lợi nhuận cổ phiếu theo chiều chéo (cross-sectional), trong đó đồ thị mã hóa các quan hệ sở hữu giữa các doanh nghiệp. Giả thuyết là các doanh nghiệp liên kết qua quan hệ sở hữu có động thái lợi nhuận tương quan mà GNN có thể khai thác.

Hiện mã
def prepare_gnn_data(firms_df, ownership_graph, returns_df, date):
    """
    Prepare node features and adjacency matrix for GNN prediction.

    Parameters
    ----------
    firms_df : DataFrame
        Firm characteristics.
    ownership_graph : nx.DiGraph
        Ownership network.
    returns_df : DataFrame
        Stock returns.

    Returns
    -------
    tuple : (node_features, adjacency, target_returns, ticker_list)
    """
    # Get stocks with both features and network presence
    tickers = sorted(
        set(firms_df["ticker"]) &
        set(ownership_graph.nodes()) &
        set(returns_df["ticker"])
    )

    if not tickers:
        return None, None, None, None

    # Node features
    feature_cols = [
        "log_size", "book_to_market", "momentum_12m",
        "profitability", "investment", "leverage",
        "beta", "volatility", "turnover"
    ]

    feat_df = firms_df[firms_df["ticker"].isin(tickers)].set_index("ticker")
    feat_df = feat_df.reindex(tickers)

    X = feat_df[feature_cols].fillna(0).values
    X = (X - X.mean(axis=0)) / (X.std(axis=0) + 1e-8)

    # Adjacency matrix
    ticker_to_idx = {t: i for i, t in enumerate(tickers)}
    n = len(tickers)
    A = np.zeros((n, n))

    for u, v, d in ownership_graph.edges(data=True):
        if u in ticker_to_idx and v in ticker_to_idx:
            weight = d.get("weight", 1)
            A[ticker_to_idx[u], ticker_to_idx[v]] = weight
            A[ticker_to_idx[v], ticker_to_idx[u]] = weight  # Symmetrize

    # Add self-loops
    A = A + np.eye(n)

    # Normalize: D^{-1/2} A D^{-1/2}
    D = np.diag(A.sum(axis=1))
    D_inv_sqrt = np.diag(1.0 / np.sqrt(np.diag(D) + 1e-10))
    A_norm = D_inv_sqrt @ A @ D_inv_sqrt

    # Target returns
    ret_df = returns_df[
        (returns_df["ticker"].isin(tickers)) &
        (returns_df["date"] == date)
    ].set_index("ticker").reindex(tickers)

    y = ret_df["ret"].fillna(0).values

    return (
        torch.tensor(X, dtype=torch.float32),
        torch.tensor(A_norm, dtype=torch.float32),
        torch.tensor(y, dtype=torch.float32),
        tickers
    )


def train_gnn_model(model, X_train, A_train, y_train,
                     X_val, A_val, y_val,
                     n_epochs=100, lr=1e-3):
    """Train GNN model with validation-based early stopping."""
    optimizer = torch.optim.Adam(model.parameters(), lr=lr,
                                  weight_decay=1e-4)

    best_val_loss = float("inf")
    patience_counter = 0

    for epoch in range(n_epochs):
        model.train()
        optimizer.zero_grad()

        pred, _ = model(X_train, A_train)
        loss = F.mse_loss(pred, y_train)
        loss.backward()
        optimizer.step()

        # Validation
        model.eval()
        with torch.no_grad():
            val_pred, _ = model(X_val, A_val)
            val_loss = F.mse_loss(val_pred, y_val).item()

        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_state = {k: v.clone() for k, v in model.state_dict().items()}
            patience_counter = 0
        else:
            patience_counter += 1
            if patience_counter >= 15:
                break

    model.load_state_dict(best_state)
    return model, best_val_loss

47.8.5 Mạng đồ thị theo thời gian

Các mạng lưới tài chính tiến triển theo thời gian. Tỷ lệ sở hữu thay đổi, các thành viên hội đồng quản trị luân chuyển, các tương quan dịch chuyển. Các GNN theo thời gian mở rộng GNN tĩnh bằng cách đưa chiều thời gian vào một cách tường minh. Mạng đồ thị theo thời gian (Temporal Graph Network, TGN) của Rossi và c.s. (2020) duy trì một trạng thái bộ nhớ cho mỗi nút, được cập nhật mỗi khi có một sự kiện liên quan đến nút đó:

\[ \mathbf{s}_i(t) = \text{GRU}\left(\mathbf{s}_i(t^-), \; \text{msg}(i, t)\right) \tag{47.18}\]

trong đó \(\mathbf{s}_i(t)\) là trạng thái bộ nhớ của nút \(i\) tại thời điểm \(t\), \(\text{msg}(i, t)\) là thông điệp được tổng hợp từ các sự kiện tại thời điểm \(t\), và GRU là đơn vị hồi quy có cổng (gated recurrent unit).

Hiện mã
class TemporalFinancialGNN(nn.Module):
    """
    Temporal GNN for dynamic financial networks.
    Combines graph convolution with temporal memory.
    """

    def __init__(self, node_dim, edge_dim=1, memory_dim=64,
                 hidden_dim=64, output_dim=1, n_heads=4):
        super().__init__()

        self.memory_dim = memory_dim

        # Memory update (GRU)
        self.memory_updater = nn.GRUCell(
            input_size=node_dim + edge_dim + memory_dim,
            hidden_size=memory_dim
        )

        # Message function
        self.message_fn = nn.Sequential(
            nn.Linear(memory_dim * 2 + edge_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, memory_dim)
        )

        # Graph attention for spatial aggregation
        self.spatial_attn = GATLayer(
            memory_dim + node_dim, hidden_dim // n_heads,
            n_heads=n_heads, concat=True
        )

        # Temporal attention for recent history
        self.temporal_attn = nn.MultiheadAttention(
            embed_dim=hidden_dim, num_heads=n_heads,
            batch_first=True
        )

        # Prediction head
        self.head = nn.Sequential(
            nn.Linear(hidden_dim * 2, hidden_dim),
            nn.ReLU(),
            nn.Dropout(0.3),
            nn.Linear(hidden_dim, output_dim)
        )

    def forward(self, node_features, adj_sequence, memory=None):
        """
        Parameters
        ----------
        node_features : list of Tensors
            Node features at each time step.
        adj_sequence : list of Tensors
            Adjacency matrices at each time step.
        memory : Tensor or None
            Initial memory state (n_nodes, memory_dim).

        Returns
        -------
        predictions : Tensor
            Predictions for the last time step.
        """
        n_nodes = node_features[0].shape[0]
        T = len(node_features)

        if memory is None:
            memory = torch.zeros(n_nodes, self.memory_dim)

        temporal_states = []

        for t in range(T):
            x_t = node_features[t]
            adj_t = adj_sequence[t]

            # Spatial aggregation via GAT
            combined = torch.cat([x_t, memory], dim=-1)
            spatial_out, _ = self.spatial_attn(combined, adj_t)

            temporal_states.append(spatial_out.unsqueeze(1))

            # Update memory with current state
            update_input = torch.cat([
                x_t, spatial_out[:, :1].squeeze(1) if spatial_out.dim() > 2
                else spatial_out,
                memory
            ], dim=-1)[:, :self.memory_updater.input_size]

        # Stack temporal states: (n_nodes, T, hidden)
        temporal_stack = torch.cat(temporal_states, dim=1)

        # Temporal attention across time steps
        temporal_out, _ = self.temporal_attn(
            temporal_stack, temporal_stack, temporal_stack
        )

        # Use last time step
        last_spatial = temporal_states[-1].squeeze(1)
        last_temporal = temporal_out[:, -1, :]

        combined = torch.cat([last_spatial, last_temporal], dim=-1)
        predictions = self.head(combined).squeeze(-1)

        return predictions

47.8.6 GNN cho rủi ro tín dụng và phát hiện gian lận

Ngoài dự báo lợi nhuận, GNN đặc biệt mạnh trong đánh giá rủi ro tín dụng và phát hiện gian lận, nơi bản thân cấu trúc mạng lưới mang thông tin. Trong rủi ro tín dụng, các doanh nghiệp liên kết với các đối tác đã vỡ nợ phải đối mặt với rủi ro gia tăng. Trong phát hiện gian lận, các mẫu hình giao dịch đáng ngờ lan truyền qua các mạng lưới thực thể liên quan.

Hiện mã
class CreditRiskGNN(nn.Module):
    """
    GNN for credit default prediction using firm and bank networks.
    Node classification: predict default probability per firm.
    """

    def __init__(self, firm_features_dim, bank_features_dim=0,
                 hidden_dim=64, n_heads=4):
        super().__init__()

        input_dim = firm_features_dim + bank_features_dim

        # GNN backbone
        self.gnn = FinancialGNN(
            input_dim=input_dim,
            hidden_dim=hidden_dim,
            output_dim=1,
            n_layers=3,
            n_heads=n_heads,
            gnn_type="gat"
        )

        # Additional head for probability output
        self.sigmoid = nn.Sigmoid()

    def forward(self, x, adj):
        logits, attn = self.gnn(x, adj)
        probs = self.sigmoid(logits)
        return probs, attn

47.9 Khi nào nên dùng các phương pháp mạng lưới

47.9.1 Khung ra quyết định

Bảng 47.8: Khung ra quyết định cho các phương pháp mạng lưới
Câu hỏi Cách tiếp cận tốt nhất Ví dụ
Doanh nghiệp \(A\) có tác động đến doanh nghiệp \(B\) không? Kiểm định từng cặp + biến kiểm soát mạng lưới Động lượng chuỗi cung ứng
Doanh nghiệp nào quan trọng nhất về mặt hệ thống? Các thước đo độ trung tâm Lây lan liên ngân hàng
Các doanh nghiệp kết nối qua mạng lưới có đồng biến động không? Tương quan so với mức trùng lặp kiêm nhiệm Khuếch tán qua kiêm nhiệm hội đồng quản trị
Cấu trúc mạng lưới có dự báo được lợi nhuận không? GNN hoặc hồi quy bổ sung biến mạng lưới Dự báo lợi nhuận bằng GCN
Một cú sốc lan truyền như thế nào? Mô phỏng dây chuyền Mô hình vỡ nợ Eisenberg-Noe
Danh mục tối ưu là gì khi xét rủi ro mạng lưới? Tối ưu hóa chính quy hóa theo mạng lưới Lọc MST của tương quan
Mạng lưới thay đổi theo thời gian như thế nào? GNN theo thời gian hoặc phân tích cửa sổ trượt Kết nối trượt DY
Thông tin có truyền giữa các vùng qua quan hệ xã hội không? Phơi nhiễm xã hội trên các vùng ở xa + IV shift-share Khuếch tán tín dụng trực tuyến (L. Allen, Peng, và Shan 2026)

47.9.2 Yêu cầu dữ liệu và mức độ sẵn có ở Việt Nam

Bảng 47.9: Các nguồn dữ liệu mạng lưới tài chính ở Việt Nam
Loại mạng lưới Dữ liệu cần thiết Mức độ sẵn có ở Việt Nam Tần suất cập nhật
Sở hữu Sổ đăng ký cổ đông Tốt (báo cáo bán niên) Nửa năm
Kiêm nhiệm hội đồng quản trị Bổ nhiệm thành viên Tốt (hồ sơ công bố) Hằng năm
Chuỗi cung ứng Công bố khách hàng–nhà cung cấp, dữ liệu thương mại Trung bình (công bố giao dịch với bên liên quan) Hằng năm
Tương quan Dữ liệu lợi nhuận Rất tốt (hằng ngày từ sở giao dịch) Hằng ngày
Liên ngân hàng Dư nợ song phương Hạn chế (dữ liệu NHNN, thuyết minh BCTC của ngân hàng) Hằng quý
Cùng nắm giữ Sở hữu của tổ chức Trung bình (công bố bán niên) Nửa năm
Cho vay Dữ liệu cấp khoản vay Hạn chế (dữ liệu giám sát ngân hàng) Hằng quý
Kết nối xã hội Liên kết bạn bè giữa các cặp vùng; ma trận di cư Trung bình (SCI của Meta cho 63 tỉnh; di cư theo tổng điều tra 2009 và 2019) Ảnh chụp định kỳ

47.10 Tóm tắt

Chương này đã phát triển bộ công cụ mạng lưới cho thị trường tài chính Việt Nam, trải từ lý thuyết đồ thị cổ điển, việc xây dựng các mạng lưới tài chính theo từng lĩnh vực, đến các mạng nơ-ron đồ thị hiện đại.

Hiểu biết trung tâm là các mạng lưới tài chính mã hóa thông tin mà các hồi quy dữ liệu bảng tiêu chuẩn không nhìn thấy. Các mạng lưới sở hữu bộc lộ cấu trúc kiểm soát kim tự tháp và rủi ro chuyển dịch lợi ích (tunneling), giải thích các khác biệt chéo về giá trị doanh nghiệp. Quan hệ kiêm nhiệm hội đồng quản trị đóng vai trò kênh thông tin qua đó các thông lệ trả thưởng, chính sách đầu tư và chuẩn mực quản trị khuếch tán. Các mạng lưới chuỗi cung ứng lan truyền các cú sốc kinh tế thực: hiện tượng bất thường động lượng khách hàng cho thấy giá thị trường phản ánh thông tin chuỗi cung ứng với độ trễ có thể dự báo. Các mạng lưới tương quan nắm bắt cấu trúc đồng biến động vừa cho thấy các cụm theo ngành vừa tiến triển qua các chế độ thị trường. Các mạng lưới liên ngân hàng quyết định liệu sự sụp đổ của từng ngân hàng có lan thành khủng hoảng hệ thống hay không. Và các mạng lưới xã hội giữa các địa phương mang thông tin mà các kênh chính thức bỏ sót: L. Allen, Peng, và Shan (2026) cho thấy các mối quan hệ giữa các cộng đồng mở rộng cả cầu lẫn cung tín dụng trực tuyến, nhất là ở các cộng đồng thiệt thòi, mà không làm tăng nợ quá hạn, điều này khiến Chỉ số Kết nối Xã hội trở thành công cụ đo lường có giá trị đặc biệt trong một thị trường mà thông tin tín dụng chính thức còn mỏng.

Thị trường Việt Nam có mật độ mạng lưới đặc biệt cao vì vai trò phổ biến của sở hữu nhà nước, vị thế nổi bật của các tập đoàn và sự tập trung của hệ thống ngân hàng. Khoảng cách giữa quyền kiểm soát và quyền dòng tiền (tức là chênh lệch giữa quyền kiểm soát và quyền dòng tiền do sở hữu kim tự tháp tạo ra) lớn hơn và biến động nhiều hơn so với các thị trường có mức bảo vệ cổ đông thiểu số mạnh hơn. Khoảng cách này, chỉ có thể tính được thông qua phân tích mạng lưới của các chuỗi sở hữu, vừa là thước đo rủi ro quản trị vừa là yếu tố dự báo giá trị doanh nghiệp.

Các mạng nơ-ron đồ thị mở rộng bộ công cụ từ các đặc trưng mạng lưới thủ công sang các biểu diễn được học. Các kiến trúc GCN và GAT tổng hợp thông tin trên các vùng lân cận mạng lưới, và GNN theo thời gian nắm bắt sự tiến triển của cấu trúc mạng lưới theo thời gian. Với dự báo lợi nhuận, câu hỏi thực nghiệm là liệu khả năng của GNN trong việc học các hàm linh hoạt của cấu trúc đồ thị có vượt trội so với cách tiếp cận đơn giản hơn là tính các thước đo độ trung tâm rồi đưa vào các mô hình tiêu chuẩn hay không. Các bài tập cung cấp một khung để trả lời câu hỏi này một cách chặt chẽ.

Góc nhìn mạng lưới không phải là sự thay thế cho các phương pháp định giá tài sản hay tài chính doanh nghiệp tiêu chuẩn; nó là một sự bổ sung. Các thiết kế nghiên cứu hiệu quả nhất kết hợp cấu trúc mạng lưới với các chiến lược nhận diện từ bộ công cụ suy luận nhân quả: sử dụng các cú sốc mạng lưới làm biến công cụ, khai thác các gián đoạn mạng lưới như các thí nghiệm tự nhiên, và kiểm soát vị trí trong mạng lưới khi ước lượng các tác động can thiệp. Giao điểm của mạng lưới và suy luận nhân quả (chẳng hạn, nhiễu chéo trong mạng lưới (network interference), nhận diện hiệu ứng ngang hàng, kinh tế lượng không gian) là biên giới của tài chính thực nghiệm.