机器学习入门,无监督学习之DBSCAN聚类算法完全指南:面向Java开发者的Python实现详解

聚类技术:发展历史、主要分类与重要论文解析 在数据挖掘和机器学习的广袤领域中,聚类作为一种强大的无监督学习技术,一直扮演着至关重要的角色。它能够帮助我们从海量的数据中发现隐藏的模式和结构,将相似的数据对象归为一类,从而为进一步的分析和决策提供有价值的信息。 阅读详情

机器学习入门:无监督学习之DBSCAN聚类算法完全指南

面向Java开发者的Python实现详解


一、DBSCAN算法核心概念

1.1 什么是DBSCAN?

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一种基于密度的聚类算法,与K-Means相比具有独特优势:

  • ✅ 不需要预先指定簇的数量
  • ✅ 可以发现任意形状的簇
  • ✅ 能够自动识别噪声点(异常值)
  • ✅ 对异常值不敏感

1.2 核心概念(Java开发者视角)

DBSCAN概念Java类比通俗解释
核心点社交网络中的"中心人物"周围有很多邻居的点
边界点“普通成员”在核心点附近,但自己朋友不多
噪声点“独行侠”周围没什么朋友,也不在任何核心点附近
ε-邻域“社交距离半径”判断是否为朋友的临界距离
最小样本数“最小朋友圈规模”成为核心人物需要的最少朋友数

二、DBSCAN算法原理详解

2.1 算法核心思想

DBSCAN基于一个简单而强大的思想:一个簇是由密度相连的点的最大集合组成。

2.2 算法步骤

// Java风格算法伪代码
public class DBSCAN {
    public List<Cluster> cluster(Point[] points, double eps, int minPts) {
        List<Cluster> clusters = new ArrayList<>();
        boolean[] visited = new boolean[points.length];
        int clusterId = 0;
        
        for (int i = 0; i < points.length; i++) {
            if (!visited[i]) {
                visited[i] = true;
                List<Point> neighbors = findNeighbors(points, i, eps);
                
                if (neighbors.size() >= minPts) {
                    // 创建新簇
                    Cluster cluster = new Cluster(++clusterId);
                    expandCluster(points, i, neighbors, cluster, visited, eps, minPts);
                    clusters.add(cluster);
                } else {
                    // 标记为噪声
                    points[i].setClusterId(-1);
                }
            }
        }
        return clusters;
    }
}

2.3 关键定义

  1. ε-邻域:以点p为中心,半径为ε的圆形区域
  2. 核心点:ε-邻域内至少包含min_samples个点
  3. 直接密度可达:点q在点p的ε-邻域内,且p是核心点
  4. 密度可达:存在一条密度相连的路径
  5. 密度相连:存在一个核心点o,使p和q都从o密度可达

三、完整Python代码实现

3.1 环境准备和导入库

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_blobs
from sklearn.preprocessing import StandardScaler
from collections import deque
import warnings
warnings.filterwarnings('ignore')

# Python库说明(Java开发者参考):
# numpy:数值计算库,类似Java的Arrays + Math组合
# matplotlib:绘图库,类似Java的JFreeChart
# sklearn:机器学习库,类似Java的Weka
# deque:双端队列,类似Java的LinkedList

3.2 自定义DBSCAN实现

class CustomDBSCAN:
    """
    自定义DBSCAN实现
    相当于Java类:public class CustomDBSCAN { ... }
    """
    
    def __init__(self, eps=0.5, min_samples=5):
        """
        构造函数
        :param eps: ε半径,相当于Java的double eps
        :param min_samples: 最小样本数,相当于Java的int minSamples
        """
        self.eps = eps
        self.min_samples = min_samples
        self.labels_ = None  # 聚类结果标签
    
    def fit_predict(self, X):
        """
        训练模型并返回聚类标签
        :param X: 输入数据,形状(n_samples, n_features)
        :return: 标签数组,-1表示噪声点
        
        Java等价方法:
        public int[] fitPredict(double[][] X) { ... }
        """
        n_samples = X.shape[0]  # 样本数量,相当于Java的X.length
        
        # 初始化标签数组,所有点标记为未分类(-1)
        # np.full 相当于Java的Arrays.fill(labels, -1)
        labels = np.full(n_samples, -1)
        
        # 访问标记数组,避免重复处理
        # 相当于Java的boolean[] visited = new boolean[n_samples]
        visited = np.zeros(n_samples, dtype=bool)
        
        cluster_id = 0  # 簇ID计数器
        
        # 遍历所有数据点
        for i in range(n_samples):
            if not visited[i]:
                visited[i] = True  # 标记为已访问
                
                # 找到当前点的ε-邻域邻居
                neighbors = self._find_neighbors(X, i)
                
                if len(neighbors) < self.min_samples:
                    # 邻居数量不足,标记为噪声点
                    labels[i] = -1
                else:
                    # 发现新簇
                    cluster_id += 1
                    labels[i] = cluster_id
                    
                    # 使用队列扩展簇(广度优先搜索)
                    # deque 相当于Java的Queue<Integer> queue = new LinkedList<>()
                    queue = deque(neighbors)
                    
                    while queue:
                        # 取出队列中的下一个点
                        j = queue.popleft()  # 相当于Java的queue.poll()
                        
                        if not visited[j]:
                            visited[j] = True
                            
                            # 找到j点的邻居
                            j_neighbors = self._find_neighbors(X, j)
                            
                            if len(j_neighbors) >= self.min_samples:
                                # j也是核心点,将其邻居加入队列
                                # 使用集合操作避免重复添加
                                new_neighbors = set(j_neighbors) - set(queue)
                                queue.extend(new_neighbors)
                        
                        # 如果j点还没有被分配到任何簇,分配到当前簇
                        if labels[j] == -1:
                            labels[j] = cluster_id
        
        self.labels_ = labels
        return labels
    
    def _find_neighbors(self, X, point_index):
        """
        找到指定点的ε-邻域内的所有邻居
        :param X: 数据集
        :param point_index: 目标点索引
        :return: 邻居索引列表
        
        Java等价方法:
        private List<Integer> findNeighbors(double[][] X, int pointIndex) { ... }
        """
        # 计算目标点到所有其他点的欧氏距离
        # 这里使用了NumPy的广播机制,避免显式循环
        # 相当于Java中的向量化计算
        distances = np.sqrt(np.sum((X - X[point_index])**2, axis=1))
        
        # 找到距离小于等于eps的所有点
        neighbors = np.where(distances <= self.eps)[0]
        
        return neighbors.tolist()  # 转换为Python列表

