소개
이 랩에서는 Python 에서 대용량 데이터 세트를 메모리 효율적으로 저장하는 방법을 배우게 됩니다. 또한 튜플, 딕셔너리, 클래스, 명명된 튜플 (named tuple) 과 같은 다양한 레코드 표현 방법을 살펴볼 것입니다.
더 나아가, 서로 다른 데이터 구조의 메모리 사용량을 비교할 것입니다. 이러한 구조 간의 트레이드 오프를 이해하는 것은 데이터 분석을 수행하는 Python 사용자에게 코드 최적화에 도움이 되므로 매우 중요합니다.
이 랩에서는 Python 에서 대용량 데이터 세트를 메모리 효율적으로 저장하는 방법을 배우게 됩니다. 또한 튜플, 딕셔너리, 클래스, 명명된 튜플 (named tuple) 과 같은 다양한 레코드 표현 방법을 살펴볼 것입니다.
더 나아가, 서로 다른 데이터 구조의 메모리 사용량을 비교할 것입니다. 이러한 구조 간의 트레이드 오프를 이해하는 것은 데이터 분석을 수행하는 Python 사용자에게 코드 최적화에 도움이 되므로 매우 중요합니다.
작업할 데이터 세트를 자세히 살펴보면서 시작해 보겠습니다. 파일 ctabus.csv는 CSV (Comma-Separated Values, 쉼표로 구분된 값) 파일입니다. CSV 파일은 각 줄이 행을 나타내고 행 내의 값이 쉼표로 구분되는 표 형식 데이터를 저장하는 일반적인 방법입니다. 이 특정 파일은 2001 년 1 월 1 일부터 2013 년 8 월 31 일까지의 시카고 교통국 (CTA) 버스 시스템의 일일 승차 데이터를 담고 있습니다.
파일 압축을 풀고 zip 파일을 제거합니다.
cd /home/labex/project
unzip ctabus.csv.zip
rm ctabus.csv.zip
이 파일의 구조를 이해하기 위해 먼저 내부를 살펴보겠습니다. Python 을 사용하여 파일을 읽고 일부 줄을 출력합니다. 터미널을 열고 다음 Python 코드를 실행합니다.
f = open('/home/labex/project/ctabus.csv')
print(next(f)) ## Read the header line
print(next(f)) ## Read the first data line
print(next(f)) ## Read the second data line
f.close()
이 코드에서는 먼저 open 함수를 사용하여 파일을 열고 변수 f에 할당합니다. next 함수는 파일에서 다음 줄을 읽는 데 사용됩니다. 우리는 세 번 사용합니다. 첫 번째는 데이터 세트의 열 이름을 일반적으로 포함하는 헤더 줄을 읽기 위해 사용합니다. 두 번째와 세 번째는 각각 첫 번째 및 두 번째 데이터 줄을 읽습니다. 마지막으로, 시스템 리소스를 확보하기 위해 close 메서드를 사용하여 파일을 닫습니다.
다음과 유사한 출력을 볼 수 있습니다.
route,date,daytype,rides
3,01/01/2001,U,7354
4,01/01/2001,U,9288
이 출력은 파일에 4 개의 데이터 열이 있음을 보여줍니다. 각 열이 무엇을 나타내는지 자세히 살펴보겠습니다.
route: 이것은 버스 노선 이름 또는 번호입니다. 데이터 세트의 첫 번째 열 (열 0) 입니다.date: MM/DD/YYYY 형식의 날짜 문자열입니다. 이것은 두 번째 열 (열 1) 입니다.daytype: 요일 유형 코드이며 세 번째 열 (열 2) 입니다.
rides: 이 열은 총 승차 인원수를 정수로 기록합니다. 이것은 네 번째 열 (열 3) 입니다.rides 열은 특정 노선의 특정 날짜에 몇 명이 버스를 탔는지 알려줍니다. 예를 들어, 위의 출력에서 2001 년 1 월 1 일에 3 번 버스에 7,354 명이 탑승했음을 알 수 있습니다.
이제 파일에 몇 줄이 있는지 알아보겠습니다. 줄 수를 알면 데이터 세트의 크기를 파악할 수 있습니다. 다음 Python 코드를 실행합니다.
with open('/home/labex/project/ctabus.csv') as f:
line_count = sum(1 for line in f)
print(f"Total lines in the file: {line_count}")
이 코드에서는 with 문을 사용하여 파일을 엽니다. with를 사용하면 완료되면 자동으로 파일을 닫아주는 장점이 있습니다. 그런 다음 제너레이터 표현식 (1 for line in f)를 사용하여 파일의 각 줄에 대해 1 의 시퀀스를 생성합니다. sum 함수는 이 모든 1 을 더하여 파일의 총 줄 수를 제공합니다. 마지막으로 결과를 출력합니다.
이것은 약 577,564 줄을 출력해야 하며, 이는 상당한 데이터 세트를 처리하고 있음을 의미합니다. 이 대규모 데이터 세트는 분석하고 통찰력을 얻을 수 있는 풍부한 데이터를 제공합니다.
이 단계에서는 데이터를 저장하는 다양한 방법이 메모리 사용량에 어떤 영향을 미치는지 살펴보겠습니다. 메모리 사용량은 특히 대용량 데이터 세트를 처리할 때 프로그래밍의 중요한 측면입니다. Python 코드에서 사용되는 메모리를 측정하기 위해 Python 의 tracemalloc 모듈을 사용합니다. 이 모듈은 Python 에서 할당된 메모리를 추적할 수 있으므로 매우 유용합니다. 이를 사용하면 데이터 저장 방법이 얼마나 많은 메모리를 소비하는지 확인할 수 있습니다.
새로운 Python 파일을 만들어 보겠습니다. /home/labex/project 디렉토리로 이동하여 memory_test1.py라는 파일을 만듭니다. 텍스트 편집기를 사용하여 이 파일을 열 수 있습니다. 파일이 열리면 다음 코드를 추가합니다. 이 코드는 파일의 전체 내용을 단일 문자열로 읽고 메모리 사용량을 측정합니다.
## memory_test1.py
import tracemalloc
def test_single_string():
## Start tracking memory
tracemalloc.start()
## Read the entire file as a single string
with open('/home/labex/project/ctabus.csv') as f:
data = f.read()
## Get memory usage statistics
current, peak = tracemalloc.get_traced_memory()
print(f"File length: {len(data)} characters")
print(f"Current memory usage: {current/1024/1024:.2f} MB")
print(f"Peak memory usage: {peak/1024/1024:.2f} MB")
## Stop tracking memory
tracemalloc.stop()
if __name__ == "__main__":
test_single_string()
코드를 추가한 후 파일을 저장합니다. 이제 이 스크립트를 실행하려면 터미널을 열고 다음 명령을 실행합니다.
python3 /home/labex/project/memory_test1.py
스크립트를 실행하면 다음과 유사한 출력이 표시됩니다.
File length: 12361039 characters
Current memory usage: 11.80 MB
Peak memory usage: 23.58 MB
정확한 숫자는 시스템에 따라 다를 수 있지만 일반적으로 현재 메모리 사용량은 약 12MB 이고 최대 메모리 사용량은 약 24MB 임을 알 수 있습니다.
다음으로, 데이터를 저장하는 또 다른 방법을 테스트합니다. 동일한 /home/labex/project 디렉토리에 memory_test2.py라는 새 파일을 만듭니다. 편집기에서 이 파일을 열고 다음 코드를 추가합니다. 이 코드는 파일을 읽고 각 줄을 목록의 별도 문자열로 저장한 다음 메모리 사용량을 측정합니다.
## memory_test2.py
import tracemalloc
def test_list_of_strings():
## Start tracking memory
tracemalloc.start()
## Read the file as a list of strings (one string per line)
with open('/home/labex/project/ctabus.csv') as f:
lines = f.readlines()
## Get memory usage statistics
current, peak = tracemalloc.get_traced_memory()
print(f"Number of lines: {len(lines)}")
print(f"Current memory usage: {current/1024/1024:.2f} MB")
print(f"Peak memory usage: {peak/1024/1024:.2f} MB")
## Stop tracking memory
tracemalloc.stop()
if __name__ == "__main__":
test_list_of_strings()
파일을 저장한 다음 터미널에서 다음 명령을 사용하여 스크립트를 실행합니다.
python3 /home/labex/project/memory_test2.py
다음과 유사한 출력이 표시됩니다.
Number of lines: 577564
Current memory usage: 43.70 MB
Peak memory usage: 43.74 MB
데이터를 단일 문자열로 저장하는 이전 방법과 비교하여 메모리 사용량이 크게 증가했음을 알 수 있습니다. 이는 목록의 각 줄이 별도의 Python 문자열 객체이고 각 객체에 자체 메모리 오버헤드가 있기 때문입니다.
두 가지 접근 방식 간의 메모리 사용량 차이는 Python 프로그래밍에서 객체 오버헤드 (object overhead) 라고 하는 중요한 개념을 보여줍니다. 데이터를 문자열 목록으로 저장하면 각 문자열은 별도의 Python 객체입니다. 각 객체에는 다음과 같은 추가 메모리 요구 사항이 있습니다.
반면에 전체 파일 내용을 단일 문자열로 저장하면 객체가 하나만 있으므로 오버헤드도 하나만 있습니다. 이는 데이터의 총 크기를 고려할 때 메모리 효율성이 더 높습니다.
대용량 데이터 세트로 작업하는 프로그램을 설계할 때는 메모리 효율성과 데이터 접근성 간의 이러한 트레이드 오프를 고려해야 합니다. 때로는 문자열 목록에 저장된 데이터를 액세스하는 것이 더 편리할 수 있지만 더 많은 메모리를 사용합니다. 다른 경우에는 메모리 효율성을 우선시하고 데이터를 단일 문자열로 저장하도록 선택할 수 있습니다.
지금까지는 원시 텍스트 데이터를 저장하는 것을 다루었습니다. 그러나 데이터 분석과 관련하여 일반적으로 데이터를 더 체계적이고 구조화된 형식으로 변환해야 합니다. 이렇게 하면 다양한 작업을 수행하고 데이터에서 통찰력을 얻기가 더 쉬워집니다. 이 단계에서는 csv 모듈을 사용하여 데이터를 튜플 목록으로 읽는 방법을 배웁니다. 튜플은 여러 값을 저장할 수 있는 Python 의 간단하고 유용한 데이터 구조입니다.
/home/labex/project 디렉토리에 readrides.py라는 새 파일을 만들어 보겠습니다. 이 파일에는 CSV 파일에서 데이터를 읽어 튜플 목록으로 저장하는 코드가 포함됩니다.
## readrides.py
import csv
import tracemalloc
def read_rides_as_tuples(filename):
'''
Read the bus ride data as a list of tuples
'''
records = []
with open(filename) as f:
rows = csv.reader(f)
headings = next(rows) ## Skip headers
for row in rows:
route = row[0]
date = row[1]
daytype = row[2]
rides = int(row[3])
record = (route, date, daytype, rides)
records.append(record)
return records
if __name__ == '__main__':
tracemalloc.start()
rows = read_rides_as_tuples('/home/labex/project/ctabus.csv')
current, peak = tracemalloc.get_traced_memory()
print(f'Number of records: {len(rows)}')
print(f'First record: {rows[0]}')
print(f'Second record: {rows[1]}')
print(f'Memory Use: Current {current/1024/1024:.2f} MB, Peak {peak/1024/1024:.2f} MB')
이 스크립트는 read_rides_as_tuples라는 함수를 정의합니다. 단계별로 수행하는 작업은 다음과 같습니다.
filename 매개변수로 지정된 CSV 파일을 엽니다. 이를 통해 파일 내부의 데이터에 액세스할 수 있습니다.csv 모듈을 사용하여 파일의 각 줄을 구문 분석합니다. csv.reader 함수는 줄을 개별 값으로 분할하는 데 도움이 됩니다.records라는 목록에 추가합니다. 이 목록은 CSV 파일의 모든 레코드를 저장합니다.이제 스크립트를 실행해 보겠습니다. 터미널을 열고 다음 명령을 입력합니다.
python3 /home/labex/project/readrides.py
다음과 유사한 출력이 표시됩니다.
Number of records: 577563
First record: ('3', '01/01/2001', 'U', 7354)
Second record: ('4', '01/01/2001', 'U', 9288)
Memory Use: Current 89.12 MB, Peak 89.15 MB
이전 예제와 비교하여 메모리 사용량이 증가했음을 알 수 있습니다. 다음과 같은 몇 가지 이유가 있습니다.
이 접근 방식을 사용하는 장점은 이제 데이터가 제대로 구조화되어 분석할 준비가 되었다는 것입니다. 인덱스로 각 레코드의 특정 필드에 쉽게 액세스할 수 있습니다. 예를 들어:
## Example of accessing tuple elements (add this to readrides.py file to try it)
first_record = rows[0]
route = first_record[0]
date = first_record[1]
daytype = first_record[2]
rides = first_record[3]
print(f"Route: {route}, Date: {date}, Day type: {daytype}, Rides: {rides}")
그러나 숫자 인덱스로 데이터에 액세스하는 것은 항상 직관적이지 않습니다. 특히 많은 필드를 처리할 때 어떤 인덱스가 어떤 필드에 해당하는지 기억하기 어려울 수 있습니다. 다음 단계에서는 코드를 더 읽기 쉽고 유지 관리 가능하게 만들 수 있는 다른 데이터 구조를 살펴보겠습니다.
Python 에서 데이터 구조는 관련 데이터를 구성하고 저장하는 데 사용됩니다. 이는 구조화된 방식으로 다양한 유형의 정보를 담는 컨테이너와 같습니다. 이 단계에서는 다양한 데이터 구조를 비교하고 메모리를 얼마나 사용하는지 살펴보겠습니다.
/home/labex/project 디렉토리에 compare_structures.py라는 새 파일을 만들어 보겠습니다. 이 파일에는 CSV 파일에서 데이터를 읽어 다양한 데이터 구조에 저장하는 코드가 포함됩니다.
## compare_structures.py
import csv
import tracemalloc
from collections import namedtuple
## Define a named tuple for rides data
RideRecord = namedtuple('RideRecord', ['route', 'date', 'daytype', 'rides'])
## A named tuple is a lightweight class that allows you to access its fields by name.
## It's like a tuple, but with named attributes.
## Define a class with __slots__ for memory optimization
class SlottedRideRecord:
__slots__ = ['route', 'date', 'daytype', 'rides']
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
## A class with __slots__ is a memory - optimized class.
## It avoids using an instance dictionary, which saves memory.
## Define a regular class for rides data
class RegularRideRecord:
def __init__(self, route, date, daytype, rides):
self.route = route
self.date = date
self.daytype = daytype
self.rides = rides
## A regular class is an object - oriented way to represent data.
## It has named attributes and can have methods.
## Function to read data as tuples
def read_as_tuples(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## Skip headers
for row in rows:
record = (row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## This function reads data from a CSV file and stores it as tuples.
## Tuples are immutable sequences, and you access their elements by numeric index.
## Function to read data as dictionaries
def read_as_dicts(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows) ## Get headers
for row in rows:
record = {
'route': row[0],
'date': row[1],
'daytype': row[2],
'rides': int(row[3])
}
records.append(record)
return records
## This function reads data from a CSV file and stores it as dictionaries.
## Dictionaries use key - value pairs, so you can access elements by their names.
## Function to read data as named tuples
def read_as_named_tuples(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## Skip headers
for row in rows:
record = RideRecord(row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## This function reads data from a CSV file and stores it as named tuples.
## Named tuples combine the efficiency of tuples with the readability of named access.
## Function to read data as regular class instances
def read_as_regular_classes(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## Skip headers
for row in rows:
record = RegularRideRecord(row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## This function reads data from a CSV file and stores it as instances of a regular class.
## Regular classes allow you to add methods to your data.
## Function to read data as slotted class instances
def read_as_slotted_classes(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
next(rows) ## Skip headers
for row in rows:
record = SlottedRideRecord(row[0], row[1], row[2], int(row[3]))
records.append(record)
return records
## This function reads data from a CSV file and stores it as instances of a slotted class.
## Slotted classes are memory - optimized and still provide named access.
## Function to measure memory usage
def measure_memory(func, filename):
tracemalloc.start()
records = func(filename)
current, peak = tracemalloc.get_traced_memory()
## Demonstrate how to use each data structure
first_record = records[0]
if func.__name__ == 'read_as_tuples':
route, date, daytype, rides = first_record
elif func.__name__ == 'read_as_dicts':
route = first_record['route']
date = first_record['date']
daytype = first_record['daytype']
rides = first_record['rides']
else: ## named tuples and classes
route = first_record.route
date = first_record.date
daytype = first_record.daytype
rides = first_record.rides
print(f"Structure type: {func.__name__}")
print(f"Record count: {len(records)}")
print(f"Example access: Route={route}, Date={date}, Rides={rides}")
print(f"Current memory: {current/1024/1024:.2f} MB")
print(f"Peak memory: {peak/1024/1024:.2f} MB")
print("-" * 50)
tracemalloc.stop()
return current
if __name__ == "__main__":
filename = '/home/labex/project/ctabus.csv'
## Run all memory tests
print("Memory usage comparison for different data structures:\n")
results = []
for reader_func in [
read_as_tuples,
read_as_dicts,
read_as_named_tuples,
read_as_regular_classes,
read_as_slotted_classes
]:
memory = measure_memory(reader_func, filename)
results.append((reader_func.__name__, memory))
## Sort by memory usage (lowest first)
results.sort(key=lambda x: x[1])
print("\nRanking by memory efficiency (most efficient first):")
for i, (name, memory) in enumerate(results, 1):
print(f"{i}. {name}: {memory/1024/1024:.2f} MB")
스크립트를 실행하여 비교 결과를 확인합니다.
python3 /home/labex/project/compare_structures.py
출력에는 각 데이터 구조의 메모리 사용량과 메모리 효율성이 가장 높은 것부터 가장 낮은 것까지의 순위가 표시됩니다.
튜플 (Tuples):
record[0], record[1] 등과 같이 숫자 인덱스로 튜플의 요소에 액세스합니다.딕셔너리 (Dictionaries):
record['route'], record['date'] 등과 같이 더 읽기 쉽습니다.네임드 튜플 (Named Tuples):
record.route, record.date 등과 같이 점 표기법을 사용하여 요소에 액세스할 수 있습니다.일반 클래스 (Regular Classes):
record.route, record.date 등과 같이 점 표기법을 사용하여 속성에 액세스할 수 있습니다.__slots__가 있는 클래스 (Classes with __slots__):
__slots__가 있는 클래스는 메모리 최적화된 클래스입니다. 인스턴스 딕셔너리를 사용하지 않아 메모리를 절약합니다.record.route, record.date 등과 같이 속성에 대한 이름 지정된 액세스를 계속 제공합니다.__slots__가 있는 클래스를 사용합니다.요구 사항에 맞는 올바른 데이터 구조를 선택하면 특히 대용량 데이터 세트로 작업할 때 Python 프로그램의 성능과 메모리 사용량을 크게 향상시킬 수 있습니다.
이 Lab 에서는 Python 에서 레코드를 표현하는 다양한 방법을 배우고 메모리 효율성을 분석했습니다. 먼저, 기본적인 CSV 데이터 세트 구조를 이해하고 원시 텍스트 저장 방식을 비교했습니다. 그런 다음 튜플을 사용하여 구조화된 데이터를 사용하고 튜플, 딕셔너리, 네임드 튜플, 일반 클래스 및 __slots__가 있는 클래스 등 5 가지 다른 데이터 구조를 구현했습니다.
주요 내용은 서로 다른 데이터 구조가 메모리 효율성, 가독성 및 기능 간에 트레이드 오프를 제공한다는 것입니다. Python 의 객체 오버헤드는 대규모 데이터 세트의 메모리 사용량에 상당한 영향을 미치며, 데이터 구조의 선택은 메모리 소비에 큰 영향을 미칠 수 있습니다. 네임드 튜플과 __slots__가 있는 클래스는 메모리 효율성과 코드 가독성 사이의 좋은 절충안입니다. 이러한 개념은 데이터 처리에서 Python 개발자에게 유용하며, 특히 메모리 효율성이 중요한 대규모 데이터 세트를 처리할 때 유용합니다.