Python - Cómo comprobar la monotonicidad de la lista

81

¿Cuál sería una forma eficiente y pitónica de comprobar la monotonicidad de la lista?
es decir, que tiene valores crecientes o decrecientes monótonamente?

Ejemplos:

[0, 1, 2, 3, 3, 4]   # This is a monotonically increasing list
[4.3, 4.2, 4.2, -2]  # This is a monotonically decreasing list
[2, 3, 1]            # This is neither
Jonathan
fuente
5
Es mejor usar los términos "estrictamente creciente" o "no decreciente" para dejar cualquier ambigüedad (y de manera similar, es mejor evitar "positivo" y usar en su lugar "no negativo" o "estrictamente positivo")
6502
13
@ 6502 el término monótono se define como un conjunto de valores ordenados que no aumentan ni disminuyen, por lo que no hubo ambigüedad en la pregunta.
Autoplectic
Si está buscando extraer la parte de datos con cierta monotonicidad , eche un vistazo a: github.com/Weilory/python-regression/blob/master/regression/…
Weilory

Respuestas:

160

Es mejor evitar términos ambiguos como "aumentar" o "disminuir", ya que no está claro si la igualdad es aceptable o no. Siempre debe usar por ejemplo "no creciente" (claramente se acepta la igualdad) o "estrictamente decreciente" (claramente NO se acepta la igualdad).

def strictly_increasing(L):
    return all(x<y for x, y in zip(L, L[1:]))

def strictly_decreasing(L):
    return all(x>y for x, y in zip(L, L[1:]))

def non_increasing(L):
    return all(x>=y for x, y in zip(L, L[1:]))

def non_decreasing(L):
    return all(x<=y for x, y in zip(L, L[1:]))

def monotonic(L):
    return non_increasing(L) or non_decreasing(L)
6502
fuente
14
Este es un código Python claro e idiomático, y su complejidad es O (n) donde las respuestas de clasificación son todas O (n log n). Una respuesta ideal iteraría sobre la lista solo una vez, por lo que funciona en cualquier iterador, pero esto suele ser lo suficientemente bueno y es, con mucho, la mejor respuesta entre las que hay hasta ahora. (Ofrecería una solución de un solo paso, pero el OP que acepta prematuramente una respuesta frena cualquier impulso que pueda tener de hacerlo ...)
Glenn Maynard
2
solo por curiosidad, probó su implementación contra el uso de sorted. El tuyo es claramente mucho más lento [usé L = rango (10000000)]. Parece que la complejidad de todo es O (n), y no pude encontrar la implementación de zip.
Asterisco
4
La clasificación es especializada si la lista ya está ordenada. ¿Probaste la velocidad con una lista aleatoria? También tenga en cuenta que con la ordenación no se puede distinguir entre estrictamente creciente y no decreciente. También considere que con Python 2.x usando en itertools.iziplugar de zipusted puede obtener una salida anticipada (en Python 3 zipya funciona como un iterador)
6502
3
@ 6502: solo se necesita una función: operador de importación; def monotone (L, op): devuelve todo (op (x, y) para x, y en zip (L, L [1:])) y luego introduce lo que quieras: operator.le o .ge o lo que sea
Akira
5
zip y el operador de corte devuelven listas completas, obviando las capacidades de atajo de all (); esto podría mejorarse en gran medida utilizando itertools.izip e itertools.islice, ya que estrictamente_incrementante o estrictamente_ decreciente debería fallar el acceso directo muy pronto.
Hugh Bothwell
36

Si tiene listas grandes de números, sería mejor usar numpy, y si es así:

import numpy as np

def monotonic(x):
    dx = np.diff(x)
    return np.all(dx <= 0) or np.all(dx >= 0)

debería hacer el truco.

Autoplectico
fuente
Tenga en cuenta que dx [0] es np.nan. Es posible que desee utilizar: dx = np.diff (x) [1:] para omitirlo. De lo contrario, al menos para mí, las llamadas np.all () siempre devuelven False.
Ryan
@Ryan, ¿por qué dx[0]será NaN? ¿Cuál es su matriz de entrada?
DilithiumMatrix
1
N / m, estaba pensando que np.diff()hizo el primer elemento NaNpara que la forma de la salida coincidiera con la entrada, pero en realidad era una pieza de código diferente que hizo eso que me mordió. :)
Ryan
24
import itertools
import operator

def monotone_increasing(lst):
    pairs = zip(lst, lst[1:])
    return all(itertools.starmap(operator.le, pairs))

