24、股票数据建模与文本分析

股票与新闻文本的TF-IDF分析

股票数据建模与文本分析

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 指标,我们可以更有效地提取文档的关键信息,为后续的分析和应用(如神经网络训练)提供有力支持。整个处理流程涵盖了数据加载、文本预处理、特征提取和特征缩放等多个步骤,每个步骤都有其特定的作用和实现方法。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值