Clustering is usually the first unsupervised technique anyone reaches for on a new dataset. It groups observations that behave alike, with no labels required. Among the many methods available, K-means clustering has stayed the dependable default. It is simple, fast and effective when the groups are reasonably compact.
K-means partitions observations into K clusters, and you pick K before fitting. A centroid summarizes each one, and it is the arithmetic mean of the observations assigned to it. It is computed from the data and need not land on any single observation. Those centroids give each group a compact profile, which is what makes the output easy to turn into a business or research decision. Remember that the segments are analytical constructs. They depend on the data and on the choices you make, so a different setup can produce different segments. Their labels (A, B and C in the figures below, or 0, 1 and 2 in code) are arbitrary identifiers with no inherent order.
The algorithm: assignment and update
The algorithm works through a short, repeating cycle. Start with K initial centroids. Assign every observation to its nearest one. Then move each centroid to the mean of the observations it just collected. These two steps, assignment and update, repeat until the assignments stop changing. Figure 1 shows the loop in motion. Watch how observations near a boundary switch groups during the assignment step, and how each centroid drifts toward the middle of its cluster during the update step. The figures use two features so the geometry is visible on the page. In practice K-means often works in many dimensions at once.
Animated scatter plot in two feature dimensions. K-means repeats two steps: an assignment step that colors each observation by its nearest centroid, and an update step that moves each centroid to the mean of the observations assigned to it. The side panel reports the current step, the iteration number, and the inertia, meaning the within-cluster sum of squared distances. Inertia never rises from one step to the next. It settles once the assignments stop changing.
The objective K-means minimizes
One quantity drives every pass. Standard K-means minimizes the within-cluster sum of squared Euclidean distances between observations and their centroids, usually reported as inertia. Because the distances are squared, an observation far from its centroid contributes far more than a nearby one. Figure 2 breaks this objective down term by term. Select an observation to reveal its squared distance, or drag a centroid away from the mean to watch the objective climb. The objective never increases during the assignment and update steps, which is why the loop in Figure 1 steadily settles toward a stable solution.
Three clusters, each with its centroid. A line joins every observation to its centroid, and squaring those lengths and adding them up gives the K-means objective. Drag a centroid off the mean of its assigned observations and the objective climbs. It bottoms out when every centroid sits exactly on its cluster mean.
Assumptions and data preparation
Two properties of that objective shape when K-means works well. First, squared Euclidean distance pushes the method toward compact, roughly spherical clusters. Elongated groups give it trouble, as do groups of very different sizes or densities, and anything non-convex. Second, the mean and the squared distance both react to scale and to extreme values. One feature on a large numeric range can dominate the result, and so can a handful of outliers. Standardizing features before clustering keeps each one contributing on comparable terms. Initialization matters as well. Because the algorithm only reaches a locally optimal solution, different starting centroids can lead to different clusters, as the New start control in Figure 1 demonstrates. Running the algorithm from several initializations, or using the K-means++ scheme, makes a poor local solution less likely.
Choosing the number of clusters
K-means takes the number of clusters as an input; K is a modeling choice you make in advance. A common aid is the elbow method, which plots inertia against K. Inertia never rises as K grows, because more centroids can only shrink squared distances. The fitted curve behaves the same way in practice. So you cannot pick K by minimizing it. Instead, you look for the elbow, the point where additional clusters stop meaningfully reducing inertia. Figure 3 lets you vary K and watch both the partition and the elbow curve respond. Treat the elbow as a practical guide. The most useful K also depends on how stable the clusters are across initializations, on domain knowledge, and on how you plan to use the segments. Real datasets do not always contain clean, well-separated groups, so a mathematically tidy solution is not automatically the most meaningful one.
Two linked views of the same dataset. On the left, the observations partitioned for a chosen K. On the right, inertia plotted against K. Watch the right-hand curve drop steeply, then flatten out near K equals 4. That flattening point is the elbow.
A fourth cluster still cuts inertia sharply. A fifth barely moves it. That is the balance point, compact enough to mean something, simple enough to act on.
Read it carefully. The best achievable inertia never increases as K grows, so the smallest inertia always belongs to the largest K. The elbow points to where extra clusters stop paying off, and it is one standard heuristic among several. The most useful K also depends on domain knowledge and how the segments will be used.
Left: the dataset split into four clusters. Right: an elbow plot of inertia against K. Inertia falls as K increases, and the gains shrink sharply after about K = 4. That bend suggests four clusters, and domain knowledge settles the choice.
The rest of the article puts K-means to work on a real dataset. Standardize the features, read the elbow to choose K, fit the model, then interpret the customer segments it produces.
Example: K-means clustering on customer data
K-MeansSegmentation
How K-Means groups nearly 9,000 credit card customers into four behavioral segments, each summarized by a centroid. The data are imputed, standardized, clustered and profiled.
K-Means Segmentation. How K-Means groups nearly 9,000 credit card customers into four behavioral segments, each summarized by a centroid. The data are imputed, standardized, clustered and profiled. Key topics covered: Credit card customer data, Impute missing values, Clusters in PCA space, Standardize features, Fit K-Means, Profile each segment, Cluster heatmap, Choose k with the elbow method.
Import dependencies
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objects as go
from sklearn.experimental import enable_iterative_imputer
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from tableone import TableOne
# Set a global random seed for reproducibility
RANDOM_STATE = 42
# Configure plotting aesthetics
sns.set(style="whitegrid", context="notebook")
plt.rcParams["figure.figsize"] = (10, 6)
plt.rcParams["axes.titlesize"] = 14
plt.rcParams["axes.labelsize"] = 12Load and inspect the data
# Load the dataset
data_path = "CC GENERAL.csv"
df = pd.read_csv(data_path)
# Show first few rows
print("First 5 rows of the dataset:")
display(df.head())
# Basic info about the dataset
print("\nDataset info:")
df.info()
# Descriptive statistics for numerical columns
print("\nDescriptive statistics for numerical variables:")
display(df.describe().T)Descriptive statistics for numerical variables:Descriptive statistics of the raw variables
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| BALANCE | 8950.0 | 1564.47 | 2081.53 | 0.00 | 128.28 | 873.39 | 2054.14 | 19043.14 |
| BALANCE_FREQUENCY | 8950.0 | 0.88 | 0.24 | 0.00 | 0.89 | 1.00 | 1.00 | 1.00 |
| PURCHASES | 8950.0 | 1003.20 | 2136.63 | 0.00 | 39.64 | 361.28 | 1110.13 | 49039.57 |
| ONEOFF_PURCHASES | 8950.0 | 592.44 | 1659.89 | 0.00 | 0.00 | 38.00 | 577.40 | 40761.25 |
| INSTALLMENTS_PURCHASES | 8950.0 | 411.07 | 904.34 | 0.00 | 0.00 | 89.00 | 468.64 | 22500.00 |
| CASH_ADVANCE | 8950.0 | 978.87 | 2097.16 | 0.00 | 0.00 | 0.00 | 1113.82 | 47137.21 |
| PURCHASES_FREQUENCY | 8950.0 | 0.49 | 0.40 | 0.00 | 0.08 | 0.50 | 0.92 | 1.00 |
| ONEOFF_PURCHASES_FREQUENCY | 8950.0 | 0.20 | 0.30 | 0.00 | 0.00 | 0.08 | 0.30 | 1.00 |
| PURCHASES_INSTALLMENTS_FREQUENCY | 8950.0 | 0.36 | 0.40 | 0.00 | 0.00 | 0.17 | 0.75 | 1.00 |
| CASH_ADVANCE_FREQUENCY | 8950.0 | 0.14 | 0.20 | 0.00 | 0.00 | 0.00 | 0.22 | 1.50 |
| CASH_ADVANCE_TRX | 8950.0 | 3.25 | 6.82 | 0.00 | 0.00 | 0.00 | 4.00 | 123.00 |
| PURCHASES_TRX | 8950.0 | 14.71 | 24.86 | 0.00 | 1.00 | 7.00 | 17.00 | 358.00 |
| CREDIT_LIMIT | 8949.0 | 4494.45 | 3638.82 | 50.00 | 1600.00 | 3000.00 | 6500.00 | 30000.00 |
| PAYMENTS | 8950.0 | 1733.14 | 2895.06 | 0.00 | 383.28 | 856.90 | 1901.13 | 50721.48 |
| MINIMUM_PAYMENTS | 8637.0 | 864.21 | 2372.45 | 0.02 | 169.12 | 312.34 | 825.49 | 76406.21 |
| PRC_FULL_PAYMENT | 8950.0 | 0.15 | 0.29 | 0.00 | 0.00 | 0.00 | 0.14 | 1.00 |
| TENURE | 8950.0 | 11.52 | 1.34 | 6.00 | 12.00 | 12.00 | 12.00 | 12.00 |
Data preprocessing
Before clustering, we need to:
- Remove the identifier column (
CUST_ID) from the feature set - Inspect and handle missing values
- Construct a clean numerical feature matrix
Xsuitable for downstream scaling and clustering
We first examine the extent of missingness in each column to understand which variables require imputation. The identifier column (CUST_ID) is excluded from the feature matrix, as it does not carry meaningful information for clustering.
Handling Missing Values
We fill the missing values with a model-based multivariate imputation strategy using IterativeImputer with a BayesianRidge estimator, which is well suited to data where the features are correlated. This approach:
- Models each feature with missing values as a function of the other features.
- Iteratively refines imputations over multiple rounds until convergence.
- Approximates multivariate relationships between variables, which is especially important for clustering.
import pandas as pd
from sklearn.impute import IterativeImputer
from sklearn.linear_model import BayesianRidge
# Keep a copy of the original dataframe with CUST_ID for later interpretation
df_original = df.copy()
# Check missing values per column
print("Missing values per column:")
missing_counts = df.isna().sum().sort_values(ascending=False)
display(missing_counts)
# Drop CUST_ID from the features (identifier, not used for clustering)
if "CUST_ID" in df.columns:
df_features = df.drop(columns=["CUST_ID"])
else:
df_features = df.copy()
# Feature matrix (numerical variables)
X = df_features.copy()
# Model-based multivariate imputer
imputer = IterativeImputer(
estimator=BayesianRidge(),
max_iter=20,
tol=1e-3,
imputation_order="ascending",
random_state=42
)
# Fit and transform
X_imputed = imputer.fit_transform(X)
# Convert back to DataFrame
X_imputed = pd.DataFrame(
X_imputed,
columns=X.columns,
index=X.index
)
print("\nPreview of imputed feature matrix X:")
display(X_imputed.head())
MINIMUM_PAYMENTS 313
CREDIT_LIMIT 1
BALANCE 0
CUST_ID 0
BALANCE_FREQUENCY 0
PURCHASES 0
CASH_ADVANCE 0
PURCHASES_FREQUENCY 0
ONEOFF_PURCHASES 0
INSTALLMENTS_PURCHASES 0
PURCHASES_INSTALLMENTS_FREQUENCY 0
ONEOFF_PURCHASES_FREQUENCY 0
CASH_ADVANCE_TRX 0
CASH_ADVANCE_FREQUENCY 0
PURCHASES_TRX 0
PAYMENTS 0
PRC_FULL_PAYMENT 0
TENURE 0
dtype: int64Feature scaling
K-means is a distance-based algorithm that uses Euclidean distances. Variables on larger scales (e.g., dollars) can dominate others (e.g., frequencies mostly in [0, 1], though CASH_ADVANCE_FREQUENCY reaches 1.50).
Therefore, we:
- Use
StandardScalerto standardize each feature:- Mean = 0
- Standard deviation = 1
This lets all variables contribute more equally to the clustering.
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_imputed)
print("Shape of scaled feature matrix:", X_scaled.shape)Shape of scaled feature matrix: (8950, 17)Selecting the Number of Clusters (Elbow Method)
The elbow method is a common heuristic for choosing the number of clusters k:
- Fit K-means for a range of
kvalues (e.g., 2 to 8) - Record the inertia (within-cluster sum of squares, WCSS) for each
k - Plot
kvs. inertia - Look for a point where the rate of decrease sharply changes, i.e., the โelbowโ
Weโll then choose a reasonable k based on this plot.
# Range of k values to try
k_values = range(2, 9)
inertias = []
for k in k_values:
kmeans = KMeans(
n_clusters=k,
init='k-means++',
random_state=RANDOM_STATE,
n_init=10
)
kmeans.fit(X_scaled)
inertias.append(kmeans.inertia_)
# Plot the elbow curve
plt.figure()
plt.plot(k_values, inertias, marker="o")
plt.xticks(k_values)
plt.xlabel("Number of clusters (k)")
plt.ylabel("Inertia (Within-Cluster Sum of Squares)")
plt.title("Elbow Method for Optimal k")
plt.grid(True)
plt.savefig("elbow_method.png", dpi=300, bbox_inches="tight")
plt.show()Inertia against the number of clusters

