本文是學習clojure的過程當中寫的第一個小程序,功能很簡單,第一次使用clojure實現本身的想法,特此記錄。 數據文件以下:java
101,1 101,2 102,1 102,3 103,2 104,4 104,1 104,5
左列是item id,右列是user id 先簡單的把文件讀入內存,使用clojure.java.io/reader:小程序
(use 'clojure.java.io) ;line-seq返回一個惰性序列,使用into強制把整個序列塞進vector中 (def data (with-open [rdr (reader "/tmp/test.txt")] (into [] (line-seq rdr)))) (println "data is: " data)
結果以下:app
data is: [101,1 101,2 102,1 102,3 103,2 104,4 104,1 104,5]
每行是一個字符串,作爲vector中的一個元素 下面考慮在讀取每一行的時候進行解析:以逗號作爲分隔符,切分爲item和user兩部分函數
(def data (with-open [rdr (reader "/tmp/test.txt")] (into [] (map #(if % (let [ [k v] (.split % ",") ] [k v])) (line-seq rdr))))) (println "原始數據:" data)
解析函數:用java.lang.split函數以逗號分隔字符串,再用let解析之,返回key、value組成的vector; 用map把解析函數作用於文件的每一行,把全部的解析結果放入vector返回。 結果以下:學習
原始數據:code
[[101 1] [101 2] [102 1] [102 3] [103 2] [104 4] [104 1] [104 5]]
再考慮按照item作group by,造成{item => {user1,user2}}的map,以便計算item之間的類似度內存
(def tmp_data (group-by #(first %) data)) (println "按第一個元素group by以後:" tmp_data)
按item group by,user作爲value,tmp_data以下:文檔
按第一個元素group by以後:字符串
{101 [[101 1] [101 2]], 102 [[102 1] [102 3]], 103 [[103 2]], 104 [[104 4] [104 1] [104 5]]}
由於行成的map中values不是想要的結構,應該是{101 {1 2}, ... },下面把它轉變爲這種形式:string
(def result (for [ [k values] tmp_data] [k (into #{} (map second values))])) (println "把values轉爲列表:" result)
這裏用for遍歷tmp_data,把其中的values中的第二個元素,也就是user id,提取出來放到集合中,輸出以下:
把values轉爲列表:
([101 #{1 2}] [102 #{1 3}] [103 #{2}] [104 #{1 4 5}])
這就是最終想要的結果了,不過它是一個list,item和user集合組成的vector作爲元素,這個不影響item-cf的計算。 上述把文件中的行轉爲item-user集合的方式有點麻煩,並且也不是clojure的慣用法,下面使用apply 配合merge-with獲得最終結果:
(def data (with-open [rdr (reader "/tmp/test.txt")] (into [] (map #(if % (let [ [k v] (.split % ",")] {(Integer/parseInt k) v})) (line-seq rdr)))))
這一步主要是文件中的每一行讀出來並解析出item和user,與上面不一樣的是,把item和user先放進map中,最終再總體放入vector中(順帶把item由string轉成int,方便後面的計算)
(defn to-set [set] (if (set? set) set #{set})) ;也可使用apply轉變 (def result (apply merge-with #(union (to-set %1) (to-set %2)) data)) (println "使用apply和merge-with:" result)
這裏用到了apply 函數,它的使用方法參見clojure文檔; merge-with函數按key合併一組hashmap; union合併兩個集合; 自定義的to-set函數檢查參數是否是集合,若是不是,則把它轉爲集合返回;若是是,則返回原值 輸出以下:
使用apply和merge-with:
{104 #{1 4 5}, 103 2, 102 #{1 3}, 101 #{1 2}}
最後計算item兩兩之間的類似度,先看類似度計算公式:
(defn sim [s1 s2] (/ (count (clojure.set/intersection s1 s2)) (Math/sqrt (* (count s1) (count s2)))))
clojure.set/intersection獲得兩個集合中共有的元素,計算類似度:
(def pair (for [[k1 v1] result :when (set? v1) [k2 v2] result :when (and (< k1 k2) (set? v2))] [k1 k2 (sim v1 v2)])) (println pair)
用for獲得item列表的笛卡爾積(任意兩個item的組合),因爲101-102的計算結果與102-101的計算結果是同樣的,這裏爲了減小重複計算,只在k1小於k2的時候計算類似度,最終結果:
([102 104 0.4082482904638631] [101 104 0.4082482904638631] [101 102 0.5])