





import warnings

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.metrics import pairwise_distances

warnings.filterwarnings("""ignore""")

_SCREAMING_SNAKE_CASE   						=		"""K-MEANS-CLUST/ """


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ,			__a=None ):
	if seed is not None:  # useful for obtaining consistent results
		np.random.seed(__a )
	snake_case_		:      Tuple           =						data.shape[0]  # number of data points

	# Pick K indices from range [0, N).
	snake_case_		:      List[str]           =						np.random.randint(0 ,			__a ,			__a )

	# Keep centroids as dense format, as many entries will be nonzero due to averaging.
	# As long as at least one document in a cluster contains a word,
	# it will carry a nonzero weight in the TF-IDF vector of the centroid.
	snake_case_		:      Tuple           =						data[rand_indices, :]

	return centroids


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ):
	return pairwise_distances(__a ,			__a ,			metric='euclidean' )


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ):
	# Compute distances between each data point and the set of centroids:
	# Fill in the blank (RHS only)
	snake_case_		:      str           =						centroid_pairwise_dist(__a ,			__a )

	# Compute cluster assignments for each data point:
	# Fill in the blank (RHS only)
	snake_case_		:      Union[str, Any]           =						np.argmin(__a ,			axis=1 )

	return cluster_assignment


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ,			__a ):
	snake_case_		:      Tuple           =						[]
	for i in range(__a ):
		# Select all data points that belong to cluster i. Fill in the blank (RHS only)
		snake_case_		:      Optional[Any]           =						data[cluster_assignment == i]
		# Compute the mean of the data points. Fill in the blank (RHS only)
		snake_case_		:      Optional[int]           =						member_data_points.mean(axis=0 )
		new_centroids.append(__a )
	snake_case_		:      Dict           =						np.array(__a )

	return new_centroids


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ,			__a ,			__a ):
	snake_case_		:      Optional[int]           =						0.0
	for i in range(__a ):
		# Select all data points that belong to cluster i. Fill in the blank (RHS only)
		snake_case_		:      Optional[Any]           =						data[cluster_assignment == i, :]

		if member_data_points.shape[0] > 0:  # check if i-th cluster is non-empty
			# Compute distances from centroid to data points (RHS only)
			snake_case_		:      Optional[int]           =						pairwise_distances(
			    __a ,			[centroids[i]] ,			metric='euclidean' )
			snake_case_		:      Dict           =						distances**2
			heterogeneity += np.sum(__a )

	return heterogeneity


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ):
	plt.figure(figsize=(7, 4) )
	plt.plot(__a ,			linewidth=4 )
	plt.xlabel('# Iterations' )
	plt.ylabel('Heterogeneity' )
	plt.title(f"""Heterogeneity of clustering over time, K={k:d}""" )
	plt.rcParams.update({'font.size': 16} )
	plt.show()


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ,			__a ,			__a=5_00 ,			__a=None ,			__a=False ):
	snake_case_		:      Tuple           =						initial_centroids[:]
	snake_case_		:      Optional[int]           =						None

	for itr in range(__a ):
		if verbose:
			print(__a ,			end='' )

		# 1. Make cluster assignments using nearest centroids
		snake_case_		:      Optional[int]           =						assign_clusters(__a ,			__a )

		# 2. Compute a new centroid for each of the k clusters, averaging all data
		#    points assigned to that cluster.
		snake_case_		:      Union[str, Any]           =						revise_centroids(__a ,			__a ,			__a )

		# Check for convergence: if none of the assignments changed, stop
		if (
		    prev_cluster_assignment is not None
		    and (prev_cluster_assignment == cluster_assignment).all()
		):
			break

		# Print number of new assignments
		if prev_cluster_assignment is not None:
			snake_case_		:      Dict           =						np.sum(prev_cluster_assignment != cluster_assignment )
			if verbose:
				print(
				    f"""    {num_changed:5d} elements changed their cluster assignment.""" )

        # Record heterogeneity convergence metric
		if record_heterogeneity is not None:
			# YOUR CODE HERE
			snake_case_		:      Any           =						compute_heterogeneity(__a ,			__a ,			__a ,			__a )
			record_heterogeneity.append(__a )

		snake_case_		:      Optional[int]           =						cluster_assignment[:]

	return centroids, cluster_assignment


