파크릿 파일의 데이터를 판다스로 필터링할 때 여러 가지 전략을 사용할 수 있습니다. 데이터 분할이 필터링 작업의 효율성을 크게 향상시킬 수 있다는 것이 널리 인정되지만, 파크릿 파일에 저장된 데이터를 쿼리하는 성능을 최적화하기 위한 추가적인 방법들도 있습니다. 분할은 단지 한 가지 옵션일 뿐입니다.
분할된 필드로 필터링
앞서 언급했듯이, 이 접근 방식은 가장 익숙하면서도 일반적으로 성능 최적화에 가장 큰 영향을 미칩니다. 이 뒤에 있는 이유는 간단합니다. 파티션을 사용할 경우, 전체 파일이나 심지어 전체 디렉토리의 파일을 선택적으로 읽지 않아도 되므로(즉, 조건 하강), 성능에 크고 획기적인 개선이 이루어집니다.
import pandas as pd
import time
from faker import Faker
fake = Faker()
MIL=1000000
NUM_OF_RECORDS=10*MIL
FOLDER="/tmp/out/"
PARTITIONED_PATH=f"{FOLDER}partitioned_{NUM_OF_RECORDS}/"
NON_PARTITIONED_PATH_PREFIX=f"{FOLDER}non_partitioned_{NUM_OF_RECORDS}.parquet"
print(f"Creating fake data")
data = {
'id': range(NUM_OF_RECORDS), # Generate IDs from 1 to 100
'name': [fake.name() for _ in range(NUM_OF_RECORDS)],
'age': [fake.random_int(min=18, max=99) for _ in range(NUM_OF_RECORDS)],
'state': [fake.state() for _ in range(NUM_OF_RECORDS)],
'city': [fake.city() for _ in range(NUM_OF_RECORDS)],
'street': [fake.street_address() for _ in range(NUM_OF_RECORDS)]
}
df = pd.DataFrame(data)
# 분할 없이 작성
df.to_parquet(path=NON_PARTITIONED_PATH)
# 분할된 데이터 작성
df.to_parquet(path=PARTITIONED_PATH, partition_cols=['state'])
# 비분할 데이터 읽기
start_time = time.time()
df1 = pd.read_parquet(path=NON_PARTITIONED_PATH)
df1 = df1[df1['state']=='California']
runtime1 = (time.time()) - start_time # 37 sec
# 분할된 데이터 읽기
start_time = time.time()
df2 = pd.read_parquet(path=PARTITIONED_PATH, filters=[('state','==','California')])
runtime2 = (time.time()) - start_time # 0.20 sec
시간 개선(메모리 및 CPU 사용량 감소 포함)은 상당합니다. 37초에서 단지 0.20초로 감소합니다.
비분할 필드로 필터링
위의 예에서 우리는 분할된 필드를 기반으로 필터링하여 데이터 검색을 향상시키는 방법을 살펴보았습니다. 그러나 일부 시나리오에서는 원하는 필드로 데이터를 효과적으로 분할할 수 없습니다. 또한 때로는 여러 필드를 기반으로 필터링이 필요합니다. 이것은 모든 입력 파일을 열어야 하므로 성능에 해로울 수 있습니다.
고맙게도 Parquet은 이 문제를 완화하기 위한 현명한 해결책을 제공합니다. Parquet 파일은 행 그룹으로 분할됩니다. 각 행 그룹 내에서 Parquet은 메타데이터를 저장합니다. 이 메타데이터에는 각 필드에 대한 최소값과 최대값이 포함됩니다.
Pandas로 Parquet 파일을 작성할 때 각 행 그룹에 대한 레코드 수를 선택할 수 있습니다.
Pandas를 사용하여 필터링된 Parquet 파일을 읽을 때, Pandas 라이브러리는 이 메타데이터를 활용하여 메모리에 로드된 데이터를 효율적으로 필터링합니다. 원하는 필드가 행 그룹의 최소/최대 범위를 벗어나는 경우, 해당 행 그룹은 우아하게 건너뜁니다.
df = pd.DataFrame(data)
# 파티션되지 않은 데이터 작성, 행 그룹 크기 지정
df.to_parquet(path=PATH_TO_PARQUET_FILE, row_group_size=1000000)
# 파티션되지 않은 데이터 읽기 및 행 그룹만 필터링
df = pd.read_parquet(path=DATASET_PATH, filters=[('state','==','California')])
Parquet 파일 내의 메타데이터를 보는 것은 PyArrow
를 사용하여 수행할 수 있습니다.
>>> import pyarrow.parquet as pq
>>> parquet_file = pq.ParquetFile(PATH_TO_PARQUET_FILE)
>>> parquet_file.metadata
<pyarrow._parquet.FileMetaData object at 0x125b21220>
created_by: parquet-cpp-arrow version 11.0.0
num_columns: 6
num_rows: 1000000
num_row_groups: 10
format_version: 2.6
serialized_size: 9325
>>> parquet_file.metadata.row_group(0).column(3)
<pyarrow._parquet.ColumnChunkMetaData object at 0x125b5b180>
file_offset: 1675616
file_path:
physical_type: BYTE_ARRAY
num_values: 100000
path_in_schema: state
is_stats_set: True
statistics:
<pyarrow._parquet.Statistics object at 0x115283590>
has_min_max: True
min: Alabama
max: Wyoming
null_count: 0
distinct_count: 0
num_values: 100000
physical_type: BYTE_ARRAY
logical_type: String
converted_type (legacy): UTF8
compression: SNAPPY
encodings: ('RLE_DICTIONARY', 'PLAIN', 'RLE')
has_dictionary_page: True
dictionary_page_offset: 1599792
data_page_offset: 1600354
total_compressed_size: 75824
total_uncompressed_size: 75891
전체 파일의 메타데이터에서 행 그룹의 수가 언급되어 있음을 주목하십시오. 그리고 각 행 그룹의 각 열에 대한 통계 섹션에 최소값과 최대값이 언급되어 있습니다.
그러나 이 Parquet 기능을 더욱 최적화된 결과를 위해 더 활용할 수 있는 방법이 있습니다: 정렬.
정렬된 필드로 필터링
앞선 섹션에서 언급한 바와 같이, Parquet에 의해 저장되는 메타데이터의 일부는 각 행 그룹 내의 각 필드에 대한 최소값과 최대값을 포함합니다. 데이터가 필터링할 필드를 기준으로 정렬되어 있으면, Pandas는 더 많은 행 그룹을 건너뛸 가능성이 높아집니다.
예를 들어, ‘state’를 나타내는 필드를 포함하는 레코드 목록이 포함된 데이터셋을 고려해 보겠습니다. 레코드가 정렬되지 않은 경우, 각 주가 대부분의 행 그룹에 나타날 가능성이 높습니다. 이전 섹션의 메타데이터를 살펴보면 첫 번째 행 그룹 만으로도 ‘앨라배마’에서 ‘와이오밍’까지 모든 주를 보유하고 있음을 알 수 있습니다.
그러나 ‘state’ 필드를 기준으로 데이터를 정렬하면 많은 행 그룹을 건너뛸 가능성이 크게 높아집니다.
df = pd.DataFrame(data)
# 'state'를 기준으로 데이터 정렬
df.sort_values("state").to_parquet(path=NON_PARTITIONED_SORTED_PATH)
이제 메타데이터를 다시 살펴보고 어떻게 변경되었는지 살펴보겠습니다.
>>> parquet_file = pq.ParquetFile(PATH_TO_PARQUET_FILE)
>>> parquet_file.metadata.row_group(0).column(3).statistics.min
'Alabama'
>>> parquet_file.metadata.row_group(0).column(3).statistics.max
'Kentucky'
>>> parquet_file.metadata.row_group(1).column(3).statistics.min
'Kentucky'
>>> parquet_file.metadata.row_group(1).column(3).statistics.max
'North Dakota'
>>> parquet_file.metadata.row_group(2).column(3).statistics.min
'North Dakota'
>>> parquet_file.metadata.row_group(2).column(3).statistics.max
'Wyoming'
보시다시피, 주별로 정렬한 후 최소-최대 값이 해당하여 각 행 그룹이 모든 주 대신 일부 주만 보유하고 있습니다. 이는 필터로 읽는 것이 이제 훨씬 빨라야 함을 의미합니다.
이제 데이터 읽기의 성능에 어떤 영향을 미치는지 살펴보겠습니다. 데이터 읽기 코드는 변경되지 않았습니다.
# 주 기준으로 정렬된 데이터를 읽고 행 그룹별로 필터링
start_time = time.time()
df = pd.read_parquet(path=DATASET_PATH, filters=[('state','==','California')])
runtime = (time.time()) - start_time # 0.24 seconds
놀랍게도 여기서의 성능은 파티션을 사용하는 것과 거의 비슷합니다.
이 원칙은 파티셔닝된 데이터와 비파티셔닝 데이터 모두에 적용됩니다. 두 방법을 동시에 사용할 수 있습니다. 때로는 필드 A를 기준으로 데이터를 필터링하고 싶을 때와 필드 B를 기준으로 필터링하고 싶을 때가 있다면, 필드 A로 파티셔닝하고 필드 B로 정렬하는 것이 좋은 옵션일 수 있습니다.
다른 경우, 예를 들어 필터링하려는 필드가 높은 카디널리티를 가진 필드인 경우, 값의 해시(버킷화)로 파티셔닝하고 데이터를 실제 값으로 정렬할 수 있습니다. 이런 식으로 파티셔닝과 행 그룹의 장점을 모두 亨용할 수 있습니다.
열의 하위 집합 읽기
덜 일반적이지만, 데이터 검색 중에 더 나은 결과를 달성하기 위한 또 다른 방법은 작업에 필수적인 특정 필드만 선택하는 것입니다. 이 전략은 때때로 성능에 향상을 가져올 수 있습니다. 이는 Parquet 형식의 특성 때문입니다. Parquet는 열 기반 형식으로 구현되어 있어 각 행 그룹 내에서 데이터를 열별로 저장합니다. 일부 열만 읽는 것은 다른 열을 건너뛰게 됩니다.
start_time = time.time()
df = pd.read_parquet(path=NON_PARTITIONED_SORTED_PATH, columns=["name", "state"])
runtime = (time.time()) - start_time # 0.08 seconds
당연히 성능 향상은 큽니다.
결론
데이터 파티셔닝은 일반적으로 최선의 접근 방식이지만, 항상 가능한 것은 아닙니다. 데이터를 정렬하면 큰 개선이 이루어질 수 있습니다. 더 많은 행 그룹을 건너뛸 수 있습니다. 또한 가능하다면 필요한 열만 선택하는 것이 항상 좋은 선택입니다.
이 게시물은 Parquet와 pandas를 사용하여 더 나은 성능을 달성하는 방법을 이해하는 데 도움이 되었습니다.
Source:
https://dzone.com/articles/parquet-data-filtering-with-pandas