3.3 数据生成和工具函数

def generate_sample_data():
    """
    生成示例数据集,包含不同形状的簇和噪声
    返回标准化后的数据
    
    Java等价方法:
    public double[][] generateSampleData() { ... }
    """
    # 生成半月形数据(非凸形状)
    # 相当于:创建两个半月形分布的点集
    X1, _ = make_moons(n_samples=300, noise=0.1, random_state=42)
    
    # 生成球形数据(凸形状)
    X2, _ = make_blobs(n_samples=100, centers=1, cluster_std=0.1, random_state=42)
    X2 = X2 + [2.5, 0]  # 平移第二组数据
    
    # 添加一些随机噪声点
    # 相当于:在数据空间中随机撒点
    noise = np.random.uniform(-2, 2, (50, 2))
    
    # 合并所有数据
    # np.vstack 相当于Java中的数组合并
    X = np.vstack([X1, X2, noise])
    
    # 数据标准化(重要:DBSCAN对数据尺度敏感)
    # 相当于Java中的:(value - mean) / std
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)
    
    return X_scaled

def plot_clustering_results(X, labels, title, algorithm_name):
    """
    可视化聚类结果
    :param X: 数据点
    :param labels: 聚类标签
    :param title: 图表标题
    :param algorithm_name: 算法名称
    
    Java等价方法:
    public void plotClusteringResults(double[][] X, int[] labels, String title) { ... }
    """
    plt.figure(figsize=(12, 5))
    
    # 子图1:聚类结果
    plt.subplot(1, 2, 1)
    
    # 获取唯一的簇标签
    unique_labels = set(labels)
    
    # 为每个簇分配颜色
    colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]
    
    for k, col in zip(unique_labels, colors):
        if k == -1:
            # 噪声点用黑色显示
            col = [0, 0, 0, 1]  # RGBA颜色:黑色
        
        # 创建布尔掩码选择当前簇的点
        # 相当于Java中的条件筛选
        class_member_mask = (labels == k)
        
        # 提取当前簇的点
        xy = X[class_member_mask]
        
        # 绘制这些点
        plt.plot(xy[:, 0], xy[:, 1], 'o', 
                markerfacecolor=tuple(col),   # 填充颜色
                markeredgecolor='k',          # 边框颜色
                markersize=8 if k != -1 else 6,  # 点大小
                label=f'Cluster {k}' if k != -1 else 'Noise')
    
    plt.title(f'{title}\n{algorithm_name}')
    plt.xlabel('Feature 1 (标准化)')
    plt.ylabel('Feature 2 (标准化)')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    # 子图2:统计信息
    plt.subplot(1, 2, 2)
    
    # 统计簇信息
    cluster_info = {}
    for label in labels:
        if label not in cluster_info:
            cluster_info[label] = 0
        cluster_info[label] += 1
    
    # 准备柱状图数据
    clusters = []
    counts = []
    colors_bar = []
    
    for label, count in cluster_info.items():
        clusters.append(f'Cluster {label}' if label != -1 else 'Noise')
        counts.append(count)
        if label == -1:
            colors_bar.append('black')
        else:
            idx = list(unique_labels).index(label)
            colors_bar.append(plt.cm.Spectral(idx / len(unique_labels)))
    
    # 绘制柱状图
    bars = plt.bar(clusters, counts, color=colors_bar, alpha=0.7)
    plt.title('簇大小分布')
    plt.xlabel('簇标签')
    plt.ylabel('点数')
    plt.xticks(rotation=45)
    
    # 在柱子上添加数值标签
    for bar in bars:
        height = bar.get_height()
        plt.text(bar.get_x() + bar.get_width()/2., height,
                f'{int(height)}', ha='center', va='bottom')
    
    plt.tight_layout()
    plt.show()

def analyze_parameters(X):
    """
    分析DBSCAN参数敏感性
    :param X: 数据集
    
    Java等价方法:
    public void analyzeParameters(double[][] X) { ... }
    """
    print("=== DBSCAN参数敏感性分析 ===")
    
    # 测试不同的参数组合
    eps_values = [0.2, 0.3, 0.4, 0.5]
    min_samples_values = [3, 5, 10]
    
    fig, axes = plt.subplots(len(eps_values), len(min_samples_values), 
                            figsize=(18, 15))
    
    for i, eps in enumerate(eps_values):
        for j, min_samples in enumerate(min_samples_values):
            # 创建DBSCAN实例
            dbscan = CustomDBSCAN(eps=eps, min_samples=min_samples)
            labels = dbscan.fit_predict(X)
            
            ax = axes[i, j]
            
            # 统计结果
            n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
            n_noise = list(labels).count(-1)
            
            # 绘制聚类结果
            unique_labels = set(labels)
            colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]
            
            for k, col in zip(unique_labels, colors):
                if k == -1:
                    col = [0, 0, 0, 1]
                
                class_member_mask = (labels == k)
                xy = X[class_member_mask]
                ax.plot(xy[:, 0], xy[:, 1], 'o', 
                       markerfacecolor=tuple(col),
                       markeredgecolor='k', 
                       markersize=4)
            
            ax.set_title(f'eps={eps}, min_samples={min_samples}\n'
                        f'Clusters: {n_clusters}, Noise: {n_noise}', 
                        fontsize=10)
            ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()

3.4 主演示函数