def monotone_decreasing(lst):
    pairs = zip(lst, lst[1:])
    return all(itertools.starmap(operator.ge, pairs))

def monotone(lst):
    return monotone_increasing(lst) or monotone_decreasing(lst)

Este enfoque está O(N)en la longitud de la lista.

Michael J. Barber
fuente
3
La solución Correct (TM) IMO. ¡Paradigma funcional para ganar!
mike3996
2
¿Por qué usar itertools en lugar de generadores simples?
6502
3
Los paradigmas funcionales generalmente no son "la victoria" en Python.
Glenn Maynard
@ 6502 Hábito, sobre todo. Por otro lado, mapaquí se necesita exactamente la abstracción, entonces, ¿por qué recrearla con una expresión generadora?
Michael J. Barber
3
Calcular pares también lo es O(N). Podrías hacer pairs = itertools.izip(lst, itertools.islice(lst, 1, None)).
Tomasz Elendt
18

@ 6502 tiene el código perfecto para listas, solo quiero agregar una versión general que funcione para todas las secuencias:

def pairwise(seq):
    items = iter(seq)
    last = next(items)
    for item in items:
        yield last, item
        last = item

def strictly_increasing(L):
    return all(x<y for x, y in pairwise(L))

def strictly_decreasing(L):
    return all(x>y for x, y in pairwise(L))

def non_increasing(L):
    return all(x>=y for x, y in pairwise(L))

def non_decreasing(L):
    return all(x<=y for x, y in pairwise(L))
Jochen Ritzel
fuente
6

El paquete Pandas lo hace conveniente.

import pandas as pd

Los siguientes comandos funcionan con una lista de números enteros o flotantes.

Aumentando monótonamente (≥):

pd.Series(mylist).is_monotonic_increasing

Estrictamente monótonamente creciente (>):

myseries = pd.Series(mylist)
myseries.is_unique and myseries.is_monotonic_increasing

Alternativa usando un método privado indocumentado:

pd.Index(mylist)._is_strictly_monotonic_increasing

Disminuyendo monótonamente (≤):

pd.Series(mylist).is_monotonic_decreasing

Estrictamente monótona decreciente (<):

myseries = pd.Series(mylist)
myseries.is_unique and myseries.is_monotonic_decreasing

Alternativa usando un método privado indocumentado:

pd.Index(mylist)._is_strictly_monotonic_decreasing
Acumenus
fuente
4
import operator, itertools

def is_monotone(lst):
    op = operator.le            # pick 'op' based upon trend between
    if not op(lst[0], lst[-1]): # first and last element in the 'lst'
        op = operator.ge
    return all(op(x,y) for x, y in itertools.izip(lst, lst[1:]))
Akira
fuente
Estaba pensando en una solución como esta, pero falla si la lista aumenta monótonamente y los dos primeros elementos son iguales.
Hugh Bothwell
@Hugh Bothwell: verifico ahora el primero y el último para obtener la tendencia: si son iguales, entonces todos los demás elementos también deberían ser iguales, lo que funciona tanto para operator.le como para operator.ge
akira
3

Aquí hay una solución funcional que utiliza reducecomplejidad O(n):

is_increasing = lambda L: reduce(lambda a,b: b if a < b else 9999 , L)!=9999

is_decreasing = lambda L: reduce(lambda a,b: b if a > b else -9999 , L)!=-9999

Reemplace 9999con el límite superior de sus valores y -9999con el límite inferior. Por ejemplo, si está probando una lista de dígitos, puede usar 10y -1.


Probé su rendimiento contra la respuesta de @ 6502 y es más rápido.

Caso verdadero: [1,2,3,4,5,6,7,8,9]

# my solution .. 
$ python -m timeit "inc = lambda L: reduce(lambda a,b: b if a < b else 9999 , L)!=9999; inc([1,2,3,4,5,6,7,8,9])"
1000000 loops, best of 3: 1.9 usec per loop

# while the other solution:
$ python -m timeit "inc = lambda L: all(x<y for x, y in zip(L, L[1:]));inc([1,2,3,4,5,6,7,8,9])"
100000 loops, best of 3: 2.77 usec per loop

Caso falso del segundo elemento[4,2,3,4,5,6,7,8,7] ::

# my solution .. 
$ python -m timeit "inc = lambda L: reduce(lambda a,b: b if a < b else 9999 , L)!=9999; inc([4,2,3,4,5,6,7,8,7])"
1000000 loops, best of 3: 1.87 usec per loop

