matplotlib (gleiche Längeneinheit): Bei gleichem Seitenverhältnis ist die z-Achse nicht gleich x- und y-

87

Wenn ich das gleiche Seitenverhältnis für das 3D-Diagramm einstelle, ändert sich die Z-Achse nicht in "gleich". Also das:

fig = pylab.figure()
mesFig = fig.gca(projection='3d', adjustable='box')
mesFig.axis('equal')
mesFig.plot(xC, yC, zC, 'r.')
mesFig.plot(xO, yO, zO, 'b.')
pyplot.show()

gibt mir folgendes: Geben Sie hier die Bildbeschreibung ein

wobei offensichtlich die Einheitslänge der z-Achse nicht gleich x- und y-Einheiten ist.

Wie kann ich die Einheitslänge aller drei Achsen gleich machen? Alle Lösungen, die ich finden konnte, funktionierten nicht. Vielen Dank.


quelle

Antworten:

69

Ich glaube, matplotlib hat die gleiche Achse in 3D noch nicht richtig eingestellt ... Aber ich habe vor einiger Zeit einen Trick gefunden (ich weiß nicht mehr, wo), den ich damit angepasst habe. Das Konzept besteht darin, einen gefälschten kubischen Begrenzungsrahmen um Ihre Daten zu erstellen. Sie können es mit folgendem Code testen:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
   ax.plot([xb], [yb], [zb], 'w')

plt.grid()
plt.show()

z-Daten sind ungefähr eine Größenordnung größer als x und y, aber selbst bei gleicher Achsenoption kann die matplotlib-Z-Achse automatisch skaliert werden:

Schlecht

Wenn Sie jedoch den Begrenzungsrahmen hinzufügen, erhalten Sie eine korrekte Skalierung:

Geben Sie hier die Bildbeschreibung ein

Remy F.
quelle
In diesem Fall brauchen Sie die equalAussage nicht einmal - sie ist immer gleich.
1
Dies funktioniert gut, wenn Sie nur einen Datensatz zeichnen, aber was ist, wenn sich mehr Datensätze auf demselben 3D-Plot befinden? In Frage, es gab 2 Datensätze, so dass es einfach ist, sie zu kombinieren, aber das könnte schnell unvernünftig werden, wenn mehrere verschiedene Datensätze geplottet werden.
Steven C. Howell
@ stvn66, ich habe mit diesen Lösungen bis zu fünf Datensätze in einem Diagramm gezeichnet, und es hat für mich gut funktioniert.
1
Das funktioniert perfekt. Für diejenigen, die dies in Funktionsform wünschen, die ein Achsenobjekt nimmt und die obigen Operationen ausführt, empfehle ich ihnen, die Antwort von @karlo unten zu lesen. Es ist eine etwas sauberere Lösung.
Spurra
@ user1329187 - Ich habe festgestellt, dass dies ohne die equalAnweisung bei mir nicht funktioniert hat .
Supergra
57

Ich mag die oben genannten Lösungen, aber sie haben den Nachteil, dass Sie die Bereiche und Mittelwerte über alle Ihre Daten hinweg verfolgen müssen. Dies kann umständlich sein, wenn Sie mehrere Datensätze haben, die zusammen geplottet werden. Um dies zu beheben, habe ich die Methoden ax.get_ [xyz] lim3d () verwendet und das Ganze in eine eigenständige Funktion eingefügt, die nur einmal aufgerufen werden kann, bevor Sie plt.show () aufrufen. Hier ist die neue Version:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

def set_axes_equal(ax):
    '''Make axes of 3D plot have equal scale so that spheres appear as spheres,
    cubes as cubes, etc..  This is one possible solution to Matplotlib's
    ax.set_aspect('equal') and ax.axis('equal') not working for 3D.

    Input
      ax: a matplotlib axis, e.g., as output from plt.gca().
    '''

    x_limits = ax.get_xlim3d()
    y_limits = ax.get_ylim3d()
    z_limits = ax.get_zlim3d()

    x_range = abs(x_limits[1] - x_limits[0])
    x_middle = np.mean(x_limits)
    y_range = abs(y_limits[1] - y_limits[0])
    y_middle = np.mean(y_limits)
    z_range = abs(z_limits[1] - z_limits[0])
    z_middle = np.mean(z_limits)

    # The plot bounding box is a sphere in the sense of the infinity
    # norm, hence I call half the max range the plot radius.
    plot_radius = 0.5*max([x_range, y_range, z_range])

    ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
    ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
    ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