# Mock test below
if False:  # change to true to run this test case.
						from sklearn import datasets as ds

						_SCREAMING_SNAKE_CASE   						=		ds.load_iris()
						_SCREAMING_SNAKE_CASE   						=		3
						_SCREAMING_SNAKE_CASE   						=		[]
						_SCREAMING_SNAKE_CASE   						=		get_initial_centroids(dataset["""data"""], k, seed=0)
						_SCREAMING_SNAKE_CASE						,				_SCREAMING_SNAKE_CASE   						=		kmeans(
						    dataset["""data"""],
						    k,
						    initial_centroids,
						    maxiter=4_00,
						    record_heterogeneity=heterogeneity,
						    verbose=True,
						)
						plot_heterogeneity(heterogeneity, k)


def 				SCREAMING_SNAKE_CASE__    (     __a ,			__a ,			__a=None ):
	# Fill missing values with given rules
	if fill_missing_report:
		snake_case_		:      Any           =						df.fillna(value=__a )
	snake_case_		:      str           =						1
	snake_case_		:      int           =						df.select_dtypes(np.number ).columns
	snake_case_		:      Tuple           =						(
	    df.groupby(['Cluster'] )[  # construct report dataframe
	        numeric_cols
	    ]  # group by cluster number
	    .agg(
	        [
	            ('sum', np.sum),
	            ('mean_with_zeros', lambda __a : np.mean(np.nan_to_num(__a ) )),
	            ('mean_without_zeros', lambda __a : x.replace(0 ,			np.NaN ).mean()),
	            (
	                'mean_25-75',
	                lambda __a : np.mean(
	                    np.nan_to_num(
	                        sorted(__a )[
	                            round(len(__a ) * 25 / 1_00 ) : round(len(__a ) * 75 / 1_00 )
	                        ] ) ),
	            ),
	            ('mean_with_na', np.mean),
	            ('min', lambda __a : x.min()),
	            ('5%', lambda __a : x.quantile(0.05 )),
	            ('25%', lambda __a : x.quantile(0.25 )),
	            ('50%', lambda __a : x.quantile(0.50 )),
	            ('75%', lambda __a : x.quantile(0.75 )),
	            ('95%', lambda __a : x.quantile(0.95 )),
	            ('max', lambda __a : x.max()),
	            ('count', lambda __a : x.count()),
	            ('stdev', lambda __a : x.std()),
	            ('mode', lambda __a : x.mode()[0]),
	            ('median', lambda __a : x.median()),
	            ('# > 0', lambda __a : (x > 0).sum()),
	        ] )
	    .T.reset_index()
	    .rename(index=__a ,			columns={'level_0': 'Features', 'level_1': 'Type'} )
	)  # rename columns
	# calculate the size of cluster(count of clientID's)
	snake_case_		:      List[str]           =						report[
	    (report['Features'] == 'dummy') & (report['Type'] == 'count')
	].copy()  # avoid SettingWithCopyWarning
	snake_case_		:      List[str]           =						(
	    'ClusterSize'  # rename created cluster df to match report column names
	)
	snake_case_		:      List[str]           =						'# of Customers'
	snake_case_		:      List[Any]           =						pd.DataFrame(
	    clustersize.iloc[:, 2:].values
	    / clustersize.iloc[:, 2:].values.sum() )
	snake_case_		:      Optional[Any]           =						'% of Customers'  # rename created cluster df to match report column names
	snake_case_		:      int           =						'ClusterProportion'
	snake_case_		:      Optional[Any]           =						clusterproportion.columns.tolist()
	snake_case_		:      Optional[Any]           =						cols[-2:] + cols[:-2]
	snake_case_		:      List[Any]           =						clusterproportion[cols]  # rearrange columns to match report
	snake_case_		:      str           =						report.columns
	snake_case_		:      Optional[Any]           =						pd.DataFrame(
	    abs(
	        report[report['Type'] == 'count'].iloc[:, 2:].values
	        - clustersize.iloc[:, 2:].values ) )  # generating df with count of nan values
	snake_case_		:      Any           =						0
	snake_case_		:      str           =						'# of nan'
	snake_case_		:      Any           =						report[
	    report['Type'] == 'count'
	].Features.tolist()  # filling values in order to match report
	snake_case_		:      int           =						a.columns.tolist()
	snake_case_		:      int           =						cols[-2:] + cols[:-2]
	snake_case_		:      Union[str, Any]           =						a[cols]  # rearrange columns to match report
	snake_case_		:      List[str]           =						report.columns  # rename columns to match report
	snake_case_		:      int           =						report.drop(
	    report[report.Type == 'count'].index )  # drop count values except cluster size
	snake_case_		:      Union[str, Any]           =						pd.concat(
	    [report, a, clustersize, clusterproportion] ,			axis=0 )  # concat report with clustert size and nan values
	snake_case_		:      int           =						report['Features'].isin(__a )
	snake_case_		:      str           =						report.columns.tolist()
	snake_case_		:      Any           =						cols[0:2] + cols[-1:] + cols[2:-1]
	snake_case_		:      List[str]           =						report[cols]
	snake_case_		:      Dict           =						{
	    'ClusterSize': 9,
	    'ClusterProportion': 8,
	    'mean_with_zeros': 7,
	    'mean_with_na': 6,
	    'max': 5,
	    '50%': 4,
	    'min': 3,
	    '25%': 2,
	    '75%': 1,
	    '# of nan': 0,
	    '# > 0': -1,
	    'sum_with_na': -2,
	}
	snake_case_		:      str           =						(
	    report.assign(
	        Sortera=lambda __a : x.Type.map(__a ) ,			Sortera=lambda __a : list(reversed(range(len(__a ) ) ) ) ,			)
	    .sort_values(['Sorter1', 'Mark', 'Sorter2'] ,			ascending=__a )
	    .drop(['Sorter1', 'Sorter2'] ,			axis=1 )
	)
	snake_case_		:      int           =						''
	snake_case_		:      int           =						report.reset_index()
	snake_case_		:      str           =						report.drop(columns=['index'] )
	return report