From the elbow plot in Figure 4, we inspect where the reduction in inertia starts to flatten out. The elbow for this dataset appears somewhere around 3 to 6 clusters.
For this blog post, we will assume that the elbow occurs at k = 4,
which balances interpretability and granularity.
In practice, you should visually inspect the elbow plot and possibly compare several
values of k based on business interpretability and cluster stability.
# Choose the number of clusters based on visual inspection of the elbow plot
optimal_k = 4 # Chosen as a reasonable elbow point
print(f"Using optimal_k = {optimal_k} clusters for the final K-Means model.")Using optimal_k = 4 clusters for the final K-Means model.Fit Final K-means Model
With optimal_k chosen, we now:
- Fit a final
KMeansmodel on the scaled features - Obtain cluster labels for each customer
- Attach the cluster labels back to the original dataframe (
df_clustered) - Inspect how many customers fall into each cluster
# Fit final KMeans model
kmeans_final = KMeans(
n_clusters=optimal_k,
init='k-means++',
random_state=RANDOM_STATE,
n_init=10
)
kmeans_final.fit(X_scaled)
# Get cluster labels
cluster_labels = kmeans_final.labels_
# Attach cluster labels to the original dataframe
df_clustered = df_original.copy()
df_clustered["cluster"] = cluster_labels
# Show cluster sizes
print("Cluster label counts:")
display(df_clustered["cluster"].value_counts().sort_index())
print("\nPreview of data with cluster labels:")
display(df_clustered.head())Cluster label counts:cluster
0 3981
1 3354
2 1219
3 396
Name: count, dtype: int64Each customer is now assigned to one of the k clusters (0 to k-1).
These clusters represent behavioral customer segments, which we will further
explore using visualization and descriptive statistics.
Visualizing Clusters with PCA
The dataset has many dimensions (17 behavioral features), which makes it hard to visualize clusters directly. To address this, we use:
- Principal Component Analysis (PCA) to reduce the standardized features
to 3 principal components (
PC1,PC2andPC3) - Then plot customers in the PCA space, both as a 2D scatter and as an interactive 3D scene, colored by their cluster
PCA helps capture as much variance as possible in a low-dimensional projection, giving us an approximate but informative visual representation of the clusters.
# Perform PCA to reduce to 3 principal components
pca = PCA(n_components=3, random_state=RANDOM_STATE)
X_pca = pca.fit_transform(X_scaled)
# Create a DataFrame for PCA results
df_pca = pd.DataFrame(
X_pca,
columns=["PC1", "PC2", "PC3"],
index=df_clustered.index
)
df_pca["cluster"] = df_clustered["cluster"]
# 2D scatter plot of the first two principal components colored by cluster
plt.figure()
sns.scatterplot(
data=df_pca,
x="PC1",
y="PC2",
hue="cluster",
palette="tab10",
alpha=0.7
)
plt.title("Customer Segments Visualized in 2D PCA Space")
plt.xlabel("Principal Component 1")
plt.ylabel("Principal Component 2")
plt.legend(title="Cluster")
plt.grid(True)
plt.savefig("pca_2d_clusters.png", dpi=300, bbox_inches="tight")
plt.show()
# 3D interactive scatter plot using the first three principal components
fig = go.Figure(
data=[
go.Scatter3d(
x=df_pca.loc[df_pca["cluster"] == c, "PC1"],
y=df_pca.loc[df_pca["cluster"] == c, "PC2"],
z=df_pca.loc[df_pca["cluster"] == c, "PC3"],
mode="markers",
marker=dict(size=4),
name=f"Cluster {c}",
)
for c in sorted(df_pca["cluster"].unique())
]
)
fig.update_layout(
title="Customer Segments Visualized in 3D PCA Space",
scene=dict(
xaxis_title="PC1",
yaxis_title="PC2",
zaxis_title="PC3",
),
legend_title_text="Cluster",
)
fig.write_html("pca_3d_clusters_nt.html")
fig.show()
# Explained variance information
explained_var = pca.explained_variance_ratio_
print(
f"Explained variance by PC1, PC2, PC3: {explained_var[0]:.2%}, {explained_var[1]:.2%}, {explained_var[2]:.2%}"
)Segments in the first two principal components

