Python / Matplotlib - Gibt es eine Möglichkeit, eine diskontinuierliche Achse zu erstellen?

78

Ich versuche, ein Diagramm mit einem Pyplot zu erstellen, das eine diskontinuierliche x-Achse hat. Die übliche Art und Weise, wie dies gezeichnet wird, ist, dass die Achse ungefähr so ​​aussieht:

(Werte) ---- // ---- (spätere Werte)

Dabei zeigt das // an, dass Sie alles zwischen (Werten) und (späteren Werten) überspringen.

Ich konnte keine Beispiele dafür finden, also frage ich mich, ob es überhaupt möglich ist. Ich weiß, dass Sie Daten über eine Diskontinuität für z. B. Finanzdaten zusammenfügen können, aber ich möchte den Sprung in die Achse deutlicher machen. Im Moment verwende ich nur Nebenhandlungen, aber ich möchte wirklich, dass am Ende alles in derselben Grafik endet.

Justin S.
quelle

Antworten:

84

Die Antwort von Paulus ist eine sehr gute Methode, dies zu tun.

Wenn Sie jedoch keine benutzerdefinierte Transformation durchführen möchten, können Sie einfach zwei Unterzeichnungen verwenden, um denselben Effekt zu erzielen.

Anstatt ein Beispiel von Grund auf neu zusammenzustellen, gibt es ein hervorragendes Beispiel dafür, das Paul Ivanov in den matplotlib-Beispielen geschrieben hat (Es ist nur im aktuellen Git-Tipp enthalten, da es erst vor einigen Monaten festgelegt wurde. Es ist noch nicht auf der Webseite.) .

Dies ist nur eine einfache Modifikation dieses Beispiels, um eine diskontinuierliche x-Achse anstelle der y-Achse zu haben. (Deshalb mache ich diesen Beitrag zu einem CW)

Grundsätzlich machst du einfach so etwas:

import matplotlib.pylab as plt
import numpy as np

# If you're not familiar with np.r_, don't worry too much about this. It's just 
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)

# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

plt.show()

Geben Sie hier die Bildbeschreibung ein

Um den //Effekt der unterbrochenen Achsenlinien hinzuzufügen , können wir dies tun (wiederum modifiziert nach Paul Ivanovs Beispiel):

import matplotlib.pylab as plt
import numpy as np

# If you're not familiar with np.r_, don't worry too much about this. It's just 
# a series with points from 0 to 1 spaced at 0.1, and 9 to 10 with the same spacing.
x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

fig,(ax,ax2) = plt.subplots(1, 2, sharey=True)

# plot the same data on both axes
ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

# zoom-in / limit the view to different portions of the data
ax.set_xlim(0,1) # most of the data
ax2.set_xlim(9,10) # outliers only

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

# Make the spacing between the two axes a bit smaller
plt.subplots_adjust(wspace=0.15)

# This looks pretty good, and was fairly painless, but you can get that
# cut-out diagonal lines look with just a bit more work. The important
# thing to know here is that in axes coordinates, which are always
# between 0-1, spine endpoints are at these locations (0,0), (0,1),
# (1,0), and (1,1). Thus, we just need to put the diagonals in the
# appropriate corners of each of our axes, and so long as we use the
# right transform and disable clipping.

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)
ax.plot((1-d,1+d),(-d,+d), **kwargs) # top-left diagonal
ax.plot((1-d,1+d),(1-d,1+d), **kwargs) # bottom-left diagonal

kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d,d),(-d,+d), **kwargs) # top-right diagonal
ax2.plot((-d,d),(1-d,1+d), **kwargs) # bottom-right diagonal

# What's cool about this is that now if we vary the distance between
# ax and ax2 via f.subplots_adjust(hspace=...) or plt.subplot_tool(),
# the diagonal lines will move accordingly, and stay right at the tips
# of the spines they are 'breaking'

plt.show()

Geben Sie hier die Bildbeschreibung ein