def main_demo():
    """
    主演示函数:展示DBSCAN的完整工作流程
    
    Java等价方法:
    public static void main(String[] args) { ... }
    """
    print("=" * 60)
    print("DBSCAN聚类算法完整演示")
    print("=" * 60)
    
    # 1. 数据生成
    print("\n1. 生成示例数据...")
    X = generate_sample_data()
    print(f"数据集形状: {X.shape} (样本数 × 特征数)")
    print(f"前5个样本点:\n{X[:5]}")
    
    # 2. 自定义DBSCAN演示
    print("\n2. 使用自定义DBSCAN实现...")
    custom_dbscan = CustomDBSCAN(eps=0.3, min_samples=5)
    custom_labels = custom_dbscan.fit_predict(X)
    
    # 统计结果
    n_clusters_custom = len(set(custom_labels)) - (1 if -1 in custom_labels else 0)
    n_noise_custom = list(custom_labels).count(-1)
    
    print(f"自定义DBSCAN结果:")
    print(f"  - 发现簇数量: {n_clusters_custom}")
    print(f"  - 噪声点数量: {n_noise_custom}")
    print(f"  - 聚类标签分布: {np.unique(custom_labels, return_counts=True)}")
    
    # 可视化自定义DBSCAN结果
    plot_clustering_results(X, custom_labels, "自定义实现", "Custom DBSCAN")
    
    # 3. 与sklearn官方实现对比
    print("\n3. 与sklearn官方实现对比...")
    from sklearn.cluster import DBSCAN
    
    sklearn_dbscan = DBSCAN(eps=0.3, min_samples=5)
    sklearn_labels = sklearn_dbscan.fit_predict(X)
    
    n_clusters_sklearn = len(set(sklearn_labels)) - (1 if -1 in sklearn_labels else 0)
    n_noise_sklearn = list(sklearn_labels).count(-1)
    
    print(f"Sklearn DBSCAN结果:")
    print(f"  - 发现簇数量: {n_clusters_sklearn}")
    print(f"  - 噪声点数量: {n_noise_sklearn}")
    print(f"  - 聚类标签分布: {np.unique(sklearn_labels, return_counts=True)}")
    
    # 可视化sklearn结果
    plot_clustering_results(X, sklearn_labels, "官方库实现", "Sklearn DBSCAN")
    
    # 4. 算法性能比较
    print("\n4. 算法性能比较...")
    
    # 计算轮廓系数(聚类质量指标,-1到1,越大越好)
    from sklearn.metrics import silhouette_score
    
    if n_clusters_custom > 1:  # 轮廓系数需要至少2个簇
        custom_score = silhouette_score(X, custom_labels)
        sklearn_score = silhouette_score(X, sklearn_labels)
        
        print(f"轮廓系数比较:")
        print(f"  - 自定义DBSCAN: {custom_score:.4f}")
        print(f"  - Sklearn DBSCAN: {sklearn_score:.4f}")
    else:
        print("轮廓系数需要至少2个非噪声簇才能计算")
    
    # 5. 参数敏感性分析
    print("\n5. 进行参数敏感性分析...")
    analyze_parameters(X)
    
    # 6. 实用建议
    print("\n6. DBSCAN使用建议:")
    print("   ✅ eps选择: 使用k-距离图找到拐点")
    print("   ✅ min_samples: 从维度+1开始尝试")
    print("   ✅ 数据预处理: 必须进行标准化")
    print("   ✅ 噪声处理: 噪声点可用于异常检测")
    print("   ✅ 适用场景: 任意形状簇、有噪声数据")
    
    return X, custom_labels, sklearn_labels

# 运行主程序
if __name__ == "__main__":
    # 执行演示
    X, custom_labels, sklearn_labels = main_demo()
    
    print("\n" + "=" * 60)
    print("演示完成!")
    print("=" * 60)

四、Python语法详细解释(针对Java开发者)

4.1 关键Python语法对比

# ==================== 类定义 ====================
# Python
class CustomDBSCAN:
    def __init__(self, eps=0.5, min_samples=5):
        self.eps = eps
        self.min_samples = min_samples

# Java等价代码
"""
public class CustomDBSCAN {
    private double eps;
    private int minSamples;
    
    public CustomDBSCAN(double eps, int minSamples) {
        this.eps = eps;
        this.minSamples = minSamples;
    }
}
"""

# ==================== 数组操作 ====================
# Python (NumPy)
labels = np.full(n_samples, -1)
distances = np.sqrt(np.sum((X - X[point_index])**2, axis=1))

# Java等价代码
"""
int[] labels = new int[nSamples];
Arrays.fill(labels, -1);

double[] distances = new double[nSamples];
for (int i = 0; i < nSamples; i++) {
    double sum = 0.0;
    for (int j = 0; j < nFeatures; j++) {
        double diff = X[i][j] - X[pointIndex][j];
        sum += diff * diff;
    }
    distances[i] = Math.sqrt(sum);
}
"""

# ==================== 布尔索引 ====================
# Python
class_member_mask = (labels == k)
xy = X[class_member_mask]

# Java等价代码
"""
List<double[]> xy = new ArrayList<>();
for (int i = 0; i < labels.length; i++) {
    if (labels[i] == k) {
        xy.add(X[i]);
    }
}
"""

# ==================== 集合操作 ====================
# Python
new_neighbors = set(j_neighbors) - set(queue)
queue.extend(new_neighbors)

# Java等价代码
"""
Set<Integer> jNeighborsSet = new HashSet<>(jNeighbors);
Set<Integer> queueSet = new HashSet<>(queue);
jNeighborsSet.removeAll(queueSet);
queue.addAll(jNeighborsSet);
"""

4.2 NumPy广播机制

# Python向量化计算(高效)
distances = np.sqrt(np.sum((X - X[point_index])**2, axis=1))

# 等价的手动循环(低效,但容易理解)
distances_manual = []
for i in range(len(X)):
    distance = 0.0
    for j in range(len(X[i])):
        distance += (X[i][j] - X[point_index][j]) ** 2
    distances_manual.append(np.sqrt(distance))

五、DBSCAN实战指南

5.1 参数选择策略