Rotating view of the segments in three components
Explained variance by PC1, PC2, PC3: 27.30%, 20.32%, 8.83%Figure 5 shows how customers are distributed in the 2D PCA space. While some overlap between clusters is expected (since we compressed many dimensions into just two), we can often see areas where certain clusters are more concentrated.
This visualization is useful for:
- Gaining intuition about cluster separation
- Communicating the segmentation story visually to non-technical stakeholders
The explained variance ratios give us an idea of how much of the original variation in the data is captured by the first two principal components: 27.30 and 20.32 percent, so under half of the variation the clustering used is on the page and any separation is shown only in part.
Descriptive Statistics by Cluster (Table 1 using tableone)
To understand what makes each cluster unique, we will:
- Use the
tableonepackage to create a Table 1 of descriptive statistics - Stratify the table by
cluster - Include the main behavioral numeric variables (e.g., balances, purchases, cash advances), summarized from the raw pre-imputation values rather than the imputed, standardized matrix the model was fit on
The table helps us attach business meaning to each segment, such as:
- High spenders
- Frequent revolvers (carry a balance)
- Cash-advance heavy users
- Low-activity or dormant customers
Note: If
tableoneis not installed in your environment, you can install it via:
pip install tableone
# Prepare variables for Table 1
# We will include all numeric behavioral variables and stratify by 'cluster'.
# Exclude 'cluster' itself and the identifier 'CUST_ID' from the summarized columns.
# Identify numeric columns
numeric_columns = df_clustered.select_dtypes(include=[np.number]).columns.tolist()
# Remove 'cluster' from the list of summarized variables
if "cluster" in numeric_columns:
numeric_columns.remove("cluster")
columns = numeric_columns
categorical = [] # No categorical variables in this example
# Create the TableOne object, stratified by cluster
table1 = TableOne(
df_clustered,
columns=columns,
categorical=categorical,
groupby="cluster",
label_suffix=True
)
print("Table 1: Descriptive statistics of customer behavior by cluster")
display(table1)Table 1: Descriptive statistics of customer behavior by clusterCustomer behavior by segment
| Grouped by cluster | |||||||
|---|---|---|---|---|---|---|---|
| Missing | Overall | 0 | 1 | 2 | 3 | ||
| n | 8950 | 3981 | 3354 | 1219 | 396 | ||
| BALANCE, mean (SD) | 0 | 1564.5 (2081.5) | 995.7 (1085.2) | 907.1 (1225.4) | 4571.0 (2746.6) | 3594.8 (3366.1) | |
| BALANCE_FREQUENCY, mean (SD) | 0 | 0.9 (0.2) | 0.8 (0.3) | 0.9 (0.2) | 1.0 (0.1) | 1.0 (0.1) | |
| PURCHASES, mean (SD) | 0 | 1003.2 (2136.6) | 271.9 (466.8) | 1253.5 (1055.7) | 489.7 (838.0) | 7815.6 (6028.6) | |
| ONEOFF_PURCHASES, mean (SD) | 0 | 592.4 (1659.9) | 209.8 (447.2) | 603.0 (880.2) | 311.9 (638.9) | 5213.1 (5426.5) | |
| INSTALLMENTS_PURCHASES, mean (SD) | 0 | 411.1 (904.3) | 62.4 (157.8) | 650.8 (635.8) | 177.8 (412.8) | 2604.1 (2760.0) | |
| CASH_ADVANCE, mean (SD) | 0 | 978.9 (2097.2) | 583.6 (908.2) | 214.3 (613.0) | 4465.3 (3599.3) | 696.0 (2015.9) | |
| PURCHASES_FREQUENCY, mean (SD) | 0 | 0.5 (0.4) | 0.2 (0.2) | 0.9 (0.1) | 0.3 (0.4) | 0.9 (0.1) | |
| ONEOFF_PURCHASES_FREQUENCY, mean (SD) | 0 | 0.2 (0.3) | 0.1 (0.1) | 0.3 (0.4) | 0.1 (0.2) | 0.7 (0.3) | |
| PURCHASES_INSTALLMENTS_FREQUENCY, mean (SD) | 0 | 0.4 (0.4) | 0.1 (0.2) | 0.7 (0.3) | 0.2 (0.3) | 0.8 (0.3) | |
| CASH_ADVANCE_FREQUENCY, mean (SD) | 0 | 0.1 (0.2) | 0.1 (0.1) | 0.0 (0.1) | 0.5 (0.2) | 0.1 (0.2) | |
| CASH_ADVANCE_TRX, mean (SD) | 0 | 3.2 (6.8) | 2.1 (2.9) | 0.8 (2.0) | 14.2 (12.2) | 2.2 (6.4) | |
| PURCHASES_TRX, mean (SD) | 0 | 14.7 (24.9) | 3.0 (4.0) | 22.4 (16.2) | 7.5 (13.8) | 90.4 (56.7) | |
| CREDIT_LIMIT, mean (SD) | 1 | 4494.4 (3638.8) | 3270.8 (2649.6) | 4240.5 (3287.5) | 7481.9 (3751.7) | 9747.5 (4821.0) | |
| PAYMENTS, mean (SD) | 0 | 1733.1 (2895.1) | 968.2 (1571.9) | 1351.5 (1298.9) | 3436.1 (4179.7) | 7413.2 (6955.0) | |
| MINIMUM_PAYMENTS, mean (SD) | 313 | 864.2 (2372.4) | 553.6 (1259.3) | 653.2 (1804.2) | 2035.8 (3961.4) | 2000.7 (5148.2) | |
| PRC_FULL_PAYMENT, mean (SD) | 0 | 0.2 (0.3) | 0.1 (0.2) | 0.3 (0.4) | 0.0 (0.1) | 0.3 (0.4) | |
| TENURE, mean (SD) | 0 | 11.5 (1.3) | 11.4 (1.4) | 11.6 (1.2) | 11.4 (1.5) | 11.9 (0.5) | |
Cluster Heatmap of Feature Means
To better understand the behavioral profile of each cluster, we create a heatmap that compares the mean value of each numerical variable across clusters.
To make the patterns more interpretable, values are standardized (z-scored) down each column, across the four cluster means, so that warmer colors represent higher values relative to other clusters.
This visualization helps reveal:
- Which clusters spend more (e.g., PURCHASES, ONEOFF_PURCHASES)
- Which clusters rely on cash advances
- Which clusters maintain higher balances
- Differences in payment behavior (PAYMENTS, PRC_FULL_PAYMENT)
- Overall engagement and activity
The heatmap provides a compact, high-level overview of each segmentโs financial behavior.
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from matplotlib.colors import LinearSegmentedColormap
# Compute cluster-wise means for all numerical behavioral variables
cluster_means = (
df_clustered
.groupby("cluster")[numeric_columns]
.mean()
.round(2)
)
# Standardize each variable across the four cluster means for better visualization
# (z-score transformation down each column)
cluster_means_scaled = (
(cluster_means - cluster_means.mean()) /
cluster_means.std()
)
# Create a custom colormap (white for 0, red for 1, and blue for -1)
cmap = LinearSegmentedColormap.from_list(
'custom_colormap', ['blue', 'white', 'red'], N=256
)
plt.figure(figsize=(14, 8))
sns.heatmap(
cluster_means_scaled,
cmap=cmap,
annot=True,
fmt=".2f",
linewidths=0.5,
cbar_kws={"label": "Standardized Mean Value"}
)
plt.title("Heatmap of Standardized Mean Feature Values by Cluster")
plt.xlabel("Variables")
plt.ylabel("Cluster")
plt.tight_layout()
plt.savefig("cluster_heatmap.png", dpi=250, bbox_inches="tight")
plt.show()Segment profiles against the four-segment average

