Python 의 일급 객체 (First-Class Objects) 메모리 모델 탐구

Intermediate

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 의 일급 객체 (first-class object) 개념에 대해 배우고 메모리 모델을 탐구합니다. Python 은 함수, 타입, 데이터를 일급 객체로 취급하여 강력하고 유연한 프로그래밍 패턴을 가능하게 합니다.

또한 CSV 데이터 처리를 위한 재사용 가능한 유틸리티 함수를 만들 것입니다. 특히, reader.py 파일에서 다양한 프로젝트에서 재사용할 수 있는 CSV 데이터를 읽기 위한 일반화된 함수를 만들 것입니다.

이것은 가이드 실험입니다. 학습과 실습을 돕기 위한 단계별 지침을 제공합니다.각 단계를 완료하고 실무 경험을 쌓기 위해 지침을 주의 깊게 따르세요. 과거 데이터에 따르면, 이것은 초급 레벨의 실험이며 완료율은 85%입니다.학습자들로부터 100%의 긍정적인 리뷰율을 받았습니다.

Python 의 일급 객체 이해

Python 에서는 모든 것이 객체로 취급됩니다. 여기에는 함수와 타입이 포함됩니다. 이것이 무엇을 의미할까요? 함수와 타입을 데이터 구조에 저장하고, 다른 함수의 인수로 전달하고, 심지어 다른 함수에서 반환할 수도 있다는 의미입니다. 이것은 매우 강력한 개념이며, CSV 데이터 처리를 예로 들어 살펴보겠습니다.

일급 타입 탐구

먼저, Python 인터프리터를 시작해 보겠습니다. WebIDE 에서 새 터미널을 열고 다음 명령을 입력합니다. 이 명령은 Python 코드를 실행할 Python 인터프리터를 시작합니다.

python3

Python 에서 CSV 파일을 사용할 때, 이러한 파일에서 읽은 문자열을 적절한 데이터 타입으로 변환해야 하는 경우가 많습니다. 예를 들어, CSV 파일의 숫자는 문자열로 읽힐 수 있지만, Python 코드에서는 정수 또는 부동 소수점으로 사용하고 싶을 수 있습니다. 이를 위해 변환 함수 목록을 만들 수 있습니다.

coltypes = [str, int, float]

문자열이 아닌 실제 타입 함수를 포함하는 목록을 만들고 있다는 점에 유의하십시오. Python 에서 타입은 일급 객체 (first-class object) 이므로 다른 객체와 마찬가지로 취급할 수 있습니다. 목록에 넣고, 전달하고, 코드에서 사용할 수 있습니다.

이제 포트폴리오 CSV 파일에서 데이터를 읽어 이러한 변환 함수를 어떻게 사용할 수 있는지 살펴보겠습니다.

import csv
f = open('portfolio.csv')
rows = csv.reader(f)
headers = next(rows)
row = next(rows)
print(row)

이 코드를 실행하면 다음과 유사한 출력을 볼 수 있습니다. 이것은 CSV 파일의 첫 번째 행 데이터이며, 문자열 목록으로 표현됩니다.

['AA', '100', '32.20']

다음으로, zip 함수를 사용합니다. zip 함수는 여러 반복 가능한 객체 (예: 목록 또는 튜플) 를 가져와 해당 요소들을 쌍으로 묶습니다. 각 행의 값과 해당 타입 변환 함수를 쌍으로 묶는 데 사용합니다.

r = list(zip(coltypes, row))
print(r)

그러면 다음과 같은 출력이 생성됩니다. 각 쌍은 타입 함수와 CSV 파일의 문자열 값을 포함합니다.

[(<class 'str'>, 'AA'), (<class 'int'>, '100'), (<class 'float'>, '32.20')]

이제 이러한 쌍이 있으므로 각 함수를 적용하여 값을 적절한 타입으로 변환할 수 있습니다.

record = [func(val) for func, val in zip(coltypes, row)]
print(record)

출력은 값이 적절한 타입으로 변환되었음을 보여줍니다. 문자열 'AA'는 문자열로 유지되고, '100'은 정수 100 이 되며, '32.20'은 부동 소수점 32.2 가 됩니다.

