파이썬 넘파이(Numpy) 인덱싱과 슬라이싱 총정리 (불리언 & 팬시 인덱싱까지)

넘파이(Numpy) 배열을 사용하여 데이터를 다룰 때 가장 중요한 것은 내가 원하는 데이터만을 쏙쏙 골라내는 능력일 것입니다. 이때 사용되는 기능이 바로 인덱싱(Indexing)입니다. 

이 글에서는 넘파이 배열의 핵심 활용법인 인덱싱(Indexing)에 대해서 기본 인덱싱부터 고급 인덱싱인 팬시 인덱싱 까지 기본 개념을 정리하고, 다차원 배열의 구조와 함께 필요할 때마다 참고할 수 있도록, 이 글 하나에 모든 넘파이 인덱싱 사용법을 총정리해 보도록 하겠습니다. 


01. 인덱싱(Indexing) 이란?

인덱싱(Indexing)이란 배열의 특정 요소나 부분을 추출하는 모든 행위를 말하며, 방식과 특징에  따라 기본 인덱싱(Basic Indexing)과 고급 인덱싱(Advanced Indexing)으로 나뉘어 집니다.

  1. 기본 인덱싱 
    • 단일 요소 인덱싱 (Single Element Indexing) : 단일 위치의 값 하나만 추출 (ex, a[0])
    • 슬라이싱 (Slicing) : 특정 범위의 연속된 값 추출 (ex, a[1:7])
    • 스트라이딩 (Striding) : 특정 범위내에서 일정 간격을 건너뛰며 값을 추출 (ex, a[1:7:2])
    1. 고급 인덱싱 = 팬시 인덱싱 (Fancy Indexing) 
      • 정수 배열 인덱싱 (Integer Array Indexing) : 원하는 인덱스를 배열 형식으로 전달하여 추출 (ex, a[[0, 2, 4]])
      • 불리언 배열 인덱싱 (Boolean Array Indexing) : 조건식(True/False)으로 필요한 데이터만 마스킹하여 추출 (ex, a[a>10])

    02. 다차원 배열의 구조와 인덱스 위치

    인덱싱 방법을 설명하기에 앞서, 다차원 배열의 구조와 각 차원별 배열의 인덱스 위치를 정확히 확인 할 필요가 있습니다. 

    먼저 각 차원별 배열의 인덱스 순서를 확인 후에, 아래에 있는 n차원 배열의 구조를 나타낸 그림을 보시면 1차원 배열부터 3차원 배열까지 배열의 구조와 인덱스 위치를 직관적으로 이해할 수 있으리라 생각됩니다.  

    • 1차원 배열 인덱스 순서 : array[ col ]
    • 2차원 배열 인덱스 순서 : array[ row, col ]
    • 3차원 배열 인덱스 순서 : array[ depth, row, col ]
    넘파이 1차원, 2차원, 3차원 배열의 구조
    n차원 배열의 구조

    03. 기본 인덱싱   

    기본 인덱싱(Basic Indexing)에는 단일 요소 인덱싱과 슬라이싱, 스트라이딩이 있습니다.

    1. 단일 요소 인덱싱(Single Element Indexing) : 특정 위치를 지정하여 값 하나를 추출 (a[index])
    2. 슬라이싱(Slicing) : 범위를 지정하여 연속된 값을 추출 (a[시작:끝])
    3. 스트라이딩(Striding) : 범위와 간격을 지정하여 일정한 간격을 두고 건너뛰며 값을 추출 (a[시작:끝:간격])

      a = np.array([11, 22, 33, 44, 55, 66, 77])
      
      print(a[0])     # 결과 : 11            (단일요소인덱싱)
      print(a[1:4])   # 결과 : [22, 33, 44]  (슬라이싱)
      print(a[1:6:2]) # 결과 : [22, 44, 66]  (스트라이딩)
      


      1) 단일 요소 인덱싱 (Single Element Indexing)

      가장 기본이 되는 단일 요소 인덱싱은 특정 위치의 값 하나를 정확히 추출하기 위해 사용됩니다. 인덱스에 마이너스(-) 부호가 있다면 반대방향으로 진행한다는 점만 기억해두면 어렵지 않게 사용할 수 있습니다. 

      • a[index]
        • a[0] : 첫번째 데이터 선택 (인덱스 위치는 0부터 시작함)
        • a[-1] : 마지막 데이터 선택 (인덱스의 마이너스는 반대반향, 즉 뒤에서부터 거꾸로 진행함)

        (1) 1차원 배열 - 단일 요소 인덱싱

        a1 = np.array([10, 20, 30, 40])
        
        print(a1[0])         # 결과: 10 
        print(a1[1])         # 결과: 20
        print(a1[-1])        # 결과: 40
        print(a1[-2])        # 결과: 30
        

        (2) 2차원 배열 - 단일 요소 인덱싱

        a2 = np.array([[0, 1, 2], [3, 4, 5]])
        
        print(a2[0])         # 결과: [0 1 2]
        print(a2[1])         # 결과: [3 4 5] 
        print(a2[0, 0])      # 결과: 0
        print(a2[1, 2])      # 결과: 5
        

        (3) 3차원 배열 - 단일 요소 인덱싱

        a3 = np.array([[[0, 1, 2], [3,  4,  5]],
                       [[6, 7, 8], [9, 10, 11]]])
        
        print(a3[0])         # 결과: [[0  1  2]
                             #        [3  4  5]]
        print(a3[1])         # 결과: [[6  7  8]
                             #        [9 10 11]] 
        print(a3[0, 0])      # 결과: [0  1  2]
        print(a3[1, 1])      # 결과: [9 10 11]
        print(a3[0, 1, 2])   # 결과: 5
        print(a3[1, 1, 1])   # 결과: 10
        


        2) 슬라이싱 (Slicing)

        슬라이싱은 콜론(:)을 이용하여 [시작:끝] 범위의 연속된 값을 추출할 때 사용합니다.   

        • a[시작:끝]
          • a[1:4] : 인덱스 1~3까지의 데이터 선택 (마지막 4는 포함 안됨)
          • a[ : ] : 전체 데이터 선택
          • a[2:] : 인덱스 2부터 이후 데이터 선택
          • a[:3] : 인덱스 0~2까지 데이터 선택 (마지막 3은 포함 안됨 )

        (1) 1차원 배열 - 슬라이싱

        a1 = np.array([0, 1, 2, 3, 4])
        
        print(a1[1:4])      # 결과: [1 2 3]
        print(a1[:])        # 결과: [0 1 2 3 4]
        print(a1[2:])       # 결과: [2 3 4]
        print(a1[:3])       # 결과: [0 1 2]
        

        (2) 2차원 배열 - 슬라이싱

        a2 = np.array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]])
        
        print(a2[0, 1:4])   # 결과: [1 2 3]
        print(a2[:, 1:4])   # 결과: [[1 2 3]
                            #        [6 7 8]]
        


        3) 스트라이딩 (Striding)

        스타라이딩은 슬라이싱에서 간격 정보를 추가하여 지정된 범위 내에서 일정한 간격을 두고 건너뛰면서 규칙적인 값을 추출합니다. 

        • a[시작:끝:간격]
          • a[1:7:2] : 인덱스 1~6까지, 2간격으로 데이터 선택 (인덱스 1, 3, 5 선택)

        (1) 1차원 배열 - 스트라이딩

        a1 = np.array([0, 1, 2, 3, 4, 5, 6, 7])
        
        print(a1[1:7:2])    # 결과: [1 3 5]
        


        04. 고급 인덱싱 = 팬시 인덱싱(Fancy Indexing)

        고급 인덱싱(Advanced Indexing)은 배열로 원하는 요소를 골라내는 인덱싱 방법을 말하며, 정수 배열 인덱싱과 불리언 배열 인덱싱이 있습니다. 이러한 고급 인덱싱 방식을 팬시 인덱싱이라고도 합니다. 

        앞서 설명한 콜론(:)을 이용하여 슬라이싱 또는 스트라이딩을 하게 되면 연속적이거나 규칙적인 데이터만 지정할 수 있습니다. 하지만 배열의 데이터를 선택할 때, 불규칙적인 데이터들을 선택해야 할 때가 있습니다. 이 때 유용하게 사용할 수 있는 인덱싱 방법이 바로 팬시 인덱싱(Fancy Indexing) 입니다


        1) 정수 배열 인덱싱 (Integer Array Indexing)

        정수 배열 인덱싱은 배열(리스트) 형식으로 전달된 인덱스 위치의 값을 가져옵니다. 사용 시 주의할 점은 다차원 배열의 경우 좌표처럼 계산되어 값을 선택하기 때문에 행과 열의 개수가 짝이 맞도록 사용해야 합니다. 

        • 1차원 배열인  경우 : a[[idx1, idx2, idx3]]
          • a[[0, 1, 7]] : 인덱스 0, 1, 7번 위치의 데이터 선택
          • a[range(0, 6, 2)] : 인덱스 0, 2, 4번 위치의 데이터 선택
        • 2차원 배열인 경우 : a[[idx11, idx12], [idx21, idx22]]
          • a[[0, 4], [0, 7]] : 인덱스 (0, 0)과 (4, 7) 위치의 데이터 선택
          • a[[2], [1, 3, 5]] : 인덱스 (2, 1), (2, 3), (2, 5) 위치의 데이터 선택  (브로드캐스팅 적용됨)

        (1) 1차원 배열 - 정수 배열 인덱싱

          a1 = np.array([10, 11, 12, 13, 14, 15, 16])
          
          index_list  = [0, 1, 5, 6]
          index_array = np.array([0, 0, 2, 2])
          
          print(a1[[0, 2, 4]])        # 결과: [10 12 14]
          print(a1[index_list])       # 결과: [10 11 15 16]
          print(a1[index_array])      # 결과: [10 10 12 12]
          print(a1[range(0, 6, 2)])   # 결과: [10 12 14]

          (2) 2차원 배열 - 정수 배열 인덱싱

            a2 = np.array([[1,  2,  3,  4], 
                           [5,  6,  7,  8],
                           [9, 10, 11, 12]])
            
            print(a2[[2, 2, 2], [0, 0, 3]])  # 결과: [9 9 12]
            print(a2[[2], [0, 0, 3]])        # 결과: [9 9 12]  (Broadcasting)
            print(a2[2, [0, 0, 3]])          # 결과: [9 9 12]
            
            print(a2[[0, 0, 2], [2, 2, 2]])  # 결과: [3 3 11]  
            print(a2[[0, 0, 2], [2]])        # 결과: [3 3 11]  (Broadcasting)
            print(a2[[0, 0, 2], 2])          # 결과: [3 3 11]
            
            print(a2[1, [0, 0, 3]])          # 결과: [5 5  8]
            print(a2[[0, 0, 2], 1])          # 결과: [2 2 10]
            
            print(a2[:, [0, 0, 3]])          # 결과: [[1 1  4]
                                             #        [5 5  8]
                                             #        [9 9 12]]
            
            print(a2[[0, 0, 2], :])          # 결과: [[1  2  3  4]
                                             #        [1  2  3  4] 
                                             #        [9 10 11 12]] 
            
            위 예제에서 처음 a2[[2, 2, 2], [0, 0, 3]] a2[[2], [0, 0, 3]]의 결과는 같습니다. 왜냐하면 a2[[2], [0, 0, 3]]의 경우에 처음 row(행)에 해당하는 [2] 부분이 브로드캐스팅(Broadcasting)되어 뒤에 이어서 나오는 col(열)에 해당하는 [0, 0, 3]의 개수에 맞춰 [2] → [2, 2, 2]로 계산되어 적용되기 때문입니다.  
            따라서 결론적으로, a2[[2], [0, 0, 3]]은 a2[[2, 2, 2], [0, 0, 3]]과 동일하게 동작하게 됩니다. 

            2) 불리언 배열 인덱싱 (Boolean Array Indexing)

            불리언 배열 인덱싱은 배열과 똑같은 크기의 True/False 배열(리스트) 사용하여 True값의 위치 데이터만 선택하여 값을 가져옵니다. 

            • a[조건식(True/False)]
              • a[[True, False, False, True]] : 인덱스 0, 3번째 데이터 선택
              • a[a > 2] : 배열 a의 데이터 중 2보다 큰 데이터만 선택
              • a[a%2 == 0] : 배열 a의 데이터 중 짝수 데이터만 선택

            (1) 1차원 배열 - 불리언 배열 인덱싱

            a1 = np.array([0, 1, 2, 3, 4])
            
            index_list  = [True, True, True, False, False]
            index_array = np.array([True, False, True, False, False])
            
            print(a1[[False, True, True, False, False]])  #결과: [1 2]
            print(a1[index_list])                         #결과: [0 1 2]  
            print(a1[index_array])                        #결과: [0 2]
            print(a1[a1 % 2 == 0])                        #결과: [0 2 4]
            print(a1[a1 > 2])                             #결과: [3 4]
            

            (2) 2차원 배열 - 불리언 배열 인덱싱

            a2 = np.array([[1,  2,  3,  4], 
                           [5,  6,  7,  8]])
            
            print(a2[a2 % 2 == 0])  #결과: [2 4 6 8]
            print(a2[a2 > 3])       #결과: [4 5 6 7 8]
            

            05. 주의사항 : 자주 실수하는 부분

            1) a[:, 1] vs a[:][1] 차이점 (다차원 인덱싱 vs 연쇄 인덱싱)

            넘파이 배열을 접근할 때, 파이썬의 리스트를 사용하듯이 접근하게 되면, 사용은 가능합니다. 하지만 아래와 같은 차이를 확실히 알아두고 사용해야 합니다. 

            • 다차원 인덱싱 : 넘파이에서 사용하는 방식으로 a[0, 1] 형식으로 사용함
            • 연쇄 인덱싱 : 파이썬의 리스트에서 사용하는 방식으로 a[0][1] 형식으로 사용함
            아래의 예제를 살펴보면 값 하나를 인덱싱 할 때는 두 방식이 같은 결과를 보여줍니다. 하지만 슬라이싱을 하게 되면 다른 결과가 나타나는 것을 볼 수 있습니다.
            이러한 이유는 넘파이에서 사용하는 다차원 인덱싱은 차원의 정보를 한번에 찾아서 실행합니다. 반면에 파이선의 리스트에서 사용하는 연쇄 인덱싱 방식은 a[:][1]의 경우 먼저 첫번째 a[:]를 실행한 후 그 값에서 다시 [1]을 실행해 그 값을 꺼내오기 때문입니다.

            a = np.array([[0, 1, 2], [3, 4, 5]])
            
            print(a[0][1])   # 결과: 1
            print(a[0, 1])   # 결과: 1
            
            print(a[:][1])   # 결과: [3 4 5]
            print(a[:, 1])   # 결과: [1 4]


            2) 뷰(View) 반환 vs 복사본(Copy) 반환

            기본 인덱싱으로 데이터를 추출하여 배열을 반환할 경우 뷰(View)를 반환하기 때문에 원본 데이터와 연동되어 값이 변경됩니다. 반면에 고급 인덱싱 방식으로 데이터를 추출하게 되면 그 값을 복사본(Copy)으로 반환하기 때문에 원본 데이터와는 무관하게 사용할 수 있습니다. 

            (1) 기본 인덱싱 - 뷰(View)반환

            original = np.array([1, 2, 3, 4])
            
            view_arr = original[1:3]   # 기본인덱싱(슬라이싱)으로 추출 (배열)
            print(original)            # 결과: [1, 2, 3, 4] 
            print(view_arr)            # 결과: [2 3]
            
            view_arr[0] = 99           # 값 변경
            print(original)            # 결과: [1, 99, 3, 4] (원본이 함께 바뀌어버림!)
            print(view_arr)            # 결과: [99 3]
            

            (2) 고급 인덱싱 - 복사본(Copy)반환

            original = np.array([1, 2, 3, 4])
            
            copy_arr = original[[1, 2]]  # 고급인덱싱(정수배열인덱싱)으로 추출 (배열)
            print(original)              # 결과: [1 2 3 4]
            print(copy_arr)              # 결과: [2 3]
            
            copy_arr[0] = 99             # 값 변경
            print(original)              # 결과: [1 2 3 4] (원본은 그대로임)
            print(copy_arr)              # 결과: [99 3]
            

            06. 마무리 

            지금까지 넘파이(Numpy) 배열의 핵심 중 하나인 인덱싱과 슬라이싱, 그리고 실수를 유발하기 쉬운 뷰(View)와 복사본(Copy)의 차이까지 정리해 보았습니다. 

            처음에는 다차원 배열의 좌표 체계나 팬시 인덱싱의 브로드캐스팅 개념이 다소 생소할 수 있지만, 이 도구들을 자유자재로 다루게 된다면, 수만 행의 데이터 속에서도 내가 원하는 정보를 단 한 줄로 뽑아낼 수 있습니다. 

            직접 주피터 노트북을 실행하여 예제 코드를 한번 실행해 보세요. 그리고 이 글을 북마크(Ctrl+D) 해두시고 필요할 때 마다 참고하시기 바랍니다. 

            마지막으로 넘파이 인덱싱 종류를 표로 정리하였습니다. 궁금한 점이 있다면 댓글로 남겨주세요!
            넘파이 인덱싱 종류
            넘파이 인덱싱 종류

            [ 함께보면 좋은 글 ]


            Previous Post
            No Comment
            Add Comment
            comment url