## 层次聚类和DBSCAN

### 1.层次聚类

```import math
import numpy as np
def euler_distance(point1, point2):
distance = 0.0
for a, b in zip(point1, point2):
distance += math.pow(a-b, 2)
return math.sqrt(distance)
# 定义聚类树的节点
class ClusterNode:
def __init__(self, vec, left=None, right=None, distance=-1, id=None, count=1):
"""
vec: 保存两个数据merge后新的中心
left: 左节点
right: 右节点
distance: 两个节点的距离
id: 保存哪个节点是计算过的
count: 这个节点的叶子节点个数
"""
self.vec = vec
self.left = left
self.right = right
self.distance = distance
self.id = id
self.count = count
# 层次聚类的类
# 不同于文中所说的先构建树，再进行切分，而是直接根据所需类别数目，聚到满足条件的节点数量即停止
# 和k-means一样，也需要指定类别数量
class Hierarchical:
def __init__(self, k=1):
assert k > 0
self.k = k
self.labels = None
def fit(self, x):
# 初始化节点各位等于数据的个数
nodes = [ClusterNode(vec=v, id=i) for i, v in enumerate(x)]
distance = {}
point_num, feature_num = np.shape(x)
self.labels = [-1] * point_num
currentclustid = -1
while len(nodes) > self.k:
min_dist = np.inf
# 当前节点的个数
nodes_len = len(nodes)
# 最相似的两个类别
closest_part = None
# 当前节点中两两距离计算，找出最近的两个节点
for i in range(nodes_len-1):
for j in range(i+1, nodes_len):
# 避免重复计算
d_key = (nodes[i].id, nodes[j].id)
if d_key not in distance:
distance[d_key] = euler_distance(nodes[i].vec, nodes[j].vec)
d = distance[d_key]
if d < min_dist:
min_dist = d
closest_part = (i, j)
part1, part2 = closest_part
node1, node2 = nodes[part1], nodes[part2]
# 将两个节点进行合并,即两个节点所包含的所有数据的平均值
new_vec = [(node1.vec[i] * node1.count + node2.vec[i] * node2.count) / (node1.count + node2.count)
for i in range(feature_num)]
new_node = ClusterNode(vec=new_vec, left=node1, right=node2, distance=min_dist, id=currentclustid,
count=node1.count + node2.count)
currentclustid -= 1
# 删掉这最近的两个节点
del nodes[part2], nodes[part1]
# 把新的节点添加进去
nodes.append(new_node)
# 树建立完成，这里要注意，在示例中是最终凝聚为1个节点，而这里到达所要指定的类别数目即停止，一个node属于一个类别
self.nodes = nodes
# 给每个node以及node包含的数据打上标签
self.calc_label()
def calc_label(self):
# 调取聚类结果
for i, node in enumerate(self.nodes):
self.leaf_traversal(node, i)
def leaf_traversal(self, node: ClusterNode, label):
# 递归遍历叶子结点
if node.left is None and node.right is None:
self.labels[node.id] = label
if node.left:
self.leaf_traversal(node.left, label)
if node.right:
self.leaf_traversal(node.right, label)```

```from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
my = Hierarchical(4)
my.fit(iris.data)
data = iris.data
data_0 = data[np.nonzero(np.array(my.labels) == 0)]
data_1 = data[np.nonzero(np.array(my.labels) == 1)]
data_2 = data[np.nonzero(np.array(my.labels) == 2)]
data_3 = data[np.nonzero(np.array(my.labels) == 3)]
plt.scatter(data_0[:, 0], data_0[:, 1])
plt.scatter(data_1[:, 0], data_1[:, 1])
plt.scatter(data_2[:, 0], data_2[:, 1])
plt.scatter(data_3[:, 0], data_3[:, 1])
print(np.array(my.labels))
from sklearn.cluster import KMeans
km = KMeans(4)
km.fit(iris.data)
print(km.labels_)
data_0_ = data[np.nonzero(np.array(km.labels_) == 0)]
data_1_ = data[np.nonzero(np.array(km.labels_) == 1)]
data_2_ = data[np.nonzero(np.array(km.labels_) == 2)]
data_3_ = data[np.nonzero(np.array(km.labels_) == 3)]
plt.figure()
plt.scatter(data_0_[:, 0], data_0_[:, 1])
plt.scatter(data_1_[:, 0], data_1_[:, 1])
plt.scatter(data_2_[:, 0], data_2_[:, 1])
plt.scatter(data_3_[:, 0], data_3_[:, 1])```

