brunch

You can make anything
by writing

C.S.Lewis

by 유윤식 Jun 16. 2024

Python: DuckDB(3)

#Relational_API #DuckDBPyRelation #Lazy

DuckDB 를 관계형 API 로 다루는 방법.


** 참고

https://duckdb.org/docs/api/python/relational_api


이전 포스팅에 이어서 nyc 데이터를 활용해보면



import duckdb


# 로컬 DB 를 활용하면서

con = duckdb.connect('./my-db.duckdb')


# 로컬 환경에 맞는 데이터 PATH 를 입력하고,

file_path = './nyc_data/*.parquet'


con.execute(f"""

CREATE TABLE nyc_tbl AS SELECT * FROM read_parquet('{file_path}')

""")


이러면 로컬 DB 파일에 parquet 파일 내용이 저장 된다.


이러고 다른 세션(쥬피터 or 터미널) 에서 해당 .duckdb 파일로 연결하면 nyc_tbl 테이블을 읽고 사용할 수 있다.


간단하게 데이터를 확인하면


con.sql("""

select * from nyc_tbl limit 5

""").show()


이제 이걸 관계형 API 로 사용하기 위해서 변수에 테이블을 담아서 duckdb.duckdb.DuckDBPyRelation 라는 타입 결과로 만들어 준다.



nyc_table = con.table('nyc_tbl')

print(type(nyc_table))

>> duckdb.duckdb.DuckDBPyRelation



바로 관계형 API 가 무엇인지 대략 감이 오는 심플한 예시를 보자면


nyc_table.pl()


pl() 이 바로 polars DataFrame 으로 결과를 반환하는 API 다.


이제 시나리오를 통해서 몇가지 더 확인해보면

우선

1. filter() 를 통해서 fare_amount 와 trip_distance 가 0보다 큰 값만 추려내고,

2. project() 로 VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, trip_distance, fare_amount, total_amount 해당 컬럼만 추출하고,

3. aggregate() 를 이용해서 timestamp 를 1시간 간격으로 roll over 하고 각 숫자 데이터의 값을 AVG 로 표현한다.


그냥 딱 코드보면 SQL 이 생각이 날 법 한데


nyc_table.filter("""

fare_amount > 0 and trip_distance > 0

"""

).project("""

VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, trip_distance, fare_amount, total_amount

""").aggregate("""

VendorID, 

DATE_TRUNC('hour', tpep_pickup_datetime) AS pickup_datetime,

DATE_TRUNC('hour', tpep_dropoff_datetime) AS dropoff_datetime,

ROUND(AVG(trip_distance), 2) AS trip_distance,

ROUND(AVG(fare_amount), 2) AS fare_amount,

ROUND(AVG(total_amount), 2) AS total_amount

""").order("""

VendorID, pickup_datetime desc, dropoff_datetime asc

""").limit(10)


결과는

여기에 trip_distance 가 길수록 수익성이 좋을까?

아니면 짧을 수록 수익성이 좋을까?



nyc_table.filter("""

fare_amount > 0 and trip_distance > 0

"""

).project("""

VendorID, tpep_pickup_datetime, tpep_dropoff_datetime, trip_distance, fare_amount, total_amount, 

total_amount / trip_distance AS amt_per_dist

""").aggregate("""

VendorID, 

DATE_TRUNC('hour', tpep_pickup_datetime) AS pickup_datetime,

DATE_TRUNC('hour', tpep_dropoff_datetime) AS dropoff_datetime,

ROUND(AVG(trip_distance), 2) AS trip_distance,

ROUND(AVG(fare_amount), 2) AS fare_amount,

ROUND(AVG(total_amount), 2) AS total_amount,

ROUND(AVG(amt_per_dist), 2) AS amt_per_dist,

count(*) AS cnt

""").filter("""

cnt > 10

""").order("""

trip_distance desc

""").limit(10)


우선 결과 데이터를 보면

amt_per_dist, cnt 컬럼이 생겼는데 

이렇게만 보아서는 잘 모르겠고...

그래프로 보면 좋겠다.



import seaborn as sns

import matplotlib.pyplot as plt


plt.figure(figsize=(10, 6))

sns.scatterplot(data=res, x='trip_distance', y='amt_per_dist', hue='VendorID', style='VendorID', s=50)

plt.title('Trip Distance vs Amount per Distance')

plt.xlabel('Trip Distance')

plt.ylabel('Amount per Distance')

plt.legend(title='NYC')

plt.show()


참고로 데이터 볼륨이 어느정도 될 땐 라인 그래프를 사용하면 컴퓨터가 멈출 수 있으니까 Scatter 를 사용하는 것을 추천한다.


일단 확인을 했는데...

뭔가 계산을 잘못한건지,

데이터 아웃라이어가 빈번해서인지...

앞부분만 보자면 운행거리가 짧을수록 수익성이 약간은 상승하는 느낌이다.


중요한건 관계형 API 를 사용하고 이러한 방식의 확장성이 좋다고 느껴졌다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari