2011-04-13 32 views
55

estoy tratando de crear un gráfico utilizando pyplot que tiene un eje x discontinua. La forma más habitual de este se extrae es que el eje tendrá algo como esto:Python/Matplotlib - ¿Hay alguna manera de hacer un eje discontinuo?

(valores) ---- ---- // (más tarde) los valores

donde el // indica que eres omitiendo todo entre (valores) y (valores posteriores).

no he podido encontrar ningún ejemplo de esto, así que me pregunto si es aún posible. Sé que puede unir datos sobre una discontinuidad para, por ejemplo, datos financieros, pero me gustaría hacer el salto en el eje más explícito. Por el momento solo estoy usando subtramas, pero realmente me gustaría que todo termine en el mismo gráfico al final.

Respuesta

56

respuesta de Pablo es un método perfectamente bien de hacer esto.

Sin embargo, si usted no quiere hacer una transformación personalizada, sólo puede utilizar dos tramas secundarias para crear el mismo efecto.

En lugar de armar un ejemplo desde cero, hay an excellent example of this written by Paul Ivanov en los ejemplos de matplotlib (Está solo en la sugerencia de git actual, ya que solo se cometió hace unos meses. Todavía no está en la página web).

Esto es sólo una simple modificación de este ejemplo para tener una discontinua eje x en lugar del eje y. (Que es por eso que estoy haciendo este post un CW)

Básicamente, que acaba de hacer algo como esto:

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

enter image description here

Para añadir las líneas de los ejes rotos // efecto, podemos hacer este (de nuevo, modificado del ejemplo de Pablo Ivanov):

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

enter image description here

+5

No podría haberlo dicho mejor;) –

+1

El método para realizar el efecto '//' solo parece funcionar bien si la relación de las subcategorías es 1: 1. ¿Sabes cómo hacerlo funcionar con cualquier proporción introducida por, por ej. 'GridSpec (width_ratio = [n, m])'? –

23

veo muchas sugerencias para esta función, pero hay indicios de que ha sido implementada. Aquí hay una solución viable para el tiempo. Aplica una transformación de función paso a paso al eje x. Es una gran cantidad de código, pero es bastante simple, ya que la mayor parte es una regla de escala personalizada. No he agregado ningún gráfico para indicar la ubicación del descanso, ya que eso es una cuestión de estilo. Buena suerte terminando el trabajo.

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

enter image description here

+0

supongo que simplemente tendrá que hacer por ahora. Esta será mi primera vez jugando con ejes personalizados, así que tendremos que ver cómo funciona. –

+0

Hay un pequeño error tipográfico en 'def transform' de' InvertedCustomTransform', donde debería leer 'self.upper' en lugar de' upper'. ¡Gracias por el gran ejemplo, sin embargo! –

+0

puede agregar un par de líneas para mostrar cómo usar su clase? –

0

pregunta de Direccionamiento Frederick Nord cómo habilitar orientación paralela de la diagonal "romper" las líneas cuando se utiliza un gridspec con relaciones desiguales de 1: 1, los siguientes cambios en función de las propuestas de Paul Ivanov y Joe Kingtons pueden ser útiles. La relación de ancho se puede variar usando las variables n y m.

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

comprobar el paquete brokenaxes:

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

example from brokenaxes

+0

No se puede '' from brokenaxes importar brokenaxes'' en Pycharm Community 2016.3.2 una vez instalado. @ ben.dichter –

+0

Hubo un error. Lo arreglé. Ejecute 'pip install brokenaxes == 0.2' para instalar la versión fija del código. –

+0

Parece interactuar mal con ax.grid (Verdadero) – innisfree

Cuestiones relacionadas