하나의 전처리 코드로 여러 개의 파일에 적용시켜야 할 때 사용했던 방법에 대해 포스팅 하고자 한다.

한 디렉토리 내의 여러 파일을 가져와서 반복 작업을 할 때는 os 모듈의 listdir을 사용하면 된다.

os.listdir('파일 경로') 는 파일 경로 내의 파일명을 리스트로 불러와주는 메소드이다.

import os 


# path는 파일 경로(str type), lst 변수에 저장
lst = os.listdir(path)

# 디렉토리 내의 파일을 for 문으로 반복
for i in lst:
	반복할 코드

나의 경우 반복할 코드 안에 전처리 코드를 넣었다.

+ 이 작업을 왜 했냐면...

20년도 1월부터 22년 10월까지의 데이터를 받았는데 데이터가 월별로 들어가있고(총 34개 파일) 그 양이 무려 1기가에 달해 어떻게 전처리를 해야하나 고민했다. 한 번에 불러와서 다 붙인 뒤 전처리를 하면 너무 오래 걸리고 중간 중간 테스트 코드 실행도 쉽지 않을 것 같았다.

그래서 일단 샘플 데이터를 가지고 전처리 코드는 완료했는데, 이걸 어떻게 자동화 할까 고민하다가 

1. 디렉토리 내 파일을 불러온다

2. 전처리한다

3. 전처리된 파일을 파일명.csv로 저장한다.

4. 1번 다음 파일을 불러온다

5. 전처리한다

6. 파일명.csv 를 불러온다.

7. 5와 6을 합친다

8. 파일명.csv에 저장한다 (덮어씌우기)

이 프로세스로 진행했다.

이 때 raw data 디렉토리와 preprocessed data 디렉토리를 나누어 raw data 디렉토리에 파일명.csv가 저장되지 않도록 했다. 그리고 for문의 앞 뒤에 time 모듈을 통해 소요 시간을 체크했다. 코드를 가지고 있으면 누구나 실행할 수 있도록 디렉토리는 input으로 받았다.

import pandas as pd
import numpy as np
import re
import os
import warnings
import time
from time import time, sleep



# 경고창 무시하는 코드
warnings.filterwarnings('ignore')

# 미리 필요한 요소를 받아주기
path = str(input('file path: '))
path2 = str(input('파일을 저장할 경로'))
lst = os.listdir(path)
start = time() # 전처리 시간 확인
for i in lst:     
	전처리 코드	
    try:        
        old = pd.read_csv(path2 + '파일명.csv') # path2 :  전처리된 파일이 저장될 디렉토리
        new = pd.concat([변수명, old])
        print('concat 완료')
        new = new.drop_duplicates().reset_index(drop=True) # 중복 제거
        new.to_csv(path2 + '파일명.csv', index = False)
        # 다운로드
    
    except FileNotFoundError:        
        print('파일을 찾을 수 없습니다.')
        # 저장 전 중복 제거
        변수명 = orders.drop_duplicates()
        변수명.to_csv(path2 + '파일명.csv', index = False)
        
    print(f'{i} 저장 완료')


end = time()
print('소요시간(분):', round((end - start) / 60))

중간에 for문 안에 try - except를 통해 파일을 저장하고 다시 불러와서 전처리 해주고 덮어쓰는 과정을 진행했다. 

전처리한 파일을 처음 저장할 때에는 '파일을 찾을 수 없습니다' 문구를 내서 전처리한 객체만 저장해주고, 그 이후부터는 이전에 전처리한 파일을 불러와 다시 같은 이름으로 (파일명.csv) 저장해주었다.

참고로 파일명 앞에 \가 자꾸 에러가 나서 디렉토리 절대 경로를 복사한 뒤 뒤에 \을 꼭 붙여주어야 실행이 제대로 된다.

그리고 불러온 파일이 중복 없이 잘 저장되는지 확인하기 위해 전처리가 끝나고 수행이 완료될 때마다 파일 저장 완료 문구를 띄웠다.

전부 전처리가 끝난 파일은 700여 메가에 달해서 총 수행 시간은 20분 대로 걸렸던 것 같다.

개발의 즐거움은 역시,,, 💖 귀찮은 작업을 코드로 자동화하고 나면 뿌듯하다.

저희는 본격적인 전처리에 들어가기 앞서 데이터의 분포 및 형태를 알아보기 위해 데이터를 여러 가지 방법으로 뜯어보았습니다. 파일 즉 테이블이 총 3개로 이루어져 있기 때문에, 각각의 테이블이 가진 특성을 파악하기로 했습니다.

전처리 전 어떤 데이터를 살펴보면 좋을 지에 대한 안내 글이므로, 실질적인 전처리 (null값 대치, 데이터 프레임 쪼개기, 컬럼 정리-레이블링 등)는 실행 전임을 미리 알립니다.

데이터는 pd.read_csv 판다스 함수를 통해서 미리 불러온 상태입니다.

customer data 분석

1. info()

info 함수를 통해 데이터의 전체적인 count와 데이터 타입을 확인합니다.