set_axes_equal(ax)
plt.show()
Karlo
quelle
Beachten Sie, dass die Verwendung von Mitteln als Mittelpunkt nicht in allen Fällen funktioniert. Sie sollten Mittelpunkte verwenden. Siehe meinen Kommentar zu Taurans Antwort.
Rainman Noodles
1
Mein Code oben nimmt nicht den Mittelwert der Daten, sondern den Mittelwert der vorhandenen Plotgrenzen. Meine Funktion behält somit garantiert alle Punkte im Blick, die gemäß den vor dem Aufruf festgelegten Plotgrenzen angezeigt wurden. Wenn der Benutzer die Plotgrenzen bereits zu restriktiv festgelegt hat, um alle Datenpunkte anzuzeigen, ist dies ein separates Problem. Meine Funktion bietet mehr Flexibilität, da Sie möglicherweise nur eine Teilmenge der Daten anzeigen möchten. Ich erweitere nur die Achsengrenzen, sodass das Seitenverhältnis 1: 1: 1 beträgt.
Karlo
Anders ausgedrückt: Wenn Sie einen Mittelwert von nur 2 Punkten nehmen, nämlich die Grenzen auf einer einzelnen Achse, dann ist dieser Mittelwert der Mittelpunkt. Soweit ich das beurteilen kann, sollte Dalums Funktion mathematisch meiner entsprechen, und es gab nichts zu reparieren.
Karlo
11
Der derzeit akzeptierten Lösung, die ein Chaos darstellt, wenn Sie anfangen, viele Objekte unterschiedlicher Art zu haben, weit überlegen.
P-Gn
1
Die Lösung gefällt mir sehr gut, aber nachdem ich anaconda aktualisiert habe, hat ax.set_aspect ("gleich") einen Fehler gemeldet: NotImplementedError: Es ist derzeit nicht möglich, den Aspekt auf 3D-Achsen manuell festzulegen
Ewan
51

Ich habe die Lösung von Remy F mithilfe der set_x/y/zlim Funktionen vereinfacht .

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max() / 2.0

mid_x = (X.max()+X.min()) * 0.5
mid_y = (Y.max()+Y.min()) * 0.5
mid_z = (Z.max()+Z.min()) * 0.5
ax.set_xlim(mid_x - max_range, mid_x + max_range)
ax.set_ylim(mid_y - max_range, mid_y + max_range)
ax.set_zlim(mid_z - max_range, mid_z + max_range)

plt.show()

Geben Sie hier die Bildbeschreibung ein

Tauran
quelle
1
Ich mag den vereinfachten Code. Beachten Sie jedoch, dass einige (sehr wenige) Datenpunkte möglicherweise nicht gezeichnet werden. Angenommen, X = [0, 0, 0, 100], so dass X.mean () = 25 ist. Wenn max_range 100 (von X) ergibt, beträgt Ihr x-Bereich 25 + - 50, also [-25, 75] und Sie verpassen den X [3] -Datenpunkt. Die Idee ist jedoch sehr schön und leicht zu ändern, um sicherzustellen, dass Sie alle Punkte erhalten.
TravisJ
1
Beachten Sie, dass die Verwendung von Mitteln als Zentrum nicht korrekt ist. Sie sollten so etwas wie verwenden midpoint_x = np.mean([X.max(),X.min()])und dann die Grenzwerte auf midpoint_x+/- setzen max_range. Die Verwendung des Mittelwerts funktioniert nur, wenn sich der Mittelwert in der Mitte des Datensatzes befindet, was nicht immer der Fall ist. Ein Tipp: Sie können max_range skalieren, damit das Diagramm besser aussieht, wenn sich Punkte in der Nähe oder an den Grenzen befinden.
Rainman Noodles
Nachdem ich anaconda aktualisiert habe, hat ax.set_aspect ("gleich") den folgenden Fehler gemeldet: NotImplementedError: Es ist derzeit nicht möglich, den Aspekt auf 3D-Achsen manuell festzulegen
Ewan
Anstatt anzurufen set_aspect('equal'), verwenden Sie set_box_aspect([1,1,1]), wie in meiner Antwort unten beschrieben. Es funktioniert für mich in Matplotlib Version 3.3.1!
AndrewCox
17

Angepasst an die Antwort von @ karlo, um die Dinge noch sauberer zu machen:

def set_axes_equal(ax: plt.Axes):
    """Set 3D plot axes to equal scale.

    Make axes of 3D plot have equal scale so that spheres appear as
    spheres and cubes as cubes.  Required since `ax.axis('equal')`
    and `ax.set_aspect('equal')` don't work on 3D.
    """
    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])
    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
    _set_axes_radius(ax, origin, radius)

def _set_axes_radius(ax, origin, radius):
    x, y, z = origin
    ax.set_xlim3d([x - radius, x + radius])
    ax.set_ylim3d([y - radius, y + radius])
    ax.set_zlim3d([z - radius, z + radius])

Verwendung:

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')         # important!

# ...draw here...

set_axes_equal(ax)             # important!
plt.show()

BEARBEITEN: Diese Antwort funktioniert bei neueren Versionen von Matplotlib aufgrund der eingeführten Änderungen pull-request #13474, die in issue #17172und nachverfolgt werden, nicht issue #1077. Um dies vorübergehend zu umgehen, können Sie die neu hinzugefügten Zeilen entfernen in lib/matplotlib/axes/_base.py:

  class _AxesBase(martist.Artist):
      ...

      def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
          ...

