matplotlib (sama panjang unit): dengan rasio aspek 'sama' sumbu z tidak sama dengan x- dan y-

89

Ketika saya mengatur rasio aspek yang sama untuk grafik 3d, sumbu z tidak berubah menjadi 'sama'. Jadi ini:

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()

beri saya yang berikut: masukkan deskripsi gambar di sini

di mana jelas panjang satuan sumbu z tidak sama dengan satuan x dan y.

Bagaimana cara membuat panjang satuan dari ketiga sumbu sama? Semua solusi yang saya temukan tidak berhasil. Terima kasih.


sumber

Jawaban:

71

Saya percaya matplotlib belum mengatur sumbu yang sama dengan benar dalam 3D ... Tetapi saya menemukan trik beberapa waktu lalu (saya tidak ingat di mana) yang telah saya adaptasi menggunakannya. Konsepnya adalah membuat kotak pembatas kubik palsu di sekitar data Anda. Anda dapat mengujinya dengan kode berikut:

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 data memiliki urutan besarnya lebih besar dari x dan y, tetapi bahkan dengan opsi sumbu yang sama, matplotlib autoscale sumbu z:

buruk

Tetapi jika Anda menambahkan kotak pembatas, Anda mendapatkan penskalaan yang benar:

masukkan deskripsi gambar di sini

Remy F
sumber
Dalam hal ini Anda bahkan tidak membutuhkan equalpernyataan itu - pernyataan itu akan selalu sama.
1
Ini berfungsi dengan baik jika Anda hanya memplot satu set data, tetapi bagaimana jika ada lebih banyak set data di plot 3d yang sama? Dalam pertanyaan, ada 2 kumpulan data jadi itu hal yang sederhana untuk menggabungkannya tetapi itu bisa menjadi tidak masuk akal dengan cepat jika memplot beberapa kumpulan data yang berbeda.
Steven C. Howell
@ stvn66, saya merencanakan hingga lima kumpulan data dalam satu grafik dengan solusi ini dan itu bekerja dengan baik untuk saya.
1
Ini bekerja dengan sempurna. Bagi mereka yang menginginkan ini dalam bentuk fungsi, yang mengambil objek sumbu dan melakukan operasi di atas, saya mendorong mereka untuk memeriksa jawaban @karlo di bawah ini. Ini adalah solusi yang sedikit lebih bersih.
spurra
@ user1329187 - Menurut saya ini tidak berhasil untuk saya tanpa equalpernyataan tersebut.
supergra
60

Saya suka solusi di atas, tetapi mereka memiliki kekurangan yaitu Anda perlu melacak rentang dan sarana di atas semua data Anda. Ini bisa merepotkan jika Anda memiliki beberapa kumpulan data yang akan diplot bersama. Untuk memperbaikinya, saya menggunakan metode ax.get_ [xyz] lim3d () dan meletakkan semuanya ke dalam fungsi mandiri yang dapat dipanggil hanya sekali sebelum Anda memanggil plt.show (). Ini versi barunya:

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
sumber
Ketahuilah bahwa menggunakan sarana sebagai titik tengah tidak akan berfungsi di semua kasus, Anda harus menggunakan titik tengah. Lihat komentar saya atas jawaban tauran.
Rainman Noodles
1
Kode saya di atas tidak mengambil mean dari data, itu mengambil mean dari batas plot yang ada. Dengan demikian, fungsi saya dijamin untuk tetap melihat titik mana pun yang terlihat sesuai dengan batas plot yang ditetapkan sebelum dipanggil. Jika pengguna telah menetapkan batas plot terlalu ketat untuk melihat semua poin data, itu adalah masalah terpisah. Fungsi saya memungkinkan lebih banyak fleksibilitas karena Anda mungkin ingin melihat hanya sebagian dari data. Yang saya lakukan hanyalah memperluas batas sumbu sehingga rasio aspeknya 1: 1: 1.
karlo
Cara lain untuk menjelaskannya: jika Anda mengambil mean dari hanya 2 titik, yaitu batas-batas pada satu sumbu, maka itu berarti IS titik tengahnya. Jadi, sejauh yang saya tahu, fungsi Dalum di bawah harus secara matematis setara dengan milik saya dan tidak ada yang perlu `` diperbaiki ''.
karlo
12
Jauh lebih unggul dari solusi yang diterima saat ini yang berantakan ketika Anda mulai memiliki banyak objek dengan sifat berbeda.
P-Gn
1
Saya sangat menyukai solusinya, tetapi setelah saya memperbarui anaconda, ax.set_aspect ("equal") melaporkan kesalahan: NotImplementedError: Saat ini tidak mungkin untuk mengatur aspek secara manual pada sumbu 3D
Ewan
52

Saya menyederhanakan solusi Remy F dengan menggunakan set_x/y/zlim fungsi .

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()

masukkan deskripsi gambar di sini

