Phân tích văn bản đã nổi lên như một trong những hướng nghiên cứu hiệu quả nhất của tài chính thực nghiệm trong hai thập kỷ qua. Nhận định rằng văn bản phi cấu trúc, chẳng hạn như hồ sơ công bố thông tin của doanh nghiệp, các cuộc họp công bố kết quả kinh doanh, báo cáo phân tích và bài báo tin tức, chứa đựng thông tin có ý nghĩa kinh tế vượt ra ngoài những gì dữ liệu số có cấu trúc nắm bắt được đã làm thay đổi cách các nhà nghiên cứu và nhà thực hành hiểu về thị trường tài chính. Chương này giới thiệu toàn bộ quy trình các phương pháp phân tích văn bản áp dụng cho các doanh nghiệp niêm yết Việt Nam, đi từ các cách tiếp cận túi từ (bag-of-words) cổ điển đến các mô hình ngôn ngữ dựa trên transformer hiện đại.
Thị trường chứng khoán Việt Nam mang lại cả cơ hội lẫn thách thức đặc thù cho phân tích văn bản. Tính đến năm 2024, Sở Giao dịch Chứng khoán Thành phố Hồ Chí Minh (HOSE) và Sở Giao dịch Chứng khoán Hà Nội (HNX) cùng niêm yết hơn 1.600 chứng khoán với tổng vốn hóa thị trường vượt 6.000 nghìn tỷ VND (khoảng 240 tỷ USD). Các bản công bố thông tin của doanh nghiệp được lập bằng tiếng Việt, một ngôn ngữ có thanh điệu với cấu tạo từ ghép, đòi hỏi các công cụ xử lý ngôn ngữ tự nhiên (NLP) chuyên biệt.
Chúng ta xây dựng dựa trên những đóng góp nền tảng của Loughran và McDonald (2011) về từ điển cảm xúc theo lĩnh vực chuyên biệt, của Hoberg và Phillips (2016) về phân loại ngành dựa trên văn bản, và cuộc cách mạng học sâu hiện đại do Devlin và c.s. (2019) khởi xướng. Chương này bao gồm các chủ đề sau:
Xây dựng tập hợp các doanh nghiệp niêm yết trên HOSE/HNX và thu thập phần mô tả hoạt động kinh doanh cùng văn bản báo cáo thường niên của họ.
Tiền xử lý văn bản đặc thù cho tiếng Việt, bao gồm tách từ bằng VnCoreNLP và underthesea.
Biểu diễn tài liệu cổ điển bằng bag-of-words, TF-IDF và các mô hình chủ đề LDA.
Phân tích cảm xúc tài chính bằng cả hai cách tiếp cận dựa trên từ điển và học máy được điều chỉnh cho tiếng Việt.
Đo lường mức độ tương đồng giữa các doanh nghiệp dựa trên văn bản và xác định nhóm doanh nghiệp cùng ngành bằng độ tương đồng cosine.
Các cách tiếp cận học sâu hiện đại, bao gồm Word2Vec, Doc2Vec, vector nhúng PhoBERT và sentence transformer.
Các ứng dụng của mô hình ngôn ngữ lớn (LLM), bao gồm phân loại zero-shot, nhận dạng thực thể có tên và trích xuất thông tin bằng các mô hình hỗ trợ tiếng Việt.
Các ứng dụng thực nghiệm liên kết các thước đo văn bản với lợi nhuận cổ phiếu, biến động và các sự kiện doanh nghiệp.
45.1 Vì sao cần phân tích văn bản cho tài chính Việt Nam?
Thị trường tài chính Việt Nam có một số đặc điểm khiến phân tích văn bản đặc biệt có giá trị. Thứ nhất, mức độ bao phủ của các nhà phân tích còn thưa thớt (dưới 30% số doanh nghiệp niêm yết được các nhà phân tích bên bán theo dõi thường xuyên), khiến các nguồn thông tin thay thế trở nên then chốt. Thứ hai, môi trường pháp lý đang thay đổi nhanh chóng, với việc Ủy ban Chứng khoán Nhà nước (SSC) liên tục cập nhật các yêu cầu công bố thông tin, tạo ra sự biến thiên phong phú của môi trường thông tin giữa các doanh nghiệp và theo thời gian. Thứ ba, thị trường chịu sự chi phối của nhà đầu tư cá nhân (chiếm khoảng 80% khối lượng giao dịch), những người có thể xử lý thông tin văn bản khác với nhà đầu tư tổ chức, từ đó tạo ra khả năng định giá sai mà các chiến lược dựa trên văn bản có thể khai thác.
From a methodological standpoint, Vietnamese poses interesting NLP challenges. Unlike English, Vietnamese is an isolating language where word boundaries are not always delimited by spaces. A single Vietnamese “word” may consist of multiple syllables separated by spaces (e.g., “công ty” for “company,” “thị trường” for “market”). This requires a word segmentation step before standard NLP pipelines can be applied.1
46 Tổng quan tài liệu
46.1 Phân tích văn bản trong tài chính
Việc áp dụng phân tích văn bản vào dữ liệu tài chính có một lịch sử phong phú. Tetlock (2007) chứng minh rằng mức độ bi quan trong một chuyên mục của Wall Street Journal dự báo hoạt động của thị trường nói chung, cung cấp bằng chứng sớm cho thấy nội dung văn bản làm dịch chuyển giá. Loughran và McDonald (2011) cho thấy từ điển cảm xúc Harvard General Inquirer được sử dụng rộng rãi cho ra kết quả sai lệch khi áp dụng vào văn bản tài chính, vì những từ như “liability” (nợ phải trả), “tax” (thuế) và “capital” (vốn) được xếp vào nhóm tiêu cực trong tiếng Anh thông thường nhưng lại mang sắc thái trung tính, thậm chí tích cực trong tài chính. Các danh sách từ theo lĩnh vực chuyên biệt của họ đã trở thành chuẩn mực cho phân tích cảm xúc tài chính.2
Hoberg và Phillips (2010) và Hoberg và Phillips (2016) là những người tiên phong sử dụng phần mô tả sản phẩm trong báo cáo 10-K để xây dựng các phân loại ngành dựa trên văn bản (TNIC), chứng minh rằng các định nghĩa ngành động, riêng cho từng doanh nghiệp này giải thích hành vi doanh nghiệp tốt hơn các mã SIC và NAICS tĩnh, bao gồm khả năng sinh lời, lợi nhuận cổ phiếu và hoạt động M&A. Công trình tiếp theo của Hoberg và Phillips (2018) mở rộng cách tiếp cận này để đánh giá các mối đe dọa cạnh tranh và tính lưu động của thị trường sản phẩm (product-market fluidity).
Các nghiên cứu gần đây hơn tận dụng những tiến bộ của học sâu. Huang, Wang, và Yang (2023) áp dụng các mô hình dựa trên BERT vào bản ghi các cuộc họp công bố kết quả kinh doanh và cho thấy các vector nhúng theo ngữ cảnh nắm bắt được thông tin về lợi nhuận kế toán tương lai mà các thước đo bag-of-words truyền thống bỏ sót. Jha và c.s. (2024) sử dụng các mô hình dựa trên GPT để phân loại văn bản tài chính theo kiểu zero-shot và chứng minh rằng các LLM có thể sánh ngang hoặc vượt các bộ phân loại chuyên dụng trên những bộ kiểm chuẩn tiêu chuẩn.
Văn bản cũng có thể đo lường mức độ bất định mà nhà đầu tư phải đối mặt, chứ không chỉ sắc thái của nó. Wei và Zhang (2026) trích xuất một thước đo mơ hồ dựa trên diễn ngôn từ các bài báo tin tức kinh tế, khác biệt với các biến đại diện cho sự mơ hồ dựa trên khảo sát và dựa trên lợi nhuận, và cho thấy nó dự báo lợi nhuận thị trường chứng khoán tổng thể cả trong mẫu lẫn ngoài mẫu. Nhà đầu tư nhìn chung e ngại sự mơ hồ (ambiguity averse), nhưng sự chấp nhận mơ hồ xuất hiện khi xác suất thua lỗ cao, và việc kết hợp sự mơ hồ diễn ngôn theo từng ngành (chủ yếu từ các tin tức liên quan đến tiêu dùng, năng lượng và công nghệ) cho ra một biến dự báo thị trường mạnh hơn. Khả năng dự báo này mở rộng sang các thị trường quốc tế và các lớp tài sản khác, khiến cách tiếp cận này trở thành ứng viên tự nhiên cho các tin tức tài chính của Việt Nam.
46.2 NLP cho tiếng Việt
NLP tiếng Việt đã tiến bộ đáng kể nhờ sự ra đời của VnCoreNLP (Vu và c.s. 2018), một bộ công cụ nền Java cung cấp các chức năng tách từ, gán nhãn từ loại, nhận dạng thực thể có tên và phân tích cú pháp phụ thuộc. Thư viện underthesea cung cấp một phương án thay thế thuần Python. Quan trọng nhất đối với các ứng dụng tài chính, PhoBERT (Nguyen và Nguyen 2020) cung cấp mô hình BERT tiền huấn luyện dành riêng cho tiếng Việt trên kho ngữ liệu 20GB, đạt kết quả tốt nhất hiện nay trên nhiều tác vụ NLP tiếng Việt.
Bảng 46.1: Các nghiên cứu chính về phân tích văn bản trong tài chính
Sự mơ hồ diễn ngôn được trích xuất từ tin tức kinh tế
Sự mơ hồ dựa trên tin tức dự báo lợi nhuận thị trường vượt trội so với các thước đo dựa trên khảo sát và lợi nhuận; ác cảm chuyển thành chấp nhận khi xác suất thua lỗ cao
Chỉ số mơ hồ tổng hợp từ tin tức tài chính Việt Nam để dự báo lợi nhuận VN-Index
Phân loại bằng ML + độ tương đồng nội dung của các tweet từ lãnh đạo doanh nghiệp S&P 1500
Nhà đầu tư phản ứng với các tweet tài chính của lãnh đạo thông qua cả kênh thông tin mới lẫn kênh uy tín
Các bài đăng Facebook/Zalo của lãnh đạo doanh nghiệp niêm yết Việt Nam (Mục 54.4)
47 Dữ liệu: Các doanh nghiệp niêm yết Việt Nam từ DataCore.vn
47.1 Xây dựng tập hợp doanh nghiệp
Chúng ta xây dựng tập hợp các doanh nghiệp niêm yết Việt Nam. Tập hợp này bao gồm tất cả doanh nghiệp niêm yết trên HOSE, HNX và UPCoM tính đến ngày phân tích.
Import required libraries
import pandas as pdimport numpy as npimport reimport unicodedataimport warningsimport matplotlib.pyplot as pltimport seaborn as snsfrom collections import defaultdictfrom typing import List, Dict, Tuple, Optionalwarnings.filterwarnings('ignore')np.random.seed(42)# Plotting configurationplt.rcParams['figure.figsize'] = (10, 6)plt.rcParams['font.size'] =11sns.set_style("whitegrid")
Connect to DataCore.vn and build universe of listed firms
Bảng 47.1: Tập hợp các doanh nghiệp niêm yết Việt Nam theo sàn giao dịch (tính đến tháng 12 năm 2024)
Sàn giao dịch
Số DN
Vốn hóa TB (tỷ VND)
Vốn hóa trung vị (tỷ VND)
Tổng vốn hóa (nghìn tỷ VND)
HOSE
403
12,847
3,215
5,177
HNX
334
2,156
687
720
UPCoM
868
1,043
298
905
Tổng
1,605
4,239
712
6,802
47.2 Thu thập phần mô tả hoạt động kinh doanh
Phần mô tả hoạt động kinh doanh của tất cả doanh nghiệp niêm yết có thể có cả bằng tiếng Việt và tiếng Anh. Chúng ta thu thập cả hai phiên bản cho phân tích. Văn bản tiếng Việt sẽ là kho ngữ liệu chính, trong khi các bản mô tả tiếng Anh là phương tiện kiểm chứng chéo hữu ích.
Retrieve business descriptions.
# Get business descriptions (Vietnamese and English)bus_desc = dc.get_business_descriptions( tickers=universe.ticker.tolist(), fields=['ticker', 'bus_desc_vi', 'bus_desc_en','main_business', 'products_services','year_established', 'num_employees' ])# Merge with universecorpus_df = universe.merge(bus_desc, on='ticker', how='inner')# Summary statistics on text lengthcorpus_df['desc_len_vi'] = corpus_df.bus_desc_vi.str.len()corpus_df['desc_len_en'] = corpus_df.bus_desc_en.str.len()corpus_df['word_count_vi'] = corpus_df.bus_desc_vi.str.split().str.len()print(corpus_df[['desc_len_vi', 'desc_len_en', 'word_count_vi']] .describe().round(0))
Bảng 47.2: Thống kê mô tả của văn bản mô tả hoạt động kinh doanh
Thống kê
Trung bình
Trung vị
Độ lệch chuẩn
Nhỏ nhất
Lớn nhất
Ký tự (VN)
2,847
2,156
1,923
87
18,432
Ký tự (EN)
3,412
2,689
2,245
102
22,156
Từ (VN)
487
372
318
15
3,216
47.3 Thu thập văn bản báo cáo thường niên
Beyond business descriptions, annual or quarterly reports provide richer and more time-varying textual data. We extract the Management Discussion and Analysis (MD&A) sections, which are most informative for financial analysis (Li và c.s. 2010; Bonsall IV và c.s. 2017). The MD&A section, known in Vietnamese annual reports as “Báo cáo của Ban Giám đốc” or “Báo cáo của Hội đồng quản trị,” discusses business performance, outlook, and risk factors.
Retrieve annual report MD&A sections
# Get annual report MD&A sections (2015-2024)annual_text = dc.get_annual_report_text( tickers=universe.ticker.tolist(), years=range(2015, 2025), sections=['mda', 'risk_factors', 'business_overview'], language='vi')# Panel structure: ticker x year x sectionprint(f'Total firm-year-section observations: {len(annual_text)}')print(f'Unique firms: {annual_text.ticker.nunique()}')print(f'Year range: {annual_text.year.min()}-{annual_text.year.max()}')# Calculate text changes year-over-yearannual_text = annual_text.sort_values(['ticker', 'year'])annual_text['text_len'] = annual_text.text.str.len()annual_text['text_change_pct'] = ( annual_text.groupby('ticker')['text_len'] .pct_change() *100)
48 Tiền xử lý văn bản tiếng Việt
48.1 Tách từ tiếng Việt
The most critical preprocessing step for Vietnamese text is word segmentation (phân đoạn từ). Unlike English where spaces reliably separate words, Vietnamese uses spaces between syllables, not between words. For example, the phrase “công ty cổ phần bất động sản” (real estate joint stock company) contains five syllables separated by spaces but consists of only two compound words: “công_ty cổ_phần” (joint stock company) and “bất_động_sản” (real estate). Failing to perform word segmentation leads to severe vocabulary fragmentation and loss of semantic meaning.
Bảng 48.1: Ví dụ về tách từ tiếng Việt
Giai đoạn
Văn bản
Diễn giải
Thô
công ty cổ phần thương mại dịch vụ
8 âm tiết, ranh giới từ chưa rõ ràng
Đã tách từ
công_ty cổ_phần thương_mại dịch_vụ
4 từ: công ty | cổ phần | thương mại | dịch vụ
Vietnamese word segmentation using underthesea
from underthesea import word_tokenizedef segment_vietnamese(text: str) ->str:"""Segment Vietnamese text into words using underthesea."""if pd.isna(text) or text.strip() =='':return''# underthesea word_tokenize joins compound words with _ segmented = word_tokenize(text, format='text')return segmented# Alternative: VnCoreNLP (Java-based, higher accuracy)# from vncorenlp import VnCoreNLP# vnlp = VnCoreNLP('VnCoreNLP-1.2.jar', annotators='wseg')# segmented = vnlp.tokenize(text)# Apply segmentation to corpuscorpus_df['bus_desc_segmented'] = ( corpus_df.bus_desc_vi.apply(segment_vietnamese))# Examplesample = corpus_df.iloc[0]print('Raw:', sample.bus_desc_vi[:200])print('Segmented:', sample.bus_desc_segmented[:200])
48.2 Quy trình làm sạch văn bản đầy đủ
After word segmentation, we apply a cleaning pipeline. The pipeline handles Vietnamese-specific challenges including: diacritical mark normalization (e.g., hoà vs hòa), removal of HTML artifacts from scraped text, Vietnamese stopword removal, and lemmatization (which for Vietnamese primarily involves handling reduplicative words and synonym normalization).
Full Vietnamese text cleaning pipeline
# Vietnamese stopwords (domain-adapted)VIETNAMESE_STOPWORDS = {'có', 'là', 'và', 'của', 'cho', 'được', 'trong','các', 'những', 'với', 'từ', 'khi', 'hoặc','đã', 'sẽ', 'đang', 'để', 'này', 'đó','như', 'theo', 'về', 'bằng', 'tại', 'trên','cũng', 'rất', 'nhiều', 'ít', 'một', 'hai',# Financial domain stopwords'năm', 'quý', 'tháng', 'ngày', 'kỳ','việt_nam', 'tổng', 'giá_trị', 'triệu', 'tỷ',}def clean_vietnamese_text( text: str, segment: bool=True, remove_stops: bool=True, lowercase: bool=True, min_word_len: int=2) ->str:""" Full Vietnamese text cleaning pipeline. Parameters ---------- text : str Raw Vietnamese text. segment : bool Whether to perform word segmentation. remove_stops : bool Whether to remove Vietnamese stopwords. lowercase : bool Whether to convert to lowercase. min_word_len : int Minimum word length to keep. Returns ------- str Cleaned text. """if pd.isna(text) or text.strip() =='':return''# 1. Unicode normalization (NFC form for Vietnamese) text = unicodedata.normalize('NFC', text)# 2. Remove HTML tags and special characters text = re.sub(r'<[^>]+>', ' ', text) text = re.sub(r'[\d]+', ' ', text) # Remove numbers text = re.sub(r'[^\w\s\u00C0-\u024F]', ' ', text) # Keep VN chars# 3. Lowercaseif lowercase: text = text.lower()# 4. Word segmentationif segment: text = word_tokenize(text, format='text')# 5. Tokenize and filter tokens = text.split()if remove_stops: tokens = [t for t in tokensif t notin VIETNAMESE_STOPWORDSandlen(t) >= min_word_len]return' '.join(tokens)# Apply to corpuscorpus_df['text_clean'] = ( corpus_df.bus_desc_vi .apply(lambda x: clean_vietnamese_text(x)))# Verify cleaning qualityprint('Sample cleaned text:')print(corpus_df.iloc[0].text_clean[:300])
48.3 Làm sạch văn bản tiếng Anh
Đối với các doanh nghiệp cũng cung cấp phần mô tả hoạt động kinh doanh bằng tiếng Anh, chúng ta áp dụng quy trình NLP tiếng Anh tiêu chuẩn bằng spaCy và NLTK. Việc xử lý song song này cho phép kiểm chứng chéo giữa các ngôn ngữ đối với các thước đo văn bản của chúng ta.
English text cleaning pipeline
import spacyfrom nltk.corpus import stopwordsimport gensimnlp = spacy.load('en_core_web_sm', disable=['parser', 'ner'])stop_words =set(stopwords.words('english'))def clean_english_text(text: str) ->str:"""Clean English text with lemmatization."""if pd.isna(text) or text.strip() =='':return'' text = text.lower().strip() text = re.sub(r'[^a-zA-Z\s]', ' ', text) doc = nlp(text) tokens = [token.lemma_ for token in docif token.lemma_ notin stop_wordsandlen(token.lemma_) >2andnot token.is_punct]return' '.join(tokens)# Apply to English descriptionscorpus_df['text_clean_en'] = ( corpus_df.bus_desc_en .apply(lambda x: clean_english_text(x)))
49 Biểu diễn tài liệu: Bag-of-Words và TF-IDF
49.1 Biểu diễn bag-of-words
Mô hình bag-of-words (BoW) biểu diễn mỗi tài liệu dưới dạng một vector tần suất từ, bỏ qua thứ tự từ. Dù đơn giản, BoW vẫn là công cụ chủ lực trong phân tích văn bản tài chính. Về mặt hình thức, với một bộ từ vựng \(V = \{w_1, w_2, \ldots, w_{|V|}\}\), tài liệu \(d\) được biểu diễn bằng một vector \(\mathbf{x}_d\) trong đó mỗi phần tử \(x_{d,j}\) đếm tần suất của từ \(w_j\) trong tài liệu \(d\):
trong đó \(\text{tf}(w, d)\) là tần suất từ của từ \(w\) trong tài liệu \(d\).
Bag-of-Words vectorization
from sklearn.feature_extraction.text import ( CountVectorizer, TfidfVectorizer)# Vietnamese corpustext_corpus = corpus_df.text_clean.tolist()# BoW vectorizationbow_vectorizer = CountVectorizer( max_features=10000, min_df=5, # Appear in at least 5 documents max_df=0.95, # Exclude terms in >95% of docs ngram_range=(1, 2) # Unigrams and bigrams)bow_matrix = bow_vectorizer.fit_transform(text_corpus)print(f'Vocabulary size: {len(bow_vectorizer.vocabulary_)}')print(f'Document-term matrix shape: {bow_matrix.shape}')print(f'Sparsity: {1- bow_matrix.nnz / np.prod(bow_matrix.shape):.4f}')# Top 20 most frequent termsword_freq = pd.DataFrame({'word': bow_vectorizer.get_feature_names_out(),'freq': bow_matrix.sum(axis=0).A1}).sort_values('freq', ascending=False)print('\nTop 20 most frequent terms:')print(word_freq.head(20).to_string(index=False))
Plot word frequency distribution
fig, ax = plt.subplots(figsize=(12, 6))top20 = word_freq.head(20)ax.barh(range(len(top20)), top20.freq.values, color='#2C5282')ax.set_yticks(range(len(top20)))ax.set_yticklabels(top20.word.values)ax.invert_yaxis()ax.set_xlabel('Frequency')ax.set_title('Top 20 Most Frequent Terms in Vietnamese Business Descriptions')plt.tight_layout()plt.show()
Hình 49.1
Bảng 49.1: 20 thuật ngữ xuất hiện thường xuyên nhất trong phần mô tả hoạt động kinh doanh tiếng Việt
#
Term (VN)
Freq
#
Term (VN)
Freq
#
Term (VN)
Freq
1
sản_xuất
4,287
8
công_nghệ
1,956
15
xuất_khẩu
1,123
2
kinh_doanh
3,891
9
tài_chính
1,845
16
bất_động_sản
1,087
3
dịch_vụ
3,654
10
ngân_hàng
1,734
17
năng_lượng
1,045
4
công_ty
3,412
11
đầu_tư
1,623
18
bảo_hiểm
987
5
thương_mại
2,876
12
xây_dựng
1,534
19
du_lịch
923
6
cổ_phần
2,543
13
vận_tải
1,345
20
viễn_thông
876
7
chứng_khoán
2,134
14
thực_phẩm
1,234
49.2 Trọng số TF-IDF
Tần suất từ – nghịch đảo tần suất tài liệu (TF-IDF) khắc phục một hạn chế chính của số đếm từ thô bằng cách giảm trọng số của những từ xuất hiện trong nhiều tài liệu (và do đó mang ít thông tin phân biệt hơn). Trọng số TF-IDF của từ \(w\) trong tài liệu \(d\) thuộc kho ngữ liệu \(D\) là:
where \(|D|\) is the total number of documents and \(\text{df}(w, D)\) is the number of documents containing term \(w\). This weighting scheme ensures that industry-specific terminology (e.g., “khai_khoáng” for mining, “dược_phẩm” for pharmaceuticals) receives higher weight than ubiquitous corporate jargon.
TF-IDF vectorization with per-industry analysis
tfidf_vectorizer = TfidfVectorizer( max_features=10000, min_df=5, max_df=0.95, ngram_range=(1, 2), sublinear_tf=True# Use 1 + log(tf) instead of raw tf)tfidf_matrix = tfidf_vectorizer.fit_transform(text_corpus)# Per-industry top TF-IDF termsfor industry in ['Ngân hàng', 'Bất động sản','Công nghệ thông tin']: mask = corpus_df.icb_sector == industryif mask.sum() ==0:continue mean_tfidf = tfidf_matrix[mask.values].mean(axis=0).A1 top_idx = mean_tfidf.argsort()[-10:][::-1] terms = tfidf_vectorizer.get_feature_names_out()print(f'\n{industry}:')for idx in top_idx:print(f' {terms[idx]}: {mean_tfidf[idx]:.4f}')
Visualize industry-distinctive terms
# Build industry x term TF-IDF matrix for top sectorstop_sectors = corpus_df.icb_sector.value_counts().head(8).index.tolist()terms = tfidf_vectorizer.get_feature_names_out()sector_tfidf = {}for sector in top_sectors: mask = corpus_df.icb_sector == sectorif mask.sum() ==0:continue mean_tfidf = tfidf_matrix[mask.values].mean(axis=0).A1 top_idx = mean_tfidf.argsort()[-5:][::-1]for idx in top_idx:if terms[idx] notin sector_tfidf: sector_tfidf[terms[idx]] = {} sector_tfidf[terms[idx]][sector] = mean_tfidf[idx]heatmap_df = pd.DataFrame(sector_tfidf).T.fillna(0)fig, ax = plt.subplots(figsize=(14, 10))sns.heatmap(heatmap_df, annot=True, fmt='.3f', cmap='Blues', linewidths=0.5, ax=ax)ax.set_title('TF-IDF Heatmap: Industry-Distinctive Terms')ax.set_xlabel('ICB Sector')ax.set_ylabel('Term')plt.tight_layout()plt.show()
Hình 49.2
50 Mô hình hóa chủ đề
50.1 Phân bổ Dirichlet tiềm ẩn (LDA)
Phân bổ Dirichlet tiềm ẩn (Latent Dirichlet Allocation) (Blei, Ng, và Jordan 2003) là một mô hình xác suất sinh dùng để khám phá các chủ đề tiềm ẩn trong một kho ngữ liệu. Mỗi tài liệu được mô hình hóa như một hỗn hợp của các chủ đề, và mỗi chủ đề là một phân phối trên các từ. LDA đã được áp dụng rộng rãi trong tài chính để nhận diện nội dung theo chủ đề trong các báo cáo 10-K (Dyer, Lang, và Stice-Lawrence 2017), các cuộc họp công bố kết quả kinh doanh (Huang và c.s. 2018) và các bài báo tin tức (Bybee, Kelly, và Su 2023).
Quá trình sinh giả định rằng:
Với mỗi chủ đề \(k\), rút một phân phối từ \(\boldsymbol{\phi}_k \sim \text{Dir}(\beta)\).
Với mỗi tài liệu \(d\), rút một phân phối chủ đề \(\boldsymbol{\theta}_d \sim \text{Dir}(\alpha)\).
Với mỗi vị trí từ \(i\) trong tài liệu \(d\), rút một chủ đề \(z_{d,i} \sim \text{Multinomial}(\boldsymbol{\theta}_d)\) rồi rút từ \(w_{d,i} \sim \text{Multinomial}(\boldsymbol{\phi}_{z_{d,i}})\).
LDA topic modeling with grid search over K
from sklearn.decomposition import LatentDirichletAllocation# Grid search over number of topicsn_topics_range = [10, 15, 20, 25, 30]perplexity_scores = []for n_topics in n_topics_range: lda = LatentDirichletAllocation( n_components=n_topics, max_iter=50, learning_method='online', random_state=42, n_jobs=-1 ) lda.fit(bow_matrix) perplexity = lda.perplexity(bow_matrix) perplexity_scores.append({'n_topics': n_topics,'perplexity': perplexity,'log_likelihood': lda.score(bow_matrix) })print(f'K={n_topics}: perplexity={perplexity:.2f}')# Select optimal K (e.g., K=20)K_OPTIMAL =20lda_model = LatentDirichletAllocation( n_components=K_OPTIMAL, max_iter=100, learning_method='online', random_state=42, n_jobs=-1)lda_model.fit(bow_matrix)# Extract topic-word distributionsfeature_names = bow_vectorizer.get_feature_names_out()for topic_idx, topic inenumerate(lda_model.components_): top_words = [feature_names[i]for i in topic.argsort()[:-11:-1]]print(f'Topic {topic_idx}: {" | ".join(top_words)}')
Plot perplexity scores for topic model selection
perp_df = pd.DataFrame(perplexity_scores)fig, ax = plt.subplots(figsize=(8, 5))ax.plot(perp_df.n_topics, perp_df.perplexity, 'o-', color='#2C5282', linewidth=2, markersize=8)ax.axvline(x=K_OPTIMAL, color='red', linestyle='--', alpha=0.7, label=f'Selected K={K_OPTIMAL}')ax.set_xlabel('Number of Topics (K)')ax.set_ylabel('Perplexity (lower is better)')ax.set_title('LDA Model Selection')ax.legend()plt.tight_layout()plt.show()
Hình 50.1
Bảng 50.1: Một số chủ đề LDA được chọn từ phần mô tả hoạt động kinh doanh tiếng Việt (K=20)
50.2 BERTopic: Mô hình hóa chủ đề bằng mạng nơ-ron
BERTopic (Grootendorst 2022) là một bước tiến đáng kể so với LDA nhờ tận dụng vector nhúng của mô hình ngôn ngữ tiền huấn luyện, giảm chiều bằng UMAP và phân cụm theo mật độ phân cấp (HDBSCAN) để khám phá các chủ đề. Khác với LDA, BERTopic nắm bắt sự tương đồng ngữ nghĩa thay vì chỉ dựa vào sự đồng xuất hiện của các từ, tạo ra các chủ đề mạch lạc hơn, đặc biệt đối với các lĩnh vực chuyên biệt.
BERTopic with PhoBERT embeddings
from bertopic import BERTopicfrom sentence_transformers import SentenceTransformerfrom umap import UMAPfrom hdbscan import HDBSCAN# Use PhoBERT-based sentence transformer for Vietnameseembedding_model = SentenceTransformer('bkai-foundation-models/vietnamese-bi-encoder')# Custom UMAP and HDBSCAN for better controlumap_model = UMAP( n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine', random_state=42)hdbscan_model = HDBSCAN( min_cluster_size=10, min_samples=5, metric='euclidean', prediction_data=True)# Fit BERTopictopic_model = BERTopic( embedding_model=embedding_model, umap_model=umap_model, hdbscan_model=hdbscan_model, language='multilingual', calculate_probabilities=True, verbose=True)# Vietnamese text (use segmented text for better results)docs = corpus_df.bus_desc_segmented.tolist()topics, probs = topic_model.fit_transform(docs)# Inspect topicstopic_info = topic_model.get_topic_info()print(topic_info.head(20))
Chúng ta xây dựng một từ điển cảm xúc tài chính tiếng Việt theo phương pháp của Loughran và McDonald (2011). Thay vì dịch trực tiếp từ điển LM tiếng Anh (cách này sẽ bỏ sót các cách diễn đạt tài chính đặc thù của Việt Nam), chúng ta áp dụng cách tiếp cận kết hợp: (1) dịch các danh sách từ cốt lõi của LM bằng các biên dịch viên tài chính chuyên nghiệp, (2) bổ sung thủ công các từ được tuyển chọn từ quy định tài chính của Việt Nam, các chuẩn mực kế toán (VAS) và các bình luận thị trường, và (3) kiểm định từ điển thu được trên văn bản tài chính tiếng Việt do con người gán nhãn.
Bảng 51.1: Từ điển cảm xúc tài chính tiếng Việt: các mục mẫu
Loại
Thuật ngữ tiếng Việt
Nghĩa tiếng Anh
Nguồn
Số lần xuất hiện trong kho ngữ liệu
Tiêu cực
lỗ
loss
Dịch từ LM
2,341
Tiêu cực
sụt_giảm
decline
Tuyển chọn thủ công
1,876
Tiêu cực
nợ_xấu
bad debt
Đặc thù VAS
1,234
Tiêu cực
rủi_ro
risk
Dịch từ LM
3,567
Tích cực
tăng_trưởng
growth
Dịch từ LM
4,123
Tích cực
lợi_nhuận
profit
Dịch từ LM
3,891
Tích cực
hiệu_quả
efficiency
Tuyển chọn thủ công
2,456
Bất định
biến_động
volatility
Dịch từ LM
1,567
Tranh chấp pháp lý
tranh_chấp
dispute
Pháp lý VN
876
Tranh chấp pháp lý
khởi_kiện
lawsuit
Pháp lý VN
234
Dictionary-based financial sentiment scoring
# Load Vietnamese financial sentiment lexicon# sentiment_dict = dc.get_sentiment_lexicon(version='vn_financial_v2')# Alternatively, construct from LM + manual curationnegative_words =set(pd.read_csv('lexicons/vn_negative.txt', header=None)[0])positive_words =set(pd.read_csv('lexicons/vn_positive.txt', header=None)[0])uncertain_words =set(pd.read_csv('lexicons/vn_uncertain.txt', header=None)[0])def compute_sentiment_scores(text: str) ->dict:""" Compute Loughran-McDonald style sentiment scores. Returns proportions (word count / total words). """ tokens = text.split() n =len(tokens)if n ==0:return {'neg_pct': 0, 'pos_pct': 0,'unc_pct': 0, 'net_tone': 0} neg =sum(1for t in tokens if t in negative_words) pos =sum(1for t in tokens if t in positive_words) unc =sum(1for t in tokens if t in uncertain_words)return {'neg_pct': neg / n,'pos_pct': pos / n,'unc_pct': unc / n,'net_tone': (pos - neg) / n }# Apply to annual report MD&A textsentiment_scores = annual_text.text_clean.apply(lambda x: pd.Series(compute_sentiment_scores(x)))annual_text = pd.concat([annual_text, sentiment_scores], axis=1)
Plot sentiment distribution
fig, axes = plt.subplots(1, 3, figsize=(15, 5))axes[0].hist(annual_text.neg_pct, bins=50, color='#E53E3E', alpha=0.7, edgecolor='white')axes[0].set_title('Negative Word Proportion')axes[0].set_xlabel('Proportion')axes[1].hist(annual_text.pos_pct, bins=50, color='#38A169', alpha=0.7, edgecolor='white')axes[1].set_title('Positive Word Proportion')axes[1].set_xlabel('Proportion')axes[2].hist(annual_text.net_tone, bins=50, color='#2C5282', alpha=0.7, edgecolor='white')axes[2].set_title('Net Tone (Positive - Negative)')axes[2].set_xlabel('Net Tone')plt.suptitle('Sentiment Distribution in Vietnamese Annual Reports', fontsize=14, fontweight='bold')plt.tight_layout()plt.show()
Hình 51.1
51.2 Phân loại cảm xúc dựa trên Transformer
Các cách tiếp cận dựa trên từ điển bị hạn chế vì không nắm bắt được ngữ cảnh, phủ định và mỉa mai. Chúng ta bổ sung cho cách tiếp cận từ điển bằng phân loại cảm xúc dựa trên PhoBERT. Chúng ta tinh chỉnh PhoBERT v2.
PhoBERT-based sentiment classification
from transformers import ( AutoModelForSequenceClassification, AutoTokenizer, pipeline)import torch# Load fine-tuned ViFinBERT for sentimentmodel_name ='vinai/phobert-base-v2'tokenizer = AutoTokenizer.from_pretrained(model_name)model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=3# positive, negative, neutral)# Create sentiment pipelinesentiment_pipe = pipeline('text-classification', model=model, tokenizer=tokenizer, device=0if torch.cuda.is_available() else-1, max_length=256, truncation=True, batch_size=32)# For long documents, split into sentences firstfrom underthesea import sent_tokenizedef document_sentiment(text: str) ->dict:"""Aggregate sentence-level sentiment for a document.""" sentences = sent_tokenize(text)ifnot sentences:return {'bert_pos': 0, 'bert_neg': 0, 'bert_neu': 0} results = sentiment_pipe(sentences[:100]) # Cap at 100 sents labels = [r['label'] for r in results] n =len(labels)return {'bert_pos': labels.count('POSITIVE') / n,'bert_neg': labels.count('NEGATIVE') / n,'bert_neu': labels.count('NEUTRAL') / n,'bert_tone': (labels.count('POSITIVE') - labels.count('NEGATIVE')) / n }
Bảng 51.2: So sánh các phương pháp cảm xúc: từ điển so với PhoBERT trên tập kiểm định (N=500)
Phương pháp
Độ chính xác
F1 (Tích cực)
F1 (Tiêu cực)
F1 (Trung lập)
Từ điển VN-LM
0.612
0.584
0.637
0.598
PhoBERT (zero-shot)
0.724
0.698
0.741
0.712
PhoBERT v2 (đã tinh chỉnh)
0.831
0.812
0.847
0.824
52 Độ tương đồng giữa các doanh nghiệp dựa trên văn bản và xác định nhóm cùng ngành
52.1 Độ tương đồng cosine trên các vector TF-IDF
Theo Hoberg và Phillips (2016), chúng ta tính độ tương đồng cosine theo từng cặp giữa các doanh nghiệp dựa trên vector TF-IDF của phần mô tả hoạt động kinh doanh. Với hai tài liệu được biểu diễn bằng các vector TF-IDF \(\mathbf{a}\) và \(\mathbf{b}\), độ tương đồng cosine được định nghĩa là:
Thước đo này nhận giá trị từ 0 (hoàn toàn khác biệt) đến 1 (nội dung giống hệt) và bất biến với độ dài tài liệu. Chúng ta dùng nó để xây dựng các mạng lưới ngành dựa trên văn bản (TNIC) cho thị trường Việt Nam, vốn có thể nắm bắt các mối quan hệ giữa doanh nghiệp mà các mã ngành ICB tĩnh bỏ sót.
Pairwise TF-IDF cosine similarity
from sklearn.metrics.pairwise import cosine_similarity# Compute pairwise similarity matrixsim_matrix = cosine_similarity(tfidf_matrix)# Convert to DataFrame for easy lookuptickers = corpus_df.ticker.tolist()sim_df = pd.DataFrame( sim_matrix, index=tickers, columns=tickers)# For each firm, find top-5 most similar peersdef get_top_peers(ticker: str, n: int=5) -> pd.DataFrame:"""Return top-n most similar firms by TF-IDF cosine.""" sims = sim_df[ticker].drop(ticker).sort_values( ascending=False ).head(n) peers = corpus_df.set_index('ticker').loc[sims.index] peers['similarity'] = sims.valuesreturn peers[['company_name', 'icb_sector','market_cap', 'similarity']]# Examplesfor ticker in ['VCB', 'VNM', 'FPT', 'VIC', 'HPG']:print(f'\nTop peers for {ticker}:')print(get_top_peers(ticker))
Bảng 52.1: Xác định nhóm cùng ngành dựa trên văn bản: các doanh nghiệp tương đồng nhất (cosine TF-IDF)
Trong khi độ tương đồng cosine TF-IDF nắm bắt sự trùng lặp về từ vựng, nó bỏ sót sự tương đồng về ngữ nghĩa. Hai doanh nghiệp có thể mô tả những hoạt động kinh doanh tương tự bằng các từ vựng khác nhau. Chúng ta giải quyết vấn đề này bằng các biểu diễn vector dày đặc (dense) từ các mô hình ngôn ngữ tiền huấn luyện. Cụ thể, chúng ta tính vector nhúng tài liệu bằng Sentence-BERT (Reimers và Gurevych 2019) với một mô hình bi-encoder tiếng Việt.3
Chúng ta cũng triển khai Doc2Vec (Le và Mikolov 2014), phương pháp học các vector dày đặc có độ dài cố định cho các tài liệu có độ dài biến thiên. Khác với việc lấy trung bình các vector nhúng từ, Doc2Vec học đồng thời vector tài liệu và vector từ, cho phép nắm bắt ngữ nghĩa ở cấp độ tài liệu. Chúng ta huấn luyện Doc2Vec trên kho ngữ liệu mô tả hoạt động kinh doanh tiếng Việt bằng cách tiếp cận DBOW+DM nối ghép do Lau và Baldwin (2016) khuyến nghị.
Doc2Vec for firm similarity (DBOW + DM ensemble)
from gensim.models.doc2vec import Doc2Vec, TaggedDocument# Prepare tagged documentstagged_docs = [ TaggedDocument( words=text.split(), tags=[ticker] )for text, ticker inzip( corpus_df.text_clean.tolist(), corpus_df.ticker.tolist() )]# PV-DBOW: paragraph vector with distributed bag of wordsd2v_dbow = Doc2Vec( vector_size=100, dm=0, min_count=5, window=5, epochs=40, workers=4, seed=42)d2v_dbow.build_vocab(tagged_docs)d2v_dbow.train( tagged_docs, total_examples=d2v_dbow.corpus_count, epochs=d2v_dbow.epochs)# PV-DM: paragraph vector with distributed memoryd2v_dm = Doc2Vec( vector_size=100, dm=1, min_count=5, window=10, epochs=40, workers=4, seed=42)d2v_dm.build_vocab(tagged_docs)d2v_dm.train( tagged_docs, total_examples=d2v_dm.corpus_count, epochs=d2v_dm.epochs)# Concatenate DBOW + DM vectors (Lau & Baldwin, 2016)d2v_vectors = np.hstack([ [d2v_dbow.dv[t] for t in tickers], [d2v_dm.dv[t] for t in tickers]])# Most similar firmsfor ticker in ['VCB', 'FPT', 'VIC']: sims = d2v_dbow.dv.most_similar(ticker, topn=5)print(f'{ticker}: {[(s[0], f"{s[1]:.3f}") for s in sims]}')
53 Các cách tiếp cận học sâu
53.1 Vector nhúng PhoBERT cho văn bản tài chính
PhoBERT (Nguyen và Nguyen 2020), pre-trained on 20GB of Vietnamese text, provides contextualized word embeddings that capture meaning based on surrounding context. Unlike static Word2Vec embeddings where “bảo” always has the same vector regardless of whether it means “insurance” (bảo hiểm) or “protect” (bảo vệ), PhoBERT produces context-dependent representations. We extract [CLS] token embeddings as document representations.
PhoBERT document embeddings with chunking strategy
from transformers import AutoModel, AutoTokenizerimport torch# Load PhoBERTphobert_tokenizer = AutoTokenizer.from_pretrained('vinai/phobert-base-v2')phobert_model = AutoModel.from_pretrained('vinai/phobert-base-v2')phobert_model.eval()device = torch.device('cuda'if torch.cuda.is_available()else'cpu')phobert_model.to(device)def get_phobert_embedding(text: str, max_len: int=256):"""Extract [CLS] embedding from PhoBERT.""" inputs = phobert_tokenizer( text, return_tensors='pt', max_length=max_len, truncation=True, padding=True ).to(device)with torch.no_grad(): outputs = phobert_model(**inputs)# [CLS] token embedding cls_embedding = outputs.last_hidden_state[:, 0, :]return cls_embedding.cpu().numpy().flatten()# For long documents: chunk + average strategydef get_long_doc_embedding( text: str, chunk_size: int=256, stride: int=128):"""Handle long documents via chunked averaging.""" tokens = phobert_tokenizer.tokenize(text) embeddings = []for i inrange(0, len(tokens), stride): chunk = tokens[i:i + chunk_size] chunk_text = phobert_tokenizer.convert_tokens_to_string( chunk ) emb = get_phobert_embedding(chunk_text) embeddings.append(emb)return np.mean(embeddings, axis=0)# Compute embeddings for all firmsphobert_embeddings = np.array([ get_long_doc_embedding(text)for text in corpus_df.bus_desc_segmented.tolist()])
53.2 Các ứng dụng của mô hình ngôn ngữ lớn
Những tiến bộ gần đây của các LLM mở ra những khả năng mới cho phân tích văn bản tài chính. Chúng ta trình bày ba ứng dụng sử dụng các LLM hỗ trợ tiếng Việt: phân loại văn bản tài chính zero-shot, trích xuất thông tin có cấu trúc từ báo cáo thường niên, và chấm điểm ESG tự động từ các bản công bố thông tin của doanh nghiệp.
53.2.1 Phân loại tài chính zero-shot
LLM zero-shot financial text classification
import anthropic # Or openai, etc.client = anthropic.Anthropic()def classify_financial_text( text: str, categories: list= ['Growth outlook', 'Risk warning','Operational update', 'Financial performance','Strategic initiative', 'Regulatory compliance' ]) ->dict:"""Zero-shot classify Vietnamese financial text.""" prompt =f""" Classify the following Vietnamese financial text into one or more of these categories: {categories} Also provide: 1. Sentiment: positive / negative / neutral 2. Confidence: 0-1 3. Key entities mentioned Text: {text[:2000]} Respond in JSON format. """ response = client.messages.create( model='claude-sonnet-4-20250514', max_tokens=500, messages=[{'role': 'user', 'content': prompt}] )return response.content[0].text
53.2.2 Trích xuất thông tin có cấu trúc
LLM structured information extraction from annual reports
import jsondef extract_financial_info(annual_report_text: str) ->dict:"""Extract structured data from Vietnamese annual report.""" prompt =f""" From the following Vietnamese annual report excerpt, extract structured information in JSON format:{{ "revenue_mentioned": true/false, "revenue_direction": "increase"/"decrease"/"stable", "key_products": [list of main products/services], "competitors_mentioned": [list], "expansion_plans": "description or null", "risk_factors": [list of mentioned risks], "esg_mentions": {{ "environmental": [topics], "social": [topics], "governance": [topics]}}, "forward_looking_statements": [list], "capex_plans": "description or null"}} Text: {annual_report_text[:3000]} """ response = client.messages.create( model='claude-sonnet-4-20250514', max_tokens=1000, messages=[{'role': 'user', 'content': prompt}] )return json.loads(response.content[0].text)
53.2.3 Chấm điểm ESG tự động
LLM-based ESG scoring from corporate disclosures
def compute_esg_scores(text: str) ->dict:"""Score ESG dimensions from Vietnamese corporate disclosure.""" prompt =f""" Analyze the following Vietnamese corporate disclosure text and score each ESG dimension on a scale of 0-100 based on the depth and quality of disclosure: Return JSON:{{ "environmental_score": 0-100, "environmental_topics": [list of specific topics discussed], "social_score": 0-100, "social_topics": [list], "governance_score": 0-100, "governance_topics": [list], "overall_esg_score": 0-100, "assessment_confidence": 0-1, "notable_commitments": [list of specific commitments], "gaps_identified": [list of missing ESG disclosures]}} Text: {text[:4000]} """ response = client.messages.create( model='claude-sonnet-4-20250514', max_tokens=800, messages=[{'role': 'user', 'content': prompt}] )return json.loads(response.content[0].text)# Apply to all firms' annual reportsesg_results = []for _, row in annual_text.iterrows():try: scores = compute_esg_scores(row.text) scores['ticker'] = row.ticker scores['year'] = row.year esg_results.append(scores)exceptExceptionas e:print(f"Error for {row.ticker}{row.year}: {e}")esg_df = pd.DataFrame(esg_results)
54 Các ứng dụng thực nghiệm
54.1 Cảm xúc văn bản và lợi nhuận cổ phiếu
Chúng ta kiểm tra xem cảm xúc văn bản từ báo cáo thường niên có dự báo lợi nhuận cổ phiếu tiếp theo hay không, theo phương pháp của Tetlock, Saar-Tsechansky, và Macskassy (2008). Chúng ta hồi quy lợi nhuận cổ phiếu hằng tháng theo các thước đo cảm xúc có độ trễ, đồng thời kiểm soát các nhân tố rủi ro tiêu chuẩn (thị trường, quy mô, giá trị, động lượng) được điều chỉnh cho thị trường Việt Nam:
trong đó \(R_{i,t}\) là lợi nhuận vượt mức hằng tháng của doanh nghiệp \(i\) trong tháng \(t\), \(\text{Tone}\) là điểm cảm xúc ròng (tỷ lệ từ tích cực trừ tỷ lệ từ tiêu cực), \(\text{Uncertainty}\) là tỷ lệ các từ bất định, và \(\mathbf{X}\) là vector các biến kiểm soát, bao gồm các nhân tố Fama-French-Carhart được điều chỉnh cho Việt Nam (xem chương về các mô hình nhân tố).
Bảng 54.1: Cảm xúc văn bản và lợi nhuận cổ phiếu: kết quả hồi quy dữ liệu bảng
Biến
(1) Từ điển
(2) PhoBERT
(3) Kết hợp
Sắc thái ròng (từ điển)
0.0234**
0.0187*
(0.0098)
(0.0102)
Bất định (từ điển)
−0.0312***
−0.0278**
(0.0087)
(0.0091)
Sắc thái BERT
0.0456***
0.0389***
(0.0112)
(0.0118)
MKT-RF
0.9123***
0.9118***
0.9115***
(0.0234)
(0.0233)
(0.0234)
SMB
0.1245**
0.1238**
0.1241**
(0.0456)
(0.0455)
(0.0456)
HML
0.0876*
0.0871*
0.0873*
(0.0512)
(0.0511)
(0.0512)
Hiệu ứng cố định doanh nghiệp
Có
Có
Có
Hiệu ứng cố định thời gian
Có
Có
Có
Phân cụm sai số chuẩn
Hai chiều
Hai chiều
Hai chiều
N
12,456
12,456
12,456
R² (trong nhóm)
0.142
0.148
0.153
54.2 Phân loại ngành dựa trên văn bản
Chúng ta xây dựng các Mạng lưới ngành dựa trên văn bản của Việt Nam (VN-TNIC) tương tự Hoberg và Phillips (2016). Với mỗi quan sát doanh nghiệp-năm, chúng ta xác định tập hợp các doanh nghiệp có độ tương đồng cosine vượt ngưỡng \(\tau\) làm nhóm doanh nghiệp cùng ngành dựa trên văn bản của doanh nghiệp đó. Sau đó chúng ta so sánh sức giải thích của VN-TNIC với các mã ngành ICB đối với nhiều kết quả tài chính khác nhau.
VN-TNIC construction and evaluation
# Construct TNIC networkTAU =0.20# Similarity thresholdtnic_edges = []for i inrange(len(tickers)):for j inrange(i+1, len(tickers)): sim = sim_matrix[i, j]if sim >= TAU: tnic_edges.append({'firm1': tickers[i],'firm2': tickers[j],'similarity': sim })tnic_df = pd.DataFrame(tnic_edges)print(f'TNIC edges (tau={TAU}): {len(tnic_df)}')print(f'Avg degree: {2*len(tnic_df)/len(tickers):.1f}')# Compare TNIC vs ICB for return comovementfrom linearmodels.asset_pricing import FamaMacBeth# Peer return = avg return of TNIC peers# vs ICB sector average returndef compute_tnic_peer_return(group, tnic_edges_df):"""Compute average return of TNIC peers for each firm.""" peer_returns = {}for ticker in group.index: peers = tnic_edges_df[ (tnic_edges_df.firm1 == ticker) | (tnic_edges_df.firm2 == ticker) ] peer_tickers =set( peers.firm1.tolist() + peers.firm2.tolist() ) - {ticker} peer_mask = group.index.isin(peer_tickers)if peer_mask.sum() >0: peer_returns[ticker] = group.loc[peer_mask, 'ret'].mean()else: peer_returns[ticker] = np.nanreturn pd.Series(peer_returns)panel['icb_peer_ret'] = panel.groupby( ['date', 'icb_sector'])['ret'].transform('mean')
Visualize TNIC network
import networkx as nx# Build network graph (subsample for visualization)G = nx.Graph()sample_edges = tnic_df.nlargest(500, 'similarity')for _, row in sample_edges.iterrows(): G.add_edge(row.firm1, row.firm2, weight=row.similarity)# Color by ICB sectorsector_map = corpus_df.set_index('ticker')['icb_sector'].to_dict()node_colors = [hash(sector_map.get(n, 'Unknown')) %10for n in G.nodes()]fig, ax = plt.subplots(figsize=(14, 12))pos = nx.spring_layout(G, k=0.5, seed=42)edges = G.edges(data=True)weights = [e[2]['weight'] *3for e in edges]nx.draw_networkx_nodes(G, pos, node_size=100, node_color=node_colors, cmap='tab10', alpha=0.7, ax=ax)nx.draw_networkx_edges(G, pos, width=weights, alpha=0.3, edge_color='gray', ax=ax)nx.draw_networkx_labels(G, pos, font_size=6, ax=ax)ax.set_title('VN-TNIC Network (Top 500 Edges by Similarity)')ax.axis('off')plt.tight_layout()plt.show()
Hình 54.1
54.3 Đo lường sự thay đổi độ tương đồng văn bản xung quanh các sự kiện doanh nghiệp
Chúng ta xem xét độ tương đồng văn bản của các doanh nghiệp thay đổi như thế nào xung quanh các sự kiện doanh nghiệp lớn như thông báo M&A, tái phân loại ngành và các chuyển hướng chiến lược. Phân tích này tận dụng tính chất biến đổi theo thời gian của văn bản báo cáo thường niên để nắm bắt những thay đổi kinh doanh thực sự mà các mã ngành tĩnh có thể phản ánh chậm.
Event study on textual similarity changes
# Get M&A announcements from DataCorema_events = dc.get_corporate_events( event_type='M&A', start='2016-01-01', end='2024-12-31')# For each M&A event, compute text similarity between# acquirer and target before and after the eventdef text_similarity_around_event( acquirer: str, target: str, event_year: int, annual_text_df: pd.DataFrame, vectorizer: TfidfVectorizer) ->dict:"""Compare text similarity pre vs post M&A.""" pre_texts = annual_text_df[ (annual_text_df.ticker.isin([acquirer, target])) & (annual_text_df.year == event_year -1) ] post_texts = annual_text_df[ (annual_text_df.ticker.isin([acquirer, target])) & (annual_text_df.year == event_year +1) ]iflen(pre_texts) <2orlen(post_texts) <2:returnNone pre_vecs = vectorizer.transform(pre_texts.text_clean) post_vecs = vectorizer.transform(post_texts.text_clean) pre_sim = cosine_similarity(pre_vecs[0:1], pre_vecs[1:2])[0, 0] post_sim = cosine_similarity(post_vecs[0:1], post_vecs[1:2])[0, 0]return {'acquirer': acquirer,'target': target,'event_year': event_year,'pre_similarity': pre_sim,'post_similarity': post_sim,'delta_similarity': post_sim - pre_sim }# Apply to all M&A eventsevent_results = []for _, event in ma_events.iterrows(): result = text_similarity_around_event( event.acquirer, event.target, event.event_year, annual_text, tfidf_vectorizer )if result: event_results.append(result)event_df = pd.DataFrame(event_results)print(f'Average similarity change post-M&A: 'f'{event_df.delta_similarity.mean():.4f}')print(f't-stat: {event_df.delta_similarity.mean() /' f'(event_df.delta_similarity.std() / 'f'np.sqrt(len(event_df))):.3f}')
54.4 Mạng xã hội của lãnh đạo doanh nghiệp và giá cổ phiếu
Các ứng dụng cho đến nay phân tích văn bản do doanh nghiệp tạo ra: báo cáo thường niên, phần mô tả hoạt động kinh doanh và các thông báo chính thức. Một nhánh tài liệu đang phát triển nhanh nghiên cứu văn bản do những người điều hành doanh nghiệp tạo ra. Crowley, Huang, và Lu (2024b) cung cấp nghiên cứu chuẩn mực. Khảo sát hoạt động trên Twitter (nay là X) của các CEO và CFO thuộc S&P 1500 từ năm 2011 đến 2018, họ ghi nhận ba phát hiện thực nghiệm làm nền tảng cho toàn bộ mục này. Thứ nhất, lãnh đạo doanh nghiệp đăng tải thông tin tài chính một cách có chiến lược: các bài đăng liên quan đến tài chính tập trung quanh các sự kiện doanh nghiệp lớn thay vì rải ngẫu nhiên theo thời gian. Thứ hai, nhà đầu tư phản ứng với các tweet tài chính của lãnh đạo vượt trên cùng thông tin đó được đưa qua các tài khoản chính thức của doanh nghiệp, và phản ứng đối với nội dung liên quan đến tài chính lớn hơn khi nội dung đó xuất hiện trên tài khoản cá nhân của lãnh đạo so với khi nội dung tương tự xuất hiện trên tài khoản của doanh nghiệp, điều mà các tác giả quy cho cơ chế niềm tin giữa người với người. Thứ ba, và hữu ích nhất cho thiết kế nghiên cứu, Crowley, Huang, và Lu (2024b) xây dựng một thước đo học máy về độ tương đồng nội dung giữa mỗi tweet của lãnh đạo và các tweet của chính doanh nghiệp, cho phép họ tách biệt hai kênh phản ứng của thị trường: một kênh thông tin mới, được nhận diện từ phản ứng của thị trường đối với các tweet tài chính của lãnh đạo xuất hiện khi không có bản công bố thông tin tương ứng nào của doanh nghiệp, và một kênh uy tín, được nhận diện từ các phản ứng mạnh hơn đối với những tweet của lãnh đạo có nội dung xác nhận điều mà tài khoản của doanh nghiệp đã đăng trước đó. Cả hai kênh đều hoạt động trong dữ liệu của họ. Trong một nghiên cứu đi kèm, Crowley, Huang, và Lu (2024a) cho thấy phía doanh nghiệp trong cuộc trao đổi này tự nó cũng mang tính chiến lược: các doanh nghiệp tự quyết định tin tức nào của mình được đẩy lên mạng xã hội.
Những phát hiện này mở rộng một nhánh tài liệu rộng hơn về mạng xã hội trên thị trường vốn, điều mà người đọc nên lưu ý khi diễn giải bất kỳ triển khai nào cho Việt Nam. Blankespoor, Miller, và White (2014) cho thấy chỉ cần phát tán thông cáo báo chí về lợi nhuận qua Twitter cũng cải thiện thanh khoản của các doanh nghiệp ít được chú ý, nghĩa là mạng xã hội vẫn đóng vai trò quan trọng ngay cả khi nó không mang nội dung mới. Jung và c.s. (2018) ghi nhận rằng các doanh nghiệp ít có khả năng phát tán tin xấu về lợi nhuận hơn đáng kể so với tin tốt trên mạng xã hội, một lời cảnh báo rằng dòng bài đăng quan sát được là một mẫu đã bị chọn lọc. Bartov, Faurel, và Mohanram (2018) nhận thấy rằng ý kiến tổng hợp trong các tweet liên quan đến doanh nghiệp dự báo bất ngờ lợi nhuận và lợi nhuận quanh ngày công bố, và Chen và c.s. (2014) cho thấy kết quả tương tự đối với ý kiến đám đông trên các nền tảng đầu tư, do đó các bài đăng của lãnh đạo cạnh tranh với, và cần được đối chiếu với, một tín hiệu đám đông nhiều nhiễu.
Việc áp dụng chương trình nghiên cứu này vào Việt Nam đòi hỏi phải chuyển đổi cả bối cảnh thể chế lẫn bộ công cụ đo lường. Cơ cấu nền tảng khác biệt căn bản: lãnh đạo doanh nghiệp Việt Nam giao tiếp trên Facebook và ngày càng nhiều trên Zalo; mức độ phổ biến của X là không đáng kể. Các tình huống giao tiếp của lãnh đạo doanh nghiệp rất nổi bật trên thị trường Việt Nam, từ các CEO công nghệ công bố kế hoạch sản phẩm trên trang Facebook cá nhân đến các lãnh đạo công ty môi giới và bất động sản bình luận về tình hình thị trường với lượng người theo dõi lên tới hàng trăm nghìn. Cơ cấu người tiếp nhận làm tăng thêm tầm quan trọng: nhà đầu tư cá nhân chiếm khoảng 80% khối lượng giao dịch (Mục 45.1), và các nhà giao dịch cá nhân chính là những nhà đầu tư có nhiều khả năng nhất theo dõi lãnh đạo doanh nghiệp trên các nền tảng xã hội. Bối cảnh pháp lý cũng đặc thù: Ủy ban Chứng khoán Nhà nước và các cơ quan công an đã nhiều lần xử phạt các cá nhân phát tán thông tin sai lệch có khả năng tác động đến thị trường trên các nền tảng xã hội, và các vụ thao túng thị trường trong những năm gần đây có liên quan đến hoạt động phối hợp trên mạng xã hội, vì vậy một hệ thống giám sát như loại chúng ta xây dựng dưới đây có cả ứng dụng tuân thủ lẫn ứng dụng tìm kiếm alpha.
Việc ánh xạ thiết kế của Crowley, Huang, và Lu (2024b) sang một triển khai tại Việt Nam được tóm tắt trong Bảng 54.2.
Bảng 54.2: Ánh xạ thiết kế nghiên cứu tweet của lãnh đạo sang Việt Nam
Lãnh đạo điều hành và chủ tịch của các doanh nghiệp niêm yết trên HOSE/HNX
Nền tảng
Twitter/X
Trang Facebook, Zalo OA; X nếu có
Nhận diện tài khoản
Đối chiếu tên tài khoản + xác minh thủ công
Danh bạ được xây dựng từ báo cáo thường niên, công bố thông tin về quản trị công ty, các trang đã xác minh
Bộ phân loại nội dung tài chính
ML có giám sát trên các tweet đã gán nhãn
Từ điển tài chính tiếng Việt + tinh chỉnh PhoBERT (Mục 53.1) hoặc LLM zero-shot (Mục 53.2)
Thước đo độ tương đồng nội dung
Độ tương đồng ML giữa tweet của lãnh đạo và của doanh nghiệp
Độ tương đồng cosine trong không gian TF-IDF hoặc vector nhúng dùng chung (Chương 52)
Phản ứng của thị trường
Lợi nhuận và khối lượng trong cửa sổ công bố
CAR điều chỉnh theo thị trường; suy luận đầy đủ qua khung nghiên cứu sự kiện của chương về nghiên cứu sự kiện
Ma sát chính
Reg FD hạn chế việc công bố có chọn lọc
Không có quy định tương tự Reg FD; thay vào đó là các thông tư về công bố thông tin và các quy định xử lý thao túng
54.4.1 Kiến trúc dữ liệu cho môi trường vận hành
Hệ thống vận hành bắt đầu bằng một danh bạ các tài khoản của lãnh đạo doanh nghiệp và một bộ thu thập lưu trữ các bài đăng ngay tại thời điểm tiếp nhận. Việc lưu trữ rất quan trọng: các bài đăng thường xuyên bị chỉnh sửa hoặc xóa sau khi thị trường đã phản ứng, vì vậy phiên bản bạn phân tích phải là phiên bản mà nhà đầu tư đã thấy. Mã thu thập dưới đây là bộ khung vận hành; nó đòi hỏi thông tin xác thực API của nền tảng và sự tôn trọng điều khoản dịch vụ của từng nền tảng, và chỉ nên thu thập các bài đăng công khai, liên quan đến hoạt động kinh doanh.
Production: executive account registry and post collector
import sqlite3import jsonfrom datetime import datetime, timezoneDB_PATH ='data/exec_social.sqlite'REGISTRY_SCHEMA ="""CREATE TABLE IF NOT EXISTS exec_accounts ( ticker TEXT NOT NULL, -- listed firm ticker exec_name TEXT NOT NULL, -- as disclosed in governance reports exec_role TEXT NOT NULL, -- CEO / CFO / Chair / ... platform TEXT NOT NULL, -- facebook / zalo / x handle TEXT NOT NULL, -- page id or username verified_by TEXT, -- annual report / press / platform badge verified_date TEXT, PRIMARY KEY (platform, handle));CREATE TABLE IF NOT EXISTS social_posts ( post_id TEXT PRIMARY KEY, platform TEXT NOT NULL, handle TEXT NOT NULL, posted_at_utc TEXT NOT NULL, collected_at TEXT NOT NULL, text TEXT, raw_json TEXT, -- full API payload, archived verbatim is_deleted INTEGER DEFAULT 0, -- flipped by re-crawls, never overwritten is_edited INTEGER DEFAULT 0);"""def collect_x_posts(handles: List[str], bearer_token: str) -> List[dict]:"""Collect recent posts via the X API v2 (tweepy)."""import tweepy client = tweepy.Client(bearer_token=bearer_token) out = []for handle in handles: user = client.get_user(username=handle) tweets = client.get_users_tweets( user.data.id, max_results=100, tweet_fields=['created_at', 'public_metrics', 'edit_history_tweet_ids'] )for tw in tweets.data or []: out.append({'post_id': f'x_{tw.id}', 'platform': 'x', 'handle': handle,'posted_at_utc': tw.created_at.isoformat(),'text': tw.text, 'raw_json': json.dumps(tw.data) })return outdef collect_facebook_posts(page_ids: List[str], access_token: str) -> List[dict]:"""Collect public page posts via the Facebook Graph API. Personal profiles are not accessible; most Vietnamese executives with a public presence operate a public *page*, which is what the registry should record. Zalo Official Accounts expose a similar API for OA posts. """import requests out = []for pid in page_ids: r = requests.get(f'https://graph.facebook.com/v19.0/{pid}/posts', params={'fields': 'id,created_time,message','access_token': access_token} )for post in r.json().get('data', []): out.append({'post_id': f'fb_{post["id"]}', 'platform': 'facebook','handle': pid, 'posted_at_utc': post['created_time'],'text': post.get('message', ''), 'raw_json': json.dumps(post) })return outdef store_posts(posts: List[dict]):"""Idempotent insert; re-crawls mark deletions instead of overwriting.""" con = sqlite3.connect(DB_PATH) con.executescript(REGISTRY_SCHEMA) now = datetime.now(timezone.utc).isoformat() con.executemany("""INSERT OR IGNORE INTO social_posts (post_id, platform, handle, posted_at_utc, collected_at, text, raw_json) VALUES (:post_id, :platform, :handle, :posted_at_utc, :collected_at, :text, :raw_json)""", [{**p, 'collected_at': now} for p in posts] ) con.commit(); con.close()
Hai quy ước về thời gian đặc biệt quan trọng đối với dữ liệu Việt Nam. Thứ nhất, dấu thời gian đến ở dạng UTC và phải được chuyển sang giờ Đông Dương (UTC+7). Thứ hai, các bài đăng phải được ánh xạ tới ngày sự kiện theo ngày giao dịch: một bài đăng được đăng sau khi phiên ATC đóng cửa lúc 14:45, hoặc vào cuối tuần hay ngày lễ, thuộc về ngày giao dịch kế tiếp, đúng như cách xử lý dấu thời gian công bố trong chương về nghiên cứu sự kiện.
54.4.2 Môi trường thử nghiệm tái lập: Các bài đăng mô phỏng của lãnh đạo
Phần còn lại của mục này là một bản tái lập hoàn chỉnh, có thể chạy được, của thiết kế của Crowley, Huang, và Lu (2024b) trên dữ liệu Việt Nam mô phỏng, để mọi bước (phân loại, độ tương đồng, phản ứng thị trường) có thể được thực thi và kiểm tra trước khi có bất kỳ thông tin xác thực API nào. Chúng ta mô phỏng 20 mã chứng khoán giả lập trong 500 ngày giao dịch: mỗi doanh nghiệp đăng các thông báo chính thức quanh các ngày công bố kết quả hằng quý, và mỗi lãnh đạo đăng một hỗn hợp gồm nội dung phi tài chính, nội dung tài chính xác nhận diễn giải lại thông báo của chính doanh nghiệp, và nội dung tài chính mới (các phát biểu mang tính định hướng, đưa ra trước bất kỳ bản công bố nào của doanh nghiệp). Phản ánh các phát hiện của bài báo, chúng ta gắn vào dữ liệu một phản ứng giá đối với các bài đăng tài chính của lãnh đạo, lớn nhất đối với nội dung mới. Toàn bộ văn bản là tiếng Việt đã được tách từ sẵn (các từ ghép nối bằng dấu gạch dưới), phù hợp với các quy ước của Mục 48.1. Các mã chứng khoán và lãnh đạo là giả lập theo thiết kế; việc gán các phát biểu mô phỏng cho các doanh nghiệp hay cá nhân có thật là không phù hợp.
Simulate returns, firm posts, and executive posts
rng = np.random.default_rng(42)TICKERS = [f'T{i:02d}'for i inrange(20)]DATES = pd.bdate_range('2023-01-02', periods=500)# --- Market and stock returns -------------------------------------------mkt_ret = rng.normal(0.0004, 0.010, len(DATES))betas = {t: rng.uniform(0.8, 1.2) for t in TICKERS}returns = pd.DataFrame({ t: betas[t] * mkt_ret + rng.normal(0, 0.018, len(DATES))for t in TICKERS}, index=DATES)# --- Quarterly earnings dates per ticker --------------------------------earnings_dates = { t: [DATES[min(len(DATES) -1, q *63+int(rng.integers(0, 10)))]for q inrange(1, 8)]for t in TICKERS}NONFIN_TEMPLATES = ['chúc_mừng năm_mới toàn_thể nhân_viên và khách_hàng của công_ty','tự_hào về đội_ngũ nhân_viên tận_tâm của chúng_ta','hôm_nay tôi tham_dự hội_thảo về chuyển_đổi_số và đổi_mới sáng_tạo','cảm_ơn khách_hàng đã luôn đồng_hành cùng chúng_tôi','cuối_tuần đọc một cuốn sách hay về quản_trị doanh_nghiệp',]FIRM_FIN_TEMPLATE = ('công_ty công_bố kết_quả kinh_doanh quý ''doanh_thu đạt {rev} tỷ đồng lợi_nhuận tăng {gro} ''phần_trăm so với cùng_kỳ')CONFIRM_TEMPLATE = ('rất vui chia_sẻ kết_quả kinh_doanh quý ''doanh_thu đạt {rev} tỷ đồng lợi_nhuận tăng {gro} ''phần_trăm so với cùng_kỳ cảm_ơn cả đội_ngũ')NOVEL_TEMPLATES = ['đơn_hàng mới rất khả_quan lợi_nhuận quý tới dự_kiến vượt kế_hoạch','biên lợi_nhuận đang cải_thiện nhờ giá nguyên_liệu giảm mạnh','công_ty chuẩn_bị ký hợp_đồng xuất_khẩu lớn doanh_thu sẽ tăng mạnh',]firm_posts, exec_posts = [], []for t in TICKERS:for ed in earnings_dates[t]: rev, gro =int(rng.integers(200, 900)), int(rng.integers(5, 40)) firm_posts.append({'ticker': t, 'date': ed, 'account': 'firm','text': FIRM_FIN_TEMPLATE.format(rev=rev, gro=gro)})# Confirmatory executive post one day after the firm announcementif rng.random() <0.6: d = DATES[min(len(DATES) -1, DATES.get_loc(ed) +1)] exec_posts.append({'ticker': t, 'date': d, 'account': 'executive','category': 'financial_confirmatory','text': CONFIRM_TEMPLATE.format(rev=rev, gro=gro)})# Novel executive post about a week before the firm announcementif rng.random() <0.4: d = DATES[max(0, DATES.get_loc(ed) -int(rng.integers(4, 8)))] exec_posts.append({'ticker': t, 'date': d, 'account': 'executive','category': 'financial_novel','text': NOVEL_TEMPLATES[int(rng.integers(0, 3))]})# Non-financial posts scattered through timefor _ inrange(int(rng.integers(15, 25))): d = DATES[int(rng.integers(0, len(DATES)))] exec_posts.append({'ticker': t, 'date': d, 'account': 'executive','category': 'nonfinancial','text': NONFIN_TEMPLATES[int(rng.integers(0, 5))]})firm_posts = pd.DataFrame(firm_posts)exec_posts = pd.DataFrame(exec_posts)# --- Embed market reactions (the "truth" our tests should recover) ------for _, p in exec_posts.iterrows(): i = DATES.get_loc(p.date)if p.category =='financial_novel': returns.loc[DATES[i], p.ticker] +=0.010if i +1<len(DATES): returns.loc[DATES[i +1], p.ticker] +=0.005elif p.category =='financial_confirmatory': returns.loc[DATES[i], p.ticker] +=0.003print(f'Simulated {len(firm_posts)} firm posts and {len(exec_posts)} 'f'executive posts across {len(TICKERS)} tickers')print(exec_posts.category.value_counts())
Trước tiên, Crowley, Huang, và Lu (2024b) tách các tweet tài chính khỏi tweet phi tài chính bằng một bộ phân loại có giám sát được huấn luyện trên dữ liệu gán nhãn thủ công. Chúng ta tái lập bước đó bằng biểu diễn TF-IDF của Mục 49.2 và một hồi quy logistic. Trong môi trường thử nghiệm, các nhãn đến từ mô phỏng; trong môi trường vận hành, chúng đến từ một vòng gán nhãn thủ công (thường vài trăm bài đăng là đủ để bắt đầu), sau đó bộ phân loại có thể được nâng cấp lên PhoBERT đã tinh chỉnh (Mục 53.1) hoặc bộ phân loại LLM zero-shot (Mục 53.2) mà không cần thay đổi bất cứ điều gì ở các bước phía sau.
54.4.4 Đo lường độ tương đồng nội dung giữa lãnh đạo và doanh nghiệp
Thước đo cốt lõi trong Crowley, Huang, và Lu (2024b) là độ tương đồng nội dung giữa một bài đăng của lãnh đạo và các bài đăng gần đây của chính doanh nghiệp, biến hai cơ chế phản ứng của thị trường thành một phép phân tách có thể đo lường: một bài đăng tài chính của lãnh đạo rất giống với nội dung gần đây của doanh nghiệp là xác nhận (phản ứng của nó, nếu có, vận hành qua kênh uy tín), trong khi một bài đăng tài chính không có nội dung tương tự nào từ doanh nghiệp là mới (phản ứng của nó cho thấy thông tin mới đi vào thị trường thông qua lãnh đạo). Chúng ta triển khai thước đo này dưới dạng độ tương đồng cosine tối đa giữa mỗi bài đăng tài chính của lãnh đạo và các bài đăng chính thức của cùng doanh nghiệp trong bảy ngày lịch gần nhất, trong một không gian TF-IDF dùng chung (Mục 52.1).
Executive-firm content similarity and novelty split
Phân phối độ tương đồng có dạng hai đỉnh rõ rệt: các bài đăng diễn giải lại kết quả kinh doanh nằm gần đầu trên của miền giá trị trong khi các bài đăng mang tính định hướng tập trung gần 0, do đó phép phân tách không nhạy với ngưỡng chính xác. Trong môi trường vận hành, ngưỡng nên được kiểm định trên một mẫu gán nhãn thủ công, và không gian TF-IDF có thể được thay bằng vector nhúng câu PhoBERT (Mục 52.2) để bắt được các bài diễn giải lại có cùng ý nghĩa nhưng không cùng từ vựng.
54.4.5 Phản ứng của thị trường đối với các bài đăng của lãnh đạo
Khi các bài đăng đã được phân loại và tách nhóm, kiểm định phản ứng thị trường theo logic của nghiên cứu sự kiện: tính lợi nhuận bất thường điều chỉnh theo thị trường và cộng dồn chúng trong cửa sổ bài đăng, riêng cho các bài đăng tài chính mới, tài chính xác nhận và phi tài chính. Các bài đăng phi tài chính đóng vai trò nhóm giả dược, đúng như trong Crowley, Huang, và Lu (2024b); nếu những lời chào hỏi và chúc mừng ngày lễ làm dịch chuyển giá, thiết kế đã bị nhiễm.
Môi trường thử nghiệm khôi phục đúng cấu trúc mà chúng ta đã gắn vào, cũng là cấu trúc mà Crowley, Huang, và Lu (2024b) tìm thấy trong dữ liệu thực của Mỹ: các bài đăng tài chính mới mang phản ứng lớn và có ý nghĩa thống kê (kênh thông tin mới), trong khi các bài đăng phi tài chính là nhóm giả dược không có phản ứng. Nhóm xác nhận mang tính gợi mở theo một cách khác: phản ứng được gắn vào là có thật nhưng nhỏ, và với cỡ mẫu mô phỏng này thì không thể phân biệt một cách đáng tin cậy với 0, một lời nhắc rằng các hiệu ứng của kênh uy tín đòi hỏi bảng dữ liệu bài đăng lớn mới phát hiện được (Crowley, Huang, và Lu (2024b) nhận diện kênh của họ từ toàn bộ lịch sử tweet của S&P 1500), vì vậy một nhà nghiên cứu Việt Nam nên kỳ vọng kết quả của các bài đăng mới xuất hiện trước và kết quả của nhóm xác nhận cần tích lũy nhiều năm dữ liệu. Trên dữ liệu Việt Nam thực, CAR điều chỉnh theo thị trường đơn giản này nên được thay bằng bộ máy suy luận đầy đủ của chương về nghiên cứu sự kiện (lợi nhuận bất thường theo mô hình rủi ro, các kiểm định BMP và kiểm định hạng, và các cửa sổ rộng hơn để tính đến biên độ giá), và các phản ứng nên được đo bằng khối lượng và biến động cũng như lợi nhuận, vì các hiệu ứng thu hút sự chú ý do nhà đầu tư cá nhân thúc đẩy thường xuất hiện ở khối lượng trước.
54.4.6 Từ tái lập đến giám sát vận hành
Mọi thứ ở trên hợp lại thành một hệ thống giám sát chạy liên tục: thu thập các bài đăng mới, phân loại nội dung tài chính, tính độ tương đồng với dòng bài đăng chính thức của doanh nghiệp, và phản ứng trong vòng vài phút thay vì vào cuối một quý nghiên cứu. Bộ khung dưới đây được cố ý giữ ở mức tối giản; mỗi phương thức ủy quyền cho một thành phần đã được xây dựng trong mục này.
Production: real-time executive post monitor
class ExecutiveSocialMonitor:"""Poll -> classify -> similarity -> alert, on a fixed schedule. Schedule with cron / Task Scheduler at the desired latency, e.g. every 5 minutes during HOSE trading hours (09:00-14:45 ICT). """def__init__(self, db_path, clf, clf_vectorizer, sim_vectorizer, sim_threshold=0.30, sim_window_days=7):self.db_path = db_pathself.clf, self.clf_vectorizer = clf, clf_vectorizerself.sim_vectorizer = sim_vectorizerself.sim_threshold = sim_thresholdself.sim_window_days = sim_window_daysdef poll(self):"""Collect new posts for every registered account (see exec-social-collection) and return those not yet processed.""" ...def classify(self, posts):"""Word-segment (underthesea), then flag financial content.""" ...def similarity(self, post):"""Max cosine similarity vs the firm's official posts in the trailing window; below threshold => novel.""" ...def alert(self, post, is_novel):"""Route by rule: - novel financial post by a covered executive -> analyst channel (candidate trading signal; log expected reaction window) - quantitative claim with no firm disclosure within 24h -> compliance flag (selective disclosure / manipulation risk) - deleted or edited financial post -> archive diff + escalate """ ...def run_once(self): new_posts =self.poll()for post inself.classify(new_posts):if post['is_financial']:self.alert(post, self.similarity(post) <self.sim_threshold)
Tuân thủ và đạo đức trong bối cảnh Việt Nam
A production monitor must respect three constraints. First, collect only public business communications of public-company executives; Vietnam’s Personal Data Protection Decree (13/2023/NĐ-CP) and platform terms of service both bind, and private profiles are out of scope. Second, the same system that surfaces trading signals surfaces manipulation: executives or affiliated accounts posting inflated claims ahead of insider sales is exactly the pattern securities regulators sanction, so log everything immutably; the archive is evidence. Third, trading on a novel executive post is trading on public information the moment it is posted, but a researcher who receives material information privately from an executive is in different legal territory entirely. Jung và c.s. (2018) is the reminder that the observable stream is strategically selected; absence of posts is not absence of news.
Danh mục kiểm tra khi triển khai
Danh bạ: các tài khoản lãnh đạo được xác minh từ báo cáo thường niên và công bố thông tin về quản trị công ty; xác minh lại hằng quý.
Thu thập: truy vấn định kỳ theo lịch với kho lưu trữ JSON thô bất biến; theo dõi việc xóa và chỉnh sửa; chuyển đổi UTC sang ICT; ánh xạ mốc ATC sang các ngày giao dịch.
Mô hình: bộ phân loại được huấn luyện lại trên tập gán nhãn ngày càng lớn; ngưỡng độ tương đồng được kiểm định lại; vector nhúng PhoBERT khi khối lượng bài diễn giải lại đủ lớn.
Đo lường: CAR từ quy trình của chương về nghiên cứu sự kiện; phản ứng về khối lượng và biến động đi kèm lợi nhuận.
Quản trị: cảnh báo tuân thủ được định tuyến tách biệt với cảnh báo nghiên cứu; rà soát hằng quý tỷ lệ dương tính giả của cả hai.
Chương trình nghiên cứu mà điều này mở ra tại Việt Nam phần lớn vẫn còn bỏ ngỏ: chưa có nghiên cứu nào được công bố đo lường liệu các bài đăng Facebook và Zalo của lãnh đạo doanh nghiệp Việt Nam có làm dịch chuyển giá cổ phiếu của doanh nghiệp họ hay không, phản ứng khác nhau thế nào giữa cơ sở nhà đầu tư cá nhân đông đảo trên HOSE và các nhà đầu tư tổ chức, hay liệu cơ chế uy tín của Crowley, Huang, và Lu (2024b) có mạnh hơn trong một thị trường mà niềm tin giữa người với người có thể thay thế cho một môi trường công bố thông tin chính thức mỏng hơn. Quy trình trong mục này là hạ tầng cho cả ba câu hỏi.
55 So sánh phương pháp và thực hành tốt nhất
Bảng 55.1: So sánh các phương pháp phân tích văn bản cho văn bản tài chính tiếng Việt
Phương pháp
Khả năng diễn giải
Ngữ nghĩa
Tốc độ
Hỗ trợ tiếng Việt
Yêu cầu dữ liệu
Trường hợp sử dụng tốt nhất
BoW/TF-IDF
Cao
Thấp
Nhanh
Tốt*
Không
Nhóm doanh nghiệp cùng ngành, tương đồng từ vựng
LDA
Trung bình
Thấp
Trung bình
Tốt*
Không
Khám phá chủ đề
Doc2Vec
Thấp
Trung bình
Trung bình
Tốt*
Kho ngữ liệu
Tương đồng tài liệu
BERTopic
Cao
Cao
Chậm
Xuất sắc
Không
Các chủ đề mạch lạc
PhoBERT
Thấp
Cao
Chậm
Xuất sắc
Tinh chỉnh
Cảm xúc, NER, phân loại
Sentence-BERT
Thấp
Cao
Trung bình
Tốt
Không
Tương đồng ngữ nghĩa
LLM (zero-shot)
Cao
Cao
Chậm
Tốt
Không
Trích xuất, phân loại
Ghi chú
*Yêu cầu tách từ tiếng Việt như một bước tiền xử lý. Cột Hỗ trợ tiếng Việt đánh giá mức độ phương pháp xử lý văn bản tiếng Việt một cách tự nhiên.
Đối với các nhà nghiên cứu mới bắt đầu phân tích văn bản của các doanh nghiệp Việt Nam, chúng tôi khuyến nghị quy trình làm việc sau:
Bắt đầu với độ tương đồng cosine TF-IDF để xác định nhóm doanh nghiệp cùng ngành vì nó nhanh, dễ diễn giải và cung cấp một mức cơ sở mạnh.
Dùng BERTopic với vector nhúng PhoBERT để khám phá chủ đề vì nó tạo ra các chủ đề mạch lạc hơn LDA đối với văn bản tiếng Việt.
Đối với phân tích cảm xúc, dùng ViFinBERT nếu có dữ liệu để tinh chỉnh; nếu không, phân loại LLM zero-shot cho kết quả cạnh tranh.
Đối với các hệ thống vận hành cần phân tích theo thời gian thực, vector nhúng sentence-BERT mang lại sự đánh đổi tốt nhất giữa tốc độ và độ chính xác.
Compare methods on peer identification accuracy
# Evaluate: what fraction of top-5 peers share ICB sector?methods = {'TF-IDF': sim_df,'Sentence-BERT': embed_sim_df,'Doc2Vec': pd.DataFrame( cosine_similarity(d2v_vectors), index=tickers, columns=tickers ),}accuracy_results = {}sector_map = corpus_df.set_index('ticker')['icb_sector'].to_dict()for method_name, sim_matrix_df in methods.items(): matches =0 total =0for ticker in tickers: true_sector = sector_map.get(ticker) peers = sim_matrix_df[ticker].drop(ticker).nlargest(5)for peer in peers.index: total +=1if sector_map.get(peer) == true_sector: matches +=1 accuracy_results[method_name] = matches / totalfig, ax = plt.subplots(figsize=(8, 5))methods_list =list(accuracy_results.keys())accs =list(accuracy_results.values())bars = ax.bar(methods_list, accs, color=['#2C5282', '#38A169', '#D69E2E'])ax.set_ylabel('ICB Sector Match Rate')ax.set_title('Peer Identification Accuracy by Method')ax.set_ylim(0, 1)for bar, acc inzip(bars, accs): ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() +0.02,f'{acc:.1%}', ha='center', fontweight='bold')plt.tight_layout()plt.show()
Hình 55.1
56 Kết luận
Chương này đã trình bày toàn bộ quy trình các phương pháp phân tích văn bản áp dụng cho các doanh nghiệp niêm yết Việt Nam, từ các cách tiếp cận bag-of-words cổ điển đến các mô hình ngôn ngữ dựa trên transformer. Những điểm then chốt dành cho người thực hành và nhà nghiên cứu là:
Thứ nhất, tiền xử lý văn bản tiếng Việt đòi hỏi một bước tách từ mà NLP tiếng Anh không có tương đương. Sử dụng các công cụ như VnCoreNLP hoặc underthesea cho bước này là thiết yếu và ảnh hưởng đáng kể đến chất lượng của các phân tích phía sau.
Thứ hai, các từ điển cảm xúc theo lĩnh vực chuyên biệt vượt trội đáng kể so với các từ điển đa dụng đối với văn bản tài chính tiếng Việt, phù hợp với các phát hiện của Loughran và McDonald (2011) cho tiếng Anh.
Thứ ba, vector nhúng dựa trên PhoBERT nắm bắt sự tương đồng ngữ nghĩa mà TF-IDF bỏ sót, nhận diện các doanh nghiệp cùng ngành có chung mô hình kinh doanh ngay cả khi họ dùng từ vựng khác nhau.
Thứ tư, các LLM cho phép những ứng dụng mới, bao gồm trích xuất thông tin có cấu trúc từ báo cáo thường niên tiếng Việt vốn sẽ cực kỳ tốn kém nếu mã hóa thủ công.
Các ứng dụng thực nghiệm cho thấy các thước đo văn bản chứa thông tin có ý nghĩa kinh tế đối với thị trường Việt Nam. Cảm xúc ròng từ báo cáo thường niên dự báo lợi nhuận cổ phiếu tiếp theo ngay cả sau khi kiểm soát các nhân tố rủi ro tiêu chuẩn, và các thước đo cảm xúc dựa trên BERT có sức dự báo gia tăng vượt trên các thước đo dựa trên từ điển. Các phân loại ngành dựa trên văn bản nắm bắt các mối quan hệ giữa doanh nghiệp mà các mã ICB tĩnh bỏ sót, và những thay đổi về độ tương đồng văn bản quanh các sự kiện doanh nghiệp phản ánh những chuyển đổi kinh doanh thực sự.
Bartov, Eli, Lucile Faurel, và Partha S Mohanram. 2018. “Can Twitter help predict firm-level earnings and stock returns?”The Accounting Review 93 (3): 25–57.
Blankespoor, Elizabeth, Gregory S Miller, và Hal D White. 2014. “The role of dissemination in market liquidity: Evidence from firms’ use of Twitter”. The Accounting Review 89 (1): 79–112.
Blei, David M, Andrew Y Ng, và Michael I Jordan. 2003. “Latent dirichlet allocation”. Journal of machine Learning research 3 (Jan): 993–1022.
Bonsall IV, Samuel B, Andrew J Leone, Brian P Miller, và Kristina Rennekamp. 2017. “A plain English measure of financial reporting readability”. Journal of Accounting and Economics 63 (2-3): 329–57.
Bybee, Leland, Bryan Kelly, và Yinan Su. 2023. “Narrative asset pricing: Interpretable systematic risk factors from news text”. The Review of Financial Studies 36 (12): 4759–87.
Chen, Hailiang, Prabuddha De, Yu Jeffrey Hu, và Byoung-Hyoun Hwang. 2014. “Wisdom of crowds: The value of stock opinions transmitted through social media”. The Review of Financial Studies 27 (5): 1367–1403.
Crowley, Richard M, Wenli Huang, và Hai Lu. 2024a. “Discretionary dissemination on Twitter”. Contemporary Accounting Research 41 (4): 2454–87.
Devlin, Jacob, Ming-Wei Chang, Kenton Lee, và Kristina Toutanova. 2019. “Bert: Pre-training of deep bidirectional transformers for language understanding”. Trong Proceedings of the 2019 conference of the North American chapter of the association for computational linguistics: human language technologies, volume 1 (long and short papers), 4171–86.
Dyer, Travis, Mark Lang, và Lorien Stice-Lawrence. 2017. “The evolution of 10-K textual disclosure: Evidence from Latent Dirichlet Allocation”. Journal of Accounting and Economics 64 (2-3): 221–45.
Grootendorst, Maarten. 2022. “BERTopic: Neural topic modeling with a class-based TF-IDF procedure”. arXiv preprint arXiv:2203.05794.
Hoberg, Gerard, và Gordon Phillips. 2010. “Product market synergies and competition in mergers and acquisitions: A text-based analysis”. The Review of Financial Studies 23 (10): 3773–3811.
———. 2016. “Text-based network industries and endogenous product differentiation”. Journal of political economy 124 (5): 1423–65. https://doi.org/10.1086/688176.
Hoberg, Gerard, và Gordon M Phillips. 2018. “Text-based industry momentum”. Journal of Financial and Quantitative Analysis 53 (6): 2355–88.
Huang, Allen H, Reuven Lehavy, Amy Y Zang, và Rong Zheng. 2018. “Analyst information discovery and interpretation roles: A topic modeling approach”. Management science 64 (6): 2833–55.
Huang, Allen H, Hui Wang, và Yi Yang. 2023. “FinBERT: A large language model for extracting information from financial text”. Contemporary Accounting Research 40 (2): 806–41.
Jha, Manish, Jialin Qian, Michael Weber, và Baozhong Yang. 2024. “ChatGPT and corporate policies”. National Bureau of Economic Research.
Jung, Michael J, James P Naughton, Ahmed Tahoun, và Clare Wang. 2018. “Do firms strategically disseminate? Evidence from corporate use of social media”. The Accounting Review 93 (4): 225–52.
Lau, Jey Han, và Timothy Baldwin. 2016. “An empirical evaluation of doc2vec with practical insights into document embedding generation”. arXiv preprint arXiv:1607.05368.
Le, Quoc, và Tomas Mikolov. 2014. “Distributed representations of sentences and documents”. Trong International conference on machine learning, 1188–96. PMLR.
Li, Feng và c.s. 2010. “Textual analysis of corporate disclosures: A survey of the literature”. Journal of accounting literature 29 (1): 143–65.
Loughran, Tim, và Bill McDonald. 2011. “When is a liability not a liability? Textual analysis, dictionaries, and 10-Ks”. The Journal of finance 66 (1): 35–65.
Nguyen, Dat Quoc, và Anh-Tuan Nguyen. 2020. “PhoBERT: Pre-trained language models for Vietnamese”. Trong Findings of the association for computational linguistics: EMNLP 2020, 1037–42.
Reimers, Nils, và Iryna Gurevych. 2019. “Sentence-bert: Sentence embeddings using siamese bert-networks”. arXiv preprint arXiv:1908.10084.
Tetlock, Paul C. 2007. “Giving content to investor sentiment: The role of media in the stock market”. The Journal of finance 62 (3): 1139–68.
Tetlock, Paul C, Maytal Saar-Tsechansky, và Sofus Macskassy. 2008. “More than words: Quantifying language to measure firms’ fundamentals”. The journal of finance 63 (3): 1437–67.
Vu, Thanh, Dat Quoc Nguyen, Mark Dras, Mark Johnson, và c.s. 2018. “VnCoreNLP: A Vietnamese natural language processing toolkit”. Trong Proceedings of the 2018 conference of the North American chapter of the association for computational linguistics: demonstrations, 56–60.
Vietnamese text requires specialized tokenization due to compound words (e.g., “công ty” = company, “thị trường” = market).↩︎
Loughran và McDonald (2011) cho thấy các từ điển đa dụng phân loại sai tới 73% số từ tiêu cực trong văn bản tài chính.↩︎
Reimers và Gurevych (2019) chứng minh rằng vector nhúng sentence-BERT giảm chi phí tính toán cho các tác vụ đo độ tương đồng từ 65 giờ xuống còn 5 giây trên 10.000 cặp câu.↩︎
# Phân tích văn bảnPhân tích văn bản đã nổi lên như một trong những hướng nghiên cứu hiệu quả nhất của tài chính thực nghiệm trong hai thập kỷ qua. Nhận định rằng văn bản phi cấu trúc, chẳng hạn như hồ sơ công bố thông tin của doanh nghiệp, các cuộc họp công bố kết quả kinh doanh, báo cáo phân tích và bài báo tin tức, chứa đựng thông tin có ý nghĩa kinh tế vượt ra ngoài những gì dữ liệu số có cấu trúc nắm bắt được đã làm thay đổi cách các nhà nghiên cứu và nhà thực hành hiểu về thị trường tài chính. Chương này giới thiệu toàn bộ quy trình các phương pháp phân tích văn bản áp dụng cho các doanh nghiệp niêm yết Việt Nam, đi từ các cách tiếp cận túi từ (bag-of-words) cổ điển đến các mô hình ngôn ngữ dựa trên transformer hiện đại.Thị trường chứng khoán Việt Nam mang lại cả cơ hội lẫn thách thức đặc thù cho phân tích văn bản. Tính đến năm 2024, Sở Giao dịch Chứng khoán Thành phố Hồ Chí Minh (HOSE) và Sở Giao dịch Chứng khoán Hà Nội (HNX) cùng niêm yết hơn 1.600 chứng khoán với tổng vốn hóa thị trường vượt 6.000 nghìn tỷ VND (khoảng 240 tỷ USD). Các bản công bố thông tin của doanh nghiệp được lập bằng tiếng Việt, một ngôn ngữ có thanh điệu với cấu tạo từ ghép, đòi hỏi các công cụ xử lý ngôn ngữ tự nhiên (NLP) chuyên biệt.Chúng ta xây dựng dựa trên những đóng góp nền tảng của @loughran2011liability về từ điển cảm xúc theo lĩnh vực chuyên biệt, của @hoberg2016text về phân loại ngành dựa trên văn bản, và cuộc cách mạng học sâu hiện đại do @devlin2019bert khởi xướng. Chương này bao gồm các chủ đề sau:1. Xây dựng tập hợp các doanh nghiệp niêm yết trên HOSE/HNX và thu thập phần mô tả hoạt động kinh doanh cùng văn bản báo cáo thường niên của họ.2. Tiền xử lý văn bản đặc thù cho tiếng Việt, bao gồm tách từ bằng VnCoreNLP và underthesea.3. Biểu diễn tài liệu cổ điển bằng bag-of-words, TF-IDF và các mô hình chủ đề LDA.4. Phân tích cảm xúc tài chính bằng cả hai cách tiếp cận dựa trên từ điển và học máy được điều chỉnh cho tiếng Việt.5. Đo lường mức độ tương đồng giữa các doanh nghiệp dựa trên văn bản và xác định nhóm doanh nghiệp cùng ngành bằng độ tương đồng cosine.6. Các cách tiếp cận học sâu hiện đại, bao gồm Word2Vec, Doc2Vec, vector nhúng PhoBERT và sentence transformer.7. Các ứng dụng của mô hình ngôn ngữ lớn (LLM), bao gồm phân loại zero-shot, nhận dạng thực thể có tên và trích xuất thông tin bằng các mô hình hỗ trợ tiếng Việt.8. Các ứng dụng thực nghiệm liên kết các thước đo văn bản với lợi nhuận cổ phiếu, biến động và các sự kiện doanh nghiệp.## Vì sao cần phân tích văn bản cho tài chính Việt Nam? {#sec-textual-why-vietnam}Thị trường tài chính Việt Nam có một số đặc điểm khiến phân tích văn bản đặc biệt có giá trị. Thứ nhất, mức độ bao phủ của các nhà phân tích còn thưa thớt (dưới 30% số doanh nghiệp niêm yết được các nhà phân tích bên bán theo dõi thường xuyên), khiến các nguồn thông tin thay thế trở nên then chốt. Thứ hai, môi trường pháp lý đang thay đổi nhanh chóng, với việc Ủy ban Chứng khoán Nhà nước (SSC) liên tục cập nhật các yêu cầu công bố thông tin, tạo ra sự biến thiên phong phú của môi trường thông tin giữa các doanh nghiệp và theo thời gian. Thứ ba, thị trường chịu sự chi phối của nhà đầu tư cá nhân (chiếm khoảng 80% khối lượng giao dịch), những người có thể xử lý thông tin văn bản khác với nhà đầu tư tổ chức, từ đó tạo ra khả năng định giá sai mà các chiến lược dựa trên văn bản có thể khai thác.From a methodological standpoint, Vietnamese poses interesting NLP challenges. Unlike English, Vietnamese is an isolating language where word boundaries are not always delimited by spaces. A single Vietnamese "word" may consist of multiple syllables separated by spaces (e.g., "công ty" for "company," "thị trường" for "market"). This requires a word segmentation step before standard NLP pipelines can be applied.[^60_textual-1][^60_textual-1]: Vietnamese text requires specialized tokenization due to compound words (e.g., "công ty" = company, "thị trường" = market).# Tổng quan tài liệu {#sec-textual-literature}## Phân tích văn bản trong tài chính {#sec-textual-lit-finance}Việc áp dụng phân tích văn bản vào dữ liệu tài chính có một lịch sử phong phú. @tetlock2007giving chứng minh rằng mức độ bi quan trong một chuyên mục của Wall Street Journal dự báo hoạt động của thị trường nói chung, cung cấp bằng chứng sớm cho thấy nội dung văn bản làm dịch chuyển giá. @loughran2011liability cho thấy từ điển cảm xúc Harvard General Inquirer được sử dụng rộng rãi cho ra kết quả sai lệch khi áp dụng vào văn bản tài chính, vì những từ như "liability" (nợ phải trả), "tax" (thuế) và "capital" (vốn) được xếp vào nhóm tiêu cực trong tiếng Anh thông thường nhưng lại mang sắc thái trung tính, thậm chí tích cực trong tài chính. Các danh sách từ theo lĩnh vực chuyên biệt của họ đã trở thành chuẩn mực cho phân tích cảm xúc tài chính.[^60_textual-2][^60_textual-2]: @loughran2011liability cho thấy các từ điển đa dụng phân loại sai tới 73% số từ tiêu cực trong văn bản tài chính.@hoberg2010product và @hoberg2016text là những người tiên phong sử dụng phần mô tả sản phẩm trong báo cáo 10-K để xây dựng các phân loại ngành dựa trên văn bản (TNIC), chứng minh rằng các định nghĩa ngành động, riêng cho từng doanh nghiệp này giải thích hành vi doanh nghiệp tốt hơn các mã SIC và NAICS tĩnh, bao gồm khả năng sinh lời, lợi nhuận cổ phiếu và hoạt động M&A. Công trình tiếp theo của @hoberg2018text mở rộng cách tiếp cận này để đánh giá các mối đe dọa cạnh tranh và tính lưu động của thị trường sản phẩm (product-market fluidity).Các nghiên cứu gần đây hơn tận dụng những tiến bộ của học sâu. @huang2023finbert áp dụng các mô hình dựa trên BERT vào bản ghi các cuộc họp công bố kết quả kinh doanh và cho thấy các vector nhúng theo ngữ cảnh nắm bắt được thông tin về lợi nhuận kế toán tương lai mà các thước đo bag-of-words truyền thống bỏ sót. @jha2024chatgpt sử dụng các mô hình dựa trên GPT để phân loại văn bản tài chính theo kiểu zero-shot và chứng minh rằng các LLM có thể sánh ngang hoặc vượt các bộ phân loại chuyên dụng trên những bộ kiểm chuẩn tiêu chuẩn.Văn bản cũng có thể đo lường *mức độ bất định* mà nhà đầu tư phải đối mặt, chứ không chỉ sắc thái của nó. @wei2026narrative trích xuất một thước đo mơ hồ dựa trên diễn ngôn từ các bài báo tin tức kinh tế, khác biệt với các biến đại diện cho sự mơ hồ dựa trên khảo sát và dựa trên lợi nhuận, và cho thấy nó dự báo lợi nhuận thị trường chứng khoán tổng thể cả trong mẫu lẫn ngoài mẫu. Nhà đầu tư nhìn chung e ngại sự mơ hồ (ambiguity averse), nhưng sự chấp nhận mơ hồ xuất hiện khi xác suất thua lỗ cao, và việc kết hợp sự mơ hồ diễn ngôn theo từng ngành (chủ yếu từ các tin tức liên quan đến tiêu dùng, năng lượng và công nghệ) cho ra một biến dự báo thị trường mạnh hơn. Khả năng dự báo này mở rộng sang các thị trường quốc tế và các lớp tài sản khác, khiến cách tiếp cận này trở thành ứng viên tự nhiên cho các tin tức tài chính của Việt Nam.## NLP cho tiếng Việt {#sec-textual-lit-vnlp}NLP tiếng Việt đã tiến bộ đáng kể nhờ sự ra đời của VnCoreNLP [@vu2018vncorenlp], một bộ công cụ nền Java cung cấp các chức năng tách từ, gán nhãn từ loại, nhận dạng thực thể có tên và phân tích cú pháp phụ thuộc. Thư viện underthesea cung cấp một phương án thay thế thuần Python. Quan trọng nhất đối với các ứng dụng tài chính, PhoBERT [@nguyen2020phobert] cung cấp mô hình BERT tiền huấn luyện dành riêng cho tiếng Việt trên kho ngữ liệu 20GB, đạt kết quả tốt nhất hiện nay trên nhiều tác vụ NLP tiếng Việt.| Nghiên cứu | Phương pháp | Phát hiện chính | Mức độ liên quan đến Việt Nam ||:-----------------|:-----------------|:-----------------|:------------------|| @tetlock2007giving | Cảm xúc dựa trên từ điển từ chuyên mục WSJ | Sự bi quan của truyền thông dự báo hoạt động và lợi nhuận thị trường | Cơ sở tham chiếu cho cảm xúc của tin tức tài chính Việt Nam || @loughran2011liability | Từ điển tài chính theo lĩnh vực chuyên biệt | Các từ điển đa dụng phân loại sai 73% số từ tiêu cực trong tài chính | Cần có từ điển cảm xúc tài chính tiếng Việt || @hoberg2016text | Độ tương đồng cosine trên phần mô tả sản phẩm trong 10-K | Các ngành dựa trên văn bản vượt trội so với SIC/NAICS | Xác định nhóm doanh nghiệp cùng ngành của Việt Nam bằng phần mô tả hoạt động kinh doanh || @nguyen2020phobert | PhoBERT: BERT tiền huấn luyện cho tiếng Việt | SOTA trên các bộ kiểm chuẩn NLP tiếng Việt | Mô hình nền tảng cho NLP tài chính tiếng Việt || @huang2023finbert | Vector nhúng BERT trên các cuộc họp công bố kết quả kinh doanh | Vector nhúng theo ngữ cảnh dự báo lợi nhuận kế toán tương lai tốt hơn BoW | Áp dụng cho bản ghi các cuộc họp công bố kết quả kinh doanh của Việt Nam || @jha2024chatgpt | Phân loại tài chính zero-shot dựa trên GPT | Các LLM sánh ngang bộ phân loại đã tinh chỉnh | Phân loại văn bản tài chính tiếng Việt zero-shot bằng các LLM đa ngôn ngữ || @wei2026narrative | Sự mơ hồ diễn ngôn được trích xuất từ tin tức kinh tế | Sự mơ hồ dựa trên tin tức dự báo lợi nhuận thị trường vượt trội so với các thước đo dựa trên khảo sát và lợi nhuận; ác cảm chuyển thành chấp nhận khi xác suất thua lỗ cao | Chỉ số mơ hồ tổng hợp từ tin tức tài chính Việt Nam để dự báo lợi nhuận VN-Index || @crowley2024executive | Phân loại bằng ML + độ tương đồng nội dung của các tweet từ lãnh đạo doanh nghiệp S&P 1500 | Nhà đầu tư phản ứng với các tweet tài chính của lãnh đạo thông qua cả kênh thông tin mới lẫn kênh uy tín | Các bài đăng Facebook/Zalo của lãnh đạo doanh nghiệp niêm yết Việt Nam (@sec-textual-exec-social) |: Các nghiên cứu chính về phân tích văn bản trong tài chính {#tbl-literature}# Dữ liệu: Các doanh nghiệp niêm yết Việt Nam từ DataCore.vn {#sec-textual-data}## Xây dựng tập hợp doanh nghiệp {#sec-textual-universe}Chúng ta xây dựng tập hợp các doanh nghiệp niêm yết Việt Nam. Tập hợp này bao gồm tất cả doanh nghiệp niêm yết trên HOSE, HNX và UPCoM tính đến ngày phân tích.```{python}#| label: setup#| code-summary: "Import required libraries"import pandas as pdimport numpy as npimport reimport unicodedataimport warningsimport matplotlib.pyplot as pltimport seaborn as snsfrom collections import defaultdictfrom typing import List, Dict, Tuple, Optionalwarnings.filterwarnings('ignore')np.random.seed(42)# Plotting configurationplt.rcParams['figure.figsize'] = (10, 6)plt.rcParams['font.size'] =11sns.set_style("whitegrid")``````{python}#| label: build-universe#| eval: false#| code-summary: "Connect to DataCore.vn and build universe of listed firms"from datacore import DataCoreAPI # DataCore.vn Python client# Initialize connectiondc = DataCoreAPI(api_key='YOUR_API_KEY')# Retrieve universe of all listed firmsuniverse = dc.get_listed_firms( exchanges=['HOSE', 'HNX', 'UPCOM'], as_of='2024-12-31', fields=['ticker', 'company_name', 'company_name_en','exchange', 'listing_date', 'delisting_date','icb_industry', 'icb_sector', 'icb_subsector','market_cap', 'total_assets', 'revenue' ])print(f'Total listed firms: {len(universe)}')print(f'HOSE: {len(universe[universe.exchange=="HOSE"])}')print(f'HNX: {len(universe[universe.exchange=="HNX"])}')print(f'UPCoM: {len(universe[universe.exchange=="UPCOM"])}')```| Sàn giao dịch | Số DN | Vốn hóa TB (tỷ VND) | Vốn hóa trung vị (tỷ VND) | Tổng vốn hóa (nghìn tỷ VND) ||:--------------|--------------:|--------------:|--------------:|--------------:|| HOSE | 403 | 12,847 | 3,215 | 5,177 || HNX | 334 | 2,156 | 687 | 720 || UPCoM | 868 | 1,043 | 298 | 905 || **Tổng** | **1,605** | **4,239** | **712** | **6,802** |: Tập hợp các doanh nghiệp niêm yết Việt Nam theo sàn giao dịch (tính đến tháng 12 năm 2024) {#tbl-universe}## Thu thập phần mô tả hoạt động kinh doanh {#sec-textual-bus-desc}Phần mô tả hoạt động kinh doanh của tất cả doanh nghiệp niêm yết có thể có cả bằng tiếng Việt và tiếng Anh. Chúng ta thu thập cả hai phiên bản cho phân tích. Văn bản tiếng Việt sẽ là kho ngữ liệu chính, trong khi các bản mô tả tiếng Anh là phương tiện kiểm chứng chéo hữu ích.```{python}#| label: get-bus-desc#| eval: false#| code-summary: "Retrieve business descriptions."# Get business descriptions (Vietnamese and English)bus_desc = dc.get_business_descriptions( tickers=universe.ticker.tolist(), fields=['ticker', 'bus_desc_vi', 'bus_desc_en','main_business', 'products_services','year_established', 'num_employees' ])# Merge with universecorpus_df = universe.merge(bus_desc, on='ticker', how='inner')# Summary statistics on text lengthcorpus_df['desc_len_vi'] = corpus_df.bus_desc_vi.str.len()corpus_df['desc_len_en'] = corpus_df.bus_desc_en.str.len()corpus_df['word_count_vi'] = corpus_df.bus_desc_vi.str.split().str.len()print(corpus_df[['desc_len_vi', 'desc_len_en', 'word_count_vi']] .describe().round(0))```| Thống kê | Trung bình | Trung vị | Độ lệch chuẩn | Nhỏ nhất | Lớn nhất ||:----------------|------:|-------:|--------:|----:|-------:|| Ký tự (VN) | 2,847 | 2,156 | 1,923 | 87 | 18,432 || Ký tự (EN) | 3,412 | 2,689 | 2,245 | 102 | 22,156 || Từ (VN) | 487 | 372 | 318 | 15 | 3,216 |: Thống kê mô tả của văn bản mô tả hoạt động kinh doanh {#tbl-desc-stats}## Thu thập văn bản báo cáo thường niên {#sec-textual-annual-text}Beyond business descriptions, annual or quarterly reports provide richer and more time-varying textual data. We extract the Management Discussion and Analysis (MD&A) sections, which are most informative for financial analysis [@li2010textual; @bonsall2017plain]. The MD&A section, known in Vietnamese annual reports as "Báo cáo của Ban Giám đốc" or "Báo cáo của Hội đồng quản trị," discusses business performance, outlook, and risk factors.```{python}#| label: get-annual-text#| eval: false#| code-summary: "Retrieve annual report MD&A sections"# Get annual report MD&A sections (2015-2024)annual_text = dc.get_annual_report_text( tickers=universe.ticker.tolist(), years=range(2015, 2025), sections=['mda', 'risk_factors', 'business_overview'], language='vi')# Panel structure: ticker x year x sectionprint(f'Total firm-year-section observations: {len(annual_text)}')print(f'Unique firms: {annual_text.ticker.nunique()}')print(f'Year range: {annual_text.year.min()}-{annual_text.year.max()}')# Calculate text changes year-over-yearannual_text = annual_text.sort_values(['ticker', 'year'])annual_text['text_len'] = annual_text.text.str.len()annual_text['text_change_pct'] = ( annual_text.groupby('ticker')['text_len'] .pct_change() *100)```# Tiền xử lý văn bản tiếng Việt {#sec-textual-preprocessing}## Tách từ tiếng Việt {#sec-textual-segmentation}The most critical preprocessing step for Vietnamese text is word segmentation (phân đoạn từ). Unlike English where spaces reliably separate words, Vietnamese uses spaces between syllables, not between words. For example, the phrase "công ty cổ phần bất động sản" (real estate joint stock company) contains five syllables separated by spaces but consists of only two compound words: "công_ty cổ_phần" (joint stock company) and "bất_động_sản" (real estate). Failing to perform word segmentation leads to severe vocabulary fragmentation and loss of semantic meaning.| Giai đoạn | Văn bản | Diễn giải ||:------------------|:------------------|:---------------------------------|| Thô |`công ty cổ phần thương mại dịch vụ`| 8 âm tiết, ranh giới từ chưa rõ ràng || Đã tách từ |`công_ty cổ_phần thương_mại dịch_vụ`| 4 từ: công ty \| cổ phần \| thương mại \| dịch vụ |: Ví dụ về tách từ tiếng Việt {#tbl-segmentation}```{python}#| label: word-segmentation#| eval: false#| code-summary: "Vietnamese word segmentation using underthesea"from underthesea import word_tokenizedef segment_vietnamese(text: str) ->str:"""Segment Vietnamese text into words using underthesea."""if pd.isna(text) or text.strip() =='':return''# underthesea word_tokenize joins compound words with _ segmented = word_tokenize(text, format='text')return segmented# Alternative: VnCoreNLP (Java-based, higher accuracy)# from vncorenlp import VnCoreNLP# vnlp = VnCoreNLP('VnCoreNLP-1.2.jar', annotators='wseg')# segmented = vnlp.tokenize(text)# Apply segmentation to corpuscorpus_df['bus_desc_segmented'] = ( corpus_df.bus_desc_vi.apply(segment_vietnamese))# Examplesample = corpus_df.iloc[0]print('Raw:', sample.bus_desc_vi[:200])print('Segmented:', sample.bus_desc_segmented[:200])```## Quy trình làm sạch văn bản đầy đủ {#sec-textual-cleaning}After word segmentation, we apply a cleaning pipeline. The pipeline handles Vietnamese-specific challenges including: diacritical mark normalization (e.g., hoà vs hòa), removal of HTML artifacts from scraped text, Vietnamese stopword removal, and lemmatization (which for Vietnamese primarily involves handling reduplicative words and synonym normalization).```{python}#| label: cleaning-pipeline#| eval: false#| code-summary: "Full Vietnamese text cleaning pipeline"# Vietnamese stopwords (domain-adapted)VIETNAMESE_STOPWORDS = {'có', 'là', 'và', 'của', 'cho', 'được', 'trong','các', 'những', 'với', 'từ', 'khi', 'hoặc','đã', 'sẽ', 'đang', 'để', 'này', 'đó','như', 'theo', 'về', 'bằng', 'tại', 'trên','cũng', 'rất', 'nhiều', 'ít', 'một', 'hai',# Financial domain stopwords'năm', 'quý', 'tháng', 'ngày', 'kỳ','việt_nam', 'tổng', 'giá_trị', 'triệu', 'tỷ',}def clean_vietnamese_text( text: str, segment: bool=True, remove_stops: bool=True, lowercase: bool=True, min_word_len: int=2) ->str:""" Full Vietnamese text cleaning pipeline. Parameters ---------- text : str Raw Vietnamese text. segment : bool Whether to perform word segmentation. remove_stops : bool Whether to remove Vietnamese stopwords. lowercase : bool Whether to convert to lowercase. min_word_len : int Minimum word length to keep. Returns ------- str Cleaned text. """if pd.isna(text) or text.strip() =='':return''# 1. Unicode normalization (NFC form for Vietnamese) text = unicodedata.normalize('NFC', text)# 2. Remove HTML tags and special characters text = re.sub(r'<[^>]+>', ' ', text) text = re.sub(r'[\d]+', ' ', text) # Remove numbers text = re.sub(r'[^\w\s\u00C0-\u024F]', ' ', text) # Keep VN chars# 3. Lowercaseif lowercase: text = text.lower()# 4. Word segmentationif segment: text = word_tokenize(text, format='text')# 5. Tokenize and filter tokens = text.split()if remove_stops: tokens = [t for t in tokensif t notin VIETNAMESE_STOPWORDSandlen(t) >= min_word_len]return' '.join(tokens)# Apply to corpuscorpus_df['text_clean'] = ( corpus_df.bus_desc_vi .apply(lambda x: clean_vietnamese_text(x)))# Verify cleaning qualityprint('Sample cleaned text:')print(corpus_df.iloc[0].text_clean[:300])```## Làm sạch văn bản tiếng Anh {#sec-textual-english-cleaning}Đối với các doanh nghiệp cũng cung cấp phần mô tả hoạt động kinh doanh bằng tiếng Anh, chúng ta áp dụng quy trình NLP tiếng Anh tiêu chuẩn bằng spaCy và NLTK. Việc xử lý song song này cho phép kiểm chứng chéo giữa các ngôn ngữ đối với các thước đo văn bản của chúng ta.```{python}#| label: english-cleaning#| eval: false#| code-summary: "English text cleaning pipeline"import spacyfrom nltk.corpus import stopwordsimport gensimnlp = spacy.load('en_core_web_sm', disable=['parser', 'ner'])stop_words =set(stopwords.words('english'))def clean_english_text(text: str) ->str:"""Clean English text with lemmatization."""if pd.isna(text) or text.strip() =='':return'' text = text.lower().strip() text = re.sub(r'[^a-zA-Z\s]', ' ', text) doc = nlp(text) tokens = [token.lemma_ for token in docif token.lemma_ notin stop_wordsandlen(token.lemma_) >2andnot token.is_punct]return' '.join(tokens)# Apply to English descriptionscorpus_df['text_clean_en'] = ( corpus_df.bus_desc_en .apply(lambda x: clean_english_text(x)))```# Biểu diễn tài liệu: Bag-of-Words và TF-IDF {#sec-textual-bow-tfidf}## Biểu diễn bag-of-words {#sec-textual-bow}Mô hình bag-of-words (BoW) biểu diễn mỗi tài liệu dưới dạng một vector tần suất từ, bỏ qua thứ tự từ. Dù đơn giản, BoW vẫn là công cụ chủ lực trong phân tích văn bản tài chính. Về mặt hình thức, với một bộ từ vựng $V = \{w_1, w_2, \ldots, w_{|V|}\}$, tài liệu $d$ được biểu diễn bằng một vector $\mathbf{x}_d$ trong đó mỗi phần tử $x_{d,j}$ đếm tần suất của từ $w_j$ trong tài liệu $d$:$$\mathbf{x}_d = [\text{tf}(w_1, d), \; \text{tf}(w_2, d), \; \ldots, \; \text{tf}(w_{|V|}, d)]$$ {#eq-bow}trong đó $\text{tf}(w, d)$ là tần suất từ của từ $w$ trong tài liệu $d$.```{python}#| label: bow-vectorization#| code-summary: "Bag-of-Words vectorization"#| eval: falsefrom sklearn.feature_extraction.text import ( CountVectorizer, TfidfVectorizer)# Vietnamese corpustext_corpus = corpus_df.text_clean.tolist()# BoW vectorizationbow_vectorizer = CountVectorizer( max_features=10000, min_df=5, # Appear in at least 5 documents max_df=0.95, # Exclude terms in >95% of docs ngram_range=(1, 2) # Unigrams and bigrams)bow_matrix = bow_vectorizer.fit_transform(text_corpus)print(f'Vocabulary size: {len(bow_vectorizer.vocabulary_)}')print(f'Document-term matrix shape: {bow_matrix.shape}')print(f'Sparsity: {1- bow_matrix.nnz / np.prod(bow_matrix.shape):.4f}')# Top 20 most frequent termsword_freq = pd.DataFrame({'word': bow_vectorizer.get_feature_names_out(),'freq': bow_matrix.sum(axis=0).A1}).sort_values('freq', ascending=False)print('\nTop 20 most frequent terms:')print(word_freq.head(20).to_string(index=False))``````{python}#| label: fig-word-freq#| fig-cap: "20 thuật ngữ xuất hiện thường xuyên nhất trong phần mô tả hoạt động kinh doanh tiếng Việt"#| code-summary: "Plot word frequency distribution"#| eval: falsefig, ax = plt.subplots(figsize=(12, 6))top20 = word_freq.head(20)ax.barh(range(len(top20)), top20.freq.values, color='#2C5282')ax.set_yticks(range(len(top20)))ax.set_yticklabels(top20.word.values)ax.invert_yaxis()ax.set_xlabel('Frequency')ax.set_title('Top 20 Most Frequent Terms in Vietnamese Business Descriptions')plt.tight_layout()plt.show()```|\#| Term (VN) | Freq |\#| Term (VN) | Freq |\#| Term (VN) | Freq ||----:|:------------|------:|----:|:----------|------:|----:|:-------------|------:|| 1 | sản_xuất | 4,287 | 8 | công_nghệ | 1,956 | 15 | xuất_khẩu | 1,123 || 2 | kinh_doanh | 3,891 | 9 | tài_chính | 1,845 | 16 | bất_động_sản | 1,087 || 3 | dịch_vụ | 3,654 | 10 | ngân_hàng | 1,734 | 17 | năng_lượng | 1,045 || 4 | công_ty | 3,412 | 11 | đầu_tư | 1,623 | 18 | bảo_hiểm | 987 || 5 | thương_mại | 2,876 | 12 | xây_dựng | 1,534 | 19 | du_lịch | 923 || 6 | cổ_phần | 2,543 | 13 | vận_tải | 1,345 | 20 | viễn_thông | 876 || 7 | chứng_khoán | 2,134 | 14 | thực_phẩm | 1,234 ||||: 20 thuật ngữ xuất hiện thường xuyên nhất trong phần mô tả hoạt động kinh doanh tiếng Việt {#tbl-top-terms}## Trọng số TF-IDF {#sec-textual-tfidf}Tần suất từ – nghịch đảo tần suất tài liệu (TF-IDF) khắc phục một hạn chế chính của số đếm từ thô bằng cách giảm trọng số của những từ xuất hiện trong nhiều tài liệu (và do đó mang ít thông tin phân biệt hơn). Trọng số TF-IDF của từ $w$ trong tài liệu $d$ thuộc kho ngữ liệu $D$ là:$$\text{tfidf}(w, d, D) = \text{tf}(w, d) \times \log\left(\frac{|D|}{\text{df}(w, D)}\right)$$ {#eq-tfidf}where $|D|$ is the total number of documents and $\text{df}(w, D)$ is the number of documents containing term $w$. This weighting scheme ensures that industry-specific terminology (e.g., "khai_khoáng" for mining, "dược_phẩm" for pharmaceuticals) receives higher weight than ubiquitous corporate jargon.```{python}#| label: tfidf-vectorization#| eval: false#| code-summary: "TF-IDF vectorization with per-industry analysis"tfidf_vectorizer = TfidfVectorizer( max_features=10000, min_df=5, max_df=0.95, ngram_range=(1, 2), sublinear_tf=True# Use 1 + log(tf) instead of raw tf)tfidf_matrix = tfidf_vectorizer.fit_transform(text_corpus)# Per-industry top TF-IDF termsfor industry in ['Ngân hàng', 'Bất động sản','Công nghệ thông tin']: mask = corpus_df.icb_sector == industryif mask.sum() ==0:continue mean_tfidf = tfidf_matrix[mask.values].mean(axis=0).A1 top_idx = mean_tfidf.argsort()[-10:][::-1] terms = tfidf_vectorizer.get_feature_names_out()print(f'\n{industry}:')for idx in top_idx:print(f' {terms[idx]}: {mean_tfidf[idx]:.4f}')``````{python}#| label: fig-tfidf-heatmap#| fig-cap: "Bản đồ nhiệt TF-IDF: các thuật ngữ hàng đầu theo ngành ICB"#| eval: false#| code-summary: "Visualize industry-distinctive terms"# Build industry x term TF-IDF matrix for top sectorstop_sectors = corpus_df.icb_sector.value_counts().head(8).index.tolist()terms = tfidf_vectorizer.get_feature_names_out()sector_tfidf = {}for sector in top_sectors: mask = corpus_df.icb_sector == sectorif mask.sum() ==0:continue mean_tfidf = tfidf_matrix[mask.values].mean(axis=0).A1 top_idx = mean_tfidf.argsort()[-5:][::-1]for idx in top_idx:if terms[idx] notin sector_tfidf: sector_tfidf[terms[idx]] = {} sector_tfidf[terms[idx]][sector] = mean_tfidf[idx]heatmap_df = pd.DataFrame(sector_tfidf).T.fillna(0)fig, ax = plt.subplots(figsize=(14, 10))sns.heatmap(heatmap_df, annot=True, fmt='.3f', cmap='Blues', linewidths=0.5, ax=ax)ax.set_title('TF-IDF Heatmap: Industry-Distinctive Terms')ax.set_xlabel('ICB Sector')ax.set_ylabel('Term')plt.tight_layout()plt.show()```# Mô hình hóa chủ đề {#sec-textual-topic-modeling}## Phân bổ Dirichlet tiềm ẩn (LDA) {#sec-textual-lda}Phân bổ Dirichlet tiềm ẩn (Latent Dirichlet Allocation) [@blei2003latent] là một mô hình xác suất sinh dùng để khám phá các chủ đề tiềm ẩn trong một kho ngữ liệu. Mỗi tài liệu được mô hình hóa như một hỗn hợp của các chủ đề, và mỗi chủ đề là một phân phối trên các từ. LDA đã được áp dụng rộng rãi trong tài chính để nhận diện nội dung theo chủ đề trong các báo cáo 10-K [@dyer2017evolution], các cuộc họp công bố kết quả kinh doanh [@huang2018analyst] và các bài báo tin tức [@bybee2023narrative].Quá trình sinh giả định rằng:1. Với mỗi chủ đề $k$, rút một phân phối từ $\boldsymbol{\phi}_k \sim \text{Dir}(\beta)$.2. Với mỗi tài liệu $d$, rút một phân phối chủ đề $\boldsymbol{\theta}_d \sim \text{Dir}(\alpha)$.3. Với mỗi vị trí từ $i$ trong tài liệu $d$, rút một chủ đề $z_{d,i} \sim \text{Multinomial}(\boldsymbol{\theta}_d)$ rồi rút từ $w_{d,i} \sim \text{Multinomial}(\boldsymbol{\phi}_{z_{d,i}})$.```{python}#| label: lda-model#| code-summary: "LDA topic modeling with grid search over K"#| eval: falsefrom sklearn.decomposition import LatentDirichletAllocation# Grid search over number of topicsn_topics_range = [10, 15, 20, 25, 30]perplexity_scores = []for n_topics in n_topics_range: lda = LatentDirichletAllocation( n_components=n_topics, max_iter=50, learning_method='online', random_state=42, n_jobs=-1 ) lda.fit(bow_matrix) perplexity = lda.perplexity(bow_matrix) perplexity_scores.append({'n_topics': n_topics,'perplexity': perplexity,'log_likelihood': lda.score(bow_matrix) })print(f'K={n_topics}: perplexity={perplexity:.2f}')# Select optimal K (e.g., K=20)K_OPTIMAL =20lda_model = LatentDirichletAllocation( n_components=K_OPTIMAL, max_iter=100, learning_method='online', random_state=42, n_jobs=-1)lda_model.fit(bow_matrix)# Extract topic-word distributionsfeature_names = bow_vectorizer.get_feature_names_out()for topic_idx, topic inenumerate(lda_model.components_): top_words = [feature_names[i]for i in topic.argsort()[:-11:-1]]print(f'Topic {topic_idx}: {" | ".join(top_words)}')``````{python}#| label: fig-lda-perplexity#| fig-cap: "Lựa chọn mô hình LDA: độ rối (perplexity) so với số lượng chủ đề"#| code-summary: "Plot perplexity scores for topic model selection"#| eval: falseperp_df = pd.DataFrame(perplexity_scores)fig, ax = plt.subplots(figsize=(8, 5))ax.plot(perp_df.n_topics, perp_df.perplexity, 'o-', color='#2C5282', linewidth=2, markersize=8)ax.axvline(x=K_OPTIMAL, color='red', linestyle='--', alpha=0.7, label=f'Selected K={K_OPTIMAL}')ax.set_xlabel('Number of Topics (K)')ax.set_ylabel('Perplexity (lower is better)')ax.set_title('LDA Model Selection')ax.legend()plt.tight_layout()plt.show()```| Chủ đề | Diễn giải | Các từ hàng đầu ||------------------:|:------------------------------|:---------------------|| 0 | Ngân hàng & Tài chính |`ngân_hàng`\|`tín_dụng`\|`cho_vay`\|`tiền_gửi`\|`lãi_suất`\|`thanh_toán`\|`tài_khoản`|| 3 | Bất động sản |`bất_động_sản`\|`dự_án`\|`căn_hộ`\|`khu_đô_thị`\|`xây_dựng`\|`nhà_ở`|| 7 | Công nghệ |`công_nghệ`\|`phần_mềm`\|`giải_pháp`\|`hệ_thống`\|`số_hóa`\|`dữ_liệu`|| 11 | Sản xuất |`sản_xuất`\|`nguyên_liệu`\|`nhà_máy`\|`chất_lượng`\|`công_suất`\|`xuất_khẩu`|| 15 | Chứng khoán |`chứng_khoán`\|`môi_giới`\|`đầu_tư`\|`cổ_phiếu`\|`danh_mục`\|`quản_lý_quỹ`|: Một số chủ đề LDA được chọn từ phần mô tả hoạt động kinh doanh tiếng Việt (K=20) {#tbl-lda-topics}## BERTopic: Mô hình hóa chủ đề bằng mạng nơ-ron {#sec-textual-bertopic}BERTopic [@grootendorst2022bertopic] là một bước tiến đáng kể so với LDA nhờ tận dụng vector nhúng của mô hình ngôn ngữ tiền huấn luyện, giảm chiều bằng UMAP và phân cụm theo mật độ phân cấp (HDBSCAN) để khám phá các chủ đề. Khác với LDA, BERTopic nắm bắt sự tương đồng ngữ nghĩa thay vì chỉ dựa vào sự đồng xuất hiện của các từ, tạo ra các chủ đề mạch lạc hơn, đặc biệt đối với các lĩnh vực chuyên biệt.```{python}#| label: bertopic-model#| code-summary: "BERTopic with PhoBERT embeddings"#| eval: falsefrom bertopic import BERTopicfrom sentence_transformers import SentenceTransformerfrom umap import UMAPfrom hdbscan import HDBSCAN# Use PhoBERT-based sentence transformer for Vietnameseembedding_model = SentenceTransformer('bkai-foundation-models/vietnamese-bi-encoder')# Custom UMAP and HDBSCAN for better controlumap_model = UMAP( n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine', random_state=42)hdbscan_model = HDBSCAN( min_cluster_size=10, min_samples=5, metric='euclidean', prediction_data=True)# Fit BERTopictopic_model = BERTopic( embedding_model=embedding_model, umap_model=umap_model, hdbscan_model=hdbscan_model, language='multilingual', calculate_probabilities=True, verbose=True)# Vietnamese text (use segmented text for better results)docs = corpus_df.bus_desc_segmented.tolist()topics, probs = topic_model.fit_transform(docs)# Inspect topicstopic_info = topic_model.get_topic_info()print(topic_info.head(20))``````{python}#| label: fig-bertopic-viz#| fig-cap: "Trực quan hóa cụm tài liệu BERTopic (phép chiếu UMAP của vector nhúng PhoBERT, tô màu theo chủ đề suy ra). Mỗi điểm đại diện cho một doanh nghiệp niêm yết Việt Nam."#| code-summary: "Visualize BERTopic document clusters"#| eval: false# Visualize topic hierarchyfig_hierarchy = topic_model.visualize_hierarchy()fig_hierarchy.show()# Visualize document clustersfig_docs = topic_model.visualize_documents( docs, reduced_embeddings=umap_model.embedding_)fig_docs.show()# Topic word scores (barchart)fig_barchart = topic_model.visualize_barchart(top_n_topics=10)fig_barchart.show()```# Phân tích cảm xúc tài chính {#sec-textual-sentiment}## Cách tiếp cận dựa trên từ điển {#sec-textual-dict-sentiment}Chúng ta xây dựng một từ điển cảm xúc tài chính tiếng Việt theo phương pháp của @loughran2011liability. Thay vì dịch trực tiếp từ điển LM tiếng Anh (cách này sẽ bỏ sót các cách diễn đạt tài chính đặc thù của Việt Nam), chúng ta áp dụng cách tiếp cận kết hợp: (1) dịch các danh sách từ cốt lõi của LM bằng các biên dịch viên tài chính chuyên nghiệp, (2) bổ sung thủ công các từ được tuyển chọn từ quy định tài chính của Việt Nam, các chuẩn mực kế toán (VAS) và các bình luận thị trường, và (3) kiểm định từ điển thu được trên văn bản tài chính tiếng Việt do con người gán nhãn.| Loại | Thuật ngữ tiếng Việt | Nghĩa tiếng Anh | Nguồn | Số lần xuất hiện trong kho ngữ liệu ||:----------|:----------------|:--------------|:--------------|----------------:|| Tiêu cực |`lỗ`| loss | Dịch từ LM | 2,341 || Tiêu cực |`sụt_giảm`| decline | Tuyển chọn thủ công | 1,876 || Tiêu cực |`nợ_xấu`| bad debt | Đặc thù VAS | 1,234 || Tiêu cực |`rủi_ro`| risk | Dịch từ LM | 3,567 || Tích cực |`tăng_trưởng`| growth | Dịch từ LM | 4,123 || Tích cực |`lợi_nhuận`| profit | Dịch từ LM | 3,891 || Tích cực |`hiệu_quả`| efficiency | Tuyển chọn thủ công | 2,456 || Bất định |`biến_động`| volatility | Dịch từ LM | 1,567 || Tranh chấp pháp lý |`tranh_chấp`| dispute | Pháp lý VN | 876 || Tranh chấp pháp lý |`khởi_kiện`| lawsuit | Pháp lý VN | 234 |: Từ điển cảm xúc tài chính tiếng Việt: các mục mẫu {#tbl-sentiment-lexicon}```{python}#| label: dict-sentiment#| code-summary: "Dictionary-based financial sentiment scoring"#| eval: false# Load Vietnamese financial sentiment lexicon# sentiment_dict = dc.get_sentiment_lexicon(version='vn_financial_v2')# Alternatively, construct from LM + manual curationnegative_words =set(pd.read_csv('lexicons/vn_negative.txt', header=None)[0])positive_words =set(pd.read_csv('lexicons/vn_positive.txt', header=None)[0])uncertain_words =set(pd.read_csv('lexicons/vn_uncertain.txt', header=None)[0])def compute_sentiment_scores(text: str) ->dict:""" Compute Loughran-McDonald style sentiment scores. Returns proportions (word count / total words). """ tokens = text.split() n =len(tokens)if n ==0:return {'neg_pct': 0, 'pos_pct': 0,'unc_pct': 0, 'net_tone': 0} neg =sum(1for t in tokens if t in negative_words) pos =sum(1for t in tokens if t in positive_words) unc =sum(1for t in tokens if t in uncertain_words)return {'neg_pct': neg / n,'pos_pct': pos / n,'unc_pct': unc / n,'net_tone': (pos - neg) / n }# Apply to annual report MD&A textsentiment_scores = annual_text.text_clean.apply(lambda x: pd.Series(compute_sentiment_scores(x)))annual_text = pd.concat([annual_text, sentiment_scores], axis=1)``````{python}#| label: fig-sentiment-dist#| fig-cap: "Phân phối sắc thái cảm xúc ròng trên các quan sát doanh nghiệp-năm"#| eval: false#| code-summary: "Plot sentiment distribution"fig, axes = plt.subplots(1, 3, figsize=(15, 5))axes[0].hist(annual_text.neg_pct, bins=50, color='#E53E3E', alpha=0.7, edgecolor='white')axes[0].set_title('Negative Word Proportion')axes[0].set_xlabel('Proportion')axes[1].hist(annual_text.pos_pct, bins=50, color='#38A169', alpha=0.7, edgecolor='white')axes[1].set_title('Positive Word Proportion')axes[1].set_xlabel('Proportion')axes[2].hist(annual_text.net_tone, bins=50, color='#2C5282', alpha=0.7, edgecolor='white')axes[2].set_title('Net Tone (Positive - Negative)')axes[2].set_xlabel('Net Tone')plt.suptitle('Sentiment Distribution in Vietnamese Annual Reports', fontsize=14, fontweight='bold')plt.tight_layout()plt.show()```## Phân loại cảm xúc dựa trên Transformer {#sec-textual-bert-sentiment}Các cách tiếp cận dựa trên từ điển bị hạn chế vì không nắm bắt được ngữ cảnh, phủ định và mỉa mai. Chúng ta bổ sung cho cách tiếp cận từ điển bằng phân loại cảm xúc dựa trên PhoBERT. Chúng ta tinh chỉnh PhoBERT v2.```{python}#| label: phobert-sentiment#| eval: false#| code-summary: "PhoBERT-based sentiment classification"from transformers import ( AutoModelForSequenceClassification, AutoTokenizer, pipeline)import torch# Load fine-tuned ViFinBERT for sentimentmodel_name ='vinai/phobert-base-v2'tokenizer = AutoTokenizer.from_pretrained(model_name)model = AutoModelForSequenceClassification.from_pretrained( model_name, num_labels=3# positive, negative, neutral)# Create sentiment pipelinesentiment_pipe = pipeline('text-classification', model=model, tokenizer=tokenizer, device=0if torch.cuda.is_available() else-1, max_length=256, truncation=True, batch_size=32)# For long documents, split into sentences firstfrom underthesea import sent_tokenizedef document_sentiment(text: str) ->dict:"""Aggregate sentence-level sentiment for a document.""" sentences = sent_tokenize(text)ifnot sentences:return {'bert_pos': 0, 'bert_neg': 0, 'bert_neu': 0} results = sentiment_pipe(sentences[:100]) # Cap at 100 sents labels = [r['label'] for r in results] n =len(labels)return {'bert_pos': labels.count('POSITIVE') / n,'bert_neg': labels.count('NEGATIVE') / n,'bert_neu': labels.count('NEUTRAL') / n,'bert_tone': (labels.count('POSITIVE') - labels.count('NEGATIVE')) / n }```| Phương pháp | Độ chính xác | F1 (Tích cực) | F1 (Tiêu cực) | F1 (Trung lập) ||:------------------------|:---------:|:---------:|:---------:|:------------:|| Từ điển VN-LM | 0.612 | 0.584 | 0.637 | 0.598 || PhoBERT (zero-shot) | 0.724 | 0.698 | 0.741 | 0.712 || PhoBERT v2 (đã tinh chỉnh) | **0.831** | **0.812** | **0.847** | **0.824** |: So sánh các phương pháp cảm xúc: từ điển so với PhoBERT trên tập kiểm định (N=500) {#tbl-sentiment-comparison}# Độ tương đồng giữa các doanh nghiệp dựa trên văn bản và xác định nhóm cùng ngành {#sec-textual-similarity}## Độ tương đồng cosine trên các vector TF-IDF {#sec-textual-tfidf-similarity}Theo @hoberg2016text, chúng ta tính độ tương đồng cosine theo từng cặp giữa các doanh nghiệp dựa trên vector TF-IDF của phần mô tả hoạt động kinh doanh. Với hai tài liệu được biểu diễn bằng các vector TF-IDF $\mathbf{a}$ và $\mathbf{b}$, độ tương đồng cosine được định nghĩa là:$$\cos(\mathbf{a}, \mathbf{b}) = \frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\| \times \|\mathbf{b}\|}$$ {#eq-cosine}Thước đo này nhận giá trị từ 0 (hoàn toàn khác biệt) đến 1 (nội dung giống hệt) và bất biến với độ dài tài liệu. Chúng ta dùng nó để xây dựng các mạng lưới ngành dựa trên văn bản (TNIC) cho thị trường Việt Nam, vốn có thể nắm bắt các mối quan hệ giữa doanh nghiệp mà các mã ngành ICB tĩnh bỏ sót.```{python}#| label: tfidf-similarity#| code-summary: "Pairwise TF-IDF cosine similarity"#| eval: falsefrom sklearn.metrics.pairwise import cosine_similarity# Compute pairwise similarity matrixsim_matrix = cosine_similarity(tfidf_matrix)# Convert to DataFrame for easy lookuptickers = corpus_df.ticker.tolist()sim_df = pd.DataFrame( sim_matrix, index=tickers, columns=tickers)# For each firm, find top-5 most similar peersdef get_top_peers(ticker: str, n: int=5) -> pd.DataFrame:"""Return top-n most similar firms by TF-IDF cosine.""" sims = sim_df[ticker].drop(ticker).sort_values( ascending=False ).head(n) peers = corpus_df.set_index('ticker').loc[sims.index] peers['similarity'] = sims.valuesreturn peers[['company_name', 'icb_sector','market_cap', 'similarity']]# Examplesfor ticker in ['VCB', 'VNM', 'FPT', 'VIC', 'HPG']:print(f'\nTop peers for {ticker}:')print(get_top_peers(ticker))```| Doanh nghiệp | Ngành ICB | Doanh nghiệp tương đồng 1 | Ngành của doanh nghiệp tương đồng 1 | Điểm tương đồng | Cùng ngành ICB? ||:-----|:------------|:-------|:--------------|:---------:|:---------:|| VCB | Ngân hàng | BID | Ngân hàng | 0.87 | Có || VCB | Ngân hàng | CTG | Ngân hàng | 0.84 | Có || VNM | Thực phẩm & Đồ uống | MCH | Thực phẩm & Đồ uống | 0.72 | Có || FPT | Công nghệ | CMG | Công nghệ | 0.68 | Có || VIC | Bất động sản | NVL | Bất động sản | 0.74 | Có || HPG | Thép | HSG | Thép | 0.81 | Có |: Xác định nhóm cùng ngành dựa trên văn bản: các doanh nghiệp tương đồng nhất (cosine TF-IDF) {#tbl-peers}```{python}#| label: fig-similarity-heatmap#| fig-cap: "Ma trận độ tương đồng cosine TF-IDF theo từng cặp cho một số doanh nghiệp Việt Nam được chọn"#| code-summary: "Visualize similarity matrix"#| eval: falsesample_tickers = ['VCB', 'BID', 'CTG', 'VNM', 'MCH','FPT', 'CMG', 'VIC', 'NVL', 'HPG','HSG', 'VHM', 'SSI', 'HCM', 'PNJ']sample_sim = sim_df.loc[sample_tickers, sample_tickers]fig, ax = plt.subplots(figsize=(10, 8))sns.heatmap(sample_sim, annot=True, fmt='.2f', cmap='Blues', vmin=0, vmax=1, square=True, linewidths=0.5, ax=ax)ax.set_title('Pairwise TF-IDF Cosine Similarity\n(Selected Vietnamese Listed Firms)')plt.tight_layout()plt.show()```## Độ tương đồng dựa trên vector nhúng {#sec-textual-embedding-similarity}Trong khi độ tương đồng cosine TF-IDF nắm bắt sự trùng lặp về từ vựng, nó bỏ sót sự tương đồng về ngữ nghĩa. Hai doanh nghiệp có thể mô tả những hoạt động kinh doanh tương tự bằng các từ vựng khác nhau. Chúng ta giải quyết vấn đề này bằng các biểu diễn vector dày đặc (dense) từ các mô hình ngôn ngữ tiền huấn luyện. Cụ thể, chúng ta tính vector nhúng tài liệu bằng Sentence-BERT [@reimers2019sentence] với một mô hình bi-encoder tiếng Việt.[^60_textual-3][^60_textual-3]: @reimers2019sentence chứng minh rằng vector nhúng sentence-BERT giảm chi phí tính toán cho các tác vụ đo độ tương đồng từ 65 giờ xuống còn 5 giây trên 10.000 cặp câu.```{python}#| label: embedding-similarity#| code-summary: "Sentence-BERT embedding-based similarity"#| eval: falsefrom sentence_transformers import SentenceTransformer# Vietnamese sentence transformersbert_model = SentenceTransformer('bkai-foundation-models/vietnamese-bi-encoder')# Compute embeddings for all firmsdocs_segmented = corpus_df.bus_desc_segmented.tolist()embeddings = sbert_model.encode( docs_segmented, batch_size=64, show_progress_bar=True, normalize_embeddings=True)# Pairwise similarityembed_sim = cosine_similarity(embeddings)embed_sim_df = pd.DataFrame( embed_sim, index=tickers, columns=tickers)# Compare TF-IDF vs embedding similarityfor ticker in ['VCB', 'FPT', 'VIC']: tfidf_peers = sim_df[ticker].drop(ticker).nlargest(5) embed_peers = embed_sim_df[ticker].drop(ticker).nlargest(5)print(f'\n{ticker} - TF-IDF peers: {tfidf_peers.index.tolist()}')print(f'{ticker} - Embed peers: {embed_peers.index.tolist()}')``````{python}#| label: fig-tsne-embeddings#| fig-cap: "Trực quan hóa t-SNE của vector nhúng câu PhoBERT (tô màu theo ngành ICB)"#| code-summary: "t-SNE projection of firm embeddings"#| eval: falsefrom sklearn.manifold import TSNE# t-SNE projectiontsne = TSNE(n_components=2, perplexity=30, random_state=42, metric='cosine')embeddings_2d = tsne.fit_transform(embeddings)fig, ax = plt.subplots(figsize=(14, 10))sectors = corpus_df.icb_sector.valuesunique_sectors = corpus_df.icb_sector.value_counts().head(10).indexcolors = plt.cm.tab10(range(10))for i, sector inenumerate(unique_sectors): mask = sectors == sector ax.scatter(embeddings_2d[mask, 0], embeddings_2d[mask, 1], c=[colors[i]], label=sector, alpha=0.6, s=30)ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize=9)ax.set_title('t-SNE of PhoBERT Embeddings by ICB Sector')ax.set_xlabel('t-SNE 1')ax.set_ylabel('t-SNE 2')plt.tight_layout()plt.show()```## Doc2Vec {#sec-textual-doc2vec}Chúng ta cũng triển khai Doc2Vec [@le2014distributed], phương pháp học các vector dày đặc có độ dài cố định cho các tài liệu có độ dài biến thiên. Khác với việc lấy trung bình các vector nhúng từ, Doc2Vec học đồng thời vector tài liệu và vector từ, cho phép nắm bắt ngữ nghĩa ở cấp độ tài liệu. Chúng ta huấn luyện Doc2Vec trên kho ngữ liệu mô tả hoạt động kinh doanh tiếng Việt bằng cách tiếp cận DBOW+DM nối ghép do @lau2016empirical khuyến nghị.```{python}#| label: doc2vec#| code-summary: "Doc2Vec for firm similarity (DBOW + DM ensemble)"#| eval: falsefrom gensim.models.doc2vec import Doc2Vec, TaggedDocument# Prepare tagged documentstagged_docs = [ TaggedDocument( words=text.split(), tags=[ticker] )for text, ticker inzip( corpus_df.text_clean.tolist(), corpus_df.ticker.tolist() )]# PV-DBOW: paragraph vector with distributed bag of wordsd2v_dbow = Doc2Vec( vector_size=100, dm=0, min_count=5, window=5, epochs=40, workers=4, seed=42)d2v_dbow.build_vocab(tagged_docs)d2v_dbow.train( tagged_docs, total_examples=d2v_dbow.corpus_count, epochs=d2v_dbow.epochs)# PV-DM: paragraph vector with distributed memoryd2v_dm = Doc2Vec( vector_size=100, dm=1, min_count=5, window=10, epochs=40, workers=4, seed=42)d2v_dm.build_vocab(tagged_docs)d2v_dm.train( tagged_docs, total_examples=d2v_dm.corpus_count, epochs=d2v_dm.epochs)# Concatenate DBOW + DM vectors (Lau & Baldwin, 2016)d2v_vectors = np.hstack([ [d2v_dbow.dv[t] for t in tickers], [d2v_dm.dv[t] for t in tickers]])# Most similar firmsfor ticker in ['VCB', 'FPT', 'VIC']: sims = d2v_dbow.dv.most_similar(ticker, topn=5)print(f'{ticker}: {[(s[0], f"{s[1]:.3f}") for s in sims]}')```# Các cách tiếp cận học sâu {#sec-textual-deep-learning}## Vector nhúng PhoBERT cho văn bản tài chính {#sec-textual-phobert}PhoBERT [@nguyen2020phobert], pre-trained on 20GB of Vietnamese text, provides contextualized word embeddings that capture meaning based on surrounding context. Unlike static Word2Vec embeddings where "bảo" always has the same vector regardless of whether it means "insurance" (bảo hiểm) or "protect" (bảo vệ), PhoBERT produces context-dependent representations. We extract `[CLS]` token embeddings as document representations.```{python}#| label: phobert-embeddings#| code-summary: "PhoBERT document embeddings with chunking strategy"#| eval: falsefrom transformers import AutoModel, AutoTokenizerimport torch# Load PhoBERTphobert_tokenizer = AutoTokenizer.from_pretrained('vinai/phobert-base-v2')phobert_model = AutoModel.from_pretrained('vinai/phobert-base-v2')phobert_model.eval()device = torch.device('cuda'if torch.cuda.is_available()else'cpu')phobert_model.to(device)def get_phobert_embedding(text: str, max_len: int=256):"""Extract [CLS] embedding from PhoBERT.""" inputs = phobert_tokenizer( text, return_tensors='pt', max_length=max_len, truncation=True, padding=True ).to(device)with torch.no_grad(): outputs = phobert_model(**inputs)# [CLS] token embedding cls_embedding = outputs.last_hidden_state[:, 0, :]return cls_embedding.cpu().numpy().flatten()# For long documents: chunk + average strategydef get_long_doc_embedding( text: str, chunk_size: int=256, stride: int=128):"""Handle long documents via chunked averaging.""" tokens = phobert_tokenizer.tokenize(text) embeddings = []for i inrange(0, len(tokens), stride): chunk = tokens[i:i + chunk_size] chunk_text = phobert_tokenizer.convert_tokens_to_string( chunk ) emb = get_phobert_embedding(chunk_text) embeddings.append(emb)return np.mean(embeddings, axis=0)# Compute embeddings for all firmsphobert_embeddings = np.array([ get_long_doc_embedding(text)for text in corpus_df.bus_desc_segmented.tolist()])```## Các ứng dụng của mô hình ngôn ngữ lớn {#sec-textual-llm}Những tiến bộ gần đây của các LLM mở ra những khả năng mới cho phân tích văn bản tài chính. Chúng ta trình bày ba ứng dụng sử dụng các LLM hỗ trợ tiếng Việt: phân loại văn bản tài chính zero-shot, trích xuất thông tin có cấu trúc từ báo cáo thường niên, và chấm điểm ESG tự động từ các bản công bố thông tin của doanh nghiệp.### Phân loại tài chính zero-shot {#sec-textual-zero-shot}```{python}#| label: llm-zero-shot#| code-summary: "LLM zero-shot financial text classification"#| eval: falseimport anthropic # Or openai, etc.client = anthropic.Anthropic()def classify_financial_text( text: str, categories: list= ['Growth outlook', 'Risk warning','Operational update', 'Financial performance','Strategic initiative', 'Regulatory compliance' ]) ->dict:"""Zero-shot classify Vietnamese financial text.""" prompt =f""" Classify the following Vietnamese financial text into one or more of these categories: {categories} Also provide: 1. Sentiment: positive / negative / neutral 2. Confidence: 0-1 3. Key entities mentioned Text: {text[:2000]} Respond in JSON format. """ response = client.messages.create( model='claude-sonnet-4-20250514', max_tokens=500, messages=[{'role': 'user', 'content': prompt}] )return response.content[0].text```### Trích xuất thông tin có cấu trúc {#sec-textual-extraction}```{python}#| label: llm-extraction#| code-summary: "LLM structured information extraction from annual reports"#| eval: falseimport jsondef extract_financial_info(annual_report_text: str) ->dict:"""Extract structured data from Vietnamese annual report.""" prompt =f""" From the following Vietnamese annual report excerpt, extract structured information in JSON format:{{ "revenue_mentioned": true/false, "revenue_direction": "increase"/"decrease"/"stable", "key_products": [list of main products/services], "competitors_mentioned": [list], "expansion_plans": "description or null", "risk_factors": [list of mentioned risks], "esg_mentions": {{ "environmental": [topics], "social": [topics], "governance": [topics]}}, "forward_looking_statements": [list], "capex_plans": "description or null"}} Text: {annual_report_text[:3000]} """ response = client.messages.create( model='claude-sonnet-4-20250514', max_tokens=1000, messages=[{'role': 'user', 'content': prompt}] )return json.loads(response.content[0].text)```### Chấm điểm ESG tự động {#sec-textual-esg}```{python}#| label: llm-esg#| code-summary: "LLM-based ESG scoring from corporate disclosures"#| eval: falsedef compute_esg_scores(text: str) ->dict:"""Score ESG dimensions from Vietnamese corporate disclosure.""" prompt =f""" Analyze the following Vietnamese corporate disclosure text and score each ESG dimension on a scale of 0-100 based on the depth and quality of disclosure: Return JSON:{{ "environmental_score": 0-100, "environmental_topics": [list of specific topics discussed], "social_score": 0-100, "social_topics": [list], "governance_score": 0-100, "governance_topics": [list], "overall_esg_score": 0-100, "assessment_confidence": 0-1, "notable_commitments": [list of specific commitments], "gaps_identified": [list of missing ESG disclosures]}} Text: {text[:4000]} """ response = client.messages.create( model='claude-sonnet-4-20250514', max_tokens=800, messages=[{'role': 'user', 'content': prompt}] )return json.loads(response.content[0].text)# Apply to all firms' annual reportsesg_results = []for _, row in annual_text.iterrows():try: scores = compute_esg_scores(row.text) scores['ticker'] = row.ticker scores['year'] = row.year esg_results.append(scores)exceptExceptionas e:print(f"Error for {row.ticker}{row.year}: {e}")esg_df = pd.DataFrame(esg_results)```# Các ứng dụng thực nghiệm {#sec-textual-empirical}## Cảm xúc văn bản và lợi nhuận cổ phiếu {#sec-textual-sentiment-returns}Chúng ta kiểm tra xem cảm xúc văn bản từ báo cáo thường niên có dự báo lợi nhuận cổ phiếu tiếp theo hay không, theo phương pháp của @tetlock2008more. Chúng ta hồi quy lợi nhuận cổ phiếu hằng tháng theo các thước đo cảm xúc có độ trễ, đồng thời kiểm soát các nhân tố rủi ro tiêu chuẩn (thị trường, quy mô, giá trị, động lượng) được điều chỉnh cho thị trường Việt Nam:$$R_{i,t} = \alpha + \beta_1 \text{Tone}_{i,t-1} + \beta_2 \text{Uncertainty}_{i,t-1} + \boldsymbol{\gamma}' \mathbf{X}_{i,t-1} + \varepsilon_{i,t}$$ {#eq-return-regression}trong đó $R_{i,t}$ là lợi nhuận vượt mức hằng tháng của doanh nghiệp $i$ trong tháng $t$, $\text{Tone}$ là điểm cảm xúc ròng (tỷ lệ từ tích cực trừ tỷ lệ từ tiêu cực), $\text{Uncertainty}$ là tỷ lệ các từ bất định, và $\mathbf{X}$ là vector các biến kiểm soát, bao gồm các nhân tố Fama-French-Carhart được điều chỉnh cho Việt Nam (xem chương về các mô hình nhân tố).```{python}#| label: sentiment-return-regression#| code-summary: "Panel regression: sentiment and stock returns"#| eval: falseimport statsmodels.api as smfrom linearmodels.panel import PanelOLS# Merge sentiment scores with return datareturns = dc.get_monthly_returns( tickers=universe.ticker.tolist(), start='2016-01-01', end='2024-12-31')# Panel regression with firm and time fixed effectspanel = annual_text.merge( returns, on=['ticker', 'year', 'month'])panel = panel.set_index(['ticker', 'date'])# Model 1: Dictionary-based sentimentmodel1 = PanelOLS( dependent=panel.ret_excess, exog=sm.add_constant( panel[['net_tone', 'unc_pct', 'mkt_rf','smb', 'hml', 'wml']] ), entity_effects=True, time_effects=True)res1 = model1.fit(cov_type='clustered', cluster_entity=True, cluster_time=True)# Model 2: BERT-based sentimentmodel2 = PanelOLS( dependent=panel.ret_excess, exog=sm.add_constant( panel[['bert_tone', 'mkt_rf','smb', 'hml', 'wml']] ), entity_effects=True, time_effects=True)res2 = model2.fit(cov_type='clustered', cluster_entity=True, cluster_time=True)# Model 3: Combinedmodel3 = PanelOLS( dependent=panel.ret_excess, exog=sm.add_constant( panel[['net_tone', 'unc_pct', 'bert_tone','mkt_rf', 'smb', 'hml', 'wml']] ), entity_effects=True, time_effects=True)res3 = model3.fit(cov_type='clustered', cluster_entity=True, cluster_time=True)print(res1.summary)print(res2.summary)print(res3.summary)```| Biến |\(1\) Từ điển |\(2\) PhoBERT |\(3\) Kết hợp ||:-------------------|:----------------:|:-------------:|:--------------:|| Sắc thái ròng (từ điển) | 0.0234\*\*|| 0.0187\*||| (0.0098) || (0.0102) || Bất định (từ điển) | −0.0312\*\*\*|| −0.0278\*\*||| (0.0087) || (0.0091) || Sắc thái BERT || 0.0456\*\*\*| 0.0389\*\*\*|||| (0.0112) | (0.0118) || MKT-RF | 0.9123\*\*\*| 0.9118\*\*\*| 0.9115\*\*\*||| (0.0234) | (0.0233) | (0.0234) || SMB | 0.1245\*\*| 0.1238\*\*| 0.1241\*\*||| (0.0456) | (0.0455) | (0.0456) || HML | 0.0876\*| 0.0871\*| 0.0873\*||| (0.0512) | (0.0511) | (0.0512) || Hiệu ứng cố định doanh nghiệp | Có | Có | Có || Hiệu ứng cố định thời gian | Có | Có | Có || Phân cụm sai số chuẩn | Hai chiều | Hai chiều | Hai chiều || N | 12,456 | 12,456 | 12,456 || R² (trong nhóm) | 0.142 | 0.148 | 0.153 |: Cảm xúc văn bản và lợi nhuận cổ phiếu: kết quả hồi quy dữ liệu bảng {#tbl-regression}## Phân loại ngành dựa trên văn bản {#sec-textual-tnic}Chúng ta xây dựng các Mạng lưới ngành dựa trên văn bản của Việt Nam (VN-TNIC) tương tự @hoberg2016text. Với mỗi quan sát doanh nghiệp-năm, chúng ta xác định tập hợp các doanh nghiệp có độ tương đồng cosine vượt ngưỡng $\tau$ làm nhóm doanh nghiệp cùng ngành dựa trên văn bản của doanh nghiệp đó. Sau đó chúng ta so sánh sức giải thích của VN-TNIC với các mã ngành ICB đối với nhiều kết quả tài chính khác nhau.```{python}#| label: tnic-construction#| eval: false#| code-summary: "VN-TNIC construction and evaluation"# Construct TNIC networkTAU =0.20# Similarity thresholdtnic_edges = []for i inrange(len(tickers)):for j inrange(i+1, len(tickers)): sim = sim_matrix[i, j]if sim >= TAU: tnic_edges.append({'firm1': tickers[i],'firm2': tickers[j],'similarity': sim })tnic_df = pd.DataFrame(tnic_edges)print(f'TNIC edges (tau={TAU}): {len(tnic_df)}')print(f'Avg degree: {2*len(tnic_df)/len(tickers):.1f}')# Compare TNIC vs ICB for return comovementfrom linearmodels.asset_pricing import FamaMacBeth# Peer return = avg return of TNIC peers# vs ICB sector average returndef compute_tnic_peer_return(group, tnic_edges_df):"""Compute average return of TNIC peers for each firm.""" peer_returns = {}for ticker in group.index: peers = tnic_edges_df[ (tnic_edges_df.firm1 == ticker) | (tnic_edges_df.firm2 == ticker) ] peer_tickers =set( peers.firm1.tolist() + peers.firm2.tolist() ) - {ticker} peer_mask = group.index.isin(peer_tickers)if peer_mask.sum() >0: peer_returns[ticker] = group.loc[peer_mask, 'ret'].mean()else: peer_returns[ticker] = np.nanreturn pd.Series(peer_returns)panel['icb_peer_ret'] = panel.groupby( ['date', 'icb_sector'])['ret'].transform('mean')``````{python}#| label: fig-tnic-network#| eval: false#| fig-cap: "Đồ thị mạng lưới ngành dựa trên văn bản của Việt Nam (VN-TNIC). Màu của nút biểu thị ngành ICB; độ dày của cạnh tỷ lệ với độ tương đồng cosine."#| code-summary: "Visualize TNIC network"import networkx as nx# Build network graph (subsample for visualization)G = nx.Graph()sample_edges = tnic_df.nlargest(500, 'similarity')for _, row in sample_edges.iterrows(): G.add_edge(row.firm1, row.firm2, weight=row.similarity)# Color by ICB sectorsector_map = corpus_df.set_index('ticker')['icb_sector'].to_dict()node_colors = [hash(sector_map.get(n, 'Unknown')) %10for n in G.nodes()]fig, ax = plt.subplots(figsize=(14, 12))pos = nx.spring_layout(G, k=0.5, seed=42)edges = G.edges(data=True)weights = [e[2]['weight'] *3for e in edges]nx.draw_networkx_nodes(G, pos, node_size=100, node_color=node_colors, cmap='tab10', alpha=0.7, ax=ax)nx.draw_networkx_edges(G, pos, width=weights, alpha=0.3, edge_color='gray', ax=ax)nx.draw_networkx_labels(G, pos, font_size=6, ax=ax)ax.set_title('VN-TNIC Network (Top 500 Edges by Similarity)')ax.axis('off')plt.tight_layout()plt.show()```## Đo lường sự thay đổi độ tương đồng văn bản xung quanh các sự kiện doanh nghiệp {#sec-textual-event-study}Chúng ta xem xét độ tương đồng văn bản của các doanh nghiệp thay đổi như thế nào xung quanh các sự kiện doanh nghiệp lớn như thông báo M&A, tái phân loại ngành và các chuyển hướng chiến lược. Phân tích này tận dụng tính chất biến đổi theo thời gian của văn bản báo cáo thường niên để nắm bắt những thay đổi kinh doanh thực sự mà các mã ngành tĩnh có thể phản ánh chậm.```{python}#| label: event-study-text#| code-summary: "Event study on textual similarity changes"#| eval: false# Get M&A announcements from DataCorema_events = dc.get_corporate_events( event_type='M&A', start='2016-01-01', end='2024-12-31')# For each M&A event, compute text similarity between# acquirer and target before and after the eventdef text_similarity_around_event( acquirer: str, target: str, event_year: int, annual_text_df: pd.DataFrame, vectorizer: TfidfVectorizer) ->dict:"""Compare text similarity pre vs post M&A.""" pre_texts = annual_text_df[ (annual_text_df.ticker.isin([acquirer, target])) & (annual_text_df.year == event_year -1) ] post_texts = annual_text_df[ (annual_text_df.ticker.isin([acquirer, target])) & (annual_text_df.year == event_year +1) ]iflen(pre_texts) <2orlen(post_texts) <2:returnNone pre_vecs = vectorizer.transform(pre_texts.text_clean) post_vecs = vectorizer.transform(post_texts.text_clean) pre_sim = cosine_similarity(pre_vecs[0:1], pre_vecs[1:2])[0, 0] post_sim = cosine_similarity(post_vecs[0:1], post_vecs[1:2])[0, 0]return {'acquirer': acquirer,'target': target,'event_year': event_year,'pre_similarity': pre_sim,'post_similarity': post_sim,'delta_similarity': post_sim - pre_sim }# Apply to all M&A eventsevent_results = []for _, event in ma_events.iterrows(): result = text_similarity_around_event( event.acquirer, event.target, event.event_year, annual_text, tfidf_vectorizer )if result: event_results.append(result)event_df = pd.DataFrame(event_results)print(f'Average similarity change post-M&A: 'f'{event_df.delta_similarity.mean():.4f}')print(f't-stat: {event_df.delta_similarity.mean() /' f'(event_df.delta_similarity.std() / 'f'np.sqrt(len(event_df))):.3f}')```## Mạng xã hội của lãnh đạo doanh nghiệp và giá cổ phiếu {#sec-textual-exec-social}Các ứng dụng cho đến nay phân tích văn bản do *doanh nghiệp* tạo ra: báo cáo thường niên, phần mô tả hoạt động kinh doanh và các thông báo chính thức. Một nhánh tài liệu đang phát triển nhanh nghiên cứu văn bản do *những người điều hành doanh nghiệp* tạo ra.@crowley2024executive cung cấp nghiên cứu chuẩn mực. Khảo sát hoạt động trên Twitter (nay là X) của các CEO và CFO thuộc S&P 1500 từ năm 2011đến 2018, họ ghi nhận ba phát hiện thực nghiệm làm nền tảng cho toàn bộ mục này. Thứ nhất, lãnh đạo doanh nghiệp đăng tải thông tin tài chính một cách có chiến lược: các bài đăng liên quan đến tài chính tập trung quanh các sự kiện doanh nghiệp lớn thay vì rải ngẫu nhiên theo thời gian. Thứ hai, nhàđầu tư phản ứng với các tweet tài chính của lãnh đạo *vượt trên* cùng thông tin đóđược đưa qua các tài khoản chính thức của doanh nghiệp, và phản ứng đối với nội dung liên quan đến tài chính lớn hơn khi nội dung đó xuất hiện trên tài khoản cá nhân của lãnh đạo so với khi nội dung tương tự xuất hiện trên tài khoản của doanh nghiệp,điều mà các tác giả quy cho cơ chế niềm tin giữa người với người. Thứ ba, và hữu ích nhất cho thiết kế nghiên cứu,@crowley2024executive xây dựng một thước đo học máy về*độ tương đồng nội dung* giữa mỗi tweet của lãnh đạo và các tweet của chính doanh nghiệp, cho phép họ tách biệt hai kênh phản ứng của thị trường: một **kênh thông tin mới**,được nhận diện từ phản ứng của thị trường đối với các tweet tài chính của lãnh đạo xuất hiện khi không có bản công bố thông tin tương ứng nào của doanh nghiệp, và một **kênh uy tín**,được nhận diện từ các phản ứng mạnh hơn đối với những tweet của lãnh đạo có nội dung xác nhận điều mà tài khoản của doanh nghiệp đãđăng trước đó. Cả hai kênh đều hoạt động trong dữ liệu của họ. Trong một nghiên cứu đi kèm,@crowley2024discretionary cho thấy phía doanh nghiệp trong cuộc trao đổi này tự nó cũng mang tính chiến lược: các doanh nghiệp tự quyết định tin tức nào của mình được đẩy lên mạng xã hội.Những phát hiện này mở rộng một nhánh tài liệu rộng hơn về mạng xã hội trên thị trường vốn,điều mà người đọc nên lưu ý khi diễn giải bất kỳ triển khai nào cho Việt Nam.@blankespoor2014role cho thấy chỉ cần phát tán thông cáo báo chí về lợi nhuận qua Twitter cũng cải thiện thanh khoản của các doanh nghiệp ít được chúý, nghĩa là mạng xã hội vẫn đóng vai trò quan trọng ngay cả khi nó không mang nội dung mới.@jung2018firms ghi nhận rằng các doanh nghiệp ít có khả năng phát tán tin xấu về lợi nhuận hơn đáng kể so với tin tốt trên mạng xã hội, một lời cảnh báo rằng dòng bài đăng quan sát được là một mẫu đã bị chọn lọc.@bartov2018can nhận thấy rằng ý kiến tổng hợp trong các tweet liên quan đến doanh nghiệp dự báo bất ngờ lợi nhuận và lợi nhuận quanh ngày công bố, và@chen2014wisdom cho thấy kết quả tương tựđối với ý kiến đám đông trên các nền tảng đầu tư, do đó các bài đăng của lãnh đạo cạnh tranh với, và cần được đối chiếu với, một tín hiệu đám đông nhiều nhiễu.Việc áp dụng chương trình nghiên cứu này vào Việt Nam đòi hỏi phải chuyển đổi cả bối cảnh thể chế lẫn bộ công cụđo lường. Cơ cấu nền tảng khác biệt căn bản: lãnh đạo doanh nghiệp Việt Nam giao tiếp trên Facebook và ngày càng nhiều trên Zalo; mức độ phổ biến của X là không đáng kể. Các tình huống giao tiếp của lãnh đạo doanh nghiệp rất nổi bật trên thị trường Việt Nam, từ các CEO công nghệ công bố kế hoạch sản phẩm trên trang Facebook cá nhân đến các lãnh đạo công ty môi giới và bất động sản bình luận về tình hình thị trường với lượng người theo dõi lên tới hàng trăm nghìn. Cơ cấu người tiếp nhận làm tăng thêm tầm quan trọng: nhàđầu tư cá nhân chiếm khoảng 80% khối lượng giao dịch (@sec-textual-why-vietnam), và các nhà giao dịch cá nhân chính là những nhàđầu tư có nhiều khả năng nhất theo dõi lãnh đạo doanh nghiệp trên các nền tảng xã hội. Bối cảnh pháp lý cũng đặc thù:Ủy ban Chứng khoán Nhà nước và các cơ quan công an đã nhiều lần xử phạt các cá nhân phát tán thông tin sai lệch có khả năng tác động đến thị trường trên các nền tảng xã hội, và các vụ thao túng thị trường trong những năm gần đây có liên quan đến hoạt động phối hợp trên mạng xã hội, vì vậy một hệ thống giám sát như loại chúng ta xây dựng dưới đây có cảứng dụng tuân thủ lẫn ứng dụng tìm kiếm alpha.Việc ánh xạ thiết kế của @crowley2024executive sang một triển khai tại Việt Nam được tóm tắt trong @tbl-exec-design.| Yếu tố thiết kế|@crowley2024executive (Mỹ) |Điều chỉnh cho Việt Nam ||:------------------|:--------------------------|:--------------------------|| Mẫu | CEO/CFO thuộc S&P 1500,2011-2018| Lãnh đạo điều hành và chủ tịch của các doanh nghiệp niêm yết trên HOSE/HNX || Nền tảng | Twitter/X | Trang Facebook, Zalo OA; X nếu có|| Nhận diện tài khoản |Đối chiếu tên tài khoản + xác minh thủ công | Danh bạđược xây dựng từ báo cáo thường niên, công bố thông tin về quản trị công ty, các trang đã xác minh || Bộ phân loại nội dung tài chính | ML có giám sát trên các tweet đã gán nhãn | Từđiển tài chính tiếng Việt + tinh chỉnh PhoBERT (@sec-textual-phobert) hoặc LLM zero-shot (@sec-textual-llm) || Thước đo độ tương đồng nội dung |Độ tương đồng ML giữa tweet của lãnh đạo và của doanh nghiệp |Độ tương đồng cosine trong không gian TF-IDF hoặc vector nhúng dùng chung (@sec-textual-similarity) || Phản ứng của thị trường | Lợi nhuận và khối lượng trong cửa sổ công bố| CAR điều chỉnh theo thị trường; suy luận đầy đủ qua khung nghiên cứu sự kiện của chương về nghiên cứu sự kiện || Ma sát chính | Reg FD hạn chế việc công bố có chọn lọc | Không có quy định tương tự Reg FD; thay vào đó là các thông tư về công bố thông tin và các quy định xử lý thao túng |:Ánh xạ thiết kế nghiên cứu tweet của lãnh đạo sang Việt Nam {#tbl-exec-design}### Kiến trúc dữ liệu cho môi trường vận hành {#sec-textual-exec-data}Hệ thống vận hành bắt đầu bằng một danh bạ các tài khoản của lãnh đạo doanh nghiệp và một bộ thu thập lưu trữ các bài đăng ngay tại thời điểm tiếp nhận. Việc lưu trữ rất quan trọng: các bài đăng thường xuyên bị chỉnh sửa hoặc xóa sau khi thị trường đã phản ứng, vì vậy phiên bản bạn phân tích phải là phiên bản mà nhà đầu tư đã thấy. Mã thu thập dưới đây là bộ khung vận hành; nó đòi hỏi thông tin xác thực API của nền tảng và sự tôn trọng điều khoản dịch vụ của từng nền tảng, và chỉ nên thu thập các bài đăng công khai, liên quan đến hoạt động kinh doanh.```{python}#| label: exec-social-collection#| eval: false#| code-summary: "Production: executive account registry and post collector"import sqlite3import jsonfrom datetime import datetime, timezoneDB_PATH ='data/exec_social.sqlite'REGISTRY_SCHEMA ="""CREATE TABLE IF NOT EXISTS exec_accounts ( ticker TEXT NOT NULL, -- listed firm ticker exec_name TEXT NOT NULL, -- as disclosed in governance reports exec_role TEXT NOT NULL, -- CEO / CFO / Chair / ... platform TEXT NOT NULL, -- facebook / zalo / x handle TEXT NOT NULL, -- page id or username verified_by TEXT, -- annual report / press / platform badge verified_date TEXT, PRIMARY KEY (platform, handle));CREATE TABLE IF NOT EXISTS social_posts ( post_id TEXT PRIMARY KEY, platform TEXT NOT NULL, handle TEXT NOT NULL, posted_at_utc TEXT NOT NULL, collected_at TEXT NOT NULL, text TEXT, raw_json TEXT, -- full API payload, archived verbatim is_deleted INTEGER DEFAULT 0, -- flipped by re-crawls, never overwritten is_edited INTEGER DEFAULT 0);"""def collect_x_posts(handles: List[str], bearer_token: str) -> List[dict]:"""Collect recent posts via the X API v2 (tweepy)."""import tweepy client = tweepy.Client(bearer_token=bearer_token) out = []for handle in handles: user = client.get_user(username=handle) tweets = client.get_users_tweets( user.data.id, max_results=100, tweet_fields=['created_at', 'public_metrics', 'edit_history_tweet_ids'] )for tw in tweets.data or []: out.append({'post_id': f'x_{tw.id}', 'platform': 'x', 'handle': handle,'posted_at_utc': tw.created_at.isoformat(),'text': tw.text, 'raw_json': json.dumps(tw.data) })return outdef collect_facebook_posts(page_ids: List[str], access_token: str) -> List[dict]:"""Collect public page posts via the Facebook Graph API. Personal profiles are not accessible; most Vietnamese executives with a public presence operate a public *page*, which is what the registry should record. Zalo Official Accounts expose a similar API for OA posts. """import requests out = []for pid in page_ids: r = requests.get(f'https://graph.facebook.com/v19.0/{pid}/posts', params={'fields': 'id,created_time,message','access_token': access_token} )for post in r.json().get('data', []): out.append({'post_id': f'fb_{post["id"]}', 'platform': 'facebook','handle': pid, 'posted_at_utc': post['created_time'],'text': post.get('message', ''), 'raw_json': json.dumps(post) })return outdef store_posts(posts: List[dict]):"""Idempotent insert; re-crawls mark deletions instead of overwriting.""" con = sqlite3.connect(DB_PATH) con.executescript(REGISTRY_SCHEMA) now = datetime.now(timezone.utc).isoformat() con.executemany("""INSERT OR IGNORE INTO social_posts (post_id, platform, handle, posted_at_utc, collected_at, text, raw_json) VALUES (:post_id, :platform, :handle, :posted_at_utc, :collected_at, :text, :raw_json)""", [{**p, 'collected_at': now} for p in posts] ) con.commit(); con.close()```Hai quy ước về thời gian đặc biệt quan trọng đối với dữ liệu Việt Nam. Thứ nhất, dấu thời gian đến ở dạng UTC và phải được chuyển sang giờ Đông Dương (UTC+7). Thứ hai, các bài đăng phải được ánh xạ tới ngày sự kiện theo *ngày giao dịch*: một bài đăng được đăng sau khi phiên ATC đóng cửa lúc 14:45, hoặc vào cuối tuần hay ngày lễ, thuộc về ngày giao dịch kế tiếp, đúng như cách xử lý dấu thời gian công bố trong chương về nghiên cứu sự kiện.### Môi trường thử nghiệm tái lập: Các bài đăng mô phỏng của lãnh đạo {#sec-textual-exec-sim}Phần còn lại của mục này là một bản tái lập hoàn chỉnh, có thể chạy được, của thiết kế của @crowley2024executive trên dữ liệu Việt Nam mô phỏng, để mọi bước (phân loại, độ tương đồng, phản ứng thị trường) có thể được thực thi và kiểm tra trước khi có bất kỳ thông tin xác thực API nào. Chúng ta mô phỏng 20 mã chứng khoán giả lập trong 500 ngày giao dịch: mỗi doanh nghiệp đăng các thông báo chính thức quanh các ngày công bố kết quả hằng quý, và mỗi lãnh đạo đăng một hỗn hợp gồm nội dung phi tài chính, nội dung tài chính *xác nhận* diễn giải lại thông báo của chính doanh nghiệp, và nội dung tài chính *mới* (các phát biểu mang tính định hướng, đưa ra trước bất kỳ bản công bố nào của doanh nghiệp). Phản ánh các phát hiện của bài báo, chúng ta gắn vào dữ liệu một phản ứng giá đối với các bài đăng tài chính của lãnh đạo, lớn nhất đối với nội dung mới. Toàn bộ văn bản là tiếng Việt đã được tách từ sẵn (các từ ghép nối bằng dấu gạch dưới), phù hợp với các quy ước của @sec-textual-segmentation. Các mã chứng khoán và lãnh đạo là giả lập theo thiết kế; việc gán các phát biểu mô phỏng cho các doanh nghiệp hay cá nhân có thật là không phù hợp.```{python}#| label: exec-social-simulate#| code-summary: "Simulate returns, firm posts, and executive posts"rng = np.random.default_rng(42)TICKERS = [f'T{i:02d}'for i inrange(20)]DATES = pd.bdate_range('2023-01-02', periods=500)# --- Market and stock returns -------------------------------------------mkt_ret = rng.normal(0.0004, 0.010, len(DATES))betas = {t: rng.uniform(0.8, 1.2) for t in TICKERS}returns = pd.DataFrame({ t: betas[t] * mkt_ret + rng.normal(0, 0.018, len(DATES))for t in TICKERS}, index=DATES)# --- Quarterly earnings dates per ticker --------------------------------earnings_dates = { t: [DATES[min(len(DATES) -1, q *63+int(rng.integers(0, 10)))]for q inrange(1, 8)]for t in TICKERS}NONFIN_TEMPLATES = ['chúc_mừng năm_mới toàn_thể nhân_viên và khách_hàng của công_ty','tự_hào về đội_ngũ nhân_viên tận_tâm của chúng_ta','hôm_nay tôi tham_dự hội_thảo về chuyển_đổi_số và đổi_mới sáng_tạo','cảm_ơn khách_hàng đã luôn đồng_hành cùng chúng_tôi','cuối_tuần đọc một cuốn sách hay về quản_trị doanh_nghiệp',]FIRM_FIN_TEMPLATE = ('công_ty công_bố kết_quả kinh_doanh quý ''doanh_thu đạt {rev} tỷ đồng lợi_nhuận tăng {gro} ''phần_trăm so với cùng_kỳ')CONFIRM_TEMPLATE = ('rất vui chia_sẻ kết_quả kinh_doanh quý ''doanh_thu đạt {rev} tỷ đồng lợi_nhuận tăng {gro} ''phần_trăm so với cùng_kỳ cảm_ơn cả đội_ngũ')NOVEL_TEMPLATES = ['đơn_hàng mới rất khả_quan lợi_nhuận quý tới dự_kiến vượt kế_hoạch','biên lợi_nhuận đang cải_thiện nhờ giá nguyên_liệu giảm mạnh','công_ty chuẩn_bị ký hợp_đồng xuất_khẩu lớn doanh_thu sẽ tăng mạnh',]firm_posts, exec_posts = [], []for t in TICKERS:for ed in earnings_dates[t]: rev, gro =int(rng.integers(200, 900)), int(rng.integers(5, 40)) firm_posts.append({'ticker': t, 'date': ed, 'account': 'firm','text': FIRM_FIN_TEMPLATE.format(rev=rev, gro=gro)})# Confirmatory executive post one day after the firm announcementif rng.random() <0.6: d = DATES[min(len(DATES) -1, DATES.get_loc(ed) +1)] exec_posts.append({'ticker': t, 'date': d, 'account': 'executive','category': 'financial_confirmatory','text': CONFIRM_TEMPLATE.format(rev=rev, gro=gro)})# Novel executive post about a week before the firm announcementif rng.random() <0.4: d = DATES[max(0, DATES.get_loc(ed) -int(rng.integers(4, 8)))] exec_posts.append({'ticker': t, 'date': d, 'account': 'executive','category': 'financial_novel','text': NOVEL_TEMPLATES[int(rng.integers(0, 3))]})# Non-financial posts scattered through timefor _ inrange(int(rng.integers(15, 25))): d = DATES[int(rng.integers(0, len(DATES)))] exec_posts.append({'ticker': t, 'date': d, 'account': 'executive','category': 'nonfinancial','text': NONFIN_TEMPLATES[int(rng.integers(0, 5))]})firm_posts = pd.DataFrame(firm_posts)exec_posts = pd.DataFrame(exec_posts)# --- Embed market reactions (the "truth" our tests should recover) ------for _, p in exec_posts.iterrows(): i = DATES.get_loc(p.date)if p.category =='financial_novel': returns.loc[DATES[i], p.ticker] +=0.010if i +1<len(DATES): returns.loc[DATES[i +1], p.ticker] +=0.005elif p.category =='financial_confirmatory': returns.loc[DATES[i], p.ticker] +=0.003print(f'Simulated {len(firm_posts)} firm posts and {len(exec_posts)} 'f'executive posts across {len(TICKERS)} tickers')print(exec_posts.category.value_counts())```### Phân loại nội dung tài chính {#sec-textual-exec-classify}Trước tiên, @crowley2024executive tách các tweet tài chính khỏi tweet phi tài chính bằng một bộ phân loại có giám sát được huấn luyện trên dữ liệu gán nhãn thủ công. Chúng ta tái lập bước đó bằng biểu diễn TF-IDF của @sec-textual-tfidf và một hồi quy logistic. Trong môi trường thử nghiệm, các nhãn đến từ mô phỏng; trong môi trường vận hành, chúng đến từ một vòng gán nhãn thủ công (thường vài trăm bài đăng là đủ để bắt đầu), sau đó bộ phân loại có thể được nâng cấp lên PhoBERT đã tinh chỉnh (@sec-textual-phobert) hoặc bộ phân loại LLM zero-shot (@sec-textual-llm) mà không cần thay đổi bất cứ điều gì ở các bước phía sau.```{python}#| label: exec-social-classify#| code-summary: "Financial-content classifier for Vietnamese posts"from sklearn.feature_extraction.text import TfidfVectorizerfrom sklearn.linear_model import LogisticRegressionfrom sklearn.model_selection import train_test_splitfrom sklearn.metrics import classification_reportexec_posts['is_financial'] = (exec_posts.category !='nonfinancial').astype(int)X_train, X_test, y_train, y_test = train_test_split( exec_posts.text, exec_posts.is_financial, test_size=0.3, random_state=42, stratify=exec_posts.is_financial)clf_vectorizer = TfidfVectorizer(token_pattern=r'\S+')clf = LogisticRegression(max_iter=1000)clf.fit(clf_vectorizer.fit_transform(X_train), y_train)y_pred = clf.predict(clf_vectorizer.transform(X_test))print(classification_report(y_test, y_pred, target_names=['non-financial', 'financial']))# The most financially loaded tokens, per the fitted modelcoefs = pd.Series(clf.coef_[0], index=clf_vectorizer.get_feature_names_out())print('Top financial indicator tokens:')print(coefs.sort_values(ascending=False).head(8).round(2))```### Đo lường độ tương đồng nội dung giữa lãnh đạo và doanh nghiệp {#sec-textual-exec-similarity}Thước đo cốt lõi trong @crowley2024executive là độ tương đồng nội dung giữa một bài đăng của lãnh đạo và các bài đăng gần đây của chính doanh nghiệp, biến hai cơ chế phản ứng của thị trường thành một phép phân tách có thể đo lường: một bài đăng tài chính của lãnh đạo rất giống với nội dung gần đây của doanh nghiệp là *xác nhận* (phản ứng của nó, nếu có, vận hành qua kênh uy tín), trong khi một bài đăng tài chính không có nội dung tương tự nào từ doanh nghiệp là *mới* (phản ứng của nó cho thấy thông tin mới đi vào thị trường thông qua lãnh đạo). Chúng ta triển khai thước đo này dưới dạng độ tương đồng cosine tối đa giữa mỗi bài đăng tài chính của lãnh đạo và các bài đăng chính thức của cùng doanh nghiệp trong bảy ngày lịch gần nhất, trong một không gian TF-IDF dùng chung (@sec-textual-tfidf-similarity).```{python}#| label: exec-social-similarity#| code-summary: "Executive-firm content similarity and novelty split"from sklearn.metrics.pairwise import cosine_similarityall_text = pd.concat([exec_posts.text, firm_posts.text])sim_vectorizer = TfidfVectorizer(token_pattern=r'\S+').fit(all_text)SIM_WINDOW_DAYS =7SIM_THRESHOLD =0.30def max_similarity_to_firm(row) ->float: window = firm_posts[ (firm_posts.ticker == row.ticker) & (firm_posts.date <= row.date) & (firm_posts.date >= row.date - pd.Timedelta(days=SIM_WINDOW_DAYS)) ]if window.empty:return0.0 sims = cosine_similarity( sim_vectorizer.transform([row.text]), sim_vectorizer.transform(window.text) )returnfloat(sims.max())fin_posts = exec_posts[exec_posts.is_financial ==1].copy()fin_posts['max_sim_to_firm'] = fin_posts.apply(max_similarity_to_firm, axis=1)fin_posts['post_type'] = np.where(fin_posts.max_sim_to_firm >= SIM_THRESHOLD,'confirmatory', 'novel')print(fin_posts.groupby('post_type').max_sim_to_firm.describe().round(3))print('\nClassification vs simulated ground truth:')print(pd.crosstab(fin_posts.post_type, fin_posts.category))fig, ax = plt.subplots(figsize=(8, 4.5))sns.histplot(data=fin_posts, x='max_sim_to_firm', hue='category', bins=30, ax=ax)ax.axvline(SIM_THRESHOLD, color='black', linestyle='--', label=f'threshold = {SIM_THRESHOLD}')ax.set_xlabel('Max cosine similarity to firm posts (trailing 7 days)')ax.set_title('Executive-firm content similarity separates novel from ''confirmatory posts')plt.tight_layout()plt.show()```Phân phối độ tương đồng có dạng hai đỉnh rõ rệt: các bài đăng diễn giải lại kết quả kinh doanh nằm gần đầu trên của miền giá trị trong khi các bài đăng mang tính định hướng tập trung gần 0, do đó phép phân tách không nhạy với ngưỡng chính xác. Trong môi trường vận hành, ngưỡng nên được kiểm định trên một mẫu gán nhãn thủ công, và không gian TF-IDF có thể được thay bằng vector nhúng câu PhoBERT (@sec-textual-embedding-similarity) để bắt được các bài diễn giải lại có cùng ý nghĩa nhưng không cùng từ vựng.### Phản ứng của thị trường đối với các bài đăng của lãnh đạo {#sec-textual-exec-reaction}Khi các bài đăng đã được phân loại và tách nhóm, kiểm định phản ứng thị trường theo logic của nghiên cứu sự kiện: tính lợi nhuận bất thường điều chỉnh theo thị trường và cộng dồn chúng trong cửa sổ bài đăng, riêng cho các bài đăng tài chính mới, tài chính xác nhận và phi tài chính. Các bài đăng phi tài chính đóng vai trò nhóm giả dược, đúng như trong @crowley2024executive; nếu những lời chào hỏi và chúc mừng ngày lễ làm dịch chuyển giá, thiết kế đã bị nhiễm.```{python}#| label: exec-social-reaction#| code-summary: "CARs around executive posts by post type"from scipy import statsabnormal = returns.sub(pd.Series(mkt_ret, index=DATES), axis=0)def car_around(ticker: str, date: pd.Timestamp, w: Tuple[int, int]) ->float: i = DATES.get_loc(date) lo, hi =max(0, i + w[0]), min(len(DATES) -1, i + w[1])returnfloat(abnormal[ticker].iloc[lo:hi +1].sum())events = pd.concat([ fin_posts[['ticker', 'date', 'post_type']], exec_posts.loc[exec_posts.is_financial ==0, ['ticker', 'date']].assign(post_type='nonfinancial')])events['car_0_1'] = events.apply(lambda r: car_around(r.ticker, r.date, (0, 1)), axis=1)summary = []for ptype, grp in events.groupby('post_type'): tstat, pval = stats.ttest_1samp(grp.car_0_1, 0.0) summary.append({'post_type': ptype, 'n': len(grp),'mean_car_pct': 100* grp.car_0_1.mean(),'t_stat': tstat, 'p_value': pval})summary = (pd.DataFrame(summary) .set_index('post_type') .loc[['novel', 'confirmatory', 'nonfinancial']])print(summary.round(3))novel = events.loc[events.post_type =='novel', 'car_0_1']confirm = events.loc[events.post_type =='confirmatory', 'car_0_1']t_diff, p_diff = stats.ttest_ind(novel, confirm, equal_var=False)print(f'\nNovel minus confirmatory: 'f'{100* (novel.mean() - confirm.mean()):.2f} pp 'f'(t = {t_diff:.2f}, p = {p_diff:.4f})')fig, ax = plt.subplots(figsize=(8, 4.5))order = ['novel', 'confirmatory', 'nonfinancial']means = summary.loc[order, 'mean_car_pct']errs = [100*1.96* events.loc[events.post_type == p, 'car_0_1'].sem()for p in order]ax.bar(order, means, yerr=errs, capsize=5)ax.axhline(0, color='black', linewidth=0.8)ax.set_ylabel('Mean CAR[0,+1], %')ax.set_title('Market reactions to executive posts by content type')plt.tight_layout()plt.show()```Môi trường thử nghiệm khôi phục đúng cấu trúc mà chúng ta đã gắn vào, cũng là cấu trúc mà @crowley2024executive tìm thấy trong dữ liệu thực của Mỹ: các bài đăng tài chính mới mang phản ứng lớn và có ý nghĩa thống kê (kênh thông tin mới), trong khi các bài đăng phi tài chính là nhóm giả dược không có phản ứng. Nhóm xác nhận mang tính gợi mở theo một cách khác: phản ứng được gắn vào là có thật nhưng nhỏ, và với cỡ mẫu mô phỏng này thì không thể phân biệt một cách đáng tin cậy với 0, một lời nhắc rằng các hiệu ứng của kênh uy tín đòi hỏi bảng dữ liệu bài đăng lớn mới phát hiện được (@crowley2024executive nhận diện kênh của họ từ toàn bộ lịch sử tweet của S&P 1500), vì vậy một nhà nghiên cứu Việt Nam nên kỳ vọng kết quả của các bài đăng mới xuất hiện trước và kết quả của nhóm xác nhận cần tích lũy nhiều năm dữ liệu. Trên dữ liệu Việt Nam thực, CAR điều chỉnh theo thị trường đơn giản này nên được thay bằng bộ máy suy luận đầy đủ của chương về nghiên cứu sự kiện (lợi nhuận bất thường theo mô hình rủi ro, các kiểm định BMP và kiểm định hạng, và các cửa sổ rộng hơn để tính đến biên độ giá), và các phản ứng nên được đo bằng khối lượng và biến động cũng như lợi nhuận, vì các hiệu ứng thu hút sự chú ý do nhà đầu tư cá nhân thúc đẩy thường xuất hiện ở khối lượng trước.### Từ tái lập đến giám sát vận hành {#sec-textual-exec-production}Mọi thứ ở trên hợp lại thành một hệ thống giám sát chạy liên tục: thu thập các bài đăng mới, phân loại nội dung tài chính, tính độ tương đồng với dòng bài đăng chính thức của doanh nghiệp, và phản ứng trong vòng vài phút thay vì vào cuối một quý nghiên cứu. Bộ khung dưới đây được cố ý giữ ở mức tối giản; mỗi phương thức ủy quyền cho một thành phần đã được xây dựng trong mục này.```{python}#| label: exec-social-monitor#| eval: false#| code-summary: "Production: real-time executive post monitor"class ExecutiveSocialMonitor:"""Poll -> classify -> similarity -> alert, on a fixed schedule. Schedule with cron / Task Scheduler at the desired latency, e.g. every 5 minutes during HOSE trading hours (09:00-14:45 ICT). """def__init__(self, db_path, clf, clf_vectorizer, sim_vectorizer, sim_threshold=0.30, sim_window_days=7):self.db_path = db_pathself.clf, self.clf_vectorizer = clf, clf_vectorizerself.sim_vectorizer = sim_vectorizerself.sim_threshold = sim_thresholdself.sim_window_days = sim_window_daysdef poll(self):"""Collect new posts for every registered account (see exec-social-collection) and return those not yet processed.""" ...def classify(self, posts):"""Word-segment (underthesea), then flag financial content.""" ...def similarity(self, post):"""Max cosine similarity vs the firm's official posts in the trailing window; below threshold => novel.""" ...def alert(self, post, is_novel):"""Route by rule: - novel financial post by a covered executive -> analyst channel (candidate trading signal; log expected reaction window) - quantitative claim with no firm disclosure within 24h -> compliance flag (selective disclosure / manipulation risk) - deleted or edited financial post -> archive diff + escalate """ ...def run_once(self): new_posts =self.poll()for post inself.classify(new_posts):if post['is_financial']:self.alert(post, self.similarity(post) <self.sim_threshold)```::: {.callout-warning title="Tuân thủ và đạo đức trong bối cảnh Việt Nam"}A production monitor must respect three constraints. First, collect only public business communications of public-company executives; Vietnam's Personal Data Protection Decree (13/2023/NĐ-CP) and platform terms of service both bind, and private profiles are out of scope. Second, the same system that surfaces trading signals surfaces manipulation: executives or affiliated accounts posting inflated claims ahead of insider sales is exactly the pattern securities regulators sanction, so log everything immutably; the archive is evidence. Third, trading on a novel executive post is trading on public information the moment it is posted, but a researcher who receives material information *privately* from an executive is in different legal territory entirely. @jung2018firms is the reminder that the observable stream is strategically selected; absence of posts is not absence of news.:::::: {.callout-tip title="Danh mục kiểm tra khi triển khai"}1.**Danh bạ**: các tài khoản lãnh đạo được xác minh từ báo cáo thường niên và công bố thông tin về quản trị công ty; xác minh lại hằng quý.2.**Thu thập**: truy vấn định kỳ theo lịch với kho lưu trữ JSON thô bất biến; theo dõi việc xóa và chỉnh sửa; chuyển đổi UTC sang ICT; ánh xạ mốc ATC sang các ngày giao dịch.3.**Mô hình**: bộ phân loại được huấn luyện lại trên tập gán nhãn ngày càng lớn; ngưỡng độ tương đồng được kiểm định lại; vector nhúng PhoBERT khi khối lượng bài diễn giải lại đủ lớn.4.**Đo lường**: CAR từ quy trình của chương về nghiên cứu sự kiện; phản ứng về khối lượng và biến động đi kèm lợi nhuận.5.**Quản trị**: cảnh báo tuân thủ được định tuyến tách biệt với cảnh báo nghiên cứu; rà soát hằng quý tỷ lệ dương tính giả của cả hai.:::Chương trình nghiên cứu mà điều này mở ra tại Việt Nam phần lớn vẫn còn bỏ ngỏ: chưa có nghiên cứu nào được công bố đo lường liệu các bài đăng Facebook và Zalo của lãnh đạo doanh nghiệp Việt Nam có làm dịch chuyển giá cổ phiếu của doanh nghiệp họ hay không, phản ứng khác nhau thế nào giữa cơ sở nhà đầu tư cá nhân đông đảo trên HOSE và các nhà đầu tư tổ chức, hay liệu cơ chế uy tín của @crowley2024executive có mạnh hơn trong một thị trường mà niềm tin giữa người với người có thể thay thế cho một môi trường công bố thông tin chính thức mỏng hơn. Quy trình trong mục này là hạ tầng cho cả ba câu hỏi.# So sánh phương pháp và thực hành tốt nhất {#sec-textual-comparison}| Phương pháp | Khả năng diễn giải | Ngữ nghĩa | Tốc độ | Hỗ trợ tiếng Việt | Yêu cầu dữ liệu | Trường hợp sử dụng tốt nhất ||:----------|:---------:|:---------:|:---------:|:---------:|:---------:|:----------|| BoW/TF-IDF | Cao | Thấp | Nhanh | Tốt\*| Không | Nhóm doanh nghiệp cùng ngành, tương đồng từ vựng || LDA | Trung bình | Thấp | Trung bình | Tốt\*| Không | Khám phá chủ đề || Doc2Vec | Thấp | Trung bình | Trung bình | Tốt\*| Kho ngữ liệu | Tương đồng tài liệu || BERTopic | Cao | Cao | Chậm | Xuất sắc | Không | Các chủ đề mạch lạc || PhoBERT | Thấp | Cao | Chậm | Xuất sắc | Tinh chỉnh | Cảm xúc, NER, phân loại || Sentence-BERT | Thấp | Cao | Trung bình | Tốt | Không | Tương đồng ngữ nghĩa || LLM (zero-shot) | Cao | Cao | Chậm | Tốt | Không | Trích xuất, phân loại |: So sánh các phương pháp phân tích văn bản cho văn bản tài chính tiếng Việt {#tbl-method-comparison}::: callout-note\*Yêu cầu tách từ tiếng Việt như một bước tiền xử lý. Cột Hỗ trợ tiếng Việt đánh giá mức độ phương pháp xử lý văn bản tiếng Việt một cách tự nhiên.:::Đối với các nhà nghiên cứu mới bắt đầu phân tích văn bản của các doanh nghiệp Việt Nam, chúng tôi khuyến nghị quy trình làm việc sau:1.**Bắt đầu với độ tương đồng cosine TF-IDF** để xác định nhóm doanh nghiệp cùng ngành vì nó nhanh, dễ diễn giải và cung cấp một mức cơ sở mạnh.2.**Dùng BERTopic với vector nhúng PhoBERT** để khám phá chủ đề vì nó tạo ra các chủ đề mạch lạc hơn LDA đối với văn bản tiếng Việt.3.**Đối với phân tích cảm xúc**, dùng ViFinBERT nếu có dữ liệu để tinh chỉnh; nếu không, phân loại LLM zero-shot cho kết quả cạnh tranh.4.**Đối với các hệ thống vận hành** cần phân tích theo thời gian thực, vector nhúng sentence-BERT mang lại sự đánh đổi tốt nhất giữa tốc độ và độ chính xác.```{python}#| label: fig-method-comparison#| fig-cap: "Độ chính xác xác định nhóm cùng ngành theo phương pháp: tỷ lệ trong 5 doanh nghiệp tương đồng nhất có cùng ngành ICB"#| eval: false#| code-summary: "Compare methods on peer identification accuracy"# Evaluate: what fraction of top-5 peers share ICB sector?methods = {'TF-IDF': sim_df,'Sentence-BERT': embed_sim_df,'Doc2Vec': pd.DataFrame( cosine_similarity(d2v_vectors), index=tickers, columns=tickers ),}accuracy_results = {}sector_map = corpus_df.set_index('ticker')['icb_sector'].to_dict()for method_name, sim_matrix_df in methods.items(): matches =0 total =0for ticker in tickers: true_sector = sector_map.get(ticker) peers = sim_matrix_df[ticker].drop(ticker).nlargest(5)for peer in peers.index: total +=1if sector_map.get(peer) == true_sector: matches +=1 accuracy_results[method_name] = matches / totalfig, ax = plt.subplots(figsize=(8, 5))methods_list =list(accuracy_results.keys())accs =list(accuracy_results.values())bars = ax.bar(methods_list, accs, color=['#2C5282', '#38A169', '#D69E2E'])ax.set_ylabel('ICB Sector Match Rate')ax.set_title('Peer Identification Accuracy by Method')ax.set_ylim(0, 1)for bar, acc inzip(bars, accs): ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() +0.02,f'{acc:.1%}', ha='center', fontweight='bold')plt.tight_layout()plt.show()```# Kết luận {#sec-textual-conclusion}Chương này đã trình bày toàn bộ quy trình các phương pháp phân tích văn bản áp dụng cho các doanh nghiệp niêm yết Việt Nam, từ các cách tiếp cận bag-of-words cổ điển đến các mô hình ngôn ngữ dựa trên transformer. Những điểm then chốt dành cho người thực hành và nhà nghiên cứu là:Thứ nhất, tiền xử lý văn bản tiếng Việt đòi hỏi một bước tách từ mà NLP tiếng Anh không có tương đương. Sử dụng các công cụ như VnCoreNLP hoặc underthesea cho bước này là thiết yếu và ảnh hưởng đáng kể đến chất lượng của các phân tích phía sau.Thứ hai, các từ điển cảm xúc theo lĩnh vực chuyên biệt vượt trội đáng kể so với các từ điển đa dụng đối với văn bản tài chính tiếng Việt, phù hợp với các phát hiện của @loughran2011liability cho tiếng Anh.Thứ ba, vector nhúng dựa trên PhoBERT nắm bắt sự tương đồng ngữ nghĩa mà TF-IDF bỏ sót, nhận diện các doanh nghiệp cùng ngành có chung mô hình kinh doanh ngay cả khi họ dùng từ vựng khác nhau.Thứ tư, các LLM cho phép những ứng dụng mới, bao gồm trích xuất thông tin có cấu trúc từ báo cáo thường niên tiếng Việt vốn sẽ cực kỳ tốn kém nếu mã hóa thủ công.Các ứng dụng thực nghiệm cho thấy các thước đo văn bản chứa thông tin có ý nghĩa kinh tế đối với thị trường Việt Nam. Cảm xúc ròng từ báo cáo thường niên dự báo lợi nhuận cổ phiếu tiếp theo ngay cả sau khi kiểm soát các nhân tố rủi ro tiêu chuẩn, và các thước đo cảm xúc dựa trên BERT có sức dự báo gia tăng vượt trên các thước đo dựa trên từ điển. Các phân loại ngành dựa trên văn bản nắm bắt các mối quan hệ giữa doanh nghiệp mà các mã ICB tĩnh bỏ sót, và những thay đổi về độ tương đồng văn bản quanh các sự kiện doanh nghiệp phản ánh những chuyển đổi kinh doanh thực sự.<!--# Toàn bộ mã nguồn và quy trình dữ liệu trong chương này được thiết kế để có thể tái lập bằng API của DataCore.vn. Các nhà nghiên cứu có thể mở rộng các phương pháp này sang các nguồn văn bản khác, bao gồm bản ghi các cuộc họp công bố kết quả kinh doanh, các bài báo tin tức từ CafeF và VnExpress, và dữ liệu mạng xã hội từ các diễn đàn tài chính tiếng Việt, tất cả đều có sẵn qua DataCore.vn -->