Joe Kington
quelle
11
Ich hätte es selbst nicht besser sagen können;)
Paul Ivanov
3
Die Methode, um den //Effekt zu erzielen, scheint nur dann gut zu funktionieren, wenn das Verhältnis der Teilzahlen 1: 1 beträgt. Wissen Sie, wie Sie es mit einem von z GridSpec(width_ratio=[n,m]).
Frederick Nord
Fantastisch. Mit kleinen Änderungen kann dies für eine beliebige Anzahl von Abschnitten der x-Achse funktionieren.
Christian Madsen
Frederick Nord ist richtig. Darüber hinaus /unterdrückt der Effekt nicht die normale Zecke, die ästhetisch
irritierend
30

Ich sehe viele Vorschläge für diese Funktion, aber keinen Hinweis darauf, dass sie implementiert wurde. Hier ist eine praktikable Lösung für den Moment. Es wendet eine Schrittfunktionstransformation auf die x-Achse an. Es ist eine Menge Code, aber es ist ziemlich einfach, da das meiste davon benutzerdefiniertes Skalierungsmaterial ist. Ich habe keine Grafiken hinzugefügt, um den Ort der Unterbrechung anzuzeigen, da dies eine Frage des Stils ist. Viel Glück beim Beenden des Jobs.

from matplotlib import pyplot as plt
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
import numpy as np

def CustomScaleFactory(l, u):
    class CustomScale(mscale.ScaleBase):
        name = 'custom'

        def __init__(self, axis, **kwargs):
            mscale.ScaleBase.__init__(self)
            self.thresh = None #thresh

        def get_transform(self):
            return self.CustomTransform(self.thresh)

        def set_default_locators_and_formatters(self, axis):
            pass

        class CustomTransform(mtransforms.Transform):
            input_dims = 1
            output_dims = 1
            is_separable = True
            lower = l
            upper = u
            def __init__(self, thresh):
                mtransforms.Transform.__init__(self)
                self.thresh = thresh

            def transform(self, a):
                aa = a.copy()
                aa[a>self.lower] = a[a>self.lower]-(self.upper-self.lower)
                aa[(a>self.lower)&(a<self.upper)] = self.lower
                return aa

            def inverted(self):
                return CustomScale.InvertedCustomTransform(self.thresh)

        class InvertedCustomTransform(mtransforms.Transform):
            input_dims = 1
            output_dims = 1
            is_separable = True
            lower = l
            upper = u

            def __init__(self, thresh):
                mtransforms.Transform.__init__(self)
                self.thresh = thresh

            def transform(self, a):
                aa = a.copy()
                aa[a>self.lower] = a[a>self.lower]+(self.upper-self.lower)
                return aa

            def inverted(self):
                return CustomScale.CustomTransform(self.thresh)

    return CustomScale

mscale.register_scale(CustomScaleFactory(1.12, 8.88))

x = np.concatenate((np.linspace(0,1,10), np.linspace(9,10,10)))
xticks = np.concatenate((np.linspace(0,1,6), np.linspace(9,10,6)))
y = np.sin(x)
plt.plot(x, y, '.')
ax = plt.gca()
ax.set_xscale('custom')
ax.set_xticks(xticks)
plt.show()

Geben Sie hier die Bildbeschreibung ein

Paul
quelle
Ich denke, das muss erst einmal reichen. Ich werde zum ersten Mal mit benutzerdefinierten Achsen herumspielen, also müssen wir nur sehen, wie es geht.
Justin S
Es gibt einen kleinen Tippfehler in def transformdem InvertedCustomTransform, wo er lesen sollte self.upperstatt upper. Vielen Dank für das großartige Beispiel!
David Zwicker
Können Sie ein paar Zeilen hinzufügen, um zu zeigen, wie Sie Ihre Klasse verwenden?
Ruggero Turra
@RuggeroTurra In meinem Beispiel ist alles da. Sie müssen wahrscheinlich nur zum Ende des Codeblocks scrollen.
Paul
2
Das Beispiel funktioniert bei mir auf matplotlib 1.4.3 nicht: imgur.com/4yHa9be . Sieht aus wie diese Version nur erkennt transform_non_affinestatt transform. Siehe meinen Patch unter stackoverflow.com/a/34582476/1214547 .
Pastafarianist
25

Überprüfen Sie das Paket mit den defekten Achsen :

import matplotlib.pyplot as plt
from brokenaxes import brokenaxes
import numpy as np