if __name__ == "__main__":
						import doctest

						doctest.testmod()



def 					SCREAMING_SNAKE_CASE  (   __UpperCamelCase			:    Dict				,    __UpperCamelCase			:    Dict							)			->							List[Any]:

				"""simple docstring"""







				if len(str(__UpperCamelCase							)							) == 1 or len(str(__UpperCamelCase							)							) == 1:
								return a * b

				A__	:						List[str]								=				max(len(str(__UpperCamelCase							)							)				,    len(str(__UpperCamelCase							)							)							)
				A__	:						Any								=				ma // 2

				A__							,				A__	:						Union[str, Any]								=				divmod(__UpperCamelCase				,    10**ma							)
				A__							,				A__	:						Tuple								=				divmod(__UpperCamelCase				,    10**ma							)

				A__	:						Optional[int]								=				karatsuba(__UpperCamelCase				,    __UpperCamelCase							)
				A__	:						Tuple								=				karatsuba((aa + aa)				,    (ba + ba)							)
				A__	:						Optional[int]								=				karatsuba(__UpperCamelCase				,    __UpperCamelCase							)

				return (z * 10 ** (2 * ma)) + ((y - z - x) * 10 ** (ma)) + (x)

def 					SCREAMING_SNAKE_CASE  (   )			->							Dict:

				"""simple docstring"""







				print(karatsuba(1_54_63				,    2_34_89							)							)


if __name__ == "__main__":
					main()