['AA', 100, 32.2]

또한 이러한 값을 열 이름과 결합하여 딕셔너리를 만들 수도 있습니다. 딕셔너리는 Python 에서 키 - 값 쌍을 저장할 수 있는 유용한 데이터 구조입니다.

record_dict = dict(zip(headers, record))
print(record_dict)

출력은 키가 열 이름이고 값이 변환된 데이터인 딕셔너리가 됩니다.

{'name': 'AA', 'shares': 100, 'price': 32.2}

이 모든 단계를 단일 컴프리헨션 (comprehension) 으로 수행할 수 있습니다. 컴프리헨션은 Python 에서 목록, 딕셔너리 또는 집합을 만드는 간결한 방법입니다.

result = {name: func(val) for name, func, val in zip(headers, coltypes, row)}
print(result)

출력은 이전과 동일한 딕셔너리가 됩니다.

{'name': 'AA', 'shares': 100, 'price': 32.2}

Python 인터프리터에서 작업을 마쳤으면 다음 명령을 입력하여 종료할 수 있습니다.

exit()

이 데모는 Python 이 함수를 일급 객체로 취급하는 방식이 강력한 데이터 처리 기술을 어떻게 가능하게 하는지 보여줍니다. 타입과 함수를 객체로 취급할 수 있으므로 더 유연하고 간결한 코드를 작성할 수 있습니다.

CSV 처리를 위한 유틸리티 함수 생성

이제 Python 의 일급 객체가 데이터 변환에 어떻게 도움이 되는지 이해했으므로, 재사용 가능한 유틸리티 함수를 만들 것입니다. 이 함수는 CSV 데이터를 읽어 딕셔너리 목록으로 변환합니다. CSV 파일은 일반적으로 표 형식 데이터를 저장하는 데 사용되며, 이를 딕셔너리 목록으로 변환하면 Python 에서 데이터를 더 쉽게 사용할 수 있으므로 매우 유용한 작업입니다.

CSV 리더 유틸리티 생성

먼저, WebIDE 를 엽니다. 열리면 프로젝트 디렉토리로 이동하여 reader.py라는 새 파일을 만듭니다. 이 파일에서 CSV 데이터를 읽고 타입 변환을 적용하는 함수를 정의합니다. CSV 파일의 데이터는 일반적으로 문자열로 읽히지만, 추가 처리를 위해 정수 또는 부동 소수점 숫자와 같은 다른 데이터 타입이 필요할 수 있으므로 타입 변환이 중요합니다.

reader.py에 다음 코드를 추가합니다.

import csv

def read_csv_as_dicts(filename, types):
    """
    Read a CSV file into a list of dictionaries, converting each field according
    to the types provided.

    Parameters:
    filename (str): Name of the CSV file to read
    types (list): List of type conversion functions for each column

    Returns:
    list: List of dictionaries representing the CSV data
    """
    records = []
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get the column headers

        for row in rows:
            ## Apply type conversions to each value in the row
            converted_row = [func(val) for func, val in zip(types, row)]

            ## Create a dictionary mapping headers to converted values
            record = dict(zip(headers, converted_row))
            records.append(record)

    return records

이 함수는 먼저 지정된 CSV 파일을 엽니다. 그런 다음 CSV 파일의 헤더, 즉 열 이름을 읽습니다. 그 후, 파일의 각 행을 반복합니다. 행의 각 값에 대해 types 목록에서 해당 타입 변환 함수를 적용합니다. 마지막으로, 키가 열 헤더이고 값이 변환된 데이터인 딕셔너리를 생성하고 이 딕셔너리를 records 목록에 추가합니다. 모든 행이 처리되면 records 목록을 반환합니다.

유틸리티 함수 테스트

유틸리티 함수를 테스트해 보겠습니다. 먼저, 터미널을 열고 다음을 입력하여 Python 인터프리터를 시작합니다.

python3

