Programming/Python

[Python] 리스트 값을 기준으로 다른 리스트 분리하기

Jonghyuk Baek 2022. 6. 15. 00:28

Introduction

가끔, 매번 외워두고 쓸 정도로 자주 쓰는 기능은 아닌데 막상 사용하려고 하면 까먹어서 다시 찾아보게 되는 기능들이 몇 가지 있다. 오늘 소개할 내용이 그런 내용인데, 바로 리스트 A의 각 요소 값을 기준으로 리스트 B를 분리하는 기능이다.

 

느낌적인 느낌은 아래와 같다.

list_a = [1, 2, 3, 4, 5]
list_b = [2.5, 4.5]

>>> split_list_by_thresholds(target=list_a, thresholds=list_b)
[[1, 2], [3, 4], [5]]

list_b를 임계값 리스트로 사용해 list_a를 분리한다고 했을 때 원하는 결과의 모습이다.


Solution

코드 공간을 더 많이 차지하지만 더 빠른 솔루션이 있고, 그냥 한 줄에 끝낼 수 있는 솔루션이 있다. 본인은 직접 안 짜고 가져다 쓸 거라면 최대한 간단하게 가져다 쓰고 싶기 때문에 더 빠른 솔루션을 소개하도록 하겠다.

 

위 기능은 Numpy 모듈의 np.searchsortednp.split을 사용해 한 줄로 간단하게 구현할 수 있다. 아래 블럭의 6번 라인이다.

import numpy as np

list_a = [1, 2, 3, 4, 5]
list_b = [2.5, 4.5]

list_a_split = np.split(list_a, np.searchsorted(list_a, list_b))

>>> print(list_a, list_b)
[1, 2, 3, 4, 5] [2.5, 4.5]

>>> print(list_a_split)
[array([1, 2]), array([3, 4]), array([5])]

세부적인 동작에 대해서는 아래에서 알아보자.

 

np.searchsorted

np.searchsorted 는 새 엘리먼트를 기존 리스트 (혹은 array) 에 추가할 때 어떤 인덱스에다 집어 넣어야 기존의 sorting이 유지될 지 판단하고 해당 인덱스를 반환하는 함수다. 기본적으로 규칙은 아래와 같다.

 

side returned index i satisfies
left a[i-1] < v <= a[i]
right
a[i-1] <= v < a[i]

left-side로 정렬되었다 가정할 경우 i번째 인덱스에 새 엘리먼트 v를 대입했을 때 그 값이 좌측 엘리먼트보다 크고, 우측 엘리먼트와 같거나 작다면 해당 인덱스가 반환된다. right-side의 경우 동등 조건이 왼쪽 항으로 이동한다.

 

결과적으로는 새 엘리먼트 값이 기존 리스트의 어떤 엘리먼트 값과 같을 경우 left 옵션은 기존 리스트의 해당 엘리먼트 인덱스 왼쪽에, right 옵션은 해당 엘리먼트 인덱스 오른쪽에 새 값을 추가하기 위한 인덱스 값을 반환한다.

 

여러 value가 한꺼번에 제공될 경우 각 결과를 한번에 모아 반환한다. 아래 결과를 참고하자.

import numpy as np

list_a = [1, 2, 3, 4, 5]
list_b = [2.5, 4.5]

>>> print(np.searchsorted(list_a, list_b))
[2 4]

주의할 점은, 해당 함수는 서치 타겟이 되는 리스트가 순차적으로 커지는 순서로 이미 정렬되어 있다고 가정하기 때문에 정렬되지 않은 리스트를 제공할 경우 동작이 예상과 다를 수 있다.

import numpy as np

list_a = [3, 2, 3, 4, 5]
list_b = [2.5, 4.5]

>>> print(np.searchsorted(list_a, list_b))
[2 4]

로컬하게는 위에서 언급한 조건이 맞아떨어지기 때문에 2.5, 4.5 가 각각 [2, 3] 사이와 [4, 5] 사이에 들어가야 한다고 결과가 나오고 있지만, 만약 사용자가 원하는 작업이 특정 대역의 값에 속하는 리스트 엘리먼트만을 모두 모아보자 했던 것이라면 이는 원치 않는 결과값일 수 있다.

 

sorted 메서드를 사용해 미리 리스트를 정렬하거나, sorter 라는 네임드 파라미터로 리스트를 정렬하기 위한 인덱스 리스트를 따로 제공해 줄 수 있다.

import numpy as np

list_a = [8, 2, 3, 4, 5]
list_b = [2.5, 4.5]

>>> print(np.searchsorted(list_a, list_b, sorter=np.argsort(list_a)))
[1 3]

[2, 3, 4, 5, 8] 로 정렬된 리스트 상에서의 삽입 위치를 반환하는 것을 볼 수 있다.

 

np.split

엘리먼트 삽입 위치를 찾았다면 np.split을 사용해 간단하게 분리할 수 있다. 이 메서드는 동작이 두 갈래로 나뉘는데, 두 번째 인자가 integer N이라면 어레이를 N등분해 반환하고, 정렬된 1-D array라면 해당 값을 split 할 인덱스로 사용해 어레이를 분리해 준다 ([:a], [a:b], [b:]).

import numpy as np

list_a = [1, 2, 3, 4, 5, 6]
list_b = [2, 4]

# split by sections
>>> print(np.split(np.array(list_a), 2))
[array([1, 2, 3]), array([4, 5, 6])]

# split by indices
>>> print(np.split(list_a, list_b))
[array([1, 2]), array([3, 4]), array([5, 6])]

참고로 N section으로 등분하는 경우 리스트가 제공되면 작동하지 않는다 (내부적으로 shape를 호출함).

 

이것을 앞의 기능과 결합하면 우리가 원하던 기능을 간단하게 수행할 수 있다.

import numpy as np

list_a = [1, 2, 3, 4, 5]
list_b = [2.5, 4.5]

list_a_split = np.split(list_a, np.searchsorted(list_a, list_b))

>>> print(list_a, list_b)
[1, 2, 3, 4, 5] [2.5, 4.5]

>>> print(list_a_split)
[array([1, 2]), array([3, 4]), array([5])]

뭔가 따로 메서드를 작성하자니 그럴 정도는 아니고 그렇다고 바로 가져다 쓸 도구가 있나 하면 생각나지 않아서 결국 구글링을 하게 되는 기능이라 한번 기록해놓고 본인도 이제부터 기억할 겸 공유한다.

 

아래는 참고할 만한 페이지들이다.

References

np.searchsorted

https://numpy.org/doc/stable/reference/generated/numpy.searchsorted.html

np.split

https://numpy.org/doc/stable/reference/generated/numpy.split.html

제시된 방법보다 코드는 길지만 50% 가량 더 빠른 솔루션

https://stackoverflow.com/a/32618697/15002409

 

반응형