customer.info()

customer data만 73메가 무슨일..?

전체 1,371,980개의 행이 있습니다. FN나 Active 컬럼의 경우는 2/3 정도는 null 값입니다. 조금 더 상세하게 살펴보기위해 전체 컬럼의 개수(col)와, 값이 있는 컬럼의 개수(rows_cnt), null값의 개수(null_cnt), 전체 컬럼에서 null값이 가지는 퍼센티지를 보여주는 데이터 프레임을 구성해 보았습니다.

col = customer.customer_id.count()
rows_cnt = customer.count()
null_cnt = customer.isnull().sum()
null_percentage = ((null_cnt/col).round(2) * 100).astype(str) + '%'
pd.DataFrame({'col': col,'rows_cnt':rows_cnt,'null_cnt':null_cnt, 'null_percentage':null_percentage})

패션 뉴스를 구독하지 않는 비율은 약 65% 즉, 35% 정도의 고객이 패션 뉴스를 구독 중입니다. Active회원은 34% 정도, fashion news frequency 컬럼에 대한 내용은 나중에 다시 살펴보도록 하겠습니다. age에도 약 1%의 null값이 있는데, 이를 어떤 값으로 채워줄 지는 분석 후 결정해보겠습니다.

 

2. describe()

describe 함수를 통해 데이터 분포를 확인합니다. percentiles 옵션을 주고, 숫자를 보기 좋게 구성하고자 round 함수를 써서 소수점 두 자리까지 나타내었습니다.

customer.describe(percentiles=[.25, .5, .75]).round(2)

스키마 정의서에 의하면 FN와 Active는 boolean 형 데이터 입니다. (1=True, 0=False)

FN은 패션 뉴스의 구독 여부, Active는 고객 활성화 여부입니다. 따라서 describe에서 사실상 가장 의미있는 숫자는 'age' 컬럼의 숫자입니다.

 

고객 테이블의 나이 분석

평균 나이는 36.39세, 표준 편차는 14.31, 가장 적은 나이의 고객은 16세, 가장 많은 나이는 99세, 중위수는 32세이고 중위수와 평균 나이를 보았을 때, 대체로 표준 편차의 종모형을 띌 것으로 예상됩니다. 다만 3분위수와 max값을 보았을 때는 이상치가 어느 정도 있을 것으로 예상됩니다.

box 플롯으로 간단히 살펴보면 아래와 같습니다.

import seaborn as sns
sns.boxplot(y='age', data = customer)

seaborn의 displot 을 이용하여 age의 분포를 더 자세히 살펴보았습니다. kind='kde' 옵션을 주어 커널밀도추정 그래프로 파악해보았습니다.

sns.displot(data=customer, x='age', kind='kde')

 

20~30대에 가장 많이 몰려있고, 40세쯤 저점을 찍었다가 50대 고객도 많이 분포되어있는 모습입니다. 20대 초반에 가장 많은 COUNT가 있는 것 같은데, statistics 라이브러리의 mode 함수를 이용하여 최빈값을 찾아보았습니다.

statistics.mode(customer.age)
21.0
21세가 가장 많은 수를 차지합니다.
 
간단히 customer 테이블의 나이에 대한 분석을 해보았습니다.
 
 
패션 뉴스 구독 관련 분석 < 글씨 크기를 바꿔야하는데, 티스토리가 아직 완전히 복구가 안 되었군요... 
글자 관련한 옵션이 설정이 안 되므로... 나중에 수정하겠습니다.
 
 
패션 뉴스 구독 컬럼이 어떻게 이루어져 있는 지 확인해보았습니다.

 

customer.FN.unique()
 
 
구독하지 않는 고객은 nan 값이, 구독 고객은 1의 값이 들어가 있습니다. 

구독하지 않는 고객과 구독하는 고객 특징을 비교할 수 있을 것 같습니다. 테이블 쪼개는 코드는 다음 번에 작성해 보겠습니다.

 

Active 관련 분석

customer.Active.unique()

Active관련한 값도 찾아보았는데, decribe에서 어느 정도 예상했지만, 값이 nan과 1로 나뉘어 있었습니다.

Active 컬럼이 가지는 의미를 정확하게 알 수 없어서  (이탈 고객?) 분석에 사용할 지 말 지를 FN 컬럼과의 차이를 알아봐야 결정할 수 있을 것 같습니다. 활성 사용자와 패션 뉴스 구독자이기 때문에 매출 등 다른 데이터와 결합했을 때 비슷한 추이가 나오지 않을까 여겨지지만, 모든 데이터는 뽑아봐야 아는 법... 

패션 뉴스를 구독하는 고객 중 Active 회원이 아닌 사람의 숫자

customer[customer['FN'] == 1].Active.isnull().sum()

12,526 명.. 제법 차이가 있습니다만, 비율로 살펴보면

(customer[customer['FN'] == 1].Active.isnull().sum() / customer.FN.count()).round(2)

round로 소수점 아래 두 자리까지 나오도록 반올림 해줬지만 전체 FN 구독자 그룹 중 Active 회원이 아닌 비중은 0.03% 이므로 패션 뉴스 구독자의 대부분은 Active 회원이라는 것을 알 수 있습니다. 

이제 결정의 시간입니다. FN 구독자와 Active 구독자를 나누는 것이 얼만큼의 의미가 있을까요?

아주 디테일한 차이까지 보고자 한다면 두 컬럼 다 진행하는 것이 좋겠으나, 리소스가 부족하다면 둘 중 하나의 컬럼만을 가지고 진행해도 대부분의 결과가 설명될 것 같다는 (개인적인) 결론입니다. 물론 이에 대해 다른 의견이 있으시다면 언제든지 알려주시면 감사하겠습니다.

간단하게 customers 테이블이 가진 데이터의 구조와 분포 등을 알아보았습니다. 여러 가지 코드와 기본적인 함수들을 사용하여 분포를 살펴보았고, 간단한 시각화를 곁들여보았습니다. 다음번에는 다른 데이터 테이블의 구조를 분석해보도록 하겠습니다.

분석에 필요한 통계량이 필요할 때, 우리는 평균(mean), 중앙값(median), 분산(variance), 표준 편차(standard deviation) 등을 주로 고려한다. 이 때, 연산하는 숫자들의 단위가 달라지면 크기를 비교하기 어렵기 때문에 우리는 '표준화' 라는 작업을 거치게 된다.

 

숫자들의 단위가 달라지면 왜 크기를 비교하기 어려운가요? (더보기 클릭)

더보기

아주 단순한 데이터셋을 가지고 가격에 따른 만족도를 비교 해보자.

가격 만족도(1 ~ 5)
3000 5
4500 4
6000 3
7500 2
9000 1

우리는 본능적으로 만족도 1을 올리기 위해서는 가격이 1500 낮아져야 한다는 음의 상관관계를 파악할 수 있다.

대략 이런 형태의 그래프가 나오게 된다.

 만약 두 변수의 기울기 변화를 같이 보고싶다면 어떻게 해야할까? 한 그래프 안에 두 변수의 기울기를 측정하는 그림을 그리면 아래와 같이 나타난다.

파란색은 price의 기울기, 주황색은 score의 기울기

절대 가치로 평가했을 때, 가격의 변화(기울기)에 비해 만족도의 변화(기울기)는 너무도 미미해보이지만, 사실 우리는 알고있다. 저 주황색이 두 칸(1단위) 움직일 때마다 파란색은 1500씩 움직여야 한다는 것을. 그림으로만 보면 가격이 만족도에 주는 영향이 거의 없어보이게 표현되지만, 사실 위 데이터에서 만족도를 예측하기 위해서는 가격이라는 요소가 크게 작용하게 된다. 

이렇게 두 변수 간의 크기 차이가 많이 나는 경우에는 통계치를 확인했을 때 평균, 분산, min, max 값의 차이가 많이 나게된다.

 

이처럼 두 데이터 간의 기울기 크기의 차이가 다를 때 (즉, 단위가 다를 때) 우리는 직관적으로 크기 비교를 하기 어려워진다.

 


scaler 의 종류

iris dataset을 이용하여 아래의 세 가지 scaler에 대해 알아보자.

0. iris dataset?

iris(붓꽃) 의 꽃받침과 꽃잎의 넓이, 길이를 측정하여 붓꽃 3종 (setosa, versicolor, virginica)을 주로 예측하는 데에 많이 쓰이는 (아주 유명한)데이터 셋이다. 얼마나 유명하냐면, 사이킷런에서 샘플 데이터로 제공할 만큼 유명하다. 

from sklearn.datasets import load_iris
iris = load_iris()

위와 같은 코드로 불러올 수도 있지만, 결과를 프린트하면 array 형태로 나오기 때문에 가독성이 좋지 않아 파일 데이터를 통해 DataFrame 형태로 가져왔다.

 

>> load_iris()로 가져오는 법 (더보기 클릭)

더보기

 

1. array의 구조를 살펴보면 dictionary 형태로 되어있는 것을 알 수 있다. data, target, target_names, DESCR(어떤 데이터셋인지 설명), feature_names, file_name, data_module

2. data가 우리가 분석해야되는 데이터 셋이 되고 target은 맞춰야하는 종속 변수 즉, 붓꽃의 종류가 된다.

3. array로 되어있기 때문에 dict를 하나씩 빼서 가독성좋게 DataFrame으로 만들어보면

import pandas as pd
pd.DataFrame(iris.data)

정보를 알아보기 쉽지 않다. columns의 값으로 feature_names (list)를 붙여주고

target도 마찬가지로 df로 만들었을 때 (코드 생략)

컬럼명을 붙여줘야 알아보기 쉬울 것 같다. target_names에 의하면

'target_names': array(['setosa', 'versicolor', 'virginica'], dtype='<U10')

0: setosa, 1: versicolor, 2: virginica 이다.

iris train data (data)에 columns 붙여주기

iris_train = pd.DataFrame(iris2.data) #train data로 학습시킬 데이터 가져오기, df로 만들어서 변수 저장
iris_train.columns = iris.feature_names #iris_train의 컬럼 이름은 iris(사이킷런에서 가져온 것)의 feature_names의 값입니다~

OUT:

iris 파일이 없다면 이렇게 만들어준 데이터 셋으로 조물조물 할 수 있다.

 

 

describe() 함수를 통해 수치형 데이터의 분포를 살펴보았다.

iris.describe()

sepal(오늘의 영단어: 꽃받침)과 petal(오늘의 영단어2: 꽃잎)의 전체적인 분포가 표준편차(std)도 크게 나타나고, min-max의 값도 상당히 차이나기 때문에 데이터를 같은 범위 안에 들어가도록 예쁘게 갈아줄 필요가 있을 것 같다.

 

1. Standard Scaler

기존 변수를 평균이 0이고 표준편차가 1인 표준정규분포로 만드는 스케일러

평균과 표준 표준 편차를 쓰기 때문에 이상치에 민감하다.

scikit-learn preprocessing의 StandardScaler 패키지를 사용한다.

from sklearn.preprocessing import StandardScaler #모듈 불러오기
standardscaler = StandardScaler() #객체에 저장해줍니다.
standardscaler.fit(train_data) #scaling 
std_iris = standardScaler.transform(iris) 
#train_data를 스케일링 된(학습된) 형태로 변환하여 변수에 저장해줍니다.

 

2. Min-Max Scaler

최대값을 1, 최소값을 0으로 설정하는 스케일러

마찬가지로 이상치가 있으면 민감하다.

from sklearn.preprocessing import MinMaxScaler
MinMaxScaler = MinMaxScaler()
MinMaxScaler.fit(train_data)
min_max_iris = MinMaxScaler.transform(train_data)

3. Robust Scaler

중앙값(median)과 IQR을 사용하여 outlier 대해 안정적으로 방어할 수 있는 스케일러 (그래서 robust)

from sklearn.preprocessing import RobustScaler
robustScaler = RobustScaler()
print(robustScaler.fit(train_data))
train_data_robustScaled = robustScaler.transform(train_data)

 

이에 더 자세히 데이터 분표별 스케일링 값을 가져오는 블로그가 있어 소개를 하려고 한다,

https://mkjjo.github.io/python/2019/01/10/scaler.html

데잇걸즈 SQL 2차 미니프로젝트로 Commerce Data Analysis를 위해 kaggle의 H&M 데이터 셋을 이용하였다. 본래 패션 개인화 추천 모델을 만드는 대회이지만, 추후 진행할 SQL을 통한 매출, 고객, 상품 데이터 분석을 위해 참고하기 좋은 데이터 셋이라 이 데이터 셋으로 작게 프로젝트를 진행했다. 즉, 이 시리즈 게시물은 일부는 Python 게시판에, 일부는 SQL 게시판에 작성될 예정이다.

https://www.kaggle.com/competitions/h-and-m-personalized-fashion-recommendations

 

H&M Personalized Fashion Recommendations | Kaggle

 

www.kaggle.com

 

Scheme나 컬럼 정의 등은 페이지를 참조하되, 설명이 부실한 측면이 있어 이 시리즈를 진행하면서 중간중간 설명을 진행할 예정이다. 아래에 첨부할 Scheme 정의서를 참고할 수도 있고, hnm 데이터를 가지고 분석 진행한 글도 많으니 참고해보면 좋을 듯 하다.

총 3개의 csv 파일로 저장되어 있으며, 

1. articles.csv : 상품 상세 정보

2. customers.csv : 고객 상세 정보

3. transactions.csv : 거래 상세 정보. 2018 2월 ~ 2020 2월까지의 거래 정보를 담고 있다. PK가 없음.

분석 전 팀원들과 함께 해당 데이터에 대해 함께 톺아보며 나름의 Scheme 정의서를 만들었다. 실제 import된 결과는 아니고, 다같이 데이터를 살펴보며 정의한 내용이므로 실제 import결과와는 다를 수 있다. 특히 0과 1의 values로 이뤄진 컬럼들은 연속적인 숫자 데이터가 아닌 categorical columns 이므로 대체로 str type(즉 sql에서는 text 또는 char, python에서는 object)으로 구분되어 있다.

https://docs.google.com/spreadsheets/d/1krlUZMndp3zJMZZIyXZbudXKW1aAQBkBmotZrVaiHa4/edit?usp=sharing 

 

스키마 정의서