이제 Python 인터프리터에 들어갔으므로, 함수를 사용하여 포트폴리오 데이터를 읽을 수 있습니다. 포트폴리오 데이터는 주식 이름, 주식 수, 가격과 같은 주식 정보를 포함하는 CSV 파일입니다.

import reader
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
for record in portfolio[:3]:  ## Show the first 3 records
    print(record)

이 코드를 실행하면 다음과 유사한 출력을 볼 수 있습니다.

{'name': 'AA', 'shares': 100, 'price': 32.2}
{'name': 'IBM', 'shares': 50, 'price': 91.1}
{'name': 'CAT', 'shares': 150, 'price': 83.44}

이 출력은 포트폴리오 데이터의 처음 세 레코드를 보여주며, 데이터 타입이 올바르게 변환되었습니다.

CTA 버스 데이터로도 함수를 사용해 보겠습니다. CTA 버스 데이터는 버스 노선, 날짜, 요일 유형 및 승차 횟수에 대한 정보를 포함하는 또 다른 CSV 파일입니다.

rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])
print(f"Total rows: {len(rows)}")
print("First row:", rows[0])

출력은 다음과 같아야 합니다.

Total rows: 577563
First row: {'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}

이것은 함수가 다른 CSV 파일을 처리하고 적절한 타입 변환을 적용할 수 있음을 보여줍니다.

Python 인터프리터를 종료하려면 다음을 입력합니다.

exit()

이제 모든 CSV 파일을 읽고 적절한 타입 변환을 적용할 수 있는 재사용 가능한 유틸리티 함수를 만들었습니다. 이것은 Python 의 일급 객체의 강력함과 유연하고 재사용 가능한 코드를 만드는 데 어떻게 사용할 수 있는지를 보여줍니다.

Python 의 메모리 모델 탐구

Python 의 메모리 모델은 객체가 메모리에 저장되는 방식과 참조되는 방식을 결정하는 데 중요한 역할을 합니다. 특히 대규모 데이터 세트를 처리할 때는 이 모델을 이해하는 것이 필수적입니다. 이는 Python 프로그램의 성능과 메모리 사용량에 큰 영향을 미칠 수 있기 때문입니다. 이 단계에서는 Python 에서 문자열 객체가 처리되는 방식에 중점을 두고 대규모 데이터 세트의 메모리 사용량을 최적화하는 방법을 살펴보겠습니다.

데이터 세트의 문자열 반복

CTA 버스 데이터에는 노선 이름과 같이 반복되는 값이 많이 포함되어 있습니다. 데이터 세트의 반복되는 값은 적절하게 처리하지 않으면 비효율적인 메모리 사용으로 이어질 수 있습니다. 이 문제의 정도를 이해하기 위해 먼저 데이터 세트에 고유한 노선 문자열이 몇 개나 있는지 살펴보겠습니다.

먼저, Python 인터프리터를 엽니다. 터미널에서 다음 명령을 실행하여 이 작업을 수행할 수 있습니다.

python3

Python 인터프리터가 열리면 CTA 버스 데이터를 로드하고 고유한 노선을 찾습니다. 다음은 이를 수행하는 코드입니다.

import reader
rows = reader.read_csv_as_dicts('ctabus.csv', [str, str, str, int])

## Find unique route names
routes = {row['route'] for row in rows}
print(f"Number of unique route names: {len(routes)}")

이 코드에서는 먼저 CSV 파일을 딕셔너리로 읽는 함수가 포함되어 있을 것으로 예상되는 reader 모듈을 가져옵니다. 그런 다음 read_csv_as_dicts 함수를 사용하여 ctabus.csv 파일에서 데이터를 로드합니다. 두 번째 인수 [str, str, str, int]는 CSV 파일의 각 열에 대한 데이터 타입을 지정합니다. 그 후, 집합 컴프리헨션을 사용하여 데이터 세트의 모든 고유한 노선 이름을 찾고 고유한 노선 이름의 수를 출력합니다.

출력은 다음과 같아야 합니다.

Number of unique route names: 181

이제 이러한 노선에 대해 몇 개의 서로 다른 문자열 객체가 생성되는지 확인해 보겠습니다. 고유한 노선 이름이 181 개뿐이지만, Python 은 데이터 세트에서 노선 이름이 나타날 때마다 새 문자열 객체를 생성할 수 있습니다. 이를 확인하기 위해 id() 함수를 사용하여 각 문자열 객체의 고유 식별자를 가져오겠습니다.

## Count unique string object IDs
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects: {len(routeids)}")

출력은 여러분을 놀라게 할 수 있습니다.

Number of unique route string objects: 542305

이는 고유한 노선 이름이 181 개뿐이지만 500,000 개 이상의 고유한 문자열 객체가 있음을 보여줍니다. 이는 Python 이 값이 동일하더라도 각 행에 대해 새 문자열 객체를 생성하기 때문에 발생합니다. 이는 특히 대규모 데이터 세트를 처리할 때 상당한 메모리 낭비로 이어질 수 있습니다.

메모리 절약을 위한 문자열 인터닝

Python 은 sys.intern() 함수를 사용하여 문자열을 "인턴"(재사용) 하는 방법을 제공합니다. 데이터 세트에 중복된 문자열이 많은 경우 문자열 인터닝을 통해 메모리를 절약할 수 있습니다. 문자열을 인터닝하면 Python 은 동일한 문자열이 이미 인터닝 풀에 있는지 확인합니다. 있는 경우 새 문자열 객체를 생성하는 대신 기존 문자열 객체에 대한 참조를 반환합니다.

간단한 예제를 통해 문자열 인터닝이 어떻게 작동하는지 살펴보겠습니다.

import sys

## Without interning
a = 'hello world'
b = 'hello world'
print(f"a is b (without interning): {a is b}")

## With interning
a = sys.intern(a)
b = sys.intern(b)
print(f"a is b (with interning): {a is b}")

이 코드에서는 먼저 인터닝 없이 동일한 값을 가진 두 개의 문자열 변수 ab를 생성합니다. is 연산자는 두 변수가 동일한 객체를 참조하는지 확인합니다. 인터닝이 없으면 ab는 서로 다른 객체이므로 a is bFalse를 반환합니다. 그런 다음 sys.intern()을 사용하여 두 문자열을 인터닝합니다. 인터닝 후 ab는 인터닝 풀의 동일한 객체를 참조하므로 a is bTrue를 반환합니다.

출력은 다음과 같아야 합니다.

a is b (without interning): False
a is b (with interning): True

이제 CTA 버스 데이터를 읽을 때 문자열 인터닝을 사용하여 메모리 사용량을 줄여보겠습니다. 또한 tracemalloc 모듈을 사용하여 인터닝 전후의 메모리 사용량을 추적합니다.

import sys
import reader
import tracemalloc

## Start memory tracking
tracemalloc.start()

## Read data with interning for the route column
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, str, str, int])

