1+ {
2+ "cells" : [
3+ {
4+ "cell_type" : " markdown" ,
5+ "metadata" : {},
6+ "source" : [
7+ " # Financial PhraseBank — Dataset Exploration\n " ,
8+ " \n " ,
9+ " Quick look at the dataset before we start fine-tuning anything. Using the `sentences_allagree` split since those are the ones where all annotators agreed on the label."
10+ ]
11+ },
12+ {
13+ "cell_type" : " code" ,
14+ "execution_count" : null ,
15+ "metadata" : {},
16+ "outputs" : [],
17+ "source" : [
18+ " from datasets import load_dataset\n " ,
19+ " from collections import Counter\n " ,
20+ " import matplotlib.pyplot as plt\n " ,
21+ " import numpy as np"
22+ ]
23+ },
24+ {
25+ "cell_type" : " code" ,
26+ "execution_count" : null ,
27+ "metadata" : {},
28+ "outputs" : [],
29+ "source" : [
30+ " ds = load_dataset(\" financial_phrasebank\" , \" sentences_allagree\" , trust_remote_code=True)\n " ,
31+ " data = ds[\" train\" ] # only has a train split\n " ,
32+ " \n " ,
33+ " print(f\" Number of samples: {len(data)}\" )\n " ,
34+ " print(f\" Features: {data.features}\" )"
35+ ]
36+ },
37+ {
38+ "cell_type" : " markdown" ,
39+ "metadata" : {},
40+ "source" : [
41+ " ok so ~2264 samples total. that's pretty small for fine-tuning but should be enough for QLoRA since we're not updating that many params."
42+ ]
43+ },
44+ {
45+ "cell_type" : " code" ,
46+ "execution_count" : null ,
47+ "metadata" : {},
48+ "outputs" : [],
49+ "source" : [
50+ " # label mapping — 0=negative, 1=neutral, 2=positive\n " ,
51+ " label_map = {0: \" negative\" , 1: \" neutral\" , 2: \" positive\" }\n " ,
52+ " \n " ,
53+ " label_counts = Counter(data[\" label\" ])\n " ,
54+ " for label_id, count in sorted(label_counts.items()):\n " ,
55+ " pct = count / len(data) * 100\n " ,
56+ " print(f\" {label_map[label_id]:>10}: {count:>5} ({pct:.1f}%)\" )"
57+ ]
58+ },
59+ {
60+ "cell_type" : " code" ,
61+ "execution_count" : null ,
62+ "metadata" : {},
63+ "outputs" : [],
64+ "source" : [
65+ " # label distribution\n " ,
66+ " names = [label_map[i] for i in sorted(label_counts.keys())]\n " ,
67+ " counts = [label_counts[i] for i in sorted(label_counts.keys())]\n " ,
68+ " colors = [\" #e74c3c\" , \" #95a5a6\" , \" #2ecc71\" ]\n " ,
69+ " \n " ,
70+ " plt.figure(figsize=(7, 4))\n " ,
71+ " plt.bar(names, counts, color=colors, edgecolor=\" black\" , linewidth=0.5)\n " ,
72+ " plt.title(\" Label Distribution (sentences_allagree)\" )\n " ,
73+ " plt.ylabel(\" Count\" )\n " ,
74+ " for i, c in enumerate(counts):\n " ,
75+ " plt.text(i, c + 15, str(c), ha=\" center\" , fontsize=10)\n " ,
76+ " plt.tight_layout()\n " ,
77+ " plt.show()"
78+ ]
79+ },
80+ {
81+ "cell_type" : " markdown" ,
82+ "metadata" : {},
83+ "source" : [
84+ " super imbalanced — neutral dominates, negative is tiny. gonna need to think about this during training (class weights or oversampling or something)"
85+ ]
86+ },
87+ {
88+ "cell_type" : " code" ,
89+ "execution_count" : null ,
90+ "metadata" : {},
91+ "outputs" : [],
92+ "source" : [
93+ " # examples from each class\n " ,
94+ " for label_id in sorted(label_map.keys()):\n " ,
95+ " print(f\"\\ n--- {label_map[label_id].upper()} ---\" )\n " ,
96+ " examples = [s for s, l in zip(data[\" sentence\" ], data[\" label\" ]) if l == label_id][:3]\n " ,
97+ " for ex in examples:\n " ,
98+ " print(f\" • {ex[:120]}\" )"
99+ ]
100+ },
101+ {
102+ "cell_type" : " code" ,
103+ "execution_count" : null ,
104+ "metadata" : {},
105+ "outputs" : [],
106+ "source" : [
107+ " # sentence length stats (just splitting on spaces, nothing fancy)\n " ,
108+ " word_counts = [len(s.split()) for s in data[\" sentence\" ]]\n " ,
109+ " char_counts = [len(s) for s in data[\" sentence\" ]]\n " ,
110+ " \n " ,
111+ " print(f\" Word counts — mean: {np.mean(word_counts):.1f}, median: {np.median(word_counts):.0f}, \"\n " ,
112+ " f\" min: {min(word_counts)}, max: {max(word_counts)}\" )\n " ,
113+ " print(f\" Char counts — mean: {np.mean(char_counts):.1f}, median: {np.median(char_counts):.0f}\" )"
114+ ]
115+ },
116+ {
117+ "cell_type" : " code" ,
118+ "execution_count" : null ,
119+ "metadata" : {},
120+ "outputs" : [],
121+ "source" : [
122+ " fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n " ,
123+ " \n " ,
124+ " axes[0].hist(word_counts, bins=30, edgecolor=\" black\" , alpha=0.7, color=\" steelblue\" )\n " ,
125+ " axes[0].set_title(\" Word Count Distribution\" )\n " ,
126+ " axes[0].set_xlabel(\" # words\" )\n " ,
127+ " axes[0].set_ylabel(\" Frequency\" )\n " ,
128+ " \n " ,
129+ " axes[1].hist(char_counts, bins=30, edgecolor=\" black\" , alpha=0.7, color=\" coral\" )\n " ,
130+ " axes[1].set_title(\" Character Count Distribution\" )\n " ,
131+ " axes[1].set_xlabel(\" # characters\" )\n " ,
132+ " \n " ,
133+ " plt.tight_layout()\n " ,
134+ " plt.show()"
135+ ]
136+ },
137+ {
138+ "cell_type" : " markdown" ,
139+ "metadata" : {},
140+ "source" : [
141+ " most sentences are pretty short, like 15-30 words. good — won't need huge context windows for this."
142+ ]
143+ },
144+ {
145+ "cell_type" : " code" ,
146+ "execution_count" : null ,
147+ "metadata" : {},
148+ "outputs" : [],
149+ "source" : [
150+ " # word frequency per class\n " ,
151+ " import re\n " ,
152+ " from collections import defaultdict\n " ,
153+ " \n " ,
154+ " stopwords = {\" the\" , \" a\" , \" an\" , \" in\" , \" of\" , \" to\" , \" and\" , \" for\" , \" is\" , \" was\" , \" its\" ,\n " ,
155+ " \" it\" , \" on\" , \" by\" , \" with\" , \" from\" , \" at\" , \" as\" , \" has\" , \" had\" , \" that\" ,\n " ,
156+ " \" this\" , \" are\" , \" were\" , \" be\" , \" been\" , \" will\" , \" or\" , \" which\" , \" also\" ,\n " ,
157+ " \" than\" , \" have\" , \" not\" , \" but\" , \" s\" , \" said\" , \" would\" , \" their\" , \" about\" }\n " ,
158+ " \n " ,
159+ " class_words = defaultdict(list)\n " ,
160+ " for sentence, label in zip(data[\" sentence\" ], data[\" label\" ]):\n " ,
161+ " tokens = re.findall(r\"\\ b[a-z]+\\ b\" , sentence.lower())\n " ,
162+ " tokens = [t for t in tokens if t not in stopwords and len(t) > 2]\n " ,
163+ " class_words[label].extend(tokens)\n " ,
164+ " \n " ,
165+ " for label_id in sorted(label_map.keys()):\n " ,
166+ " top = Counter(class_words[label_id]).most_common(15)\n " ,
167+ " print(f\"\\ n{label_map[label_id].upper()} — top 15 words:\" )\n " ,
168+ " print(\" , \" .join(f\" {w} ({c})\" for w, c in top))"
169+ ]
170+ },
171+ {
172+ "cell_type" : " code" ,
173+ "execution_count" : null ,
174+ "metadata" : {},
175+ "outputs" : [],
176+ "source" : [
177+ " # quick keyword pattern check\n " ,
178+ " keywords = {\n " ,
179+ " \" positive\" : [\" profit\" , \" growth\" , \" increased\" , \" rose\" , \" improved\" , \" gains\" ],\n " ,
180+ " \" negative\" : [\" loss\" , \" declined\" , \" fell\" , \" dropped\" , \" decreased\" , \" lower\" ],\n " ,
181+ " \" neutral\" : [\" reported\" , \" announced\" , \" according\" , \" expects\" , \" company\" , \" shares\" ]\n " ,
182+ " }\n " ,
183+ " \n " ,
184+ " print(\" Keyword hit rates per class:\\ n\" )\n " ,
185+ " for sentiment, kws in keywords.items():\n " ,
186+ " label_id = {v: k for k, v in label_map.items()}[sentiment]\n " ,
187+ " sents = [s.lower() for s, l in zip(data[\" sentence\" ], data[\" label\" ]) if l == label_id]\n " ,
188+ " total = len(sents)\n " ,
189+ " for kw in kws:\n " ,
190+ " hits = sum(1 for s in sents if kw in s)\n " ,
191+ " print(f\" {sentiment:>8} | '{kw}': {hits}/{total} ({hits/total*100:.1f}%)\" )\n " ,
192+ " print()"
193+ ]
194+ },
195+ {
196+ "cell_type" : " markdown" ,
197+ "metadata" : {},
198+ "source" : [
199+ " makes sense that words like \" profit\" and \" growth\" show up more in positive, and \" loss\" /\" declined\" in negative. the neutral class is more about reporting language (\" announced\" , \" reported\" ). these patterns are actually pretty strong — explains why even bag-of-words models can get decent accuracy on this."
200+ ]
201+ },
202+ {
203+ "cell_type" : " markdown" ,
204+ "metadata" : {},
205+ "source" : [
206+ " ## Takeaways\n " ,
207+ " \n " ,
208+ " so the dataset is small (~2264 samples) but seems well-curated — the `sentences_allagree` subset means all annotators agreed, so labels should be clean. the class imbalance is real though, especially the tiny negative set. the keyword patterns are pretty clear which is why even simple models do ok on this. Let's see if QLoRA can push it further by understanding the actual financial context beyond just keywords."
209+ ]
210+ }
211+ ],
212+ "metadata" : {
213+ "kernelspec" : {
214+ "display_name" : " Python 3" ,
215+ "language" : " python" ,
216+ "name" : " python3"
217+ },
218+ "language_info" : {
219+ "name" : " python" ,
220+ "version" : " 3.10.12"
221+ }
222+ },
223+ "nbformat" : 4 ,
224+ "nbformat_minor" : 4
225+ }
0 commit comments