import numpy as np구조화된 데이터: NumPy 구조화된 배열
데이터가 동질적인 데이터 배열로 잘 표현되는 경우가 많지만 그렇지 않은 상황도 있습니다. 이번 장에서는 다양한 형식의 데이터를 효율적으로 저장할 수 있는 NumPy의 구조적 배열 및 레코드 배열의 사용을 보여줍니다. 여기에 표시된 패턴은 간단한 작업에 유용하지만, 이런 경우에는 대개 Part 3에서 살펴볼 Pandas DataFrame을 사용하는 데 적합합니다.
많은 사람에 대한 여러 범주의 데이터(예: 이름, 나이, 체중)가 있고 파이썬(Python) 프로그램에서 사용하기 위해 이러한 값을 저장하려 한다고 가정해 봅시다. 이를 세 개의 개별 배열에 저장합니다.
name = ["Alice", "Bob", "Cathy", "Doug"]
age = [25, 45, 37, 19]
weight = [55.0, 85.5, 68.0, 61.5]그런데 이게 좀 번거롭습니다. 여기에는 세 개의 배열이 서로 관련되어 있음을 보여주지 못합니다. NumPy의 구조화된 배열을 사용하면 단일 구조를 사용하여 이 모든 데이터를 저장함으로써 더 직관적으로 처리합니다.
이전에 다음과 같은 표현식을 사용하여 간단한 배열을 만들었습니다.
x = np.zeros(4, dtype=int)마찬가지로 복합 데이터 타입을 정의하여 구조화된 배열을 만들 수 있습니다.
# Use a compound data type for structured arrays
data = np.zeros(
4, dtype={"names": ("name", "age", "weight"), "formats": ("U10", "i4", "f8")}
)
print(data.dtype)[('name', '<U10'), ('age', '<i4'), ('weight', '<f8')]
여기서 'U10'은 “최대 길이 10의 유니코드 문자열”로 의미하며, 'i4'는 “4바이트(즉, 32비트) 정수”로 변환되며, 'f8'은 “8바이트(즉, 64비트) 부동 소수점”으로 변환됩니다. 다음 섹션에서는 이러한 유형 코드에 대한 다른 옵션에 대해 다룹니다.
이제 빈 컨테이너 배열을 만들었으므로 값 목록으로 배열을 채울 수 있습니다.
data["name"] = name
data["age"] = age
data["weight"] = weight
print(data)[('Alice', 25, 55. ) ('Bob', 45, 85.5) ('Cathy', 37, 68. )
('Doug', 19, 61.5)]
우리가 바라던 대로 이제 데이터는 하나의 구조화된 배열로 정리되었습니다.
구조화된 배열의 장점은 이제 인덱스나 이름으로 값을 참조할 수 있다는 것입니다.
# Get all names
data["name"]array(['Alice', 'Bob', 'Cathy', 'Doug'], dtype='<U10')
# Get first row of data
data[0]('Alice', 25, 55.)
# Get the name from the last row
data[-1]["name"]'Doug'
부울 마스킹을 사용하면 연령 조건을 활용해 특정 데이터만 추출할 수도 있습니다.
# Get names where age is under 30
data[data["age"] < 30]["name"]array(['Alice', 'Doug'], dtype='<U10')
이보다 더 복잡한 작업을 수행하려면 4부에서 다루는 Pandas 패키지를 활용하는 것이 좋습니다. 보시다시피 Pandas는 여기서 본 것과 유사한 다양하고 유용한 데이터 조작 기능을 제공하는 NumPy 배열을 기반으로 구축된 구조인 ‘DataFrame’ 객체를 제공합니다.
구조적 배열 생성 탐색
구조적 배열 데이터 유형은 다양한 방법으로 정의합니다. 앞서 우리는 사전 메소드를 보았습니다:
np.dtype({"names": ("name", "age", "weight"), "formats": ("U10", "i4", "f8")})dtype([('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])
더 명확하게 파이썬(Python) 유형이나 NumPydtype을 사용하여 숫자 유형을 정의합니다.
np.dtype(
{"names": ("name", "age", "weight"), "formats": ((np.str_, 10), int, np.float32)}
)dtype([('name', '<U10'), ('age', '<i8'), ('weight', '<f4')])
복합 유형은 튜플 목록으로 지정할 수도 있습니다.
np.dtype([("name", "S10"), ("age", "i4"), ("weight", "f8")])dtype([('name', 'S10'), ('age', '<i4'), ('weight', '<f8')])
유형 이름이 중요치 않다면 쉼표로 구분된 문자열로 유형만 정의합니다.
np.dtype("S10,i4,f8")dtype([('f0', 'S10'), ('f1', '<i4'), ('f2', '<f8')])
단축된 문자열 형식 코드는 한눈에 들어오지 않을 수 있지만, 간단한 원칙을 기반으로 구축되었습니다. 첫 번째(선택 사항) 문자 < 또는 >는 각각 “리틀 엔디안” 또는 “빅 엔디안”을 의미하며 중요한 비트의 순서 지정 규칙을 의미합니다. 다음 문자는 데이터 유형(문자, 바이트, 정수, 부동 소수점 등)을 지정합니다(아래 표 참조). 마지막 문자는 객체의 크기를 바이트 단위로 나타냅니다.
| 캐릭터 | 설명 | 예 |
|---|---|---|
b'` | 바이트 | `np.dtype('b')` | |나’| 부호 있는 정수 |np.dtype(‘i4’) == np.int32| | ``u' |
부호 없는 정수 | np.dtype('u1') == np.uint8 |
f'` | 부동 소수점 | `np.dtype('f8') == np.int64` | |c’| 복소수 부동 소수점|np.dtype(‘c16’) == np.complex128| | ``S', a'` | 문자열 | `np.dtype('S5')` | |U’| 유니코드 문자열 |np.dtype(‘U’) == np.str_| | ``V' |
원시 데이터(무효) | np.dtype('V') == np.void |
고급 복합 타입
더욱 고급 복합 유형을 정의합니다. 예를 들어 각 요소에 값의 배열이나 행렬이 포함된 유형을 생성합니다. 여기서는 \(3\times 3\) 부동 소수점 행렬로 구성된 mat 구성 요소를 사용하여 데이터 유형을 생성합니다.
tp = np.dtype([("id", "i8"), ("mat", "f8", (3, 3))])
X = np.zeros(1, dtype=tp)
print(X[0])
print(X["mat"][0])(0, [[0., 0., 0.], [0., 0., 0.], [0., 0., 0.]])
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
이제 X 배열의 각 요소는 id와 \(3\times 3\) 행렬로 구성됩니다. 단순한 다차원 배열이나 파이썬(Python) 사전 대신 이것을 사용하는 이유는 무엇일까요? 한 가지 이유는 이 NumPy dtype이 C 구조 정의에 직접 매핑되므로 적절하게 작성된 C 프로그램 내에서 배열 내용이 포함된 버퍼에 접근할 수 있기 때문입니다. 구조화된 데이터를 조작하는 레거시 C 또는 Fortran 라이브러리에 파이썬(Python) 인터페이스를 작성하는 경우 구조화된 배열이 강력한 인터페이스를 역할을 합니다.
레코드 배열: 속성으로 접근하는 구조화된 배열
NumPy는 또한 방금 설명한 구조적 배열과 유사한 레코드 배열(np.recarray 클래스의 인스턴스)을 제공하지만 한 가지 추가 기능이 있습니다. 필드는 인덱싱이 아닌 속성(attribute)으로 접근할 수 있다는 점입니다. 이전에 다음을 작성하여 샘플 데이터 세트의 연령에 액세스했다는 점을 기억하세요.
data["age"]array([25, 45, 37, 19], dtype=int32)
대신 데이터를 레코드 배열로 보면 약간 더 적은 키 입력으로 이에 액세스합니다.
data_rec = data.view(np.recarray)
data_rec.agearray([25, 45, 37, 19], dtype=int32)
단점은 레코드 배열의 경우 접근 방식에 따라 필드에 액세스하는 데 약간의 추가 오버헤드가 있다는 것입니다.
%timeit data['age']
%timeit data_rec['age']
%timeit data_rec.age121 ns ± 1.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
2.41 µs ± 15.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
3.98 µs ± 20.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
더 편리한 표기법이 (약간의) 오버헤드보다 편의성이 더 중요한지는 애플리케이션에 따라 다릅니다.
Pandas로의 연결
구조화된 배열과 레코드 배열에 관한 이 장은 의도적으로 책의 이 부분 끝에 배치됩니다. 왜냐하면 이 장은 우리가 다룰 다음 패키지인 Pandas에 매우 잘 이어지기 때문입니다. 구조화된 배열은 NumPy 배열을 사용하여 C, Fortran 또는 다른 언어의 이진 데이터 형식에 매핑하는 경우와 같은 특정 상황에서 유용합니다. 그러나 구조화된 데이터를 일상적으로 사용하려면 Pandas 패키지가 훨씬 더 나은 선택입니다. 다음 장에서 이에 대해 자세히 살펴보겠습니다.