股票数据建模与文本分析
1. 股票数据加载与处理
1.1 股票数据格式与加载库
股票价格数据通常为逗号分隔值(CSV)格式,虽然并非最丰富的数据格式,但非常流行。可以使用
clojure.data.csv
库来加载这种格式的数据。
1.2 定义股票数据类型
为了更方便地处理股票数据,需要定义一个记录类型。在
src/financial/types.clj
文件中添加以下代码:
(defrecord StockData [date open high low close volume])
1.3 命名空间声明
在
src/financial/cvs_data.clj
文件中添加以下命名空间声明:
(ns financial.csv-data
(:require [clojure.data.csv :as csv]
[clojure.java.io :as io]
[clj-time.core :as clj-time]
[clj-time.format :as time-format])
(:use [financial types utils]))
1.4 日期解析
股票数据中的日期格式为
29-Dec-00
,可以使用以下格式规范进行解析:
(def date-format (time-format/formatter "d-MMM-YY"))
1.5 创建
StockData
实例
根据 CSV 文件的每一行数据创建
StockData
实例,代码如下:
(defn row->StockData [row]
(let [[date open high low close vol] row]
(->StockData (time-format/parse date-format date)
(->double open)
(->double high)
(->double low)
(->double close)
(->long vol))))
1.6 读取整个文件
读取整个 CSV 文件的代码如下:
(defn read-stock-prices [filename]
(with-open [f (io/reader filename)]
(doall (map row->StockData (drop 1 (csv/read-csv f))))))
需要注意的是,要跳过第一行,因为它是表头。
1.7 加载数据
使用以下代码加载数据:
user=> (def sp (csvd/read-stock-prices "d/d-1995-2001.csv"))
user=> (first sp)
#financial.types.StockData{:date #<DateTime 2000-12-29T00:00:00.000Z>,
:open 33.47, :high 33.56, :low 33.09, :close 33.5, :volume 857800}
user=> (count sp)
1263
2. 新闻文章文本分析
2.1 分析目标
分析新闻文章的目标是生成文档集合的向量空间模型,将文档的显著特征提取到一个浮点数向量中。特征可以是单词或文档元数据中的信息,特征值可以是 0 或 1 表示存在与否,也可以是原始频率或经过某种形式缩放后的频率。
2.2 命名空间声明
在
src/financial/nlp.clj
文件中添加以下命名空间声明:
(ns financial.nlp
(:require [clojure.string :as str]
[clojure.set :as set])
(:use [financial types utils]))
2.3 词汇分析
2.3.1 分词
使用简单的
tokenize
函数进行分词,代码如下:
(defn tokenize [string]
(map str/lower-case (re-seq #"[\p{L}\p{M}]+" string)))
(defn tokenize-text [m] (update-in m [:text] tokenize))
tokenize
函数处理实际的分词操作,
tokenize-text
函数将
NewsArticle
实例的原始文本属性替换为从文本生成的标记序列。
2.3.2 标记频率统计
使用
token-freqs
函数将标记序列替换为其频率映射,代码如下:
(defn token-freqs [m] (update-in m [:text] frequencies))
2.3.3 语料库频率统计
使用
corpus-freqs
函数计算整个语料库的频率,代码如下:
(defn corpus-freqs [coll]
(reduce #(merge-with + %1 %2) {} (map :text coll)))
2.4 示例代码
以下是使用上述函数获取频率的示例代码:
user=> (def tokens (map nlp/tokenize-text articles))
user=> (take 10 (:text (first tokens)))
("harmonic" "convergences" "you" "re" "right" "maxim" "s" "strong"
"point" "is")
user=> (def freqs (map nlp/token-freqs tokens))
user=> (take 10 (:text (first freqs)))
(["sillier" 1] ["partly" 2] ["mags" 4] ["new" 1] ["advisor" 1]
["a" 13] ["worry" 1] ["unsentimental" 1] ["method" 1] ["pampering"
1])
user=> (def c-freqs (nlp/corpus-freqs freqs))
user=> (take 10 (reverse (sort-by second c-freqs)))
(["the" 266011]
["of" 115973]
["to" 107951]
["a" 101017]
["and" 96375]
["in" 74558]
["s" 66349]
["that" 64447]
["is" 49311]
["it" 38175])
2.5 停用词处理
2.5.1 停用词列表
常见的停用词列表可以从网上下载,例如从
http://jmlr.org/papers/volume5/lewis04a/a11-smart-stop-list/english.stop
下载。使用以下函数加载停用词:
(defn load-stop-words [filename]
(set (tokenize (slurp filename))))
2.5.2 移除停用词
使用以下函数移除
NewsArticle
实例中的停用词:
(defn remove-stop-words [stop-words m]
(update-in m [:text] #(remove stop-words %)))
2.5.3 示例代码
user=> (def stop-words (nlp/load-stop-words "d/english.stop"))
user=> (def filtered
(map #(nlp/remove-stop-words stop-words %) tokens))
user=> (take 10 (:text (first filtered)))
("harmonic" "convergences" "maxim" "strong" "point" "totally"
"unsentimental" "ungenteel" "sendup" "model")
user=> (def freqs (map nlp/token-freqs filtered))
user=> (def c-freqs (nlp/corpus-freqs freqs))
user=> (pprint (take 10 (reverse (sort-by second c-freqs))))
(["clinton" 8567]
["times" 6528]
["people" 6351]
["time" 6091]
["story" 5645]
["president" 5223]
["year" 4539]
["york" 4516]
["world" 4256]
["years" 4144])
2.6 白名单处理
使用
keep-white-list
函数保留白名单中的标记,代码如下:
(defn keep-white-list [white-list-set m]
(over :text #(filter white-list-set %) m))
2.7 罕用词处理
2.7.1 罕用词定义
只出现一次的词称为单现词(hapax legomena),只出现两次的词称为双现词(dis legomena)。由于这些词对研究贡献不大,需要将出现次数少于 10 次的词作为罕用词过滤掉。
2.7.2 生成罕用词列表
使用以下函数生成罕用词列表:
(defn make-rare-word-list [freqs n]
(map first (filter #(< (second %) n) freqs)))
2.7.3 生成罕用词文件
(with-open [f (io/writer "d/english.rare")]
(binding [*out* f]
(doseq [t (sort (nlp/make-rare-word-list c-freqs 8))]
(println t))))
2.7.4 移除罕用词
user=> (def rare (nlp/load-stop-words "d/english.rare"))
user=> (def filtered2
(map #(nlp/remove-stop-words rare %) filtered))
user=> (take 10 (:text (first filtered2)))
("maxim" "strong" "point" "totally" "unsentimental" "sendup" "model"
"hustler" "difference" "surprise")
2.8 文章处理函数
使用
process-articles
函数一次性完成文章的处理,代码如下:
(defn process-articles
([articles]
(process-articles
articles ["d/english.stop" "d/english.rare"]))
([articles stop-files]
(let [stop-words (reduce set/union #{}
(map load-stop-words stop-files))
process (fn [text]
(frequencies
(remove stop-words (tokenize text))))]
(map #(over :text process %) articles))))
使用以下代码调用该函数:
user=> (def freqs (nlp/process-articles articles))
2.9 白名单加载文章
使用
load-articles
函数根据白名单加载文章,代码如下:
(defn load-articles [white-list-set articles]
(let [process (fn [text]
(frequencies
(filter white-list-set (tokenize text))))]
(map #(over :text process %) articles)))
2.10 文本分析流程
graph TD;
A[文章数据] --> B[分词];
B --> C[标记频率统计];
C --> D[语料库频率统计];
D --> E[移除停用词];
E --> F[移除罕用词];
F --> G[文章处理];
G --> H[白名单加载文章];
2.11 部分结果对比
| 操作 | 示例结果 |
|---|---|
| 分词后前 10 个标记 | (“harmonic” “convergences” “you” “re” “right” “maxim” “s” “strong” “point” “is”) |
| 标记频率统计后前 10 对 | ([“sillier” 1] [“partly” 2] [“mags” 4] [“new” 1] [“advisor” 1] [“a” 13] [“worry” 1] [“unsentimental” 1] [“method” 1] [“pampering” 1]) |
| 移除停用词后前 10 个标记 | (“harmonic” “convergences” “maxim” “strong” “point” “totally” “unsentimental” “ungenteel” “sendup” “model”) |
| 移除罕用词后前 10 个标记 | (“maxim” “strong” “point” “totally” “unsentimental” “sendup” “model” “hustler” “difference” “surprise”) |
3. TF-IDF 计算
3.1 频率存在的问题
当前的频率数据存在一些问题。一方面,不同长度的文档之间频率难以直接比较,例如一个 100 词的文档和一个 500 词的文档;另一方面,在每个文档中都出现多次的词(如标题中的词),不如只在少数文档中出现几次的词有价值。
3.2 TF-IDF 原理
为了解决这些问题,我们使用术语频率 - 逆文档频率(TF - IDF)指标。它将文档 - 术语频率与包含该术语的文档百分比的对数相结合。
3.2.1 术语频率(TF)
术语频率可以使用多种度量标准,这里使用增强频率,即通过文档中任何词的最大频率对原始频率进行缩放。代码如下:
(defn tf [term-freq max-freq]
(+ 0.5 (/ (* 0.5 term-freq) max-freq)))
(defn tf-article [article term]
(let [freqs (:text article)]
(tf (freqs term 0) (reduce max 0 (vals freqs)))))
第一个函数
tf
是基本的增强频率方程,接受原始值作为参数;第二个函数
tf-article
包装了
tf
,接受
NewsArticle
实例和一个词,并为该对生成 TF 值。
3.2.2 逆文档频率(IDF)
逆文档频率使用总文档数除以包含该术语的文档数的对数,并加 1 以防止除零错误。代码如下:
(defn has-term?
([term] (fn [a] (has-term? term a)))
([term a] (not (nil? (get (:text a) term)))))
(defn idf [corpus term]
(Math/log
(/ (count corpus)
(inc (count (filter (has-term? term) corpus))))))
3.2.3 缓存 IDF 值
由于一个词的 IDF 在不同文档之间不会改变,我们可以一次性计算语料库中所有词的 IDF 值并进行缓存。代码如下:
(defn get-vocabulary [corpus]
(reduce set/union #{} (map #(set (keys (:text %))) corpus)))
(defn get-idf-cache [corpus]
(reduce #(assoc %1 %2 (idf corpus %2)) {}
(get-vocabulary corpus)))
第一个函数
get-vocabulary
返回语料库中使用的所有词的集合;第二个函数
get-idf-cache
遍历词汇集,构建缓存的 IDF 值映射。
3.2.4 计算 TF - IDF 值
tf-idf
函数将 TF 和 IDF 的输出相结合,计算 TF - IDF 值。代码如下:
(defn tf-idf [idf-value freq max-freq]
(* (tf freq max-freq) idf-value))
3.3 适配函数
为了方便处理
NewsArticle
实例和更高级的 Clojure 数据结构,我们编写了以下适配函数:
(defn tf-idf-freqs [idf-cache freqs]
(let [max-freq (reduce max 0 (vals freqs))]
(into {}
(map #(vector (first %)
(tf-idf
(idf-cache (first %))
(second %)
max-freq))
freqs))))
(defn tf-idf-over [idf-cache article]
(over :text (fn [f] (tf-idf-freqs idf-cache f)) article))
(defn tf-idf-cached [idf-cache corpus]
(map #(tf-idf-over idf-cache %) corpus))
(defn tf-idf-all [corpus]
(tf-idf-cached (get-idf-cache corpus) corpus))
3.4 示例代码
使用之前移除罕用词后的文档集合
filtered2
来生成 TF - IDF 频率,并比较一个文档的频率。代码如下:
(def tf-idfs (nlp/tf-idf-all filtered2))
(doseq [[t f] (sort-by second (:text (first filtered2)))]
(println t \tab f \tab (get (:text (first tf-idfs)) t)))
以下是部分有趣术语的原始频率、文档频率和 TF - IDF 分数的对比:
| Token | Raw frequency | Document frequency | TF - IDF |
| ---- | ---- | ---- | ---- |
| sillier | 1 | 8 | 3.35002 |
| politics | 1 | 749 | 0.96849 |
| british | 1 | 594 | 1.09315 |
| reason | 2 | 851 | 0.96410 |
| make | 2 | 2350 | 0.37852 |
| military | 3 | 700 | 1.14842 |
| time | 3 | 2810 | 0.29378 |
| mags | 4 | 18 | 3.57932 |
| women | 11 | 930 | 1.46071 |
| men | 13 | 856 | 1.66526 |
从这个表格可以看出,原始频率与 TF - IDF 分数的相关性较差。例如,“sillier” 和 “politics” 在文档中都只出现一次,但 “sillier” 在整个集合中可能不常出现,其 TF - IDF 分数超过 3,而 “politics” 比较常见,分数略低于 1。
3.5 TF - IDF 计算流程
graph TD;
A[文档集合] --> B[计算词汇表];
B --> C[计算 IDF 缓存];
C --> D[计算每个文档的 TF];
D --> E[结合 TF 和 IDF 计算 TF - IDF];
E --> F[输出 TF - IDF 频率];
3.6 加载文章的实用函数
使用
load-text-files
函数加载一组文章,同时考虑标记白名单和 IDF 缓存。代码如下:
(defn load-text-files [token-white-list idf-cache articles]
(tf-idf-cached idf-cache
(load-articles token-white-list articles)))
这个函数在训练神经网络后实际使用时非常重要,因为需要保持相同的特征和顺序,并使用相同的 IDF 值进行缩放。
综上所述,通过对股票数据的加载处理以及新闻文章的文本分析,特别是使用 TF - IDF 指标,我们可以更有效地提取文档的关键信息,为后续的分析和应用(如神经网络训练)提供有力支持。整个处理流程涵盖了数据加载、文本预处理、特征提取和特征缩放等多个步骤,每个步骤都有其特定的作用和实现方法。
股票与新闻文本的TF-IDF分析
超级会员免费看
1583

被折叠的 条评论
为什么被折叠?



