-
<TIL> 2024-03-06내일배움캠프(데이터 분석 부트캠프 1기) 2024. 3. 6. 23:46
- 오늘 진행한 일
- 팀 프로젝트 트러블 슈팅
- 팀 프로젝트 관련 튜터링 진행
https://jinhyunbae.tistory.com/173
오늘도 어제자에 진행하던 팀프로젝트 내용에 이어서 프로젝트를 진행하였다.
1. 하나의 세션 안에 두 유저가 있는지 여부2. 한 유저가 두 도시에서 접속한 경우3. 유저 ID가 없는 경우(Null)4. Event가 발생하지 않았는데 구매 이력이 존재하는 경우
5. 30분 이상 지속되는 이상 세션의 존재 여부
원래는 오늘 중으로 4번 과제를 마무리하고 태블로로 시각화까지 마친 뒤
5번 문제를 풀어보려고 했으나 태블로까지 진행되지는 못하였다.
대신 4번을 심도깊게 파보았고 5번 문제에 대한 전처리를 진행해보았으며
이에 대한 튜터링도 받았다.
우선 Event가 발생하지 않았는데 구매 이력이 존재하는 경우의 코드에 대한 피드백이다.
이전에는 조인을 하는데 있어서 on=['user_id', 'product_id', 'created_at'] 로 3개의 컬럼이 동일한 경우에 대해서
조인을 하였다. 그런데 튜터링을 통해서 알게 된 것은
출제의도는 created_at까지 조인의 기준 컬럼으로 삼는 것은 아니었다는 것이다. 왜냐하면 events의 purchase 시각(created_at)과 order_items의 created_at이 현재 데이터에서는 동일했지만실제 현업에서는 다른 경우가 대부분이기 때문이다.
따라서 출제의도대로라면 user_id와 product_id만으로 조인을 한 뒤 중복을 제거해주는 방식으로 진행을 했어야했다는 것이다.
'user_id', 'product_id'이 동일한 다수의 이벤트 그리고 다수의 order_items가 존재할 수 있는데 이 두 칼럼을 기준으로 drop_duplicates를 진행한 경우 어떤 데이터는 남고 어떤 데이터는 사라지는지 제어할 수 없는게 아닌가라는
의문이 들었고 튜터님께 해당 부분에 대해서 질문 드렸다.
거기에 대한 답변은 "현업에서는 그 부분까지 제어할 수 있게 코드를 작성하지만 그렇게 되면 과제 난이도가
너무 많이 올라가서 그렇게 출제하지는 않았다." 였다.
조금 아쉬움이 남는 문제 풀이방식이었지만 해당 제어부분도 추후 질문을 통해
개인 학습에 반영하면 좋을 것 같다는 생각이 들었다.
5. 30분 이상 지속되는 이상 세션의 존재 여부
오늘 주로 진행했던 부분은 5번에 대한 코드였다.
events_session_group = dict(list(events.groupby('session_id'))) # 반복문 사용을 위한 세션id기준 그룹바이 딕셔너리 저장 events_session_group_keys = list(events_session_group.keys()) # 키값 리스트 형변환 -> 인덱스 슬라이싱을 위해서 # 데이터 적재용 빈 데이터 프레임 생성 minute_diff = pd.DataFrame(columns=events.columns) for i in range(len(events_session_group_keys)) : # anti-pattern if i%1000==0 : print(f'{i}번째 반복문 코드 실행 중') # 세션 ID(key) 별 그룹 불러오기(temp에 적재), sequence 번호 순으로 정렬 temp = events_session_group[events_session_group_keys[i]].sort_values('sequence_number').reset_index(drop=True) # 다음 이벤트와의 시간차를 계산하기 위해 created_at을 1칸씩 미룬 created_at_shift컬럼을 생성 temp = temp.assign(minute_diff, temp['created_at'].diff()) # 밀려난 최초의 row에 NaT값을 timedelta 0값으로 대체 temp['minute_diff'] = temp['minute_diff'].fillna(timedelta(days=0)) # datetime 라이브러리의 timedelta의 total_seconds함수를 적용하여 이전 이벤트와 몇 초 차이나는 지로 값을 변경함 temp['minute_diff'] = temp['minute_diff'].apply(timedelta.total_seconds) # .dt.total_seconds() # 필요 없어진 created_at_shift 컬럼 삭제 temp = temp.drop('created_at_shift', axis=1) # 이전 이벤트와의 시간차가 30분(1800초)이 넘어가는 이벤트이면 1, 아니면 0 temp.loc[temp['minute_diff']>1800, 'is_over_30minutes'] = 1 temp.loc[temp['minute_diff']<=1800, 'is_over_30minutes'] = 0 # 적재용 데이터 프레임에 결과 minute_diff = pd.concat([minute_diff, temp], axis=0) # concat한 뒤 reset_index() minute_diff = minute_diff.reset_index(drop=True) display(minute_diff)
코드를 돌아가게끔 작성하는데까지는 성공을 했고 반복문을 실행시키던 와중에
1시간을 넘게 코드를 돌리는데도 약 68만건의 session_id 그룹들 중에 7만건 정도 밖에 실행이 되지 않아서
이 부분에 대해서 내가 맞게 코드를 짠 건지 튜터님께 문의를 드렸다.
이 부분에 대해서 다음과 같은 피드백들을 받았다.
- groupby 데이터를 굳이 dictionary 자료형에 적재 한 뒤 불러오는 행위는 메모리를 중복으로 사용하는 행위이다.
- groupby한 데이터를 반복하는데 있어서 리스트의 길이(len())를 이용해 i를 iterator로 사용했는데 이는 안티 패턴이다. 안티 패턴(anti-pattern)이란 비효율적이거나 생산성이 저해되는 소프트웨어 설계 관행을 말한다. 따라서 list 자체를 in 안에 집어넣고 값을 반복시키는 것이 더 효율적이다.
- 빈 데이터프레임을 생성한 뒤 결과를 기존 빈 데이터 프레임에 concat하는 방식은 기존 데이터프레임, 합칠 데이터프레임, 새 데이터프레임까지 총 3개의 데이터프레임을 계속 생성하는 행동을 하는 것이다. 이런 코드는 좋지 않다. 따라서 빈 리스트에 데이터 프레임을 적재한 후 리스트를 concat하는 방법이 좋다.
굵직한 피드백은 위와 같았고 그 외 diff함수나 tqdm등 자잘한 피드백들을 받았다.
그리고 해당 부분을 반영하여 코드를 작성하여 실행시켜 보았다.
# 튜터링 반영 코드 작성 # 데이터 적재용 빈 리스트 생성 minute_df_list = [] for keys, groups in tqdm(events.groupby('session_id')) : # 그룹 불러오기, sequence 번호 순으로 정렬 temp = groups.sort_values('sequence_number').reset_index(drop=True) # 이전 이벤트와의 시간차를 나타내는 컬럼 minute_diff 생성 temp = temp.assign(minute_diff=temp['created_at'].diff()) # datetime 라이브러리의 timedelta의 total_seconds함수를 적용하여 이전 이벤트와 몇 초 차이나는 지로 값을 변경함 temp['minute_diff'] = temp['minute_diff'].dt.total_seconds() # 이전 이벤트와의 시간차가 30분(1800초)이 넘어가는 이벤트이면 1, 아니면 0 temp.loc[temp['minute_diff']>1800, 'is_over_30minutes'] = 1 temp.loc[temp['minute_diff']<=1800, 'is_over_30minutes'] = 0 # 리스트에 결과 적재 minute_df_list.append(temp) # list 내의 데이터를 concat minute_diff_df = pd.concat(minute_df_list) minute_diff_df = minute_diff_df.reset_index(drop=True) minute_diff_df
놀랍게도 1시간동안 7만건 밖에 실행되지 않던 코드가 20분만에 모든 반복이 끝났고
이를 concat을 하는 데도 훨씬 더 적은 시간이 소모되었다.
코드 최적화의 길은 아직 멀었지만 어떤 방식으로 코드를 짜는게 더 효율적인지에 대한
고민을 할 수 있게 해준 좋은 튜터링 시간이었다.
앞으로는 코드를 단순히 동작하는 것에만 집중해서 짜는 것이 아니라 시간, 공간적인 효율성을
고민하면서 최적화 하는 방법도 고민해야겠다.
내일은 이제 정말 5번까지 파이썬코드를 다 마무리하고 태블로 시각화에 돌입하려고 한다.
- 오늘 진행한 일