본문 바로가기

TIL

[240322] 파이썬 실습 코드: 메모리 경량화 (with parquet)

* 데이터 출처: App_review_data 

 

1. csv 파일 병합 후 전처리하여 parquet로 저장하기  

1) 고용량 데이터 불러오기 

- glob 라이브러리 및 함수:  인자로 받은 패턴과 이름이 일치하는 모든 파일과 디렉터리의 리스트 반환

- tqdm 라이브러리 및 함수: 코드 진행률을 파악할 수 있는 프로세스바 지원 

from glob import glob
from datetime import timedelta
from tqdm import tqdm
import pandas as pd
import matplotlib.pyplot as plt

glob(".../app_review_data/*.csv")

# ['.../app_review_data\\FACEBOOK_REVIEWS.csv',
# '.../app_review_data\\FLIPKART_REVIEWS.csv',
# '.../app_review_data\\INSTAGRAM_REVIEWS.csv',
# '.../app_review_data\\SPOTIFY_REVIEWS.csv',
# '.../app_review_data\\TIKTOK_REVIEWS.csv',
# '.../app_review_data\\TWITTER_REVIEWS.csv',
# '.../app_review_data\\WHATSAPP_REVIEWS.csv']


# 데이터를 추출하는 함수 작성
def extract_csvs(): 
    res = []
    for path in tqdm(glob("D:/2024 내일배움캠프/스파크/app_review_data/*.csv")):
        app_name = path.split("/")[-1].replace(".csv","")
        df_temp = pd.read_csv(path)
        df_temp = df_temp.assign(app_name=app_name)
        res.append(df_temp)
    df = pd.concat(res)
    return df

df = extract_csvs()  
# 100%|██████████| 7/7 [01:14<00:00, 10.65s/it]
# 2분 20초 소요

 

2) 데이터 기본 정보 확인 및 전처리

- 데이터 기본 정보 파악 및 칼럼 정리 

- 현재 메모리는 2.7GB 

df.info()
df.isna().mean()
df.review_rating.value_counts()
df.review_rating.sort_values()

# 필요한 칼럼 값만 불러오기
target_columns = [
    'review_id', 'pseudo_author_id', 'author_name', 'review_text', 'review_rating',
    'review_likes' , 'review_datetime_utc', 'review_timestamp' 
    ]

df_target = df[target_columns]

# 기존 df에 새로 생성한 데이터 프레임 할당 후 삭제
df = df_target 
del df_target

 

- 데이터 전처리: 시간 데이터 

 └ 데이터 타입 수정 및 결측치 처리

df.isna().sum()
# 심상치 않은 null 개수 차이
# review_datetime_utc    19716190
# review_timestamp        8438195

df['review_timestamp'].dropna().head(2) # 2009-09-09 07:36:50 # object
df['review_datetime_utc'].dropna().head(2) # 2011-07-08T05:26:10.000Z # object

# 형식이 다른 timestamp와 datetime_utc를 datetime으로 변환
df['review_timestamp'] = pd.to_datetime(df['review_timestamp']) # object > datetime64[ns]
df['review_datetime_utc'] = pd.to_datetime(df['review_datetime_utc']) # object > datetime64[ns, UTC]

# timestamp와 datetime_utc이 동시에 존재하면 에러가 발생하도록 설정 
if (df.review_timestamp.notna() & df.review_datetime_utc.notna()).sum() > 0:
    raise Exception("review_timestamp와 review_datetime_utc가 동시에 존재합니다.")

# timestamp의 null값에 datetime_utc 데이터 추가  
df['review_timestamp'].fillna(df['review_datetime_utc']).notna().mean()

 

└ 시간대 타임존 확인

# datetime_utc은 utc인데, timestamp도 utc 시간대가 맞을까? 
# 시간대별로 그래프 차이를 확인 
# 2개 그래프의 추이는 유사한 것으로 파악되어 원본 데이터는 UTC로 추정 
df['review_timestamp'].dt.hour.value_counts(normalize=True).sort_index().plot()
df['review_datetime_utc'].dt.hour.value_counts(normalize=True).sort_index().plot()
plt.legend(['review_timestamp','review_datetime_utc'])
plt.show()

 

 

└ 시간대 현지시간에 맞춰서 수정

    : tz_localize() 로 타임존 정보 기입 가능

# 데이터 원본은 utc나 실제 사용국은 미국인 것으로 추정 
# 1. 앱스토어가 미국 것이고
# 2. 24시쯤 트래픽이 최저 흐름을 보이고 있음(국가 막론 새벽 4~5시 트래픽 최저 > 미국은 UTC랑 5시간 차이가 남) 
# 하여, 미국 현지 시간에 맞춰 타임 정보 조정 

# 미국 시간으로 조정할 거니까 타임존 정보는 none으로 없애기 
df['review_datetime_utc'] = df['review_datetime_utc'].dt.tz_localize(None)

