본문 바로가기

문제풀이

데이터 분석 기초: 개인 과제 풀이 및 해설 정리


과제 목표
- 주어진 데이터는 1년 동안 Github public repository(개발자들의 코드 저장소)의 푸시 횟수입니다.
└ Github의 공개 저장소에 대한 기록은 모두에게 공개되어 있으며, Bigquery에 데이터베이스 형태로도 저장되어 있습니다.
- 아래 데이터는 2019년 2월 1일 ~ 2020년 1월 14일, 약 1년간 각 일자별 Push(코드 업데이트) 횟수입니다
 각 개인 별 데이터는 취합되었기 때문에 확인이 어려우며, 전 세계의 데이터이기 때문에 하루에도 수십만회 Push가 이루어집니다.
- 본 과제의 목표는 요일 별 Push 횟수에 유의미한 차이가 있는지 확인하는 것입니다.
이를 위한 데이터의 기본적인 전처리가 과제이며, 실제 통계 분석은 예시로 제공됩니다.

 

Question 1: 날짜 전처리
## 날짜를 변환
df['log_date'] = pd.to_datetime(df['log_date'], format='%y-%m-%d')

## 변환된 날짜 칼럼으로부터 요일dayofweek을 숫자로 추출
df = df.assign(day_of_week = df['log_date'].dt.dayofweek)
df.info()

 

- 편의성 때문에 df.log_date 식으로 표기도 가능하나 정석은 df['log_date'] 

- 칼럼 할당할 때는 df['day_of_week'] = ~~ 으로 하는 것보다는 assign이 바람직 

- 요일별 데이터를 구하는 dayofweek는 weekday로도 대체 가능

 

 

Question 2: Groupby로 통계량 집계
## 요일별 푸시 횟수의 평균과 중간값 추출
push_count_by_dow = df.groupby('day_of_week')['push_count'].agg(['mean', 'median'])
push_count_by_dow = push_count_by_dow.sort_index()

display(push_count_by_dow)

 

- 다중 집계 함수를 사용할 대는 agg() 함수 이용 

 

Question 3: Bar chart 시각화
## 요일별 평균을 bar chart로 시각화
bar_data = push_count_by_dow["mean"].reset_index()   #정답은 push_count_by_dow["mean"].plot.bar()

plt.bar(bar_data["day_of_week"], bar_data["mean"], width=0.5)
plt.xticks(rotation=90)
plt.xlabel('day_of_week')
plt.ylabel('mean')
plt.title('Mean by Day_Of_Week')
plt.show()

 

 

 

Question4(Optional): 아웃라이어 제거
## 함수 기반으로 z score 기반의 이상치 탐지
def z_score_outlier_remover(df:pd.DataFrame, threshold:float) -> pd.DataFrame:
    """
    z-score가 Threshold가 넘는 값들을 이상치로 판단하고 제거.
    단, 이상치가 제거될 때마다 평균과 분산이 바뀌므로 이상치가 존재하지 않을 때까지 무한 반복
    """
    total_outlier_count = 0
    
    if threshold <= 0:
        raise ValueError("Threshold must larger than zero")
    
    while True:
        ## 평균과 표준 편차를 집계
        m = df['push_count'].mean()
        s = df['push_count'].std()
        
        ## push_count의 각 요소가 이상치인지를 나타내는 Boolean series 생성
        ## pd.Series 이건 빼도 무관 
        ## 정답은
        ## ser_outlier_bool = (df.push_count - m).abs() > (threshold*s)
        ser_outlier_bool = pd.Series(abs((df['push_count'] - m) / s)  > threshold, name='Outlier')
        
        ## 이상치의 숫자를 집계
        ## 정답은
        ## outlier_count = ser_outlier_bool.sum()
        outlier_count = df[ser_outlier_bool].count()['push_count']

        ## 이상치가 존재한다면 그 숫자를 세고, 제거
        if outlier_count > 0:
            total_outlier_count += outlier_count
            ## 정답은 
            ## df = df[~ser_outlier_bool]
            df = df.drop(df[ser_outlier_bool==1].index , axis=0)
        ## 이상치가 존재하지 않으면 
        else:
            ## 제거한 이상치가 1개 이상이라면 출력하고 종료
            if total_outlier_count > 0:
                print(f"The number of outliers(z-score > {threshold}): {total_outlier_count}")
            break
    return df

 

- 방어 코드(if threshold <= 0: raise ValueError("Threshold must larger than zero")가 있어야 컴퓨터가 제대로 작동함

- 코드 작성 시 독스트링이나 타입힌트을 표시해주는 것이 좋음  

└ 타입힌트: 함수명 옆에 타입을 표시해주는 것. ex. def sth(df:pd.DataFrame, threshold:float) -> pd.DataFrame:

└ 독스트링(docstring): 함수 시작 부분에 함수 인터페이스를 설명하는 문자열. 따옴표(""")나 작은 따옴표 세 개(''')로 표시

- 표준편차는 numpy인지 pandas 인지에 따라 값이 달라져서 기준을 명확하게 하는 것이 좋음 (자유도에서 -1 여부)
- 통상 데이터가 많으면 numpy가 2배 정도 빠름 
- 변수 앞에 ~를 넣으면 Ture & False 값이 반전됨 

 

▶ 요일별 이상치

## 요일 별로 이상치를 제거
threshold = 3

n_before = len(df)
df_outlier_removed_list = []
for day_of_week, group in df.groupby("day_of_week"):
    
    print(f"Day of week: {day_of_week}: Started")
    group_outlier_removed = z_score_outlier_remover(group, threshold)
    df_outlier_removed_list.append(group_outlier_removed)
    print(f"Day of week: {day_of_week}: End\n")

## 원본 데이터 대체
df = pd.concat(df_outlier_removed_list)
n_after = len(df)
outlier_ratio = (n_before - n_after) / n_before * 100

print(f"Outlier removed: {n_before} -> {n_after}({outlier_ratio:.2f}%)")

 

 

기타: 통계 결과 
더보기

▶ ANOVA

F, p_value = stats.f_oneway(*[group.push_count for dow, group in df.groupby('day_of_week')])
print(f"F statistics: {F:.4f}")
print(f"P-value: {p_value:.4f}")

▶ Assumption test

df['residual'] = df['push_count'] - df.groupby('day_of_week').push_count.transform('mean')
df['std_resi'] = (df['residual'] - df['residual'].mean()) / df['residual'].std()

## 날짜 순으로 정렬
df = df.sort_values("log_date")
## 이동평균 생성
df = df.assign(push_count_ma = df.push_count.rolling(28).mean())

# MA plot
plt.plot(df.log_date, df.push_count_ma)
plt.xticks(rotation=20)
plt.show()

# QQ plot
sm.qqplot(df['std_resi'], line='45')
plt.xlabel("Theoretical Quantiles")
plt.ylabel("Standardized Residuals")
plt.show()

# normality
w, pvalue = stats.shapiro(df.residual)
print(f"Normaility test p-value: {pvalue:.4f}")

# equal variance
w, pvalue = stats.bartlett(*[group.residual for dow, group in df.groupby('day_of_week')])
print(f"Equal variance test p-value: {pvalue:.4f}")

# independency
d = durbin_watson(df.residual)
print(f"Independence test statistic: {d:.4f}")