Contribution
l1=[1,3,5,6,7] l2=[1,2,2,3,4] l3=[5,4,3,2,1] l4=[5,5,3,2,1] l5=[1,2,3,4.1,3,2] l6=[3,2,1,0.4,1,2,3] l7=[1,2,10,4,8,9,2] l8=[1,2,3,4,4,3,2,1] l9=[-0.05701686, 0.57707936, -0.34602634, -0.02599778] l10=[ 0.13556905, 0.45859 , -0.34602634, -0.09178798, 0.03044908] l11=[-0.38643975, -0.09178798, 0.57707936, -0.05701686, 0.00649252]
Remarque: la valeur dans la séquence est float.
Attendu
find_targeted_seq
qui retourne une séquence qu'elle soit strictement monotone ou qu'il y ait un tournant où les deux côtés sont strictement monotones.Par exemple, l1
, l3
, l5
, l6
sont attendus.Essayer
l8
.3 Réponses :
J'utilise une combinaison de np.sgn
et np.diff
pour vérifier les parties ascendantes / descendantes de la séquence. D'après vos exemples en l2 et l4, les éléments doubles (où diff == 0) ne comptent pas comme croissant ou décroissant. Ceux-ci sont rejetés dans la première clause if-else
. Dans la plupart des cas, le np.sign(np.diff(x))
est tous les -1 et 1, selon les parties ascendantes / descendantes. Nous calculons un deuxième np.diff pour voir combien de points de retournement il y a et retournons Vrai / Faux en conséquence.
Voir le code ci-joint :)
1 True 2 False 3 True 4 False 5 True 6 True 7 False 8 False 9 False 10 False 11 False
Cela produit:
import numpy as np def legal_seq(seq): arr = np.array(seq) diffs = np.diff(arr) sgn_diff = np.sign(diffs) if np.isin(0, sgn_diff): # if the difference is 0, we reject the seq return False else: sgn_diff2 = np.diff(sgn_diff) # the sgn_diff is only 1's and -1's we need to count how many constant segments there are, so we use np.diff again num_turning_points = len(np.where(sgn_diff2)[0]) # np.where will see how many non-zero elements there are. nonzero elements in the np.diff are turning points, so we count these if num_turning_points < 2: # if num_turning_points is 0, the seq is mono. if num_turning_points is 1, we return True. Otherwise, False is returned. return True else: return False ## TESTING ## l1=[1,3,5,6,7] l2=[1,2,2,3,4] l3=[5,4,3,2,1] l4=[5,5,3,2,1] l5=[1,2,3,4.1,3,2] l6=[3,2,1,0.4,1,2,3] l7=[1,2,10,4,8,9,2] l8=[1,2,3,4,4,3,2,1] l9=[-0.05701686, 0.57707936, -0.34602634, -0.02599778] l10=[ 0.13556905, 0.45859 , -0.34602634, -0.09178798, 0.03044908] l11=[-0.38643975, -0.09178798, 0.57707936, -0.05701686, 0.00649252] ls = [l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11] for i,l in enumerate(ls): print(i + 1, count_turning_points(l))
L'idée de base est fondamentalement valide, mais l'implémentation pourrait être un peu améliorée (par exemple, len(np.where(sgn_diff2)[0])
peut être écrit plus efficacement avec np.count_nonzero()
, sgn_diff = np.sign(diffs)
peut être calculé dans la branche else
du premier if
, expression de la forme if condition: return True else return False
peut devenir simplement une return condition
). De plus, le code de test ne correspond pas au reste du code (par exemple, legal_seq()
n'est jamais appelé, count_turning_points()
n'est jamais défini).
IIUC, vous avez 3 cas:
Vous pouvez donc faire ce qui suit:
[1, 3, 5, 6, 7] True [1, 2, 2, 3, 4] False [5, 4, 3, 2, 1] True [5, 5, 3, 2, 1] False [1, 2, 3, 4.1, 3, 2] True [3, 2, 1, 0.5, 1, 2] True [3, 2, 1, 0.4, 1, 2, 3] True [1, 2, 10, 4, 8, 9, 2] False [1, 2, 3, 4, 4, 3, 2, 1] False [-0.05701686, 0.57707936, -0.34602634, -0.02599778] False [0.13556905, 0.45859, -0.34602634, -0.09178798, 0.03044908] False [-0.38643975, -0.09178798, 0.57707936, -0.05701686, 0.00649252] False
Production
from scipy.signal import argrelmin, argrelmax import pandas as pd def is_strictly_monotonic_increasing(s): return s.is_unique and s.is_monotonic_increasing def is_strictly_monotonic_decreasing(s): return s.is_unique and s.is_monotonic_decreasing def find_targeted_seq(lst): ser = pd.Series(lst) if is_strictly_monotonic_increasing(ser) or is_strictly_monotonic_decreasing(ser): return True minima, *_ = argrelmin(ser.values) if len(minima) == 1: # only on minimum turning point idx = minima[0] return is_strictly_monotonic_decreasing(ser[:idx]) and is_strictly_monotonic_increasing(ser[idx:]) maxima, *_ = argrelmax(ser.values) if len(maxima) == 1: # only on maximum turning point idx = maxima[0] return is_strictly_monotonic_increasing(ser[:idx]) and is_strictly_monotonic_decreasing(ser[idx:]) return False data = [[1, 3, 5, 6, 7], # l1 [1, 2, 2, 3, 4], # l2 [5, 4, 3, 2, 1], # l3 [5, 5, 3, 2, 1], # l4 [1, 2, 3, 4.1, 3, 2], # l5 [3, 2, 1, 0.5, 1, 2], # this value was added in addition to the existing ones [3, 2, 1, 0.4, 1, 2, 3], # l6 [1, 2, 10, 4, 8, 9, 2], # l7 [1, 2, 3, 4, 4, 3, 2, 1], # l8 [-0.05701686, 0.57707936, -0.34602634, -0.02599778], # l9 [0.13556905, 0.45859, -0.34602634, -0.09178798, 0.03044908], # l10 [-0.38643975, -0.09178798, 0.57707936, -0.05701686, 0.00649252]] # l11 for datum in data: print(datum, find_targeted_seq(datum))
Pourriez-vous peut-être commenter l'efficacité de cette approche? Pour moi, il semble que pd.Series.is_unique
, qui est assez cher, est appelé plus souvent qu'il ne le devrait. Il en va de même pour pd.Series.is_monotonic_increasing
et pd.Series.is_monotonic_decreasing
. De plus, scipy.signal.argrelmin()
/ scipy.signal.argrelmax()
peut être efficacement remplacé par les fonctions np.argmin()
/ np.argmax()
plus performantes.
@ norok2 Argmin ne renvoie-t-il pas le minimum global?
Oui, mais cela suffit car nous nous intéressons à exactement deux branches monotones. Donc, pour que toute la fonction réussisse, le minimum local doit être le minimum global.
@ norok2 Vous avez raison!
Ni la bibliothèque standard de Python ni NumPy n'ont de primitive spécifique pour résoudre cette tâche. Cependant, la méthode traditionnelle dans NumPy est d'examiner les différences en utilisant np.diff()
.
Pour étudier les points de retournement, vous pouvez utiliser respectivement np.argmin()
et np.argmax()
.
La condition de monotonicité stricte correspond à: np.all(np.diff(arr) > 0)
(croissant) ou np.all(np.diff(arr) < 0)
(décroissant). L'exigence d'un point de retournement (pivot) équivaut à trouver ce point de retournement et à vérifier que la séquence est séparément monotone. Pour plusieurs minima ou maxima consécutifs, si l'on utilise les premiers minima ou maxima rencontrés et vérifie la monotonie de la branche gauche excluant ces minima ou maxima, cela suffit à garantir que deux minima ou maxima consécutifs seront correctement identifiés.
Par conséquent, une implémentation simple suit:
for func in funcs: print(func.__name__) %timeit [func(seq) == result for seq, result in data] print() # find_targeted_seq_np # 1000 loops, best of 3: 530 µs per loop # find_targeted_seq_np2 # 10000 loops, best of 3: 187 µs per loop # find_targeted_seq_pd # 100 loops, best of 3: 4.68 ms per loop # find_targeted_seq # 100000 loops, best of 3: 14.6 µs per loop # find_targeted_seq_nb # 10000 loops, best of 3: 19.9 µs per loop
(C'est fondamentalement la même idée que dans la réponse de @ DaniMesejo ).
Une autre option serait d'utiliser une combinaison de np.diff()
, np.sign()
et np.count_nonzero()
pour compter le nombre de fois où il y a un changement de monotonie. Si c'est 0 ou 1, alors la séquence est valide. Éviter les éléments répétitifs est intégré dans le comptage des changements de signe, sauf lorsque les éléments répétitifs sont au début ou à la fin de la séquence et cette situation doit être vérifiée explicitement. Cela conduit à une solution très concise:
data = ( ((1, 3, 5, 6, 7), True), # l1 ((1, 2, 2, 3, 4), False), # l2 ((5, 4, 3, 2, 1), True), # l3 ((5, 5, 3, 2, 1), False), # l4 ((1, 2, 3, 4.1, 3, 2), True), # l5 ((3, 2, 1, 0.5, 1, 2), True), # this value was added in addition to the existing ones ((3, 2, 1, 0.4, 1, 2, 3), True), # l6 ((1, 2, 10, 4, 8, 9, 2), False), # l7 ((1, 2, 3, 4, 4, 3, 2, 1), False), # l8 ((-0.05701686, 0.57707936, -0.34602634, -0.02599778), False), # l9 ((0.13556905, 0.45859, -0.34602634, -0.09178798, 0.03044908), False), # l10 ((-0.38643975, -0.09178798, 0.57707936, -0.05701686, 0.00649252), False), # l11 ) funcs = find_targeted_seq_np, find_targeted_seq_np2, find_targeted_seq_pd, find_targeted_seq, find_targeted_seq_nb for func in funcs: print(func.__name__, all(func(seq) == result for seq, result in data)) # find_targeted_seq_np True # find_targeted_seq_np2 True # find_targeted_seq_pd True # find_targeted_seq True # find_targeted_seq_nb True
(C'est fondamentalement la même idée que dans la réponse de @ yonatansc97 , mais sans utiliser np.isin()
comme suggéré dans les commentaires de @DaniMesejo).
Alternativement, on peut envisager d'utiliser la boucle explicite. Cela a l'avantage d'être considérablement plus efficace en mémoire et de bien meilleures propriétés de court-circuit:
from scipy.signal import argrelmin, argrelmax import pandas as pd def is_strictly_monotonic_increasing(s): return s.is_unique and s.is_monotonic_increasing def is_strictly_monotonic_decreasing(s): return s.is_unique and s.is_monotonic_decreasing def find_targeted_seq_pd(lst): ser = pd.Series(lst) if is_strictly_monotonic_increasing(ser) or is_strictly_monotonic_decreasing(ser): return True minima, *_ = argrelmin(ser.values) if len(minima) == 1: # only on minimum turning point idx = minima[0] return is_strictly_monotonic_decreasing(ser[:idx]) and is_strictly_monotonic_increasing(ser[idx:]) maxima, *_ = argrelmax(ser.values) if len(maxima) == 1: # only on maximum turning point idx = maxima[0] return is_strictly_monotonic_increasing(ser[:idx]) and is_strictly_monotonic_decreasing(ser[idx:]) return False
De plus, si la stabilité de type des éléments de la séquence peut être garantie, elle peut être facilement accélérée via Numba:
import numba as nb _find_targeted_seq_nb = nb.njit(find_targeted_seq) def find_targeted_seq_nb(seq): return _find_targeted_seq_nb(np.array(seq))
À titre de comparaison, ici, il est rapporté une implémentation utilisant pandas
(qui fournit quelques primitives pour le contrôle de monotonie) et scipy.signal.argrelmin()
/ scipy.signal.argrelmax()
pour trouver des points de retournement (ce code est sensiblement le même que celui trouvé dans @ Réponse de DaniMesejo ), par exemple:
def find_targeted_seq(seq): n = len(seq) changes = 0 x = seq[1] last_x = seq[0] diff = x - last_x if diff > 0: monotonic = 1 elif diff < 0: monotonic = -1 else: # diff == 0 return False for i in range(1, n): x = seq[i] diff = x - last_x if diff == 0: return False elif (diff > 0 and monotonic == -1) or (diff < 0 and monotonic == 1): changes += 1 monotonic = -monotonic if changes > 1: return False last_x = x return True
Ces solutions appliquées à l'entrée donnée donnent toutes les bons résultats:
import numpy as np def find_targeted_seq_np2(seq): diffs = np.diff(seq) return \ diffs[0] != 0 and diffs[-1] != 0 \ and np.count_nonzero(np.diff(np.sign(diffs))) < 2
Dans le temps, quelques benchmarks simples sur les données proposées indiquent clairement que le bouclage direct (avec ou sans accélération Numba) est le plus rapide. La deuxième approche Numpy devient beaucoup plus rapide que la première approche NumPy, tandis que l'approche basée sur les pandas
est la plus lente:
import numpy as np def find_targeted_seq_np(seq): diffs = np.diff(seq) incr = diffs > 0 decr = diffs < 0 if np.all(incr) or np.all(decr): return True maximum = np.argmax(seq) if np.all(incr[:maximum]) and np.all(decr[maximum:]): return True minimum = np.argmin(seq) if np.all(decr[:minimum]) and np.all(incr[minimum:]): return True return False
Alors que la boucle directe est plus rapide pour ces données de test que les approches basées sur NumPy sur l'entrée donnée, ces dernières devraient mieux évoluer avec une taille d'entrée plus grande. L'approche Numba est susceptible d'être plus rapide que les approches NumPy à toutes les échelles.
Le tournant sera toujours flottant? ou pourrait être comme dans
l2
?@ MrNobody33 ne sera pas toujours flottant. Parfois c'est float ou int