Search code examples
pythonmatplotlibhistogrammatplotlib-3d

Plotting histograms on 3D axes


I am trying to plot a few histograms on a 3d axis using the PolyCollection function, my desired plot looks something like this: (except of course the coloured plots are histograms) enter image description here

For me, my x-values are the distribution of values of a parameter C_l, y-values are the values of l (ranging from 2 to 33) and z is the frequency of each C_l (so the histogram is on the x-z plane, with y specifying the histogram for each value of l). This is the code I have but I can't seem to get it to work:

fig = plt.figure()
ax = fig.gca(projection='3d')
nside=16

'multi_dens_auto_cl_dist is just a 33x1001 matrix of all the C_l values, with the rows denoting each iteration I calculated previously and the columns being the l's)
xs=np.linspace(multi_dens_auto_cl_dist.min(),multi_dens_auto_cl_dist.max(),num=1001)

def cc(arg):
    return mcolors.to_rgba(arg, alpha=0.6)

verts = []
zs = np.arange(2,2*nside+1,1)

for z in zs:
    ys,binvals,_ = plt.hist(multi_dens_auto_cl_dist[:][z],bins=xs)
    ys[0], ys[-1] = 0, 0
    verts.append(list(zip(xs, ys)))

poly = PolyCollection(verts,facecolors=[cc('r'), cc('g'), cc('b'), cc('y')]*4+[cc('r')])
poly.set_alpha(0.7)
ax.add_collection3d(poly, zs=zs, zdir='y')

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_zlabel('Z')


plt.title('Density auto power spectrum distribution')
plt.show()