# 각 UTC 시간에 5시간 더 해주기 
df['review_datetime_utc'] += timedelta(hours=5)
df['review_timestamp'] += timedelta(hours=5)

# timestamp 칼럼 null값에 datetime 넣기 
df['review_timestamp'].fillna(df['review_datetime_utc'])

#datetime_utc는 제거하기
df = df.drop(['review_datetime_utc'], axis = 1)

 

- 파일 내보내기

└ 파일 병합 후 메모리 2.7GB에서 전처리 작업 후 1.7GB로 약 37% 경량화  

df.info()
df.to_parquet("./target_raw.parquet")

 

2. I/O 

- csv와 parquet 파일별 입출력 시간 확인 

import pandas as pd

%%time 
df = pd.read_parquet("target_raw.parquet")
# parquet #불러오는 시간
# CPU times: total: 32 s
# Wall time: 1min 34s

%%time 
df.to_parquet("target_raw.parquet")
# parquet #저장하는 시간
# CPU times: total: 15.7 s
# Wall time: 1min 5s

%%time 
df.to_csv("target_raw.csv", index=False)
# csv #저장하는 시간
# CPU times: total: 28.7 s
# Wall time: 1min 58s

%%time 
df = pd.read_csv("target_raw.csv")
# csv #불러오는 시간
# CPU times: total: 33.4 s
# Wall time: 3min 40s

 

-  데이터 타입 규모 확인 

# null 값 확인
# null 값이 섞여 있으면 float(실수)가 됨 
df.isna().mean()

# 샘플링
df_raw = df.copy()
df = df_raw.sample(frac=0.1)
df.info()

# 각 칼럼별 데이터 규모 확인 (데이터 타입 축소를 위함)
# df.dtypes.items 입력 시 칼럼명, 데이터 타입이 출력
for col, data_type in df.dtypes.items():
    if data_type == 'object':
        ser_target = df[col].value_counts()
        print(f"{col}({data_type}): {len(ser_target):,}가지")
    elif ('int' in str(data_type)) or ('float' in str(data_type)):
        max_value = df[col].max()
        min_value = df[col].min()
        # null을 제외하고 1로 나눈 값의 나머지가 0이 아니다 = 실수(null 값 때문에 실수로 변환)
        # 그래서 4.0, 5.0과 같이 실수 형태이나 나머지를합친 값이 0이면 int로 변환!  
        if (((df[col].dropna() % 1) != 0).sum() == 0) or (('int') in str(data_type)):
            target_data_type = 'int'
        else:
            target_data_type = 'float'
        print(f'{col}({target_data_type}): {min_value: ,.2f} ~ {max_value: ,.2f}')
    elif ('datetime') in str(data_type):
        target_data_type = 'datetime'
        print(f"{col}({data_type}): {len(ser_target):,}가지")
    else: 
        raise Exception('New data type:', data_type)
        
        
# review_id(object): 1,971,519가지
# app_name(object): 7가지
# pseudo_author_id(object): 1,959,195가지
# author_name(object): 583,877가지
# review_text(object): 1,465,193가지
# review_rating(int):  0.00 ~  5.00
# review_likes(int):  0.00 ~  108,000.00
# review_timestamp(datetime64[ns]): 1,465,193가지

 

- 데이터 규모에 맞춰서 데이터 타입 수정

└ df.memory_usage() : 각 컬럼별 메모리 사용량 조회  

assert: assert는 뒤의 조건이 True가 아니면 AssertError를 발생

df = df_raw
del df_raw
df.info()

# 변경할 칼럼 설정
int32_cols = ['review_likes']
int8_cols = ['review_rating']
cate_cols = ['app_name']

# 변경 전 메모리 사용량
memory_usage_before = df.memory_usage().sum()
memory_usage_before

# 데이터 타입 범위를 넘어가면 오류가 발생하도록 설정 
for col in int32_cols:
    assert abs(df[col].max()) < 2_147_483_647 # False 시 오류 발생 
    df[col] = df[col].astype(pd.Int32Dtype())

for col in int8_cols:
    assert abs(df[col].max()) < 127 # False 시 오류 발생 
    df[col] = df[col].astype(pd.Int8Dtype())
    
for col in cate_cols:
    assert df[col].nunique() < 10_000 # False 시 오류 발생 
    df[col] = df[col].astype("category")
    
df.info()

# 변경 후 메모리 사용량과 감소율
memory_usage_after = df.memory_usage().sum()
reduction_ratio = 1 - (memory_usage_after / memory_usage_before)

print(f"Memory Usage: {memory_usage_before:,} -> {memory_usage_after:,} ({reduction_ratio*100:.2f}% reduced)")

# Memory Usage: 1,801,880,768 -> 1,351,410,964 (25.00% reduced)