# while the other solution:
$ python -m timeit "inc = lambda L: all(x<y for x, y in zip(L, L[1:]));inc([4,2,3,4,5,6,7,8,7])"
100000 loops, best of 3: 2.15 usec per loop
grandeOtros
fuente
1
L = [1,2,3]
L == sorted(L)

L == sorted(L, reverse=True)
Asterisco
fuente
Habría ido por sorted()si en realidad no hubiera solucionado nada, solo verifique. Mal nombre: suena como un predicado cuando no lo es.
mike3996
13
¿Que sigue? ¿Utilizando en sorted(L)[0]lugar de min?
6502
4
Esto es algorítmicamente deficiente; esta solución es O (n log n), cuando este problema se puede resolver trivialmente en O (n).
Glenn Maynard
@todos de acuerdo con todos ustedes, gracias por la crítica constructiva.
Asterisco
1
Probé todas las soluciones en este hilo aquí y descubrí que el método ordenado en realidad es el mejor ... si la lista aumenta de manera monótona. Si la lista tiene elementos desordenados, se vuelve el más lento.
Matthew Moisen
1

Calculé todas las respuestas de esta pregunta en diferentes condiciones y descubrí que:

  • La clasificación fue la más rápida por mucho si la lista ya estaba aumentando monótonamente
  • La clasificación fue la más lenta por mucho SI la lista era aleatoria o aleatoria o si el número de elementos fuera de orden era mayor que ~ 1. Cuanto más desordenada la lista, por supuesto, se corresponde con un resultado más lento.
  • El método de Michael J. Barbers fue el más rápido SI la lista aumentaba de manera monótona o completamente aleatoria.

Aquí está el código para probarlo:

import timeit

setup = '''
import random
from itertools import izip, starmap, islice
import operator

def is_increasing_normal(lst):
    for i in range(0, len(lst) - 1):
        if lst[i] >= lst[i + 1]:
            return False
    return True

def is_increasing_zip(lst):
    return all(x < y for x, y in izip(lst, islice(lst, 1, None)))

def is_increasing_sorted(lst):
    return lst == sorted(lst)

def is_increasing_starmap(lst):
    pairs = izip(lst, islice(lst, 1, None))
    return all(starmap(operator.le, pairs))

if {list_method} in (1, 2):
    lst = list(range({n}))
if {list_method} == 2:
    for _ in range(int({n} * 0.0001)):
        lst.insert(random.randrange(0, len(lst)), -random.randrange(1,100))
if {list_method} == 3:
    lst = [int(1000*random.random()) for i in xrange({n})]
'''

n = 100000
iterations = 10000
list_method = 1