Solution

  • There are still several unknowns here. For one, it is still unclear what the structure of your dataframe is. Even more problematic, we don't know how you want to create your histograms. Your code creates 1001 bins for 1001 data points. Why? It is also not clear why you try to create polygon shapes when a histogram is a specific type of bar chart. I have tried to keep the script as flexible as possible given these unknowns:

    from matplotlib import pyplot as plt
    import numpy as np
    from cycler import cycler
    import pandas as pd
    
    inputarr = np.loadtxt("data.txt")
    df = pd.DataFrame(inputarr.reshape(1001, 33))
    #determine the number of columns
    ncol = df.shape[1]
    
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(projection="3d")
    
    #since you have so many columns, it is difficult to give them all unique colors
    #but we can define through which colors we cycle
    #you could also create a range of colors along a color map and give each histogram 
    #its unique color, which would then be similar to neighbouring colors
    color_cycler = (cycler(color=["tab:orange", "yellow", "red", "blue", "green"]))
    ax.set_prop_cycle(color_cycler)
    
    #define the yticks, i.e., the column numbers
    yticks = np.arange(ncol)
    
    #just to demonstrate that bins don't have to be evenly spaced, we define normalized bins 
    xbinnorm = [0, 0.1, 0.2, 0.3, 0.5, 1]
    #we adapt the normalized bins to the minimum and maximum of the entire dataframe
    xbins = [df.min().min() + i * (df.max().max()-df.min().min()) for i in xbinnorm]
    
    #calculate now the histogram and plot it for each column
    for ytick in yticks:
        
        #extract the current column from your df by its number
        col =  df.iloc[:, ytick]
        
        #determine the histogram values, here you have to adapt it to your needs
        histvals, edges = np.histogram(col, bins=xbins)
        
        #calculate the center and width of each bar
        #obviously not necessary to do this for each column if you always have the same bins 
        #but if you choose for np.histogram other parameters, the bins may not be the same for each histogram
        xcenter = np.convolve(edges, np.ones(2), "valid")/2
        xwidth = np.diff(edges)
    
        #plot the histogram as a bar for each bin
        ax.bar(left=xcenter, height=histvals, width=xwidth, zs=ytick, zdir="y", alpha=0.666)
    
    ax.set_xlabel("bin")
    ax.set_ylabel("column")
    ax.set_zlabel("value")
    
    #label every other column number
    ax.set_yticks(yticks[::2])
    #label bin edges, obviously only possible if all have the same bins
    ax.set_xticks(xbins)
    
    plt.show()
    

    Sample output:

    enter image description here

    Update
    Given that we actually see in your data a development, a continuous colormap might be more informative (and cause less ophthalmological emergencies). Not much change needed to achieve this.

    from matplotlib import pyplot as plt
    import numpy as np
    import pandas as pd
    
    inputarr = np.loadtxt("data.txt")
    df = pd.DataFrame(inputarr.reshape(1001, 33))
    #determine the number of columns
    ncol = df.shape[1]
    
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(projection="3d")
    
    #define the colormap 
    my_cmap = plt.cm.inferno
    
    #define the yticks, i.e., the column numbers
    yticks = np.arange(ncol)
    
    #just to demonstrate that bins don't have to be evenly spaced, we define normalized bins 
    xbinnorm = [0, 0.1, 0.3, 0.5, 0.8, 1]
    #we adapt the normalized bins to the minimum and maximum of the entire dataframe
    xbins = [df.min().min() + i * (df.max().max()-df.min().min()) for i in xbinnorm]
    
    #calculate now the histogram and plot it for each column
    for i, ytick in enumerate(yticks):
    
        #extract the current column from your df by its number
        col =  df.iloc[:, ytick]
    
        #determine the histogram values, here you have to adapt it to your needs
        histvals, edges = np.histogram(col, bins=xbins)
    
        #calculate the center and width of each bar
        #obviously not necessary to do this for each column if you always have the same bins 
        #but if you choose for np.histogram other parameters, the bins may not be the same for each histogram
        xcenter = np.convolve(edges, np.ones(2), "valid")/2
        xwidth = np.diff(edges)
    
        #plot the histogram as a bar for each bin
        #now with continuous color mapping and edgecolor, so we can better see all bars
        ax.bar(left=xcenter, height=histvals, width=xwidth, zs=ytick, zdir="y", color=my_cmap(1-i/ncol), alpha=0.666, edgecolor="grey")
    
    ax.set_xlabel("bin")
    ax.set_ylabel("column")
    ax.set_zlabel("value")
    
    #label every other column number
    ax.set_yticks(yticks[::2])
    #label bin edges, obviously only possible if all have the same bins
    ax.set_xticks(xbins)
    
    plt.show()
    

    Sample output (with different bins): enter image description here

    This version can also easily adapted to the bins="auto" option in np.histogram by removing everything related to xbins. Sample output with view from the opposite site:

    enter image description here

    Update2

    Given your data structure, you most likely prefer evenly spaced bins. In this case, we don't have to calculate the bar position for each slice individually.

    from matplotlib import pyplot as plt
    import numpy as np
    import pandas as pd
    
    inputarr = np.loadtxt("data.txt")
    df = pd.DataFrame(inputarr.reshape(1001, 33))
    #determine the number of columns
    ncol = df.shape[1]
    
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(projection="3d")
    
    #define the colormap 
    my_cmap = plt.cm.inferno
    
    #define the yticks, i.e., the column numbers
    yticks = np.arange(ncol)
    
    #we create evenly spaced bins between the minimum and maximum of the entire dataframe
    xbins = np.linspace(df.min().min(), df.max().max(), 100)
    #and calculate the center and widths of the bars
    xcenter = np.convolve(xbins, np.ones(2), "valid")/2
    xwidth = np.diff(xbins)
    
    #calculate now the histogram and plot it for each column
    for i, ytick in enumerate(yticks):
    
        #extract the current column from your df by its number
        col =  df.iloc[:, ytick]
    
        #determine the histogram values, here you have to adapt it to your needs
        histvals, _ = np.histogram(col, bins=xbins)
    
        #plot the histogram as a bar for each bin
        #now with continuous color mapping and edgecolor, but thinner lines, so we can better see all bars
        ax.bar(left=xcenter, height=histvals, width=xwidth, zs=ytick, zdir="y", color=my_cmap(i/ncol), alpha=0.666, edgecolor="grey", linewidth=0.3)
    
    ax.set_xlabel("bin")
    ax.set_ylabel("column")
    ax.set_zlabel("value")
    
    #label every other column number
    ax.set_yticks(yticks[::2])
    ax.set_zlim3d(0,60)
    plt.show()
    

    Sample output (view from the opposite site, the first histograms are cut off because of excessive values in comparison to the rest):

    ![![enter image description here

    Disclaimer: The rolling mean calculation was adapted from this SO answer.