ε (eps) 选择方法:
def find_optimal_eps(X, k=4):
    """
    使用k-距离图找到最优eps
    :param X: 数据集
    :param k: 最近邻数量(通常取min_samples-1)
    """
    from sklearn.neighbors import NearestNeighbors
    
    # 计算每个点到第k个最近邻的距离
    neighbors = NearestNeighbors(n_neighbors=k)
    neighbors_fit = neighbors.fit(X)
    distances, indices = neighbors_fit.kneighbors(X)
    
    # 按距离排序
    k_distances = np.sort(distances[:, k-1], axis=0)
    
    # 绘制k-距离图
    plt.figure(figsize=(10, 6))
    plt.plot(k_distances)
    plt.xlabel('Points sorted by distance')
    plt.ylabel(f'{k}-th nearest neighbor distance')
    plt.title('K-Distance Graph for EPS selection')
    plt.grid(True, alpha=0.3)
    plt.show()
    
    # 寻找"拐点"(曲率最大的点)
    return k_distances

# 使用示例
# optimal_eps = find_optimal_eps(X, k=4)
min_samples 经验法则:
  • 最小值:min_samples ≥ 数据维度 + 1
  • 常用值:2 × 数据维度
  • 大数据集:适当增大min_samples减少噪声

5.2 与K-Means对比

特性DBSCANK-Means
簇形状任意形状只能发现球形簇
噪声处理✅ 自动识别噪声点❌ 对噪声敏感
簇数量✅ 自动确定❌ 需要预先指定
初始化✅ 不敏感❌ 对初始值敏感
复杂度O(n log n)O(n)
数据分布假设基于密度基于距离和方差

5.3 实际应用场景

场景1:客户细分
# 使用DBSCAN进行客户行为分析
def customer_segmentation(customer_data):
    """
    基于客户行为数据进行细分
    """
    # 1. 数据预处理
    scaler = StandardScaler()
    scaled_data = scaler.fit_transform(customer_data)
    
    # 2. 参数调优
    eps = 0.5  # 通过k-距离图确定
    min_samples = 10  # 根据业务理解调整
    
    # 3. 聚类分析
    dbscan = DBSCAN(eps=eps, min_samples=min_samples)
    segments = dbscan.fit_predict(scaled_data)
    
    return segments
场景2:异常检测
def detect_anomalies(transaction_data):
    """
    使用DBSCAN检测异常交易
    """
    dbscan = DBSCAN(eps=0.3, min_samples=5)
    labels = dbscan.fit_predict(transaction_data)
    
    # 噪声点即为异常点
    anomalies = transaction_data[labels == -1]
    
    return anomalies, labels

六、总结

6.1 核心要点

  1. DBSCAN优势:

    • 不需要预设簇数量
    • 能发现任意形状的簇
    • 对噪声鲁棒
    • 结果可解释性强
  2. 关键参数:

    • eps:邻域半径,决定簇的紧密程度
    • min_samples:核心点所需的最小邻居数
  3. 适用场景:

    • 数据分布未知或非球形
    • 需要自动识别异常值
    • 簇的数量和形状不确定

6.2 下一步学习建议

  1. 进阶主题:

    • HDBSCAN(层次DBSCAN)
    • OPTICS(改进的DBSCAN)
    • 聚类评估指标
  2. 实践项目:

    • 电商用户行为聚类
    • 网络入侵检测
    • 图像分割应用
  3. 相关算法:

    • 高斯混合模型(GMM)
    • 层次聚类
    • 谱聚类

通过本指南,你不仅掌握了DBSCAN算法的原理和实现,还了解了如何在Python中应用它解决实际问题。作为Java开发者,理解Python的向量化操作和科学计算库的使用,将为你在数据科学领域打开新的大门。

记住:理论理解 + 实践编码 = 真正掌握!

附:聚类效果

在这里插入图片描述
在这里插入图片描述

附:python源码

# -*- coding: utf-8 -*-
"""
DBSCAN聚类算法完整实现
面向Java开发者的Python代码详解
"""

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons, make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN as SklearnDBSCAN
from sklearn.metrics import silhouette_score
from collections import deque
import warnings

warnings.filterwarnings('ignore')


class CustomDBSCAN:
    """
    自定义DBSCAN实现
    相当于Java类:public class CustomDBSCAN { ... }
    """

    def __init__(self, eps=0.5, min_samples=5):
        """
        构造函数
        :param eps: ε半径,相当于Java的double eps
        :param min_samples: 最小样本数,相当于Java的int minSamples
        """
        self.eps = eps
        self.min_samples = min_samples
        self.labels_ = None  # 聚类结果标签

    def fit_predict(self, X):
        """
        训练模型并返回聚类标签
        :param X: 输入数据,形状(n_samples, n_features)
        :return: 标签数组,-1表示噪声点
        """
        n_samples = X.shape[0]  # 样本数量,相当于Java的X.length

        # 初始化标签数组,所有点标记为未分类(-1)
        labels = np.full(n_samples, -1)

        # 访问标记数组,避免重复处理
        visited = np.zeros(n_samples, dtype=bool)

        cluster_id = 0  # 簇ID计数器

        # 遍历所有数据点
        for i in range(n_samples):
            if not visited[i]:
                visited[i] = True  # 标记为已访问

                # 找到当前点的ε-邻域邻居
                neighbors = self._find_neighbors(X, i)

                if len(neighbors) < self.min_samples:
                    # 邻居数量不足,标记为噪声点
                    labels[i] = -1
                else:
                    # 发现新簇
                    cluster_id += 1
                    labels[i] = cluster_id

                    # 使用队列扩展簇(广度优先搜索)
                    queue = deque(neighbors)

                    while queue:
                        # 取出队列中的下一个点
                        j = queue.popleft()

                        if not visited[j]:
                            visited[j] = True

                            # 找到j点的邻居
                            j_neighbors = self._find_neighbors(X, j)

                            if len(j_neighbors) >= self.min_samples:
                                # j也是核心点,将其邻居加入队列
                                # 使用集合操作避免重复添加
                                new_neighbors = set(j_neighbors) - set(queue)
                                queue.extend(new_neighbors)

                        # 如果j点还没有被分配到任何簇,分配到当前簇
                        if labels[j] == -1:
                            labels[j] = cluster_id

        self.labels_ = labels
        return labels

    def _find_neighbors(self, X, point_index):
        """
        找到指定点的ε-邻域内的所有邻居
        :param X: 数据集
        :param point_index: 目标点索引
        :return: 邻居索引列表
        """
        # 计算目标点到所有其他点的欧氏距离
        distances = np.sqrt(np.sum((X - X[point_index]) ** 2, axis=1))

        # 找到距离小于等于eps的所有点
        neighbors = np.where(distances <= self.eps)[0]

        return neighbors.tolist()