Table 2 summarizes key behavioral variables by cluster:
- BALANCE and CREDIT_LIMIT help differentiate customers by typical outstanding balance and available credit.
- PURCHASES, ONEOFF_PURCHASES, and INSTALLMENTS_PURCHASES indicate spending style and purchase patterns.
- CASH_ADVANCE and CASH_ADVANCE_TRX highlight segments that rely heavily on cash advances.
- PAYMENTS, MINIMUM_PAYMENTS, and PRC_FULL_PAYMENT describe payment behavior (e.g., transactors vs. revolvers).
- TENURE suggests how long customers have been with the card issuer.
By comparing means and distributions across clusters, you can identify:
- High-spending, high-limit customers with frequent full payments
- Revolvers who maintain higher balances and pay minimum amounts
- Cash-advance users who may be riskier, though a small credit line is not the reason. Their average credit limit of 7481.9 sits well above the 4494.4 overall average, and they carry the highest balances of the four segments
- Low-activity customers with low purchase volume and limited engagement
The four clusters K-means found in the credit-card dataset, profiled on eight behaviors from Table 2. Segment 0, low-activity holders, is 44.5 percent of customers with low purchases and light engagement. Segment 1, everyday transactors, is 37.5 percent with frequent purchases and higher full repayment. Segment 2, cash-advance revolvers, is 13.6 percent with high balances and frequent cash advances. Segment 3, high spenders, is 4.4 percent with very high purchases and payments on the largest credit limits. Each bar shows how a segment compares with the four-segment average for one behavior, standardized so behaviors on different units are comparable.
- Low-activity holdersSegment 0 ยท 44.5% ยท 3,981 customers
Buy infrequently, keep modest balances, and show light overall engagement.
- Everyday transactorsSegment 1 ยท 37.5% ยท 3,354 customers
Frequent purchasers who often pay in full and rarely take cash advances.
- Cash-advance revolversSegment 2 ยท 13.6% ยท 1,219 customers
High balances from frequent cash advances, low purchases, seldom pay in full.
- High spendersSegment 3 ยท 4.4% ยท 396 customers
Very high purchases and payments on the largest credit limits.
Summary
In this notebook, we:
- Performed K-means clustering on a credit card customer dataset
- Used the elbow method to narrow the number of clusters to roughly 3 to 6, then assumed (
k = 4) - Applied PCA to visualize clusters and gain intuition about separation
- Generated a Table 1 using
tableoneto describe each clusterโs behavioral profile
From a marketing and product strategy perspective, this segmentation can support:
- Targeted offers for high-value customers (e.g., premium rewards, higher credit limits)
- Risk management strategies for cash-advance heavy or high-balance customers
- Engagement campaigns for low-activity or dormant segments
- Personalized communication based on spending and payment behavior
This workflow provides a reproducible template for building and interpreting unsupervised segmentation models on customer-level data.
Key Takeaways
K-means takes the number of clusters as an input and partitions the data into whatever K you supply. The elbow here bends somewhere around 3 to 6, and K = 4 is chosen within that range for interpretability. The data leaves the choice open.
Standard K-means minimizes the within-cluster sum of squared distances to the centroids. Inertia never increases across the assignment and update steps, which is why the loop settles, and its best achievable value cannot rise as K grows, which rules out minimizing it to pick K.
The algorithm converges to a local optimum that depends on where the centroids start, which lets different starting points yield different segments. The example runs k-means++ from ten initializations, which makes a poor solution less likely without ruling one out.
An observation far from its centroid contributes disproportionately, and a feature on a wide numeric range crowds out the rest. The example standardizes all 17 features to mean 0 and standard deviation 1, putting each one into the distance on comparable terms. Equal scale is not equal weight, though: PURCHASES is very nearly one-off plus installment purchases (Table 2 gives 592.4 + 411.1 against a mean of 1003.2, so a handful of records aside the columns are near-collinear), and three columns describe cash advances, letting those behaviors enter the distance several times over.
Squared Euclidean distance favors compact, roughly spherical clusters of similar size, and elongated, non-convex or very unequal groups sit further from that shape. PURCHASES is strongly right-skewed, with a median of 361.28, a mean of 1003.20 and a maximum of 49039.57.
K-means emits arbitrary labels 0 to 3 with no order. The profiles come from Table 2, where segment 3 averages 7815.6 in purchases against 271.9 for segment 0. The 2D PCA view holds 27.30 and 20.32 percent of the variance, so it separates only in part.
Data & License
Credit Card Dataset for Clustering by Arjun Bhasin (Kaggle) — the file CC_GENERAL.csv, about 8,950 anonymized card-holder records across 18 fields: a customer identifier and 17 behavioral variables. Source: kaggle.com/datasets/arjunbhasin2013/ccdata.License: CC0 1.0 Universal (Public Domain Dedication) — no rights reserved; credited here as a courtesy. CC0 1.0
© 2025 Philip Sarajlic. All rights reserved for the article’s original text and figures; the third-party data remains under the license shown above.
Code examples in this article are licensed under the Common Public Attribution License Version 1.0 (CPAL-1.0), an OSI-approved copyleft license based on the Mozilla Public License 1.1. Initial Developer: Philip Sarajlic.
Attribution required by CPAL Exhibit B: © 2025 Philip Sarajlic · “Based on code by Philip Sarajlic” · philipsarajlic.com · no graphic image. This attribution must be displayed in Larger Works.
Modifications must be released in Source Code form under CPAL-1.0. Making the code usable by anyone other than you over a network is External Deployment under the license and is treated as distribution, so the Source Code must be made available to those users.
Full text: opensource.org/license/cpal-1-0 (SPDX identifier CPAL-1.0)


