tauran
sumber
1
Saya suka kode yang disederhanakan. Perlu diketahui bahwa beberapa (sangat sedikit) titik data mungkin tidak diplot. Misalkan X = [0, 0, 0, 100] sehingga X.mean () = 25. Jika max_range keluar menjadi 100 (dari X), maka x-range Anda akan menjadi 25 + - 50, jadi [-25, 75] dan Anda akan kehilangan titik data X [3]. Idenya sangat bagus, dan mudah dimodifikasi untuk memastikan Anda mendapatkan semua poin.
TravisJ
1
Berhati-hatilah karena menggunakan sarana sebagai pusat tidak benar. Anda harus menggunakan sesuatu seperti midpoint_x = np.mean([X.max(),X.min()])dan kemudian menetapkan batas ke midpoint_x+/- max_range. Menggunakan mean hanya berfungsi jika mean terletak di titik tengah kumpulan data, yang tidak selalu benar. Juga, tip: Anda dapat menskalakan max_range untuk membuat grafik terlihat lebih bagus jika ada titik di dekat atau di perbatasan.
Rainman Noodles
Setelah saya memperbarui anaconda, ax.set_aspect ("equal") melaporkan kesalahan: NotImplementedError: Saat ini tidak memungkinkan untuk mengatur aspek secara manual pada sumbu 3D
Ewan
Daripada menelepon set_aspect('equal'), gunakan set_box_aspect([1,1,1]), seperti yang dijelaskan dalam jawaban saya di bawah ini. Ini berfungsi untuk saya di matplotlib versi 3.3.1!
AndrewCox
18

Diadaptasi dari jawaban @ karlo untuk membuat segalanya lebih bersih:

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])

Pemakaian:

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

# ...draw here...

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

EDIT: Jawaban ini tidak berfungsi pada versi Matplotlib yang lebih baru karena perubahan yang digabungkan pull-request #13474, yang dilacak dalam issue #17172dan issue #1077. Sebagai solusi sementara untuk ini, seseorang dapat menghapus baris yang baru ditambahkan di 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
sumber
Suka ini, tetapi setelah saya memperbarui anaconda, ax.set_aspect ("equal") melaporkan kesalahan: NotImplementedError: Saat ini tidak memungkinkan untuk mengatur aspek secara manual pada sumbu 3D
Ewan
@Ewan Saya menambahkan beberapa tautan di bagian bawah jawaban saya untuk membantu penyelidikan. Sepertinya orang-orang MPL sedang memecahkan masalah tanpa memperbaiki masalah dengan benar karena suatu alasan. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq
Saya rasa saya menemukan solusi (yang tidak memerlukan modifikasi kode sumber) untuk NotImplementedError (deskripsi lengkap dalam jawaban saya di bawah); pada dasarnya tambahkan ax.set_box_aspect([1,1,1])sebelum meneleponset_axes_equal
AndrewCox
Baru saja menemukan posting ini dan mencoba, gagal di ax.set_aspect ('equal'). Bukan masalah jika Anda hanya menghapus ax.set_aspect ('equal') dari skrip Anda tetapi tetap menggunakan dua fungsi kustom set_axes_equal dan _set_axes_radius ... pastikan untuk memanggilnya sebelum plt.show (). Solusi bagus untuk saya! Saya telah mencari selama beberapa tahun, akhirnya. Saya selalu kembali ke modul vtk python untuk plot 3D, terutama ketika jumlah hal menjadi ekstrim.
Tony A
15

Perbaikan sederhana!

Saya telah berhasil membuatnya berfungsi di versi 3.3.1.

Sepertinya masalah ini mungkin telah diselesaikan dalam PR # 17172 ; Anda dapat menggunakan ax.set_box_aspect([1,1,1])fungsi untuk memastikan aspeknya benar (lihat catatan untuk fungsi set_aspect ). Saat digunakan bersama dengan fungsi kotak pembatas yang disediakan oleh @karlo dan / atau @Matee Ulhaq, plot sekarang terlihat benar dalam 3D!

matplotlib plot 3d dengan sumbu yang sama

Contoh Kerja Minimum

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
sumber
Ya akhirnya! Terima kasih - jika saya hanya dapat meningkatkan Anda ke atas :)
N. Jonas Figge
7

EDIT: kode user2525140 seharusnya bekerja dengan baik, meskipun jawaban ini seharusnya mencoba untuk memperbaiki kesalahan yang tidak ada. Jawaban di bawah ini hanyalah implementasi duplikat (alternatif):

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
sumber
Anda masih perlu melakukan: ax.set_aspect('equal')atau nilai centang mungkin kacau. Jika tidak, solusi yang bagus. Terima kasih,
Tony Power
2

Pada matplotlib 3.3.0, Axes3D.set_box_aspect tampaknya menjadi pendekatan yang direkomendasikan.

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
sumber
Cara 2021. Bekerja seperti pesona.
Jan Joneš