fig = plt.figure(figsize=(5,2))
bax = brokenaxes(xlims=((0, .1), (.4, .7)), ylims=((-1, .7), (.79, 1)), hspace=.05)
x = np.linspace(0, 1, 100)
bax.plot(x, np.sin(10 * x), label='sin')
bax.plot(x, np.cos(10 * x), label='cos')
bax.legend(loc=3)
bax.set_xlabel('time')
bax.set_ylabel('value')

Beispiel aus gebrochenen Achsen

ben.dichter
quelle
Kann from brokenaxes import brokenaxesnach der Installation nicht in Pycharm Community 2016.3.2 verwendet werden. @ ben.dichter
emmmphd
1
Es gab einen Fehler. Ich habe es repariert. Bitte ausführen pip install brokenaxes==0.2, um eine feste Version des Codes zu installieren.
ben.dichter
Scheint schlecht mit ax.grid (True) zu interagieren
innisfree
1
Können gebrochene Achsen die Zecke unterdrücken? Oder die Achsen horizontal näher beieinander formatieren?
ifly6
1
Hallo Ben, ich möchte die y-Achse entfernen, habe jedoch eine Reihe von Befehlen ausprobiert, funktioniert aber nicht richtig in Kombination mit defekten Achsen (Anmerkung x-Achse ist die defekte Achse), thx
user3737702
0

Bei der Beantwortung der Frage von Frederick Nord, wie die parallele Ausrichtung der diagonalen "Bruchlinien" bei Verwendung einer Gitterspezifikation mit einem Verhältnis von 1: 1 ungleich sein kann, können die folgenden Änderungen auf der Grundlage der Vorschläge von Paul Ivanov und Joe Kingtons hilfreich sein. Das Breitenverhältnis kann unter Verwendung der Variablen n und m variiert werden.

import matplotlib.pylab as plt
import numpy as np
import matplotlib.gridspec as gridspec

x = np.r_[0:1:0.1, 9:10:0.1]
y = np.sin(x)

n = 5; m = 1;
gs = gridspec.GridSpec(1,2, width_ratios = [n,m])

plt.figure(figsize=(10,8))

ax = plt.subplot(gs[0,0])
ax2 = plt.subplot(gs[0,1], sharey = ax)
plt.setp(ax2.get_yticklabels(), visible=False)
plt.subplots_adjust(wspace = 0.1)

ax.plot(x, y, 'bo')
ax2.plot(x, y, 'bo')

ax.set_xlim(0,1)
ax2.set_xlim(10,8)

# hide the spines between ax and ax2
ax.spines['right'].set_visible(False)
ax2.spines['left'].set_visible(False)
ax.yaxis.tick_left()
ax.tick_params(labeltop='off') # don't put tick labels at the top
ax2.yaxis.tick_right()

d = .015 # how big to make the diagonal lines in axes coordinates
# arguments to pass plot, just so we don't keep repeating them
kwargs = dict(transform=ax.transAxes, color='k', clip_on=False)

on = (n+m)/n; om = (n+m)/m;
ax.plot((1-d*on,1+d*on),(-d,d), **kwargs) # bottom-left diagonal
ax.plot((1-d*on,1+d*on),(1-d,1+d), **kwargs) # top-left diagonal
kwargs.update(transform=ax2.transAxes) # switch to the bottom axes
ax2.plot((-d*om,d*om),(-d,d), **kwargs) # bottom-right diagonal
ax2.plot((-d*om,d*om),(1-d,1+d), **kwargs) # top-right diagonal

plt.show()
Ausflug
quelle
0

Für Interessenten habe ich die Antwort von @ Paul erweitert und zum ProPlot-Matplotlib-Paket hinzugefügt . Es kann "Sprünge", "Beschleunigungen" und "Verlangsamungen" der Achse ausführen .

Derzeit gibt es keine Möglichkeit, "Kreuze" hinzuzufügen, die den diskreten Sprung wie in Joes Antwort anzeigen, aber ich plane, dies in Zukunft hinzuzufügen. Ich plane auch, einen Standard-Tick-Locator hinzuzufügen, der abhängig von den CutoffScaleArgumenten sinnvolle Standard-Tick-Positionen festlegt .

Luke Davis
quelle