本文是学习clojure的过程中写的第一个小程序,功能很简单,第一次使用clojure实现自己的想法,特此记录。
数据文件如下:
<!-- lang: shell -->
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)
结果如下:
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返回。
结果如下:
原始数据: [[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}, … },下面把它转变为这种形式:
(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])