import logging import hdbscan import pandas as pd from sklearn import metrics from ..plot_metrics import draw_graphs logging.getLogger().setLevel(logging.INFO) # TODO! Optimize this somehow. Unfortunately dask-ml does not have HDBSCAN. Neither does using dask parallel backend work well with multiprocessing. # https://github.com/dask/dask-tutorial/issues/80 def get_best_params(X, cluster_size): """ Get best epsilon value for clusters. This determine the nbr of clusters in HDBSCAN algo. """ logging.info("Calculating best parameters for HDBSCAN") A = [] B = [] C = [] D = [] E = [] for i in [1, 5]: # min_samples selection for j in [0.01, 0.05, 0.1, 0.25, 0.5, 0.75]: # cluster_selection_epsilon selection db = hdbscan.HDBSCAN(cluster_selection_epsilon=j, min_samples=i, min_cluster_size=cluster_size, metric='euclidean').fit(X) # core_samples_mask = np.zeros_like(db.labels_, dtype=bool) # core_samples_mask[db.core_sample_indices_] = True labels = db.labels_ n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0) try: s_score = metrics.silhouette_score(X, labels) except Exception as e: logging.warning(f"Cannot calculate silhouette score for algo DBSCAN: {e}") s_score = 0 sum = 0 for t in labels: if t == -1: sum = sum + 1 C.append(sum) A.append(i) B.append(int(n_clusters_)) D.append(s_score) E.append(j) # TODO! automatically pick up best epsilon #logging.info(f"Distance: {A}") results = pd.DataFrame([A, B, C, D, E]).T results.columns = ['min samples parameter', 'Number of clusters', 'Number of outliers', 'silhouette score', 'cluster selection epsilon parameter'] results.sort_values(['silhouette score', 'Number of clusters'], ascending=[False, False], inplace=True) new_min_samples = results['min samples parameter'].iloc[0] # pick the eps for run with highest silhouette score new_epsilon = results['cluster selection epsilon parameter'].iloc[0] # pick the eps for run with highest silhouette score return new_min_samples, new_epsilon def run_algo(training_set, fan_ids, modelling_config, algo_params): X = training_set # this algo has parameter min_cluster_size # in order to calculate this parameter we use n_clusters parameter nr_of_clusters = algo_params['n_clusters'] desired_cluster_size = round(X.shape[0] / nr_of_clusters) if isinstance(fan_ids, pd.Series): fan_ids = fan_ids.to_frame() best_min_samples, best_epsilon = get_best_params(X, desired_cluster_size) hyperparams = algo_params['hyperparams'] hyperparams['core_dist_n_jobs'] = -1 hyperparams['min_cluster_size'] = int(desired_cluster_size) hyperparams['min_samples'] = int(best_min_samples) hyperparams['cluster_selection_epsilon'] = float(best_epsilon) db = hdbscan.HDBSCAN(**hyperparams).fit(X) labels = db.labels_ validity_index = hdbscan.validity.validity_index(X, labels, metric="euclidean") cluster_output = fan_ids.join(pd.DataFrame(labels)).rename(columns={0: 'cluster', '0': 'cluster'}) X = pd.DataFrame(X).join(pd.DataFrame(labels), lsuffix='left_').rename(columns={0: 'cluster', '0': 'cluster'}) # Number of clusters in labels, ignoring noise if present. n_clusters_ = len(set(labels)) - (1 if -1 in labels else 0) n_noise_ = list(labels).count(-1) # TODO! color noise (label = -1) black or osmething distinct # HDBSCAN doesn't have centers graph_locations = draw_graphs(X, labels, modelling_config, n_clusters_, None) """ Gets S_DBW score. According to the following paper S_Dbw performs best across 5 different aspects of clustering when compared against Silhouette, DB, Dunn, Calinski, etc. http://datamining.rutgers.edu/publication/internalmeasures.pdf """ #s_dbw = CDbw(X, labels, metric="euclidean", alg_noise='comb', # intra_dens_inf=False, s=3, multipliers=False) try: s_score = metrics.silhouette_score(X, labels) except Exception as e: logging.warning(f"Cannot calculate silhouette score for algo HDBSCAN: {e}") s_score = 0 # silhouette_score is looked at when choosing best model eval_metrics = {'clusters': n_clusters_, 'noise': n_noise_, 'silhouette_coefficient': s_score, 'validity_index': validity_index #'s_dbw': s_dbw } return db, eval_metrics, hyperparams, cluster_output, graph_locations if __name__ == '__main__': """ Instead of generating df, perhaps load your own dataframe from csv. We need a dataframe with training cols only (preprocessed, and standardscaled) and a fan_id. """ training_set = pd.DataFrame({ 'fan_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 'a': [1, 2, 2, 4, 2, 6, 4, 3, 1, 5, 5, 6, 7, 9, 10, 11, 12, 13, 14], 'b': [2, 3, 3, 3, 3, 7, 8, 9, 2, 3, 4, 5, 3, 9, 10, 11, 12, 13, 14] }) print(training_set) fan_ids = training_set['fan_id'] modelling_config = { 'plot_2d': True, 'plot_3d': True, 'plot_silhouette': True, 'boxplot': True, } algo_params = {'n_clusters': 6, 'hyperparams': {'min_samples': 1, 'cluster_selection_epsilon': 0.05, 'metric': 'euclidean'}} # print(run_algo(X, fan_ids, modelling_config, algo_params)[1]) print(run_algo(training_set, fan_ids, modelling_config, algo_params)[1])