def generate_sample_data():
    """
    生成示例数据集,包含不同形状的簇和噪声
    返回标准化后的数据
    """
    # 生成半月形数据(非凸形状)
    X1, _ = make_moons(n_samples=300, noise=0.1, random_state=42)

    # 生成球形数据(凸形状)
    X2, _ = make_blobs(n_samples=100, centers=1, cluster_std=0.1, random_state=42)
    X2 = X2 + [2.5, 0]  # 平移第二组数据

    # 添加一些随机噪声点
    noise = np.random.uniform(-2, 2, (50, 2))

    # 合并所有数据
    X = np.vstack([X1, X2, noise])

    # 数据标准化(重要:DBSCAN对数据尺度敏感)
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X)

    return X_scaled


def plot_clustering_results(X, labels, title, algorithm_name):
    """
    可视化聚类结果
    :param X: 数据点
    :param labels: 聚类标签
    :param title: 图表标题
    :param algorithm_name: 算法名称
    """
    plt.rcParams['font.sans-serif'] = ['Hiragino Sans GB']  # 指定默认字体为黑体
    plt.rcParams['axes.unicode_minus'] = False  # 解决负号 '-' 显示为方块的问题

    plt.figure(figsize=(12, 5))

    # 子图1:聚类结果
    plt.subplot(1, 2, 1)

    # 获取唯一的簇标签
    unique_labels = set(labels)

    # 为每个簇分配颜色
    colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]

    for k, col in zip(unique_labels, colors):
        if k == -1:
            # 噪声点用黑色显示
            col = [0, 0, 0, 1]  # RGBA颜色:黑色

        # 创建布尔掩码选择当前簇的点
        class_member_mask = (labels == k)

        # 提取当前簇的点
        xy = X[class_member_mask]

        # 绘制这些点
        plt.plot(xy[:, 0], xy[:, 1], 'o',
                 markerfacecolor=tuple(col),
                 markeredgecolor='k',
                 markersize=8 if k != -1 else 6,
                 label=f'Cluster {k}' if k != -1 else 'Noise')

    plt.title(f'{title}\n{algorithm_name}')
    plt.xlabel('Feature 1 (标准化)')
    plt.ylabel('Feature 2 (标准化)')
    plt.legend()
    plt.grid(True, alpha=0.3)

    # 子图2:统计信息
    plt.subplot(1, 2, 2)

    # 统计簇信息
    cluster_info = {}
    for label in labels:
        if label not in cluster_info:
            cluster_info[label] = 0
        cluster_info[label] += 1

    # 准备柱状图数据
    clusters = []
    counts = []
    colors_bar = []

    for label, count in cluster_info.items():
        clusters.append(f'Cluster {label}' if label != -1 else 'Noise')
        counts.append(count)
        if label == -1:
            colors_bar.append('black')
        else:
            idx = list(unique_labels).index(label)
            colors_bar.append(plt.cm.Spectral(idx / len(unique_labels)))

    # 绘制柱状图
    bars = plt.bar(clusters, counts, color=colors_bar, alpha=0.7)
    plt.title('簇大小分布')
    plt.xlabel('簇标签')
    plt.ylabel('点数')
    plt.xticks(rotation=45)

    # 在柱子上添加数值标签
    for bar in bars:
        height = bar.get_height()
        plt.text(bar.get_x() + bar.get_width() / 2., height,
                 f'{int(height)}', ha='center', va='bottom')

    plt.tight_layout()
    plt.show()


def analyze_parameters(X):
    """
    分析DBSCAN参数敏感性
    :param X: 数据集
    """
    print("=== DBSCAN参数敏感性分析 ===")

    # 测试不同的参数组合
    eps_values = [0.2, 0.3, 0.4, 0.5]
    min_samples_values = [3, 5, 10]

    fig, axes = plt.subplots(len(eps_values), len(min_samples_values),
                             figsize=(18, 15))

    for i, eps in enumerate(eps_values):
        for j, min_samples in enumerate(min_samples_values):
            # 创建DBSCAN实例
            dbscan = CustomDBSCAN(eps=eps, min_samples=min_samples)
            labels = dbscan.fit_predict(X)

            ax = axes[i, j]

            # 统计结果
            n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
            n_noise = list(labels).count(-1)

            # 绘制聚类结果
            unique_labels = set(labels)
            colors = [plt.cm.Spectral(each) for each in np.linspace(0, 1, len(unique_labels))]

            for k, col in zip(unique_labels, colors):
                if k == -1:
                    col = [0, 0, 0, 1]

                class_member_mask = (labels == k)
                xy = X[class_member_mask]
                ax.plot(xy[:, 0], xy[:, 1], 'o',
                        markerfacecolor=tuple(col),
                        markeredgecolor='k',
                        markersize=4)

            ax.set_title(f'eps={eps}, min_samples={min_samples}\n'
                         f'Clusters: {n_clusters}, Noise: {n_noise}',
                         fontsize=10)
            ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()


def find_optimal_eps(X, k=4):
    """
    使用k-距离图找到最优eps
    :param X: 数据集
    :param k: 最近邻数量(通常取min_samples-1)
    """
    from sklearn.neighbors import NearestNeighbors

    # 计算每个点到第k个最近邻的距离
    neighbors = NearestNeighbors(n_neighbors=k)
    neighbors_fit = neighbors.fit(X)
    distances, indices = neighbors_fit.kneighbors(X)

    # 按距离排序
    k_distances = np.sort(distances[:, k - 1], axis=0)

    # 绘制k-距离图
    plt.figure(figsize=(10, 6))
    plt.plot(k_distances)
    plt.xlabel('Points sorted by distance')
    plt.ylabel(f'{k}-th nearest neighbor distance')
    plt.title('K-Distance Graph for EPS selection')
    plt.grid(True, alpha=0.3)
    plt.show()

    # 寻找"拐点"(曲率最大的点)
    return k_distances