timeit.timeit('is_increasing_normal(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)

timeit.timeit('is_increasing_zip(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)

timeit.timeit('is_increasing_sorted(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)

timeit.timeit('is_increasing_starmap(lst)', setup=setup.format(n=n, list_method=list_method), number=iterations)

Si la lista ya estaba aumentando monótonamente ( list_method == 1), el más rápido al más lento era:

  1. ordenado
  2. mapa estelar
  3. normal
  4. Código Postal

Si la lista aumentaba en su mayoría de forma monótona ( list_method == 2), el más rápido al más lento era:

  1. mapa estelar
  2. Código Postal
  3. normal
  4. ordenado

(Si el mapa de estrellas o el zip eran más rápidos o no dependía de la ejecución y no pude identificar un patrón. El mapa de estrellas parecía ser más rápido)

Si la lista era completamente aleatoria ( list_method == 3), la más rápida a la más lenta era:

  1. mapa estelar
  2. Código Postal
  3. normal
  4. ordenado (extremadamente malo)
Matthew Moisen
fuente
No probé el método de @Assem Chelli ya que requería conocimiento del elemento máximo en la lista
Matthew Moisen
Las comparaciones de tiempo también dependerían en gran medida ndel tamaño de la lista, y que podría variar considerablemente de 100000
nealmcb
0

@ 6502 tiene un elegante código Python para esto. Aquí hay una solución alternativa con iteradores más simples y sin cortes temporales potencialmente costosos:

def strictly_increasing(L):
    return all(L[i] < L[i+1] for i in range(len(L)-1))

def strictly_decreasing(L):
    return all(L[i] > L[i+1] for i in range(len(L)-1))

def non_increasing(L):
    return all(L[i] >= L[i+1] for i in range(len(L)-1))

def non_decreasing(L):
    return all(L[i] <= L[i+1] for i in range(len(L)-1))

def monotonic(L):
    return non_increasing(L) or non_decreasing(L)
chqrlie
fuente
-1
>>> l = [0,1,2,3,3,4]
>>> l == sorted(l) or l == sorted(l, reverse=True)
Senthil Kumaran
fuente
-1

Aquí hay una variante que acepta secuencias materializadas y no materializadas . Determina automáticamente si es o no monotonic, y si es así, su dirección (es decir, increasingo decreasing) y strictness. Se proporcionan comentarios en línea para ayudar al lector. De manera similar para los casos de prueba proporcionados al final.

    def isMonotonic(seq):
    """
    seq.............: - A Python sequence, materialized or not.
    Returns.........:
       (True,0,True):   - Mono Const, Strict: Seq empty or 1-item.
       (True,0,False):  - Mono Const, Not-Strict: All 2+ Seq items same.
       (True,+1,True):  - Mono Incr, Strict.
       (True,+1,False): - Mono Incr, Not-Strict.
       (True,-1,True):  - Mono Decr, Strict.
       (True,-1,False): - Mono Decr, Not-Strict.
       (False,None,None) - Not Monotonic.
    """    
    items = iter(seq) # Ensure iterator (i.e. that next(...) works).
    prev_value = next(items, None) # Fetch 1st item, or None if empty.
    if prev_value == None: return (True,0,True) # seq was empty.

    # ============================================================
    # The next for/loop scans until it finds first value-change.
    # ============================================================
    # Ex: [3,3,3,78,...] --or- [-5,-5,-5,-102,...]
    # ============================================================
    # -- If that 'change-value' represents an Increase or Decrease,
    #    then we know to look for Monotonically Increasing or
    #    Decreasing, respectively.
    # -- If no value-change is found end-to-end (e.g. [3,3,3,...3]),
    #    then it's Monotonically Constant, Non-Strict.
    # -- Finally, if the sequence was exhausted above, which means
    #    it had exactly one-element, then it Monotonically Constant,
    #    Strict.
    # ============================================================
    isSequenceExhausted = True
    curr_value = prev_value
    for item in items:
        isSequenceExhausted = False # Tiny inefficiency.
        if item == prev_value: continue
        curr_value = item
        break
    else:
        return (True,0,True) if isSequenceExhausted else (True,0,False)
    # ============================================================

    # ============================================================
    # If we tricked down to here, then none of the above
    # checked-cases applied (i.e. didn't short-circuit and
    # 'return'); so we continue with the final step of
    # iterating through the remaining sequence items to
    # determine Monotonicity, direction and strictness.
    # ============================================================
    strict = True
    if curr_value > prev_value: # Scan for Increasing Monotonicity.
        for item in items:
            if item < curr_value: return (False,None,None)
            if item == curr_value: strict = False # Tiny inefficiency.
            curr_value = item
        return (True,+1,strict)
    else:                       # Scan for Decreasing Monotonicity.
        for item in items: 
            if item > curr_value: return (False,None,None)
            if item == curr_value: strict = False # Tiny inefficiency.
            curr_value = item
        return (True,-1,strict)
    # ============================================================


# Test cases ...
assert isMonotonic([1,2,3,4])     == (True,+1,True)
assert isMonotonic([4,3,2,1])     == (True,-1,True)
assert isMonotonic([-1,-2,-3,-4]) == (True,-1,True)
assert isMonotonic([])            == (True,0,True)
assert isMonotonic([20])          == (True,0,True)
assert isMonotonic([-20])         == (True,0,True)
assert isMonotonic([1,1])         == (True,0,False)
assert isMonotonic([1,-1])        == (True,-1,True)
assert isMonotonic([1,-1,-1])     == (True,-1,False)
assert isMonotonic([1,3,3])       == (True,+1,False)
assert isMonotonic([1,2,1])       == (False,None,None)
assert isMonotonic([0,0,0,0])     == (True,0,False)

Supongo que esto podría ser más Pythonic, pero es complicado, ya que evita la creación de colecciones intermedias (por ejemplo list, genexps, etc); además de emplear un enfoque fall/trickle-throughy short-circuitpara filtrar a través de los diversos casos: por ejemplo, secuencias de borde (como secuencias vacías o de un elemento; o secuencias con todos los elementos idénticos); Identificar monotonicidad creciente o decreciente, rigor, etc. Espero que ayude.

NYCeyes
fuente
¿Por qué el voto negativo? Está bien explicado y ofrece al lector un enfoque alternativo a los demás.
NYCeyes