Related
SOLVED (see below)
On 2D matpotlib scatter plot I can turn on and off points by accessing _offsets property of scatter plot object and setting it's .mask attribute True/False for indexes of those points we want to show/hide like this:
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import random
def TogglePoints(event, plot):
plot._offsets.mask = [ [1, 1], [1, 1], [1, 1], [0, 0], [0, 0], [0, 0] ]
plot.figure.canvas.draw()
x = [random.randint(-10, 10) for n in range(6)]
y = [random.randint(-10, 10) for n in range(6)]
ax = plt.axes()
sc = ax.scatter(x, y, marker='o', s=20, alpha=1)
ax_button = plt.axes([0.81, 0.01, 0.1, 0.05])
button= Button(ax_button, "Toggle")
button.on_clicked(lambda event: TogglePoints(event, sc))
plt.show()
When you click the "Toggle" button on the figure, points with indexes 0, 1, 2 will disappear. You can make them re-appear by setting _offsets.mask back to False and re-drawing plot.
This is what I want to achieve with matplotlib 3D scatter plot.
Using _offsets.mask = [ [1, 1], [1, 1], [1, 1], [0, 0], [0, 0], [0, 0] ] on 3D scatter plot doesn't seem to work.
Actually it alters type of underlying array from MaskedArray to numpy.ndarray for some reason (see: Numpy MaskedArray in matplotlib 3D scatter plot, turns into ndarray when called by PyQt5 button click).
I know that 3D scatter plots have _offsets3d property. However I don't know how I can use it to show/hide points on the plot. Or maybe there's some other way ?
Does anyone know how I can do that ?
Thanks to this post:
Get working alpha value of scatter points in mpl_toolkits.basemap.Basemap
I've found a workaround that serves my purpose.
It concerns setting alpha values of points with set_facecolors().
So the working code now looks like this:
...
import pandas as pd #added
def TogglePointsOFF(event, plot):
for n in range(3): # n = index of point
fc_colors[n, 3] = 0 # 4th value is alpha
plot.set_facecolors(fc_colors)
plot.figure.canvas.draw()
def TogglePointsON(event, plot):
for n in range(3): # n = index of point
fc_colors[n, 3] = 1 # 4th value is alpha
plot.set_facecolors(fc_colors)
plot.figure.canvas.draw()
#I've put it into DataFrame() so you can better see
df = pd.DataFrame()
df['label'] = ["data_"+str(n) for n in range(6)]
df['id'] = [1, 1, 1, 2, 2, 2]
['x'] = [random.randint(-10, 10) for n in range(6)]
['y'] = [random.randint(-10, 10) for n in range(6)]
['z'] = [random.randint(-10, 10) for n in range(6)]
colors = {1:'red', 2:'blue'} # to map colors with df 'id'
#plot points colored according to value of df['id']
ax = plt.axes()
sc = ax.scatter(df['x'], df['y'], df['z'], c=df['id'].map(colors), marker='o', s=20, depthshade=False)
global fc_colors #yeah yeah globals...
face_colors = sc._face_colors
ax_button = plt.axes([0.81, 0.01, 0.1, 0.05])
ax_button_1 = plt.axes([0.68, 0.01, 0.12, 0.05])
button= Button(ax_button, "OFF")
button_1= Button(ax_button_1, "ON")
button.on_clicked(lambda event: TogglePointsOFF(event, sc))
button_1.on_clicked(lambda event: TogglePointsON(event, sc))
plt.show()
Clicking buttons "ON" and "OFF" will hide/show group of points based on index.
I've tried using set_alpha() and passing iterable of alpha values like: [0, 0, 0, 1, 1, 1] however it seemed to work on random points and set alpha of incorrect points.
Also getting face_colors from get_facecolors() seemed to get colors with random index alignment. This may be connected why passing iterable with alpha values to set_alpha() didn't work. That's why I take colors of points from: sc._face_colors .
Thank you for your time.
WARNING! Be advised.
This doesn't work when you use any 'official' colormap like this:
sc = ax.scatter(df['x'], df['y'], df['z'], cmap='tab10, vmin=10, vmax=10, marker='o', s=20, depthshade=False)
For setting alpha of points as described above you have to "kind-off" make you own colormap mapping like it was done here:
c=df['id'].map(colors)
or, use Normalizer object to map any colormap to some custom values like this:
from matplotlib.colors import Normalize #added
#let's assume we have some score values coresponding with data points:
score = [random.uniform(0.101, 100.123) for n in range(6)]
#but we can use any iterable with numbers
norm = Normalize(min(score), max(score)
cmap = matplotlib.cm.get_cmap('Spectral') #get some built in colormap
colors = cmap(norm(score))
#now you can use colors as 'c' parameter:
sc = ax.scatter(df['x'], df['y'], df['z'], c=colors, marker='o', s=20, depthshade=False)
Remember! don't put any alpha parameter and use depthshade=False to prevent fading of points in the back of the plot.
I hope you found this usefull.
Keep scrolling.
A good way to show the concentration of the data points in a plot is using a scatter plot with non-unit transparency. As a result, the areas with more concentration would appear darker.
# this is synthetic example
N = 10000 # a very very large number
x = np.random.normal(0, 1, N)
y = np.random.normal(0, 1, N)
plt.scatter(x, y, marker='.', alpha=0.1) # an area full of dots, darker wherever the number of dots is more
which gives something like this:
Imagine the case we want to emphasize on the outliers. So the situation is almost reversed: A plot in which the less-concentrated areas are bolder. (There might be a trick to apply for my simple example, but imagine a general case where a distribution of points are not known prior, or it's difficult to define a rule for transparency/weight on color.)
I was thinking if there's anything handy same as alpha that is designed for this job specifically. Although other ideas for emphasizing on outliers are also welcomed.
UPDATE: This is what happens when more then one data point is scattered on the same area:
I'm looking for something like the picture below, the more data point, the less transparent the marker.
To answer the question: You can calculate the density of points, normalize it and encode it in the alpha channel of a colormap.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
# this is synthetic example
N = 10000 # a very very large number
x = np.random.normal(0, 1, N)
y = np.random.normal(0, 1, N)
fig, (ax,ax2) = plt.subplots(ncols=2, figsize=(8,5))
ax.scatter(x, y, marker='.', alpha=0.1)
values = np.vstack([x,y])
kernel = stats.gaussian_kde(values)
weights = kernel(values)
weights = weights/weights.max()
cols = plt.cm.Blues([0.8, 0.5])
cols[:,3] = [1., 0.005]
cmap = LinearSegmentedColormap.from_list("", cols)
ax2.scatter(x, y, c=weights, s = 1, marker='.', cmap=cmap)
plt.show()
Left is the original image, right is the image where higher density points have a lower alpha.
Note, however, that this is undesireable, because high density transparent points are undistinguishable from low density. I.e. in the right image it really looks as though you have a hole in the middle of your distribution.
Clearly, a solution with a colormap which does not contain the color of the background is a lot less confusing to the reader.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
# this is synthetic example
N = 10000 # a very very large number
x = np.random.normal(0, 1, N)
y = np.random.normal(0, 1, N)
fig, ax = plt.subplots(figsize=(5,5))
values = np.vstack([x,y])
kernel = stats.gaussian_kde(values)
weights = kernel(values)
weights = weights/weights.max()
ax.scatter(x, y, c = weights, s=9, edgecolor="none", marker='.', cmap="magma")
plt.show()
Here, low density points are still emphazised by darker color, but at the same time it's clear to the viewer that the highest density lies in the middle.
As far as I know, there is no "direct" solution to this quite interesting problem. As a workaround, I propose this solution:
N = 10000 # a very very large number
x = np.random.normal(0, 1, N)
y = np.random.normal(0, 1, N)
fig = plt.figure() # create figure directly to be able to extract the bg color
ax = fig.gca()
ax.scatter(x, y, marker='.') # plot all markers without alpha
bgcolor = ax.get_facecolor() # extract current background color
# plot with alpha, "overwriting" dense points
ax.scatter(x, y, marker='.', color=bgcolor, alpha=0.2)
This will plot all points without transparency and then plot all points again with some transparency, "overwriting" those points with the highest density the most. Setting the alpha value to other higher values will put more emphasis to outliers and vice versa.
Of course the color of the second scatter plot needs to be adjusted to your background color. In my example this is done by extracting the background color and setting it as the new scatter plot's color.
This solution is independent of the kind of distribution. It only depends on the density of the points. However it produces twice the amount of points, thus may take slightly longer to render.
Reproducing the edit in the question, my solution is showing exactly the desired behavior. The leftmost point is a single point and is the darkest, the rightmost is consisting of three points and is the lightest color.
x = [0, 1, 1, 2, 2, 2]
y = [0, 0, 0, 0, 0, 0]
fig = plt.figure() # create figure directly to be able to extract the bg color
ax = fig.gca()
ax.scatter(x, y, marker='.', s=10000) # plot all markers without alpha
bgcolor = ax.get_facecolor() # extract current background color
# plot with alpha, "overwriting" dense points
ax.scatter(x, y, marker='.', color=bgcolor, alpha=0.2, s=10000)
Assuming that the distributions are centered around a specific point (e.g. (0,0) in this case), I would use this:
import numpy as np
import matplotlib.pyplot as plt
N = 500
# 0 mean, 0.2 std
x = np.random.normal(0,0.2,N)
y = np.random.normal(0,0.2,N)
# calculate the distance to (0, 0).
color = np.sqrt((x-0)**2 + (y-0)**2)
plt.scatter(x , y, c=color, cmap='plasma', alpha=0.7)
plt.show()
Results:
I don't know if it helps you, because it's not exactly you asked for, but you can simply color points, which values are bigger than some threshold. For example:
import matplotlib.pyplot as plt
num = 100
threshold = 80
x = np.linspace(0, 100, num=num)
y = np.random.normal(size=num)*45
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(x[np.abs(y) < threshold], y[np.abs(y) < threshold], color="#00FFAA")
ax.scatter(x[np.abs(y) >= threshold], y[np.abs(y) >= threshold], color="#AA00FF")
plt.show()
I am trying to make use the polar plot projection to make a radar chart. I would like to know how to put only one grid line in bold (while the others should remain standard).
For my specific case, I would like to highlight the gridline associated to the ytick "0".
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
#Variables
sespi = pd.read_csv("country_progress.csv")
labels = sespi.country
progress = sespi.progress
angles=np.linspace(0, 2*np.pi, len(labels), endpoint=False)
#Concatenation to close the plots
progress=np.concatenate((progress,[progress[0]]))
angles=np.concatenate((angles,[angles[0]]))
#Polar plot
fig=plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.plot(angles, progress, '.--', linewidth=1, c="g")
#ax.fill(angles, progress, alpha=0.25)
ax.set_thetagrids(angles * 180/np.pi, labels)
ax.set_yticklabels([-200,-150,-100,-50,0,50,100,150,200])
#ax.set_title()
ax.grid(True)
plt.show()
The gridlines of a plot are Line2D objects. Therefore you can't make it bold. What you can do (as shown, in part, in the other answer) is to increase the linewidth and change the colour but rather than plot a new line you can do this to the specified gridline.
You first need to find the index of the y tick labels which you want to change:
y_tick_labels = [-100,-10,0,10]
ind = y_tick_labels.index(0) # find index of value 0
You can then get a list of the gridlines using gridlines = ax.yaxis.get_gridlines(). Then use the index you found previously on this list to change the properties of the correct gridline.
Using the example from the gallery as a basis, a full example is shown below:
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
ax = plt.subplot(111, projection='polar')
ax.set_rmax(2)
ax.set_rticks([0.5, 1, 1.5, 2]) # less radial ticks
ax.set_rlabel_position(-22.5) # get radial labels away from plotted line
ax.grid(True)
y_tick_labels = [-100, -10, 0, 10]
ax.set_yticklabels(y_tick_labels)
ind = y_tick_labels.index(0) # find index of value 0
gridlines = ax.yaxis.get_gridlines()
gridlines[ind].set_color("k")
gridlines[ind].set_linewidth(2.5)
plt.show()
Which gives:
It is just a trick, but I guess you could just plot a circle and change its linewidth and color to whatever could be bold for you.
For example:
import matplotlib.pyplot as plt
import numpy as np
Yline = 0
Npoints = 300
angles = np.linspace(0,360,Npoints)*np.pi/180
line = 0*angles + Yline
ax = plt.subplot(111, projection='polar')
plt.plot(angles, line, color = 'k', linewidth = 3)
plt.ylim([-1,1])
plt.grid(True)
plt.show()
In this piece of code, I plot a line using plt.plot between any point of the two vectors angles and line. The former is actually all the angles between 0 and 2*np.pi. The latter is constant, and equal to the 'height' you want to plot that line Yline.
I suggest you try to decrease and increase Npoints while having a look to the documentaion of np.linspace() in order to understand your problem with the roundness of the circle.
I'm trying to build a scatterplot of a large amount of data from multiple classes in python/matplotlib. Unfortunately, it appears that I have to choose between having my data randomised and having legend labels. Is there a way I can have both (preferably without manually coding the labels?)
Minimum reproducible example:
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
X = np.random.normal(0, 1, [5000, 2])
Y = np.random.normal(0.5, 1, [5000, 2])
data = np.concatenate([X,Y])
classes = np.concatenate([np.repeat('X', X.shape[0]),
np.repeat('Y', Y.shape[0])])
Plotting with randomized points:
plot_idx = np.random.permutation(data.shape[0])
colors = pd.factorize(classes)
fig, ax = plt.subplots()
ax.scatter(data[plot_idx, 0],
data[plot_idx, 1],
c=colors[plot_idx],
label=classes[plot_idx],
alpha=0.4)
plt.legend()
plt.show()
This gives me the wrong legend.
Plotting with the correct legend:
from matplotlib import cm
unique_classes = np.unique(classes)
colors = cm.Set1(np.linspace(0, 1, len(unique_classes)))
for i, class in enumerate(unique_classes):
ax.scatter(data[classes == class, 0],
data[classes == class, 1],
c=colors[i],
label=class,
alpha=0.4)
plt.legend()
plt.show()
But now the points are not randomized and the resulting plot is not representative of the data.
I'm looking for something that would give me a result like I get as follows in R:
library(ggplot2)
X <- matrix(rnorm(10000, 0, 1), ncol=2)
Y <- matrix(rnorm(10000, 0.5, 1), ncol=2)
data <- as.data.frame(rbind(X, Y))
data$classes <- rep(c('X', 'Y'), times=nrow(X))
plot_idx <- sample(nrow(data))
ggplot(data[plot_idx,], aes(x=V1, y=V2, color=classes)) +
geom_point(alpha=0.4, size=3)
You need to create the legend manually. This is not a big problem though. You can loop over the labels and create a legend entry for each. Here one may use a Line2D with a marker similar to the scatter as handle.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
X = np.random.normal(0, 1, [5000, 2])
Y = np.random.normal(0.5, 1, [5000, 2])
data = np.concatenate([X,Y])
classes = np.concatenate([np.repeat('X', X.shape[0]),
np.repeat('Y', Y.shape[0])])
plot_idx = np.random.permutation(data.shape[0])
colors,labels = pd.factorize(classes)
fig, ax = plt.subplots()
sc = ax.scatter(data[plot_idx, 0],
data[plot_idx, 1],
c=colors[plot_idx],
alpha=0.4)
h = lambda c: plt.Line2D([],[],color=c, ls="",marker="o")
plt.legend(handles=[h(sc.cmap(sc.norm(i))) for i in range(len(labels))],
labels=list(labels))
plt.show()
Alternatively you can use a special scatter handler, as shown in the quesiton Why doesn't the color of the points in a scatter plot match the color of the points in the corresponding legend? but that seems a bit overkill here.
It's a bit of a hack, but you can save the axis limits, set the labels by drawing points well outside the limits of the plot, and then resetting the axis limits as follows:
plot_idx = np.random.permutation(data.shape[0])
color_idx, unique_classes = pd.factorize(classes)
colors = cm.Set1(np.linspace(0, 1, len(unique_classes)))
fig, ax = plt.subplots()
ax.scatter(data[plot_idx, 0],
data[plot_idx, 1],
c=colors[color_idx[plot_idx]],
alpha=0.4)
xlim = ax.get_xlim()
ylim = ax.get_ylim()
for i in range(len(unique_classes)):
ax.scatter(xlim[1]*10,
ylim[1]*10,
c=colors[i],
label=unique_classes[i])
ax.set_xlim(xlim)
ax.set_ylim(ylim)
plt.legend()
plt.show()
I have sparse scatter plot to visualize the comparison of predicted vs actual values. The range of the values are 1-4 and there are no decimal points.
I have tried plotly so far with hte following code (but I can also use a matplotlib solution):
my_scatter = go.Scatter(
x = y_actual, y = y_pred, mode = 'markers',
marker = dict(color = 'rgb(240, 189, 89)', opacity=0.5)
)
This prints the graph nicely (see below). I use opacity to see the density at each point. I.e. if two points lie on top of each other, the point will be shown in darker color. However, this is not explanatory enough. Is it possible to add the counts at each point as a label? There are some overlaps at certain intersections. I want to display how many points intersects. Can this be done automatically using matplotlib or plotly?
This answer uses matplotlib.
To answer the initial question first: You need to find out how often the data produces a point at a given coordinate to be able to annotate the points. If all values are integers this can easily be done using a 2d histogram. Out of the hstogram one would then select only those bins where the count value is nonzero and annotate the respective values in a loop:
x = [3, 0, 1, 2, 2, 0, 1, 3, 3, 3, 4, 1, 4, 3, 0]
y = [1, 0, 4, 3, 2, 1, 4, 0, 3, 0, 4, 2, 3, 3, 1]
import matplotlib.pyplot as plt
import numpy as np
x = np.array(x)
y = np.array(y)
hist, xbins,ybins = np.histogram2d(y,x, bins=range(6))
X,Y = np.meshgrid(xbins[:-1], ybins[:-1])
X = X[hist != 0]; Y = Y[hist != 0]
Z = hist[hist != 0]
fig, ax = plt.subplots()
ax.scatter(x,y, s=49, alpha=0.4)
for i in range(len(Z)):
ax.annotate(str(int(Z[i])), xy=(X[i],Y[i]), xytext=(4,0),
textcoords="offset points" )
plt.show()
You may then decide not to plot all points but the result from the histogramming which offers the chance to change the color and size of the scatter points,
ax.scatter(X,Y, s=(Z*20)**1.4, c = Z/Z.max(), cmap="winter_r", alpha=0.4)
Since all values are integers, you may also opt for an image plot,
fig, ax = plt.subplots()
ax.imshow(hist, cmap="PuRd")
for i in range(len(Z)):
ax.annotate(str(int(Z[i])), xy=(X[i],Y[i]), xytext=(0,0), color="w",
ha="center", va="center", textcoords="offset points" )
Without the necesity to calculate the number of occurances, another option is to use a hexbin plot. This gives slightly inaccurate positions of the dots, du to the hexagonal binning, but I still wanted to mention this option.
import matplotlib.pyplot as plt
import matplotlib.colors
import numpy as np
x = np.array(x)
y = np.array(y)
fig, ax = plt.subplots()
cmap = plt.cm.PuRd
cmaplist = [cmap(i) for i in range(cmap.N)]
cmaplist[0] = (1.0,1.0,1.0,1.0)
cmap = matplotlib.colors.LinearSegmentedColormap.from_list('mcm',cmaplist, cmap.N)
ax.hexbin(x,y, gridsize=20, cmap=cmap, linewidth=0 )
plt.show()