上面看到的就是这次做的结果,从 疾病表 的近200万条数据分析得到的结果,可以从这里看到实时演示
广告一下,个人博客icodeit.cn
原始数据处理
该表展现了用户所患疾病的关系,每一条数据包含两个有效数据字段“用户id”、“疾病名称”。但是原始数据中包含一些无效数据:
1 2
|
select fld_UserId, count(*) c from tab_UserDisease group by fld_UserId order by c desc limit 10;
|
通过该sql分析发现数据中有部分用户存在大量疾病关系,这部分数据不满足合理性,应该剔除。
1 2 3 4 5 6 7 8 9 10 11 12
|
//抓取原始数据,同时加以筛选,患有2-9种疾病的认为有效 $sql = "SELECT fld_UserId userid, group_concat(fld_DiseaseName) diseases FROM tab_UserDisease where fld_UserId in ( select fld_UserId from (SELECT fld_UserId, count(*) c FROM tab_UserDisease group by fld_UserId ) ta where ta.c>1 and ta.c<10) group by fld_UserId"; $data = $dbexecuter->query($sql);
//疾病数据 $sql = "SELECT fld_DiseaseName FROM tab_Disease"; $this->diseases = $dbexecuter->queryColumn($sql, 'fld_DiseaseName');
|
这里获取到的原始数据是以id、疾病的形式展现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
array(150720) { [0] => array(2) { 'userid' => string(1) "0" 'diseases' => string(31) "肺动脉瓣狭窄,性病,宫颈炎,妇科病" } [1] => array(2) { 'userid' => string(1) "1" 'diseases' => string(41) "乙肝,消化道出血,直肠肛管疾病,功能性胃肠病" } [2] => array(2) { 'userid' => string(1) "3" 'diseases' => string(60) "高血压,心肌梗死,冠心病,小三阳,糖尿病,心脏病,肺癌,癫痫,银屑病" }
(more elements)... }
|
为方便后续处理,需要对原始数据进行转置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
//原始数据转置,转换为疾病为key,患者为value的数组 $this->record = array(); foreach ($data as $id => $row) { $diseases = preg_split("/,/", $row['diseases']); foreach ($diseases as $disease) { if (!isset($this->record[$disease])) { $this->record[$disease] = array(); } $this->record[$disease][] = $id; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
array() { '便秘' => array(1268) { [0] => int(55) [1] => int(290) } '发烧' => array(1525) { [0] => int(54) [1] => int(643) }
(more elements)... }
|
Apriori算法
Apriori算法是一种最有影响的挖掘布尔关联规则频繁项集的算法Apriori使用一种称作逐层搜索的迭代方法,“K-1项集”用于搜索“K项集”。
首先,找出频繁“1项集”的集合,该集合记作L1。L1用于找频繁“2项集”的集合L2,而L2用于找L3。如此下去,直到不能找到“K项集”。找每个Lk都需要一次数据库扫描。
核心思想是:连接步和剪枝步。连接步是自连接,原则是保证前k-2项相同,并按照字典顺序连接。剪枝步,是使任一频繁项集的所有非空子集也必须是频繁的。反之,如果某
个候选的非空子集不是频繁的,那么该候选肯定不是频繁的,从而可以将其从CK中删除。
简单的讲,1、发现频繁项集,过程为(1)扫描(2)计数(3)比较(4)产生频繁项集(5)连接、剪枝,产生候选项集 重复步骤(1)~(5)直到不能发现更大的频集
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11
|
C[k]: 长度为 k的候选项集 L[k] : 长度为k的频繁项集
L[1] = {频繁项}; for (k = 1; L[k] !=∅; k++) do begin C[k+1] = 由 L[k]产生的候选; for each 数据库中的事务t do 增加包含在t 中的所有候选C[k+1]的计数 L[k+1] = C[k+1]中满足 min_support的候选 end return L[1..k];
|
实现
核心部分,这里节省时间,只计算到二元频繁项:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
//获取首个候选项集 $itemlist = $this->getFirstCandidate(); $idx = 0; while($idx >= 2) { //计算支持度,获取满足支持度的候选项集 $itemlist = $this->getSupportedItemset($itemlist); //获取候选项计数+1的候选项集 $itemlist = $this->getNextCandidate($itemlist);
$idx++; } file_put_contents('/tmp/diseaselink.out', $itemlist);
|
获取首个候选项集,这里直接取了所有的疾病:
1 2 3 4 5 6 7 8 9
|
private function getFirstCandidate() { $itemset = array(); foreach ($this->diseases as $disease) { $itemset[] = array($disease); } return $itemset; }
|
连接步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
private function getNextCandidate($itemset) { $nowCount = count($itemset[0]); $nextItemset = array(); for ($i = 0; $i < count($itemset); $i++) { for ($j = $i; $j < count($itemset); $j++) { $temp = array_unique(array_merge($itemset[$i], $itemset[$j])); { if (count($temp) == $nowCount + 1) { $nextItemset[] = $temp; } } } } return $nextItemset; }
|
减枝步 计算支持度,因为在原始数据处理阶段已经进行了数据转置,当我们需要计算同时患有某些疾病的人数时,只需要计算这些疾病患者数据的交集并计数即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
private function getSupportedItemset($itemset) { $supportedItemset = array(); foreach ($itemset as $item) { $patients = isset($this->record[$item[0]]) ? $this->record[$item[0]] : null; foreach ($item as $disease) { if (!is_array($patients)) { break; } $patients = array_intersect($patients, $this->record[$disease]); } if (is_array($patients) && count($patients) >= self::$MIN_SUPPORT) { $supportedItemset[] = $item; } } return $supportedItemset; }
|
如此循环便可以得出各级支持的频繁项集,这里只获取了两级频繁项,最终序列化写入到了文件中。后续还需要对输出的数据进行json序列化处理,以便于页面使用,页面展现使用了数据可视化常用的前端库d3js
by zc