def main_demo():
    """
    主演示函数:展示DBSCAN的完整工作流程
    """
    print("=" * 60)
    print("DBSCAN聚类算法完整演示")
    print("=" * 60)

    # 1. 数据生成
    print("\n1. 生成示例数据...")
    X = generate_sample_data()
    print(f"数据集形状: {X.shape} (样本数 × 特征数)")
    print(f"前5个样本点:\n{X[:5]}")

    # 2. 使用k-距离图寻找最优eps
    print("\n2. 使用k-距离图寻找最优eps...")
    k_distances = find_optimal_eps(X, k=4)
    # 通常选择拐点处的eps值,这里我们手动选择0.3

    # 3. 自定义DBSCAN演示
    print("\n3. 使用自定义DBSCAN实现...")
    custom_dbscan = CustomDBSCAN(eps=0.3, min_samples=5)
    custom_labels = custom_dbscan.fit_predict(X)

    # 统计结果
    n_clusters_custom = len(set(custom_labels)) - (1 if -1 in custom_labels else 0)
    n_noise_custom = list(custom_labels).count(-1)

    print(f"自定义DBSCAN结果:")
    print(f"  - 发现簇数量: {n_clusters_custom}")
    print(f"  - 噪声点数量: {n_noise_custom}")
    print(f"  - 聚类标签分布: {np.unique(custom_labels, return_counts=True)}")

    # 可视化自定义DBSCAN结果
    plot_clustering_results(X, custom_labels, "自定义实现", "Custom DBSCAN")

    # 4. 与sklearn官方实现对比
    print("\n4. 与sklearn官方实现对比...")
    sklearn_dbscan = SklearnDBSCAN(eps=0.3, min_samples=5)
    sklearn_labels = sklearn_dbscan.fit_predict(X)

    n_clusters_sklearn = len(set(sklearn_labels)) - (1 if -1 in sklearn_labels else 0)
    n_noise_sklearn = list(sklearn_labels).count(-1)

    print(f"Sklearn DBSCAN结果:")
    print(f"  - 发现簇数量: {n_clusters_sklearn}")
    print(f"  - 噪声点数量: {n_noise_sklearn}")
    print(f"  - 聚类标签分布: {np.unique(sklearn_labels, return_counts=True)}")

    # 可视化sklearn结果
    plot_clustering_results(X, sklearn_labels, "官方库实现", "Sklearn DBSCAN")

    # 5. 算法性能比较
    print("\n5. 算法性能比较...")

    # 计算轮廓系数(聚类质量指标,-1到1,越大越好)
    if n_clusters_custom > 1:  # 轮廓系数需要至少2个簇
        custom_score = silhouette_score(X, custom_labels)
        sklearn_score = silhouette_score(X, sklearn_labels)

        print(f"轮廓系数比较:")
        print(f"  - 自定义DBSCAN: {custom_score:.4f}")
        print(f"  - Sklearn DBSCAN: {sklearn_score:.4f}")
    else:
        print("轮廓系数需要至少2个非噪声簇才能计算")

    # 6. 参数敏感性分析
    print("\n6. 进行参数敏感性分析...")
    analyze_parameters(X)

    # 7. 实用建议
    print("\n7. DBSCAN使用建议:")
    print("   ✅ eps选择: 使用k-距离图找到拐点")
    print("   ✅ min_samples: 从维度+1开始尝试")
    print("   ✅ 数据预处理: 必须进行标准化")
    print("   ✅ 噪声处理: 噪声点可用于异常检测")
    print("   ✅ 适用场景: 任意形状簇、有噪声数据")

    return X, custom_labels, sklearn_labels


# 运行主程序
if __name__ == "__main__":
    # 执行演示
    X, custom_labels, sklearn_labels = main_demo()

    print("\n" + "=" * 60)
    print("演示完成!")
    print("=" * 60)
```
聚类算法实战指南:K-Means与DBSCAN选型、调优与业务落地 聚类是无监督学习的核心技术,旨在从无标签数据中发现内在结构,其本质是基于距离或密度的模式识别。理解K-Means的球状假设与DBSCAN的密度拓扑思想,是避免模型失效的关键前提;而特征标准化、业务可解释性验证和噪声鲁棒性处理,则直接决定聚类结果能否支撑真实决策。该技术广泛应用于用户分群、异常检测、地理热点分析及产品需求洞察等场景。本文聚焦聚类落地中的典型痛点——如K值选择玄学、DBSCAN参数难调、结果不可解释、新数据无法预测等,提供融合数学原理与工程经验的系统化解决方案。 阅读详情

相关推荐

Python实现经典机器学习算法(附代码+原理介绍)

本专栏内包含基于原生Python从零实现经典机器学习算法,通过自复现帮助新手小白对算法有更深刻的认识,理论与实践相结合,每一篇文章都附带有完整的代码+原理讲解。

优快云 精品推荐 1万+

【机器学习】利用numpy实现DBSCAN聚类算法(Python代码)

  简 介:下面是我在学习时候的记录并加上自己的理解。本文意在记录自己近期学习过程中的所学所得,如有错误,欢迎大家指正。   关键词:Python、机器学习、DBSCAN """ * Created with PyCharm * 作者: 阿光 * 日期: 2021/7/18 * 时间: 21:53 * 描述: 使用numpy实现DBSCAN """ import warnings import numpy as np import pandas as pd from..

优快云 精品推荐 1777

Mathcad在LLC谐振变换器设计中的公式推导实战

本文通过Mathcad软件,详细演示了LLC谐振变换器设计的核心公式推导实战。文章阐述了如何利用Mathcad作为“会思考的草稿纸”,从定义基本参数、应用基波近似法(FHA)模型,到逐步推导电压增益公式及确定工作边界频率(fmin, fmax),并展示了如何通过动态参数扫描进行优化设计,帮助电源工程师高效、直观地完成理论计算,为后续仿真与实现奠定坚实基础。

q1w3e5r7t9y的博客 299

人工智能系列 之机器学习DBSCAN聚类算法

1 介绍 DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一个出现得比较早(1996年),比较有代表性的基于密度的聚类算法。DBSCAN能够将足够高密度的区域划分成簇,并能在具有噪声的空间数据库中发现任意形状的簇。聚类的时候不需要预先指定簇的个数,最终簇的个数不确定。 DBScan需要二个参数: 扫描半径 (Eps)和最小包含点数(MinPts)。 DBSCAN算法将数据点分为三类: (1)、核心点:在半径Eps内含有超

langxiaolin的博客 2481

【Origin+Python】使用External Python批量出图代码参考2

不使用part2 的代码,自己打开一个origin2022的界面。这里主要介绍一些进阶代码以及使用盗版origin要如何。3 批量绘制相同设置的曲线图。然后就是没有水印的图片了。

zeng001201的博客 1908

Proteus实现电子秤仿真与称重检测显示

Proteus实现电子秤仿真与称重检测显示测量原理(或设计方案)原理图代码 测量原理(或设计方案) 数字电子秤总体框图如图所示: 金属箔式应变片工作原理 电阻丝在外力作用下发生机械变形时,其电阻值发生变化,这就是电阻应变效应,描述 电阻应变效应的关系式 金属箔式应变片就是通过光刻、腐蚀等工艺制成的应变敏感元件。如图所示,将四个金属箔应变片(R1、R2、R3、R4)分别贴在双杆式悬臂梁弹性体的上下两侧,弹性体受到压力发生形变,应变片随悬臂梁形变被拉伸或被压缩 测量电路 通过应变片转换悬臂梁被测部位受力

Matataya的博客 1万+

【机器学习项目实战10例】(八):基于KMeans、DBSCAN新闻聚类分群

这里我们发现分类出的标签有问题,全部都是-1,说明,全部都是离散点,因为词频处理后,特征过多,导致样本点之间距离较远,从而产生离群点,因此对于新闻文本而言,KMeans算法的聚类效果很好,而DBSCAN的效果较差。他的原理是将words中的每个词去重以及出去无意义的词,进行编号,然后对应每个原文本计数该次出现的次数。这也说明了对于特征较多的数据,KMeans算法的聚类效果要好于DBSCAN算法的聚类效果。当文本出现重复值时,可以将原词频矩阵的数据进行余弦相似度处理,处理后的特征矩阵再带入模型。

优快云 精品推荐 2765

Unity与IOS交互,调用IOS系统相机和相册

与IOS的交互,在步骤上要比android简单许多,不会产生因为系统版本,或者包名导致的许多问题。过程中最头疼的一个问题是IOS回调Unity,很简单的一个问题,搞了半天。

Hello Mingo 1万+

无监督学习与生成式人工智能(MEAP)(一)

世界上只有模式,模式之上的模式,影响其他模式的模式。被模式隐藏的模式。模式中的模式。” - 查克·帕拉尼克我们喜欢模式。无论是我们的业务还是我们的生活,我们都会发现模式,并且(通常)倾向于坚持它们。我们有自己喜欢购买的杂货,使用的电信运营商和通话套餐,关注的新闻文章,电影类型和音轨 - 这些都是我们喜好模式的例子。我们热爱模式,而且比模式更喜欢的是发现它们,整理它们,也许习惯于它们!有一句俗语流传开来 - “数据是新的电力”。数据确实宝贵;这一点无人能否认。但纯粹的数据将毫无用处。

龙哥盟 1510

CANoe连接Option Scope使用方法

本文档主要介绍如何使用CANoe Option .Scope捕获CAN总线上的物理波形,并利用眼图进行分析。

测试领域技术分享 1357

深入浅出:使用Python爬取链家二手房数据

在数据驱动的时代,房地产数据作为社会经济的重要指标,一直是数据分析师、投资者和研究人员关注的焦点。链家作为国内领先的房产中介平台,其二手房数据具有较高的真实性和时效性。然而,官方API的访问限制和数据获取的门槛,让很多初学者望而却步。本文将带你从零开始,使用Python编写一个符合robots.txt规范的链家二手房爬虫,获取标题、户型、价格等信息。文章不仅会提供完整代码,还会深入讲解反爬策略、请求头伪装、数据解析、异常处理等实战技术。目录前言:为什么要写这篇博客?一、爬虫前的准备:法律与道德。

2201_76125261的博客 93

dbscan算法_用户地理位置的聚类算法实现—基于DBSCAN和Kmeans的混合算法

1. 聚类算法简介聚类的目标是使同一类对象的相似度尽可能地大;不同类对象之间的相似度尽可能地小。目前聚类的方法很多,根据基本思想的不同,大致可以将聚类算法分为五大类:层次聚类算法、分割聚类算法、基于约束的聚类算法、机器学习中的聚类算法和用于高维度的聚类算法。以下实现主要选取了基于划分的Kmeans算法和基于密度的DBSCAN算法来处理1.1 基于划分的Kmeans算法一种典型的划分聚类算法,它用一...

weixin_39940901的博客 855

Java炫酷烟花③(完整代码)

Java语言实现炫酷烟花动画的完整代码。 下载代码 下载链接:我是一只有趣的兔子,感谢你的喜欢。

AIGC 784

【机器学习项目实战10例目录】项目详解 + 数据集 + 完整源码

本专栏整理了《机器学习项目实战10例》,内包含了各种不同的入门级机器学习项目,包含项目原理以及源码,每一个项目实例都附带有完整的代码+数据集。

优快云 精品推荐 4万+

48-OpenCVSharp —-Cv2.MatchTemplate() 函数功能(模板匹配)详解

是 OpenCV 中一个常见且简单的模板匹配算法,适用于目标检测和图像识别中的静态场景。它的优点是易于理解和实现,但在面对旋转、尺度变化、复杂背景或光照变化等挑战时,效果可能大打折扣。在这些情况下,结合多尺度图像、特征匹配算法(如 SIFT、SURF、ORB 等)以及深度学习模型可以显著提高匹配的鲁棒性和精度。模板匹配仍然是一个非常基础但强大的工具,适用于一些简单的图像匹配任务。对于更加复杂和动态的图像识别任务,我们通常会使用其他更加复杂的算法或者深度学习模型来处理。

weixin_45590420的博客 4523

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

10种顶流聚类算法实现(附完整代码)

Datawhale干货编辑:Peter,Datawhale成员来自:海豚科学实验室分享一篇关于聚类的文章:10种聚类算法和Python代码。文末提供jupyter notebook的完整代码获取方式。聚类或聚类分析是无监督学习问题。它通常被用作数据分析技术,用于发现数据中的有趣模式,例如基于其行为的客户群。有许多聚类算法可供选择,对于所有情况,没有单一的最佳聚类算法。相反,最好探索一系列聚类算...

Datawhale 799

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Python机器学习入门:从环境搭建到核心算法实战

机器学习作为人工智能的核心技术,通过算法让计算机从数据中学习规律。Python凭借简洁语法和丰富库生态成为机器学习首选语言,其scikit-learn库封装了各类经典算法,TensorFlow/PyTorch则支持深度学习开发。在工程实践中,正确的环境配置(Python 3.8+、虚拟环境隔离)和工具链选择(pandas数据处理、matplotlib可视化)能显著提升开发效率。典型应用场景包括金融风控(逻辑回归)、推荐系统(随机森林)和图像识别(CNN)。通过掌握特征工程技巧和模型评估方法,开发者可以快速实

weixin_29056145的博客 300

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

机器学习之DBSCAN聚类

机器学习之DBSCAN聚类 # -*- coding: utf-8 -*- """ Created on Wed Nov 28 18:50:57 2018 @author: muli """ import numpy as np from sklearn.datasets.samples_generator import make_blobs from sklearn import cl...

muli 584

如何学好机器学习?机器学习一定需要下面这几方面知识!

学好机器学习需要有一定的数学基础和计算机编程基础。总结一下,学机器学习需要下面这几方面知识

pythonhy的博客 1106

我该如何入门Python机器学习?

有了Python基础,。我是在研一的时候开始学习机器学习的。相对于题主来说,我更是一窍不通,Python都没有一点点基础。总结一下我在学习Python的过程,以及自己在学习机器学习过程中的用到的优质资源,也总结一下我的学习心得。

xiangxueerfei的博客 231

如何系统学习机器学习?

而机器学习要做的事情也差不多,在面对一个新问题的时候,机器从已有的历史数据总结出一个规律,套用在新问题上,进而解决这个问题。算法可以理解为一个优化的过程,最终确定最优模型。翻译过来是:“一个电脑程序要完成任务(T),如果电脑获取的关于T的经验(E)越多就表现(P)得越好,那么我们就可以说这个程序‘学习’了关于T的经验。3.训练模型:对经过预处理后的数据进行模型训练,结合自己的目的(分类、回归、聚类等)选择合适的训练方式。,通过找到最好的参数可以让模型的性能提高一大截,有传统的手工调参、网格搜索、随机搜索、

HUANGXIN9898的博客 1448

【异常检测算法‌】基于机器学习方法的异常检测算法‌

基于机器学习方法的异常检测算法的应用领域极为广泛,它在多个行业中发挥着至关重要的作用。

大雨淅淅的博客 2648

机器学习入门实战:从数据预处理到模型部署完整指南

机器学习作为人工智能的核心技术,通过算法让计算机从数据中自动学习规律并进行预测。其基本原理基于数据、算法和模型三要素的协同作用,能够处理传统编程难以解决的复杂模式识别问题。在技术价值层面,机器学习显著提升了数据处理效率和预测准确性,广泛应用于推荐系统、风险预测、医疗诊断等关键场景。特别是在数据预处理和特征工程环节,通过处理缺失值、异常值检测以及特征选择等技术,为模型训练奠定坚实基础。本文聚焦监督学习和无监督学习两大方向,结合线性回归、决策树等经典算法,详细解析机器学习项目的完整实施流程。

weixin_33933118的博客 458

python从入门到进阶整理书籍推荐

附Java/C/C++/机器学习/算法与数据结构/前端/安卓/Python/程序员必读书籍书单大全: 书单导航页(点击右侧 极客侠栈 即可打开个人博客):极客侠栈 ①【Java】学习之路吐血整理技术书从入门到进阶最全50+本(珍藏版): ②【算法数据结构+acm】从入门到进阶吐血整理书单50+本(珍藏版): ③【数据库】从入门到进阶必读18本技术书籍网盘吐血整理网盘(珍藏版): ④【Web前端...

qq_43340946的博客 4962

Cosmos 机器学习入门路线图:从数学基础到动手编码的完整实战指南

本指南基于 Cosmos 开源算法库中 `getting_started_with_ml` 目录下的入门文档,围绕其提出的"四步入门路线图"(数学 → Python → Python 库 → 动手编码)展开,并结合仓库中真实可运行的机器学习算法源码(线性回归、K-Means、朴素贝叶斯、决策树、感知机等)逐层深入。读完本文,你将获得一条清晰、可执行的机器学习自学路径,并能在本地运行、理解多个经典算

gitblog_00749的博客 199

使用Python进行机器学习:基本的技巧和窍门

机器学习入门从未如此简单。 除了结构化的大规模开放在线课程(MOOC),网络上还有大量令人难以置信的免费资源。 这里有一些对我有帮助的。 从YouTube上的一些很棒的视频开始。 阅读几本好书或文章,例如“主算法:对终极学习机的追求将如何重塑我们的世界” 。 而且,我保证您会爱上这个关于机器学习的有趣的交互式页面 。 学习清楚区分流行语,例如机器学习,人工智能,深度学习,数据科学,计算...

cumo7370的博客 335
上一篇: 机器学习入门,无监督学习之K-Means聚类算法完全指南:面向Java开发者的Python实现详解
下一篇: 机器学习入门,微积分之导数概念
明月(Alioo)
博客等级 码龄18年 267粉丝 207原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明月(Alioo)

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值