Needed a few tweaks (plt.cm.spectral
is the danged weirdest colormap I've ever dealt with), but it seems to be good now:
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
from sklearn import decomposition
from sklearn import datasets
np.random.seed(5)
centers = [[1, 1], [-1, -1], [1, -1]]
iris = datasets.load_iris()
X = iris.data#the floating point values
y = iris.target#unsigned integers specifying group
fig = plt.figure(1, figsize=(4, 3))
plt.clf()
ax = Axes3D(fig, rect=[0, 0, .95, 1], elev=48, azim=134)
plt.cla()
pca = decomposition.PCA(n_components=3)
pca.fit(X)
X = pca.transform(X)
labelTups = [('Setosa', 0), ('Versicolour', 1), ('Virginica', 2)]
for name, label in labelTups:
ax.text3D(X[y == label, 0].mean(),
X[y == label, 1].mean() + 1.5,
X[y == label, 2].mean(), name,
horizontalalignment='center',
bbox=dict(alpha=.5, edgecolor='w', facecolor='w'))
# Reorder the labels to have colors matching the cluster results
y = np.choose(y, [1, 2, 0]).astype(np.float)
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=y, cmap=plt.cm.spectral, edgecolor='k')
ax.w_xaxis.set_ticklabels([])
ax.w_yaxis.set_ticklabels([])
ax.w_zaxis.set_ticklabels([])
colors = [plt.cm.spectral(np.float(i/2)) for i in [1, 2, 0]]
custom_lines = [Line2D([0], [0], linestyle="none", marker='.', markeredgecolor='k', markerfacecolor=c, markeredgewidth=.1, markersize=20) for c in colors]
ax.legend(custom_lines, [lt[0] for lt in labelTups], loc='right', bbox_to_anchor=(1.7, .5))
plt.show()

Here's a link to an online Jupyter notebook with a live version of the script (requires an account for rerunning, though).
Short explanation
You're trying to add three legend markers for a single plot, which is nonstandard behavior. Thus, you need to manually create the shapes that your legend will display.
Longer explanation
This line of code recreates the colors you used in your plot:
colors = [plt.cm.spectral(np.float(i/2)) for i in [1, 2, 0]]
and then this line of code draws some appropriate-looking dots that we'll eventually display on your legend:
custom_lines = [Line2D([0], [0], linestyle="none", marker='.', markeredgecolor='k', markerfacecolor=c, markeredgewidth=.1, markersize=20) for c in colors]
The first two args are just the (internal) x and y coords of the single dot that will be drawn, linestyle="none"
suppresses the line that Line2D
would normally draw by default, and the rest of the args create and style the dot itself (referred to as a marker
in the terminology of the matplotlib
api).
Finally, this statement actually creates the legend:
ax.legend(custom_lines, [lt[0] for lt in labelTups], loc='right', bbox_to_anchor=(1.7, .5))
The first arg is of course a list of the dots we just drew, and the second arg is a list of the labels (one per dot). The remaining two args tell matplotlib where to draw the actual box containing the legend. The last arg, bbox_to_anchor
, is basically a way to manually fiddle with the positioning of the legend, which I had to do since matplotlib
support for 3D anything is still a little behind the curve. On 2D plots you typically don't need it, and, since matplotlib
usually does a decent job of automatically positioning the legend on 2D plots in the first place, you often don't even need the loc
arg either.
Some colormap weirdness
Don't quite know what was going on with plt.cm.spectral
, but in order to get it to behave, for every value I fed it I had to:
a) first cast the value to float
b) then divide the value by 2
a)
does occur explicitly in the OP's original code, right before they plot. The divide by 2 thing, I don't know where that comes from. Somehow the call to ax.scatter
is implicitly normalizing all of the y values so that the maximum is 1? I guess?