시트1 테이블,컬럼,detail,data type,memo articles,article_id,상품의 옵션별 unique한 id값,str 제품의 특징,product_code,상품 분류 코드값,str prod_name,상품명,str,graphic에 따라 상품명이 구분됨 (추후 Graphic 분류 없

docs.google.com

ERD는 아래와 같으며 Workbench에서 ERD 만드는 법은 아래의 블로그를 참고하였다. 참고로 해당 테이블은 프로젝트용 전처리가 모두 완료된 테이블이라 articles, transactions의 실제 데이터셋과 조금은 다르다는 점을 미리 일러둔다.

articles의 경우 상품 그룹을 일부 drop 하였고, transactions의 경우 trasnactions_id 컬럼을 생성하여 PK를 만들어줬다.

transactions 의 customer_id와 article_id를 각각의 테이블에서 FK로 참조한다.

https://m.blog.naver.com/ajdkfl6445/221540488900

 

[MySQL-Workbench] 워크벤치에서 ERD 자동생성

안녕하세요 개발자 마스터정(Master Jung)입니다. 오늘은 워크벤치를 이용하여 자동으로 ERD를 추출하...

blog.naver.com

 


프로젝트를 시작하자마자 한 가지 문제에 부딪혔는데, 우선 파일의 크기가 너무 크다는 점이었다. articles와 customer의 경우 어떻게는 욱여넣을 수 있었지만, 일반적으로 사용하는 랩탑의 사양 수준에서는 3기가 이상의  csv 파일을 감당하기에는 무리였다. 

Mysql 워크벤치는 물론 Pymysql을 통한 import에서도 너무 많은 시간과 메모리 에러로 인해 import가 거의 불가한 수준이었으며, 혹여 import에 성공하더라도 분석을 위한 런타임 문제 등을 고려하여 데이터를 잘라주기로 했다.

1. Data import

 

import pandas as pd
import numpy as np

#Working Directory 확인
import os

# os모듈의 chdir로 미리 file path를 지정해 주었음
os.chdir(r'filepath')
tt = pd.read_csv('transactions_train.csv')
tt.head()

 

 

2. 자를 부분 골라내기: index를 활용한 data slicing

 

곧 가을-겨울 시즌이기도 하지만, 계절의 변화에 따라서 어떤 추이를 보이는 지를 확인하기 위해 19년도 7월 ~ 19년도 12월까지 총 6개월의 data를 통해 분석을 진행하기로 결정하였다.

따라서 t_dat의 컬럼 값이 2019-07-01 인 데이터부터 2019-12-31 인 데이터까지 자르기로 하였다.

중간에 들어가 있는 값들이므로 index 값을 활용하여 추출해주기로 했다.

pandas의 iloc는 인덱스를 기준으로 DataFrame을 잘라내는 기능을 한다. (index-location based)

Syntax: 데이터프레임 변수명.iloc[] 의 input으로 사용될 수 있는 것은
- 정수
- list나 array e.g. [4, 3, 0]
- 정수로 slice 된 것 (범위) e.g. [0:5]
  참고로 범위의 맨 마지막 인덱스는 포함하지 않는다. 따라서 위의 예시는 index 0번부터 4번까지 slice 한다.
- bool 형태의 array인덱스를 가져오는 함수

출처: Pandas 공식 Documentation

2-1. 우선 혹시나 날짜 순서가 다를 수 있으므로 t_dat을 기준으로 value를 정렬해주었다.

tt=tt.sort_values(by='t_dat').reset_index(drop=True)
# index를 꼭 reset해줘야 인덱스 정리가 되므로 필수

2-2. 정렬된 데이터 프레임에서 index value를 가져오기

# t_dat col의 value가 '2019-07-01'인 첫 번째 인덱스 값, '2019-12-31'인 마지막 인덱스 값
print(tt[tt['t_dat'] == '2019-07-01'].index.values[0])
print(tt[tt['t_dat'] == '2019-12-31'].index.values[-1])
13056870 # 2019년 7월 1일의 첫 번째 거래
20808191 # 2019년 12월 31일의 마지막 거래

아래와 같이 iloc를 통해 tt를가져오되, Syntax에서 설명한 바와 같이 19년의 마지막 거래는 20808191 이지만, 그대로 복사 + 붙여넣기 하면 마지막 index는 가져오지 않게 되므로, +1한 값인 20808192를 slice 할 index로 가져와야 한다.

만약 그대로 index 값을 복-붙 하고싶다면

print(tt[tt['t_dat'] == '2019-07-01'].index.values[0])
print(tt[tt['t_dat'] == '2020-01-01'].index.values[0]) # 20년 1월 1일의 0번 인덱스
13056870
20808192

이렇게 가져올 수도 있다. 그러나 그 다음 시퀀스에 대해 확신이 없을 수도 있으니(1월 1일에 휴무일이라 거래 데이터가 없는 경우 등등) 그냥 마지막 인덱스를 뽑고 +1 하는 게 정확할 수 있다는 생각이 들었다.

(+ 9.10 추가

생각해보니 그냥 index 뽑아온 값에서 +1 해주면 될 일이었다. 혹시 나중에라도 함수로 만들 분들은 아래의 코드 방식을 활용해보시라.. 나의 경우에는 일회성으로 쓰고 말거라 그냥 코드만 짰다.

print(tt[tt['t_dat'] == '2019-07-01'].index.values[0])
print(tt[tt['t_dat'] == '2019-12-31'].index.values[-1] +1) # 19년 12월 31일의 마지막 인덱스 +1

)

2-3. 데이터 프레임 슬라이스

# idex 값을 바탕으로 pandas의 .iloc 를 활용하여 추출, index reset해주기
# tt2 변수에 저장
tt2=tt.iloc[13056870:20808192].reset_index(drop=True)

그러면 아래와 같은 데이터 프레임이 생성된다.

 

이렇게 인덱스를 활용한 데이터 슬라이스를 완료하였다.

 

프로젝트를 하면서 사용하는 데이터 셋 중 구매 기록을 나타낸 transactions 파일에 unique하면서도 not-null인 컬럼이 없어 DB에 넣을 때 PK를 지정하기 위해 id값을 임의로 생성할 필요가 있었다.

 

import pandas as pd
import numpy as np

#file path는 미리 변경해주었음
transactions = pd.read_csv('transactions_train.csv')

# index를 풀어줘서 인덱스와 같은 컬럼을 생성, inplace=True로 바로 적용
transactions.reset_index(inplace=True)
# colume rename
transactions.rename(columns={'Unnamed: 0':'id'}, inplace=True)

output:

transaction 파일에서 reset_index()를 통해 인덱스를 복사한 컬럼을 생성해주었고, id값을 1부터 시작하기 위해서 id 컬럼의 값을 바꿔줄 필요가 있었다.

 


 

index의 값보다 +1인 값을 적용하기

 

Solution 1: for문

i = 0
for i in range(len(transactions)):
    transactions['id'].iloc[i] += 1

i를 0으로 초기화하고 for loop를 통해 id 컬럼의 index에 +1하고 다음 루프 진행

Problem:

rows가 770만 이상이었기 때문에 (...) 시간이 너무 오래 걸렸다. for loop 자체가 메모리도 많이 잡아먹는데다 구동하는 다른 프로그램도 있어 몇십 분이 지나도록 루프가 끝나지 않았다. 마치 무한 루프에 걸린 것 같았다. 결국 포기하고 id를 0부터 시작할까 고민하다 for loop를 사용하지 않는 방법이 있을 것 같아서 검색을 시작했다. (여러 번의 경험으로 데이터 프레임 일부 value를 한 번에 바꿔줄 대에는 반복문을 사용하지 않고 바로 적용할 수 있는 방법이 존재하는 경우가 많았다.)

 

Solution 2: index사용

#transactions의 id 컬럼의 값은 transactions의 index +1
transactions['id'] = transactions.index +1

index를 1부터 시작하고 싶다면, .index method에 +1 해주면 된다는 글 발견. < 이걸 조금 변형해서 사용했다.

너무 허무하게 해결되어서 좀 어질했다. 암튼 작업은 빨리 끝남. 실행 시간 밀리세컨 단위에서 끝난듯..  python은 가끔씩 이렇게 허를 찌를 때가 있다. 

output:

 

원하던대로 1부터 마지막 행의 index 값 +1 까지 잘 나와줬다.

 

지난번에는 groupby와 pivot_table 을 통해 데이터를 일정 기준으로 묶어 재구성하는 법을 배웠다. 그냥 피봇테이블로 사용하려는 목적이라면 그정도로 끝내도 상관없지만, 머신러닝에서 컬럼별 영향력을 확인하기 위해서는 각각의 컬럼을 feature로 삼아 분석하는 것이 좋기 때문에, 멀티인덱싱이 되어있으면 분석 결과에 부정적인 영향을 미칠 수 있다.

그리고 python으로 피봇테이블만 만들거면... 차라리 UI로 빠르게 확인 가능한 엑셀을 사용하는 게 더 직관적일수도...

 

그런 이유로 이번에는 멀티인덱스 되어있는 데이터를 풀어주는 .unstack을 사용해볼 예정이다. 

 

 

지난번에 그룹핑한 데이터이다. 보통 일자는 행렬에서 열로 사용하지 않는다. 이유는 분석이 어렵기 때문이다.

기본적인 내용이지만 열에는 하나의 상위 개념을 갖고 그 상위 개념이 가지는 value들이 들어가는 형태가 일반적이다. 그래야 통일된 하나의 개념에 다양한 값이 할당되기 때문이다. 그러나 위 행렬처럼 컬럼이 2018.1 이런식이면 그 내용이 상당히 지엽적이기 때문에 (즉 하나로 묶어주는 상위 개념 범위가 너무 좁음)

[일자 - 업종 - 광고주 - 상품 - 매체 - 항목] 순서 형태의 데이터로 변환해줄 예정이다. 

깔-끔

 이렇게 !

 

추가로 년, 월 컬럼을 새로 생성했는데, 이는 추후 시각화 및 머신러닝 과정에서 각 연도와 월의 영향력 등을 알아볼 수 있는 feature가 될 수 있다.

 

 

1. unstack()

 

unstack이란 무엇인가?

stack을 구글링하면 '사전적으로 Stack(스택)은 '쌓다', '더미'라는 의미를 가지고 있다. 접시를 차곡차곡 쌓아 올리듯이 데이터를 쌓아올리는 형상을 생각하면 된다. Stack(스택)은 Queue(큐)와 함께 자바에서 사용되는 가장 기본적인 자료구조 중 하나다. 라는 설명이 처음으로 나온다. (저만 그런가요?)

 

요컨대 '쌓다' 라는 의미를 가진 단어이다. 

 

그렇다면 unstack은 반대로 쌓인 것을 풀어헤치는 뜻이 되겠다.

 

우리가 앞에서 만든 데이터는 인덱스가 상위 개념에서 하위 개념 순서대로 쌓여있었다. 이를 하나의 행으로 다 해체시키는 것이 이번 역할이다. 

 

.unstack 이라는 method를 쓰는 것을 알았으니, 적용해보자.

 

Syntax:
DataFrame.unstack(level=- 1, fill_value=None)

level은 어느 수준까지 풀어해칠 것인가를 의미하고, fill_value는 nan 값에 대해 어떤 겂을 채울 것인지 넣어주면 된다.

level의 디폴트 값은 -1로 지정되어있는데, 전체를 풀어헤친다는 의미다.

 

 

 

지정한 변수에 넣어줘도 되지만 (df_), 바로 기존의 코드 뒤에 붙여줘도 괜찮다.

 

df_.unstack()

그러면

 

이런 모양이 나온다. 

 

아니 인덱스 풀어주래서 풀어줬더니 항목이 컬럼으로 붙고 인덱스는 그대로 남아있는 결과를 뱉는가? 라고 물으신다면..

 

간단하게 설명하자면, unstack은 컬럼의 내용을 인덱스로 보내주는 함수이기 때문이다.

즉, 현재 데이터에서는 행렬전치를 먼저 행해줘야 인덱스의 모든 값들이 value로 펼쳐지게 된다.

 

df_.T.unstack().reset_index() #unstack은 인덱스로 반환을 해주기 때문에, 반드시 reset_index()를 적용해야만 원하는 값을 얻을 수 있다.

얼추 원하던 그림과 비슷한 모양이 나왔다.

 

더보기

참고로 unstack을 설명하기 위해 서치하던 중 굉장히 설명이 잘 되어있는 블로그를 발견했다.

https://gibles-deepmind.tistory.com/41 

 

Pandas | Stack VS Unstack

안녕하세요 신입 초보 분석가입니다. 데이터 분석을 공부하면서 고민이 됐던 내용들을 포스팅합니다. github.com/GiblesDeepMind/deepPythonAnalysis GiblesDeepMind/deepPythonAnalysis I'm JDM! The man who wi..

gibles-deepmind.tistory.com

.stack과 .unstack에 대한 설명인데, 사실 전처리 하면서 일자가 모두 컬럼이 지정되어있는 진짜 더러운 데이터는 처음 만나봤기 때문에 행렬 전치를 하고 unstack을 사용했으나 (unstack 쓸 일이 훨씬 많았기 때문에 stack 함수는 사용을 하지 않았다.) 해당 포스트를 읽고 stack을 사용한 방법도 함께 진행해 보았다.

df_.stack().reset_index()

 결과는 같게 나왔으므로, 이미지 첨부는 생략한다.

 

이 모든 과정들을 두 포스팅으로 나누어 설명했지만, 사실 이 코드들은 한 줄에 붙여서 아래의 네 가지 방식으로 표현이 가능하다.

 

#1
df_=df.groupby(['업종', '광고주', '상품', '매체', '항목']).sum().T.unstack().reset_index()
#2
df_=df.groupby(['업종', '광고주', '상품', '매체', '항목']).sum().stack().reset_index()
#3
df_=df.pivot_table(index=['업종', '광고주', '상품', '매체', '항목'], aggfunc='sum').T.unstack().reset_index()
#4
df_=df.pivot_table(index=['업종', '광고주', '상품', '매체', '항목'], aggfunc='sum').stack().reset_index()

이렇게!

 

쓰는 함수를 계속 쓸 수도 있지만 이렇게 다양한 방법으로 코드를 공부하고 짜는 것이 새로운 데이터를 만났을 때 어떤 방식으로 다루면 되는 지 각을 잡는 데에 도움이 많이 되는 것 같다. 이런 것도 있구나 하면서 써먹는 계기도 되는 것 같다.

 

이렇게 데이터를 재구성하고 해체, 행렬을 전치하는 방법까지 두 포스트에 걸쳐 알아봤다.

 

syntax의 경우 모두 pandas documentation (https://pandas.pydata.org/docs)에서 가져오고 있으니, 설명이 부족한 부분은 참고하여 다양한 파라미터와 함께 유용하게 사용하길 바란다. (우선 나부터..!)

 

 

 

데이터를 여러 기준으로 그룹화 하는 방법 (데이터 재구성)

 

아래 가상의 데이터를 그룹핑 하는 법을 두 가지 방식으로 안내할 예정이다. 통상적으로 '데이터 재구성'이라는 표현을 많이 쓰는 것 같다.

 

아래의 DataFrame을 df라는 변수로 불러온 뒤 업종, 광고주, 상품, 매체별 금액으로 보고자 한다.

 

한 눈에 어느 매체에 얼마나 쓰였는지 알기 어렵다.


 

1. groupby()

 

데이터를 여러 가지 기준으로 그룹화 하고싶을 때 쓰는 방법이 있다. 우선 가장 흔히 그룹핑할 때 사용하는 pandas DataFrame의 .groupby

문법은 아래와 같다.

 

syntax: DataFrame.groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=NoDefault.no_default, observed=False, dropna=True)

by: 그룹핑 기준. 자료 형태는 mapping, function, label, or list of labels 가 들어올 수 있다.

axis: 행을 그룹화 할 것인지, 열을 그룹화 할 것인지 지정할 수 있다. 디폴트는 axis=0 즉, 행을 그룹핑 하는 것이다. 열을 그룹핑 하고싶으면 axis=1

level: 축이 멀티인덱싱 되어있을 때 (하나의 그룹이 아닌 다중 그룹으로 묶여있을 때) 특정 기준을 통해 

 

여기서 위에 설명한 것처럼 by의 값을 list로 묶어 DataFrame.groupby(by=[a,b,c] ... ) 형태로 사용할 수 있다. 즉, by 내의 list value들을 다중 인덱싱(multi-indexing) 할 수 있다.

 

사실 글로 설명하면 멀티인덱싱에 대한 개념이 잘 들어오지 않으므로 예제를 통해서 알아보자.


 

업종, 광고주, 상품, 매체별 금액을 보는 것이 목적이므로

그룹핑 대상: 업종, 광고주, 상품, 매체 가 될 것이며, 그룹별 금액을 보고자 하므로 금액을 모두 합산해 주기로 한다.

 

df=df[df['항목']=='금액'] #df의 항목 컬럼의 값이 '금액'인 것만 추출
df.drop('계', axis=1, inplace=True) #금액의 합산값을 사용하므로, 총계 컬럼을 제외, inplace=True로 데이터에 바로 적용
df_=df.groupby(by=['업종', '광고주', '상품', '매체', '항목']).sum() #df_ 라는 변수에 아래와 같이 지정
#그룹핑한 테이블에 어떤 값을 반영할 것인지 (이 경우에는 sum값) .groupby 뒤 method로 지정 (.sum())

 

위 코드를 실행하면 아래와 같이 업종-광고주-상품-매체-항목별 하위 그룹을 생성하며 데이터가 그룹핑 된다. 

 

2. pivot_table()

 

pivot_table()을 사용해서 같은 결과를 만들 수 있다.

 

Syntax:
DataFrame.pivot_table(values=None, index=None, columns=None, aggfunc='mean', fill_value=None, margins=False, dropna=True, margins_name='All', observed=False, sort=True)

엑셀의 pivot table을 자주 사용하는 사람이라면 어떤 식으로 데이터를 만들어주는 지 바로 이해할 수 있을 것 같다.

 

index, columns, values: 피벗테이블에 지정할 인덱스, 컬럼, 값

aggfunc: 연산 방법을 지정. 통계 함수 사용 가능. groupby 에서 method로 지정한 것과 같다. default는 mean

fill_value: NaN 값 대치 

margins: 부분합에 대한 컬럼 추가

dropna: 피벗테이블을 생성한 뒤 생기는 null 값을 떨군다

margins_name: margins의 컬럼 이름을 지정, default 는 'All'

 

df를 다시 불러와서 이번에는 pivot_table 로 재구성해보자.


이번엔 pivot_table 차례

 

df2=df.pivot_table(index=['업종', '광고주', '상품', '매체', '항목'], aggfunc='sum')
#index에 그룹핑할 기준들을 지정, value는 들어갈 수 있는 자료가 하나밖에 없으므로 (금액) 생략했다

 

이렇게 pivot_table로 지정해도 같은 값이 나온다. 문제는 글자순으로 정렬되다보니 컬럼 순서를 그대로 가져오지 않고 자동으로 오름차순 정렬을 한 것이다. 구글링을 해도 찾아보기 쉽지 않았는데, 추후 해결되면 추가로 작성해야겠다.

사실 추후에 작업할 내용에 sorting이 있기때문에 지금 굳이 정렬을 하지는 않아줘도 괜찮다.

 

다음은 컬럼을 행으로 변환시켜 '일자'컬럼을 만들고, 추후 분석하기 쉽도록 pivot table의 멀티 인덱스를 해제해줄 예정이다.

 

데이터 재구성은 주로 groupby 만 사용했고, pivot_table을 습관적으로 사용하지는 않았는데 게시글을 작성하면서 조사하다보니 초기에 설정 가능한 유용한 기능이 많다는걸 알았다. groupby 뿐만 아니라 pivot 관련한 메소드도 자유자재로 사용할 수 있도록 연습해야겠다.

 

 

 

+ Recent posts