+         if (not cbook._str_equal(aspect, 'auto')) and self.name == '3d':
+             raise NotImplementedError(
+                 'It is not currently possible to manually set the aspect '
+                 'on 3D axes')
Mateen Ulhaq
quelle
Ich liebe das, aber nachdem ich anaconda aktualisiert habe, hat ax.set_aspect ("gleich") einen Fehler gemeldet: NotImplementedError: Es ist derzeit nicht möglich, den Aspekt auf 3D-Achsen manuell festzulegen
Ewan
@Ewan Ich habe am Ende meiner Antwort einige Links hinzugefügt, um bei der Untersuchung zu helfen. Es sieht so aus, als würden die MPL-Leute Problemumgehungen brechen, ohne das Problem aus irgendeinem Grund ordnungsgemäß zu beheben. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq
Ich glaube, ich habe eine Problemumgehung (für die der Quellcode nicht geändert werden muss) für den NotImplementedError gefunden (vollständige Beschreibung in meiner Antwort unten). Grundsätzlich hinzufügen, ax.set_box_aspect([1,1,1])bevor Sie anrufenset_axes_equal
AndrewCox
10

Einfache Lösung!

Ich habe es geschafft, dies in Version 3.3.1 zum Laufen zu bringen.

Es sieht so aus, als ob dieses Problem möglicherweise in PR # 17172 behoben wurde . Mit der ax.set_box_aspect([1,1,1])Funktion können Sie sicherstellen, dass der Aspekt korrekt ist (siehe Hinweise zur Funktion set_aspect ). In Verbindung mit den von @karlo und / oder @Matee Ulhaq bereitgestellten Begrenzungsrahmenfunktionen sehen die Diagramme jetzt in 3D korrekt aus!

Matplotlib 3D-Plot mit gleichen Achsen

Minimum Arbeitsbeispiel

import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d
import numpy as np

# Functions from @Mateen Ulhaq and @karlo
def set_axes_equal(ax: plt.Axes):
    """Set 3D plot axes to equal scale.

    Make axes of 3D plot have equal scale so that spheres appear as
    spheres and cubes as cubes.  Required since `ax.axis('equal')`
    and `ax.set_aspect('equal')` don't work on 3D.
    """
    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])
    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
    _set_axes_radius(ax, origin, radius)

def _set_axes_radius(ax, origin, radius):
    x, y, z = origin
    ax.set_xlim3d([x - radius, x + radius])
    ax.set_ylim3d([y - radius, y + radius])
    ax.set_zlim3d([z - radius, z + radius])

# Generate and plot a unit sphere
u = np.linspace(0, 2*np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v)) # np.outer() -> outer vector product
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_surface(x, y, z)

ax.set_box_aspect([1,1,1]) # IMPORTANT - this is the new, key line
# ax.set_proj_type('ortho') # OPTIONAL - default is perspective (shown in image above)
set_axes_equal(ax) # IMPORTANT - this is also required
plt.show()
AndrewCox
quelle
Ja, endlich! Danke - wenn ich dich nur nach oben bringen könnte :)
N. Jonas Figge
7

BEARBEITEN : Der Code von user2525140 sollte einwandfrei funktionieren, obwohl diese Antwort angeblich versucht hat, einen nicht vorhandenen Fehler zu beheben. Die folgende Antwort ist nur eine doppelte (alternative) Implementierung:

def set_aspect_equal_3d(ax):
    """Fix equal aspect bug for 3D plots."""

    xlim = ax.get_xlim3d()
    ylim = ax.get_ylim3d()
    zlim = ax.get_zlim3d()

    from numpy import mean
    xmean = mean(xlim)
    ymean = mean(ylim)
    zmean = mean(zlim)

    plot_radius = max([abs(lim - mean_)
                       for lims, mean_ in ((xlim, xmean),
                                           (ylim, ymean),
                                           (zlim, zmean))
                       for lim in lims])

    ax.set_xlim3d([xmean - plot_radius, xmean + plot_radius])
    ax.set_ylim3d([ymean - plot_radius, ymean + plot_radius])
    ax.set_zlim3d([zmean - plot_radius, zmean + plot_radius])
Dalum
quelle
Sie müssen noch tun: ax.set_aspect('equal')oder die Tick-Werte können vermasselt werden. Ansonsten gute Lösung. Vielen Dank,
Tony Power
1

Ab matplotlib 3.3.0 scheint Axes3D.set_box_aspect der empfohlene Ansatz zu sein.

import numpy as np

xs, ys, zs = <your data>
ax = <your axes>

# Option 1: aspect ratio is 1:1:1 in data space
ax.set_box_aspect((np.ptp(xs), np.ptp(ys), np.ptp(zs)))

# Option 2: aspect ratio 1:1:1 in view space
ax.set_box_aspect((1, 1, 1))
Matt Panzer
quelle