```from sklearn.cluster import AgglomerativeClustering
from sklearn.preprocessing import MinMaxScaler
model = AgglomerativeClustering(n_clusters=4, affinity='euclidean', memory=None, connectivity=None,
"""

n_cluster: 聚类数目
affinity: 计算距离的方法，'euclidean'为欧氏距离, 'manhattan'曼哈顿距离, 'cosine'余弦距离, 'precompute'预先计算的affinity matrix;
memory: None, 给定一个地址，层次聚类的树缓存在相应的地址中；
"""
"""

labels_： 每个数据的分类标签
n_leaves_：分层树的叶节点数量
n_components：连接图中连通分量的估计值
children：一个数组，给出了每个非节点数量
"""
min_max_scalar = MinMaxScaler()
data_scalar = min_max_scalar.fit_transform(data_array)
model.fit(min_max_scalar)
plt.figure(figsize=(20, 6))
p = dendrogram(Z, 0)
plt.show()```

#### 层次聚类的优缺点：

1、距离的定义比较容易，而且比较自由；

2、有时可以不用指定所需类别个数，就像前面说的，我们可以通过阈值来进行类的划分；

3、可以生成非球形的簇，发现层次间的关系。

1、在建树过程中要计算每个样本间的距离，计算复杂度较高；

2、算法对于异常值比较敏感，影响聚类效果；

3、容易形成链状的簇。

### 2.DBSCAN

DBSCAN的主旨思想是 只要一个区域中的点的密度大于一定的阈值，就把它加到与之相近的类别当中去 。

（1） ε-邻域： 一个对象在半径为ε内的区域，简单来说就是在给定一个数据为圆心画一个半径为ε的圈；

（2） 核心对象： 对于给定的一个数值m，在某个对象的邻域内，至少包含m个点，则称之为核心，简单来说就是某个对象的圈内的数据大于m个，则这个对象就是核心；

（3） 直接密度可达： 结合上图，给定一个对象q，如果这个对象的邻域内有大于m个点，而另一个对象p又在这个邻域内，则称之为p是q的直接密度可达；

（4） 间接密度可达： 如下一张图，p1是q的直接密度可达，而p是p1的直接密度可达，那幺p则是q的密度可达；

（5） 密度相连 ： 假设一个对象O，是对象p的密度可达，而q是O的密度可达，那幺p和q则是密度相连的。

（6） 簇 ：基于密度聚类的簇就是最大的密度相连的所有对象的集合；

（7） 噪声 ：不属于任何簇中的对象称之为噪声；

（1）p1的邻域内有点{p1,p2,p3,p13}，因此p1是核心点；

（2）以p1为核心点，建立簇C1；找出所有与p1的密度可达的点；

（3）p2的邻域内为{p1,p2,p3,p4,p13}，因此p4属于p1的密度可达，p4属于簇C1；

（4）p3的邻域内为{p1,p2,p3,p4,p13}，这些点都已属于簇C1，继续；

（5）p4的邻域内为{ p3,p4,p13 }，这些点也都属于簇C1，继续；

（6）p13的邻域内为{p2,p3,p4,p13}，也都处理过了

（1）计算p5邻域内的点{ p5,p6,p7,p8 }，因此p5也是核心点；

（2）以p5为核心点 ，建立簇C2，找出所有与p5的密度可达的点；

（3）同第一步中一样，依次扫描p6、p7、p8;

（1）p9的邻域内的点为{p9}，所以p9b不是核心点，进行下一步

（1）p10的领域内的点为{ p10,p11 }，所以p10不是核心点，进行下一步。

（1）计算p11邻域内的点为{ p11,p10,p12 }，所以p11是核心点；

（2）以p11为核心点建立簇C3，找出所有的密度可达点；

（3）p10已被处理处理过，继续扫描；

（4）扫描p12，p12邻域内{ p12,p11 }；

