Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
A latency dashboard reports a mean of 60 milliseconds. Half its requests finish near 30 milliseconds, while the other half wait near 90. No request in that sample finishes near the mean, and a hard cluster assignment would force an in-between request into one story.
Clustering, PCA, and Representation Learning grouped examples by geometric similarity. That helps assign points, but it doesn't describe how likely each group is. A Gaussian mixture model asks how hidden populations could generate the observed values and keeps uncertain membership visible.[1][2]

Describe a population with hidden components
Start with six observed request latencies: [28, 30, 32, 86, 90, 94]. Their single mean is 60 milliseconds. Before naming a model, ask whether that average is a useful description of any request in the sample.
| Request | Latency | Plausible hidden population |
|---|---|---|
| A | 28 ms | Mostly fast |
| B | 30 ms | Mostly fast |
| C | 32 ms | Mostly fast |
| D | 86 ms | Mostly slow |
| E | 90 ms | Mostly slow |
| F | 94 ms | Mostly slow |
A generative model asks how an observed latency might have been produced. One possible story first chooses a component, then samples from that component's probability distribution.
That component is a latent variable: the request log records 28 or 90 milliseconds, not the hidden label that supposedly generated it. The diagram makes the two-stage story explicit:

The two components are useful statistical descriptions, not proof that a specific queue, GPU, or tenant caused a latency. Causal attribution requires measurements such as queue state or device traces outside this mixture.
Compute soft responsibilities
The six values suggest two groups, but the model still needs to decide how much each group explains each request. For components, the mixture density is:
Here is a component weight, its mean, and its variance. The weights sum to one. Once those parameters are fixed, an observation's responsibility for component is:
Responsibilities sum to one across components. A request near both component centers can receive fractional membership instead of a forced hard label.
Use equal weights, centers at 30 and 90 milliseconds, and a common standard deviation of 20. For a 50-millisecond request, which component should receive more responsibility? The next cell computes the normalized values.
1from math import exp, pi, sqrt
2
3def gaussian_density(value: float, mean: float, deviation: float) -> float:
4 exponent = -((value - mean) ** 2) / (2 * deviation**2)
5 return exp(exponent) / (sqrt(2 * pi) * deviation)
6
7latency = 50.0
8weighted_fast = 0.5 * gaussian_density(latency, mean=30.0, deviation=20.0)
9weighted_slow = 0.5 * gaussian_density(latency, mean=90.0, deviation=20.0)
10total = weighted_fast + weighted_slow
11
12fast = weighted_fast / total
13slow = weighted_slow / total
14assert abs(fast + slow - 1.0) < 1e-12
15print(f"fast responsibility: {fast:.3f}")
16print(f"slow responsibility: {slow:.3f}")1fast responsibility: 0.818
2slow responsibility: 0.182The 50-millisecond request is closer to the fast center, so the fast component gets responsibility 0.818 and the slow component gets 0.182. Those values are conditional on the assumed weights, means, and variances.
They don't certify that a real fast queue handled the request. The model only saw latency, not queue identity.
A 50-millisecond request receives fast-component responsibility 0.818. What happens to the remaining 0.182, and does the larger number identify the physical queue that handled the request?
Answer
The remaining 0.182 is assigned to the slow component, so both responsibilities sum to one. The 0.818 value is a probability inside the assumed mixture model; no physical queue identity was observed, so it doesn't establish infrastructure ownership or causation.
Fit the mixture with expectation-maximization
Responsibilities need parameters, but the parameters are exactly what we want to learn. Expectation-maximization (EM) handles that loop by alternating two operations:[2]
- In the E-step, estimate each observation's fractional component membership using the current parameters.
- In the M-step, recompute component weights, means, and variances from those fractional memberships.
The M-step's weighted mean update is:
With exact E-steps and M-steps, each completed EM iteration doesn't decrease the observed-data log likelihood:
The outer sum runs over observed requests. The inner sum adds the possible hidden component assignments for each request. A higher value means the observed latencies fit the stated model better; it doesn't guarantee a global optimum or identify a real infrastructure cause.
The next cell starts with provisional means and runs eight one-dimensional EM iterations. It adds a variance floor, checks the observed-data log likelihood after each update, and prints the fitted means. Before running it, predict whether the two groups will settle near 30 and 90 milliseconds.
1from math import exp, log, pi, sqrt
2
3values = [28.0, 30.0, 32.0, 86.0, 90.0, 94.0]
4means = [25.0, 95.0]
5variances = [100.0, 100.0]
6weights = [0.5, 0.5]
7
8def observed_log_likelihood() -> float:
9 total = 0.0
10 for value in values:
11 density = sum(
12 weight * exp(-((value - mean) ** 2) / (2 * variance))
13 / sqrt(2 * pi * variance)
14 for weight, mean, variance in zip(weights, means, variances)
15 )
16 total += log(density)
17 return total
18
19log_likelihoods = [observed_log_likelihood()]
20
21for _ in range(8):
22 responsibilities = []
23 for value in values:
24 scores = [
25 weight * exp(-((value - mean) ** 2) / (2 * variance))
26 / sqrt(2 * pi * variance)
27 for weight, mean, variance in zip(weights, means, variances)
28 ]
29 responsibilities.append([score / sum(scores) for score in scores])
30
31 for component in range(2):
32 count = sum(row[component] for row in responsibilities)
33 mean = sum(row[component] * value for row, value in zip(responsibilities, values)) / count
34 variance = sum(
35 row[component] * (value - mean) ** 2
36 for row, value in zip(responsibilities, values)
37 ) / count
38 weights[component] = count / len(values)
39 means[component] = mean
40 variances[component] = max(variance, 1.0)
41
42 log_likelihoods.append(observed_log_likelihood())
43
44assert all(
45 current + 1e-12 >= previous
46 for previous, current in zip(log_likelihoods, log_likelihoods[1:])
47)
48print(f"log likelihood: {log_likelihoods[0]:.3f} -> {log_likelihoods[-1]:.3f}")
49for index, (weight, mean) in enumerate(zip(weights, means), start=1):
50 print(f"component {index}: weight={weight:.3f}, mean={mean:.1f} ms")1log likelihood: -24.438 -> -17.694
2component 1: weight=0.500, mean=30.0 ms
3component 2: weight=0.500, mean=90.0 msThe floor prevents a component from collapsing onto one observation with nearly zero variance. Because it changes the optimization problem, the code measures the constrained fit instead of assuming that an unconstrained proof transfers automatically. Larger mixtures should compute densities in log space with a log-sum-exp operation to avoid numerical underflow.
The fitted model's observed-data log likelihood rises from -24.438 to -17.694. What does that improvement establish, and which conclusions remain unsupported?
Answer
The observed latencies receive higher likelihood under the updated two-component model, and the recorded EM iterations didn't decrease that objective. The increase doesn't prove global optimality, establish that two is the correct number of components, or identify either component with a real serving-system cause.
Why is a responsibility of 0.82 different from observing that a request definitely came from the fast queue?
Answer
The responsibility is a conditional probability inside the assumed mixture model. The true queue identity wasn't observed, and the mixture components may not correspond to actual infrastructure causes.
Compare hard clusters with probabilistic models
The responsibility calculation answers a different question from a hard cluster label. Put the two views side by side:
| Property | K-means | Gaussian mixture |
|---|---|---|
| Assignment | Exactly one cluster per observation | Fractional responsibility across components |
| Cluster shape | Distance to a center | Learned mean and covariance structure |
| Uncertainty | Not directly represented | Model-conditional membership probability |
| Common failure | Sensitive to initialization and feature scale | Local optima, collapsing covariance, unsupported component stories |
Maximum likelihood compares how compatible the observations are with candidate parameters. EM can improve the observed-data likelihood, but a better likelihood alone doesn't prove that the number of components is correct or that the labels describe real causes.[1]
That limitation changes how you validate a fit. Run multiple initializations, inspect held-out likelihood, check component sizes, and compare the generated distribution with actual traffic. A component containing one request with near-zero variance is a collapse warning, not a new scientific discovery.
Practice receipt: For this six-point fixture, record each component's weight, fitted mean, variance, effective responsibility total, and observed-data log likelihood after every EM iteration. The expected receipt has weights of
0.500, means near 30 and 90 milliseconds, responsibilities summing to one for every request, and likelihood increasing from -24.438 to -17.694.
Then try several starting means. Flag a collapsed variance, a tiny component, or a lower held-out likelihood instead of hiding the run that produced it. Draw samples from the fitted mixture and compare their latency shape with held-out traffic. That posterior predictive check asks whether the model can reproduce both modes, not only whether it scored the six training values well.
Finally, keep the causal boundary visible: a statistical component doesn't name a real queue until separate infrastructure evidence confirms it.
Create a mixture-fit scorecard for each initialization. Record weights, means, variances, responsibilities, held-out likelihood, and posterior-predictive checks; compare the scorecards and flag collapsed components or a poor traffic match. Keep the selected scorecard with the model handoff, including the evidence that still can't identify an infrastructure cause.