## Check unique route objects again
routeids = {id(row['route']) for row in rows}
print(f"Number of unique route string objects (with interning): {len(routeids)}")

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

이 코드에서는 먼저 tracemalloc.start()를 사용하여 메모리 추적을 시작합니다. 그런 다음, sys.intern을 첫 번째 열의 데이터 타입으로 전달하여 노선 열에 대한 인터닝으로 CTA 버스 데이터를 읽습니다. 그 후, 고유한 노선 문자열 객체의 수를 다시 확인하고 현재 및 최대 메모리 사용량을 출력합니다.

출력은 다음과 같아야 합니다.

Number of unique route string objects (with interning): 181
Current memory usage: 189.56 MB
Peak memory usage: 209.32 MB

인터프리터를 다시 시작하고 노선 및 날짜 문자열을 모두 인터닝하여 메모리 사용량을 더 줄일 수 있는지 시도해 보겠습니다.

exit()

Python 을 다시 시작합니다.

python3
import sys
import reader
import tracemalloc

## Start memory tracking
tracemalloc.start()

## Read data with interning for both route and date columns
rows = reader.read_csv_as_dicts('ctabus.csv', [sys.intern, sys.intern, str, int])

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage (interning route and date): {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage (interning route and date): {peak / 1024 / 1024:.2f} MB")

출력은 메모리 사용량의 추가 감소를 보여야 합니다.

Current memory usage (interning route and date): 170.23 MB
Peak memory usage (interning route and date): 190.05 MB

이는 Python 의 메모리 모델을 이해하고 문자열 인터닝과 같은 기술을 사용하면 특히 반복되는 값이 포함된 대규모 데이터 세트를 처리할 때 프로그램을 최적화하는 데 도움이 될 수 있음을 보여줍니다.

마지막으로, Python 인터프리터를 종료합니다.

exit()

열 중심 데이터 저장

지금까지 CSV 데이터를 행 딕셔너리 목록으로 저장해 왔습니다. 즉, CSV 파일의 각 행은 딕셔너리로 표시되며, 여기서 키는 열 헤더이고 값은 해당 행의 해당 데이터입니다. 그러나 대규모 데이터 세트를 처리할 때는 이 방법이 비효율적일 수 있습니다. 열 중심 형식으로 데이터를 저장하는 것이 더 나은 선택일 수 있습니다. 열 중심 접근 방식에서는 각 열의 데이터가 별도의 목록에 저장됩니다. 이로 인해 유사한 데이터 유형이 함께 그룹화되므로 메모리 사용량이 크게 줄어들 수 있으며, 열별 데이터 집계와 같은 특정 작업의 성능도 향상될 수 있습니다.

열 중심 데이터 리더 생성

이제 열 중심 형식으로 CSV 데이터를 읽는 데 도움이 되는 새 파일을 만들 것입니다. 프로젝트 디렉토리에 colreader.py라는 새 파일을 만들고 다음 코드를 입력합니다.

import csv

class DataCollection:
    def __init__(self, headers, columns):
        """
        Initialize a column-oriented data collection.

        Parameters:
        headers (list): Column header names
        columns (dict): Dictionary mapping header names to column data lists
        """
        self.headers = headers
        self.columns = columns
        self._length = len(columns[headers[0]]) if headers else 0

    def __len__(self):
        """Return the number of rows in the collection."""
        return self._length

    def __getitem__(self, index):
        """
        Get a row by index, presented as a dictionary.

        Parameters:
        index (int): Row index

        Returns:
        dict: Dictionary representing the row at the given index
        """
        if isinstance(index, int):
            if index < 0 or index >= self._length:
                raise IndexError("Index out of range")

            return {header: self.columns[header][index] for header in self.headers}
        else:
            raise TypeError("Index must be an integer")

def read_csv_as_columns(filename, types):
    """
    Read a CSV file into a column-oriented data structure, converting each field
    according to the types provided.

    Parameters:
    filename (str): Name of the CSV file to read
    types (list): List of type conversion functions for each column

    Returns:
    DataCollection: Column-oriented data collection representing the CSV data
    """
    with open(filename, 'r') as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get the column headers

        ## Initialize columns
        columns = {header: [] for header in headers}

        ## Read data into columns
        for row in rows:
            ## Convert values according to the specified types
            converted_values = [func(val) for func, val in zip(types, row)]

            ## Add each value to its corresponding column
            for header, value in zip(headers, converted_values):
                columns[header].append(value)

    return DataCollection(headers, columns)

이 코드는 두 가지 중요한 작업을 수행합니다.

  1. DataCollection 클래스를 정의합니다. 이 클래스는 데이터를 열에 저장하지만, 행 딕셔너리 목록인 것처럼 데이터에 액세스할 수 있도록 합니다. 이는 데이터로 작업하는 익숙한 방법을 제공하므로 유용합니다.
  2. read_csv_as_columns 함수를 정의합니다. 이 함수는 파일에서 CSV 데이터를 읽어 열 중심 구조로 저장합니다. 또한 제공된 유형에 따라 CSV 파일의 각 필드를 변환합니다.

열 중심 리더 테스트

CTA 버스 데이터를 사용하여 열 중심 리더를 테스트해 보겠습니다. 먼저, Python 인터프리터를 엽니다. 터미널에서 다음 명령을 실행하여 이 작업을 수행할 수 있습니다.

python3

Python 인터프리터가 열리면 다음 코드를 실행합니다.

import colreader
import tracemalloc
from sys import intern

## Start memory tracking
tracemalloc.start()

## Read data into column-oriented structure with string interning
data = colreader.read_csv_as_columns('ctabus.csv', [intern, intern, intern, int])

## Check that we can access the data like a list of dictionaries
print(f"Number of rows: {len(data)}")
print("First 3 rows:")
for i in range(3):
    print(data[i])

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

출력은 다음과 같아야 합니다.

Number of rows: 577563
First 3 rows:
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
{'route': '4', 'date': '01/01/2001', 'daytype': 'U', 'rides': 9288}
{'route': '6', 'date': '01/01/2001', 'daytype': 'U', 'rides': 6048}
Current memory usage: 38.67 MB
Peak memory usage: 103.42 MB

이제 이전의 행 중심 접근 방식과 비교해 보겠습니다. 동일한 Python 인터프리터에서 다음 코드를 실행합니다.

import reader
import tracemalloc
from sys import intern

## Reset memory tracking
tracemalloc.reset_peak()

## Read data into row-oriented structure with string interning
rows = reader.read_csv_as_dicts('ctabus.csv', [intern, intern, intern, int])

## Check memory usage
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage (row-oriented): {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage (row-oriented): {peak / 1024 / 1024:.2f} MB")

출력은 다음과 같아야 합니다.

Current memory usage (row-oriented): 170.23 MB
Peak memory usage (row-oriented): 190.05 MB

보시다시피, 열 중심 접근 방식은 메모리를 훨씬 적게 사용합니다!

이전과 마찬가지로 데이터를 분석할 수 있는지 테스트해 보겠습니다. 다음 코드를 실행합니다.

## Find all unique routes in the column-oriented data
routes = {row['route'] for row in data}
print(f"Number of unique routes: {len(routes)}")

## Count rides per route (first 5)
from collections import defaultdict
route_rides = defaultdict(int)
for row in data:
    route_rides[row['route']] += row['rides']

## Show the top 5 routes by total rides
top_routes = sorted(route_rides.items(), key=lambda x: x[1], reverse=True)[:5]
print("Top 5 routes by total rides:")
for route, rides in top_routes:
    print(f"Route {route}: {rides:,} rides")

출력은 다음과 같아야 합니다.

Number of unique routes: 181
Top 5 routes by total rides:
Route 9: 158,545,826 rides
Route 49: 129,872,910 rides
Route 77: 120,086,065 rides
Route 79: 109,348,708 rides
Route 4: 91,405,538 rides

마지막으로, 다음 명령을 실행하여 Python 인터프리터를 종료합니다.

exit()

열 중심 접근 방식은 메모리를 절약할 뿐만 아니라 이전과 동일한 분석을 수행할 수 있도록 해줍니다. 이는 서로 다른 데이터 저장 전략이 성능에 큰 영향을 미칠 수 있으며, 데이터를 처리하기 위한 동일한 인터페이스를 제공하는 방법을 보여줍니다.

요약

이 Lab 에서 몇 가지 주요 Python 개념을 배웠습니다. 첫째, Python 이 함수, 유형 및 기타 엔티티를 일급 객체 (first-class objects) 로 취급하여 일반 데이터처럼 전달하고 저장할 수 있도록 하는 방법을 이해했습니다. 둘째, 자동 유형 변환을 통해 CSV 데이터 처리를 위한 재사용 가능한 유틸리티 함수를 만들었습니다.

또한 Python 의 메모리 모델을 탐구하고 문자열 인터닝 (string interning) 을 사용하여 반복적인 데이터의 메모리 사용량을 줄였습니다. 또한 익숙한 사용자 인터페이스를 제공하면서 대규모 데이터 세트를 위한 보다 효율적인 열 중심 저장 방식 (column-oriented storage method) 을 구현했습니다. 이러한 개념은 데이터 처리에서 Python 의 유연성과 강력함을 보여주며, 이러한 기술은 실제 데이터 분석 프로젝트에 적용될 수 있습니다.