```import numpy as np
import random
import matplotlib.pyplot as plt
import copy
from sklearn import datasets
# 搜索邻域内的点
def find_neighbor(j, x, eps):
"""
:param j: 核心点的索引
:param x: 数据集
:param eps:邻域半径
:return:
"""
temp = np.sum((x - x[j]) ** 2, axis=1) ** 0.5
N = np.argwhere(temp <= eps).flatten().tolist()
return N
def DBSCAN(X, eps, MinPts):
k = -1
# 保存每个数据的邻域
neighbor_list = []
# 核心对象的集合
omega_list = []
# 初始化，所有的点记为未处理
gama = set([x for x in range(len(X))])
cluster = [-1 for _ in range(len(X))]
for i in range(len(X)):
neighbor_list.append(find_neighbor(i, X, eps))
if len(neighbor_list[-1]) >= MinPts:
omega_list.append(i)
omega_list = set(omega_list)
while len(omega_list) > 0:
gama_old = copy.deepcopy(gama)
# 随机选取一个核心点
j = random.choice(list(omega_list))
# 以该核心点建立簇Ck
k = k + 1
Q = list()
# 选取的核心点放入Q中处理，Q中只有一个对象
Q.append(j)
# 选取核心点后，将核心点从核心点列表中删除
gama.remove(j)
# 处理核心点，找出核心点所有密度可达点
while len(Q) > 0:
q = Q[0]
# 将核心点移出，并开始处理该核心点
Q.remove(q)
# 第一次判定为True，后面如果这个核心点密度可达的点还有核心点的话
if len(neighbor_list[q]) >= MinPts:
# 核心点邻域内的未被处理的点
delta = set(neighbor_list[q]) & gama
delta_list = list(delta)
# 开始处理未被处理的点
for i in range(len(delta)):
# 放入待处理列表中
Q.append(delta_list[i])
# 将已处理的点移出标记列表
gama = gama - delta
# 本轮中被移除的点就是属于Ck的点
Ck = gama_old - gama
Cklist = list(Ck)
# 依次按照索引放入cluster结果中
for i in range(len(Ck)):
cluster[Cklist[i]] = k
omega_list = omega_list - Ck
return cluster
X1, y1 = datasets.make_circles(n_samples=2000, factor=.6, noise=.02)
X2, y2 = datasets.make_blobs(n_samples=400, n_features=2, centers=[[1.2, 1.2]], cluster_std=[[.1]], random_state=9)
X = np.concatenate((X1, X2))
eps = 0.08
min_Pts = 10
C = DBSCAN(X, eps, min_Pts)
plt.figure()
plt.scatter(X[:, 0], X[:, 1], c=C)
plt.show()```

```from sklearn.cluster import DBSCAN
model = DBSCAN(eps=0.08, min_samples=10, metric='euclidean', algorithm='auto')
"""
eps: 邻域半径
min_samples：对应MinPts
metrics: 邻域内距离计算方法，之前在层次聚类中已经说过，可选有：
欧式距离：“euclidean”
曼哈顿距离：“manhattan”
切比雪夫距离：“chebyshev”
闵可夫斯基距离：“minkowski”
带权重的闵可夫斯基距离：“wminkowski”
标准化欧式距离： “seuclidean”
马氏距离：“mahalanobis”
algorithm：最近邻搜索算法参数，算法一共有三种，
第一种是蛮力实现‘brute’，
第二种是KD树实现‘kd_tree’，
第三种是球树实现‘ball_tree’，
‘auto’则会在上面三种算法中做权衡
leaf_size：最近邻搜索算法参数，为使用KD树或者球树时， 停止建子树的叶子节点数量的阈值
p: 最近邻距离度量参数。只用于闵可夫斯基距离和带权重闵可夫斯基距离中p值的选择，p=1为曼哈顿距离， p=2为欧式距离。
"""
model.fit(X)
plt.figure()
plt.scatter(X[:, 0], X[:, 1], c=model.labels_)
plt.show()```

#### 关于DBSCAN的优缺点：

1、不必指定聚类的类别数量；

2、可以形成任意形状的簇，而K-means只适用于凸数据集；

3、对于异常值不敏感；

1、计算量较大，对于样本数量和维度巨大的样本，计算速度慢，收敛时间长，这时可以采用KD树进行改进；

2、对于eps和MinPts敏感，调参复杂，需要联合调参；

3、 样本集的密度不均匀、聚类间距差相差很大时，聚类质量较差，这时用 DBSCAN 算法一般不适合；

4、样本同采用一组参数聚类，有时不同的簇的密度不一样，有人提出OPTICS聚类算法（有空会把这一算法补上）；

5、由于对噪声不敏感，在一些领域，如异常检测不适用。

https://blog.csdn.net/xyisv/article/details/88918448

https://blog.csdn.net/hansome_hong/article/details/107596543

https://www.cnblogs.com/pinard/p/6217852.html