데이터 분석 및 전처리시 자주 사용되진 않지만 필수적으로 알아두어야 할 방법들입니다.

- explode, Reverse explode, melt, cut, qcut

여기선 다루지 않지만 행(row), 열(column)을 쉽게 조작하게 도와주는 stack(), unstack() 함수도 유용합니다.

 

explode

list로 이루어진 데이터(column)를 다수의 행으로 펼쳐주는 방법, explode() 를 사용하면 손쉽게 해결할 수 있습니다.

df = pd.DataFrame({
    'SEG': ['A', 'B'],
    'VALUE': [[17, 23, 54], [19, 28, 51]]
})

df.explode('VALUE')

Reverse explode

위 방법의 반대입니다.
각 seg 별로 값을 list 형태로 합쳐줄 수 있습니다.

# as_index : index 해제
# agg : 각 SEG가 가지고 있는 VALUE를 tolist()함
ep_df.groupby('SEG', as_index = False).agg(lambda x: x.tolist())

 

melt

열을 행으로 펼쳐주고, 새로운 열을 생성합니다. 행과 열을 프로덕트(product)하여 column을 row value로 펼쳐줍니다. melt()를 활용합니다.

df = pd.DataFrame({
    'SEG': ['A', 'B'],
    'VALUE_1': [17, 23],
    'VALUE_2': [19, 28],
})

df.melt(id_vars = ['SEG'], value_vars = ['VALUE_1', 'VALUE_2'], 
		var_name = 'VALUE_SEG', value_name = 'VALUES')

stack() 을 활용하면 동일한 결과를 얻을 수 있습니다.

df.set_index('SEG').stack().to_frame(name = 'VALUES').reset_index()

 

cut, quct을 활용한 category encoding

숫자 형태 데이터에 category encoding을 수행하고 싶을때,

qcut(), cut() 을 활용하면 원하는 전처리를 수행할 수 있습니다.

df = pd.DataFrame({
    'AGES': [11, 16, 18, 
             21, 24, 29,
             30, 35, 36,
             40, 42, 43]
})

age_bins = [10, 20, 30, 40, 50]

df['AGE_SEG_cut'] = pd.cut(df['AGES'], age_bins)
df['AGE_SEG_qcut'] = pd.qcut(df['AGES'], q = 5)

파이썬 문법 중 하나인 데코레이터(Decorator)는 wrapping 방식을 통해 함수를 유연하게 적용하여, 특정 기능을 편리하게 사용하게 해줍니다.

아래처럼 특수기호 @를 붙여서 사용하는 방식이 데코레이터입니다.

@tf.function
def train_step(args):
    pass

글에서는 데코레이터의 개념, 동작방식이 아닌 몇 가지 유용하게 사용되는 또는 사용될만한 데코레이터를 알아보도록 하겠습니다.


@retry

@retry 데코레이터는 특정 예외가 발생한 경우, 설정한 횟수만큼 재시도하는 데코레이터입니다.

일반적인 상황에서는 잘 사용되진 않지만, 네트워크, DB 통신에서 유용하게 사용되는 함수입니다.

retrying 라이브러리나 직접 구현하여 사용하는 방법이 있지만, tenacity 라이브러리가 유명하게 사용됩니다. 아래 코드는 tenacity 공식 문서에 나와있는 예제입니다.

  - tenacity 공식 문서: https://tenacity.readthedocs.io/en/latest/

import random
from tenacity import retry

@retry
def do_something_unreliable():
    if random.randint(0, 10) > 1:
        raise IOError("Broken sauce, everything is hosed!!!111one")
    else:
        return "Awesome sauce!"

print(do_something_unreliable())

성공할때까지의 시도, wait 횟수 등도 parameter를 통해 조절할 수 있게 제공해줍니다.

@classmethod

@classmethod는 아래 @staticmethod와 함께 보면 좋을 것 같습니다.

어느정도 자바/C 언어를 경험한 분들이라면 클래스 또는 정적 변수에 익숙할 것입니다. 그 개념 그대로 파이썬에서도 클래스 변수, 클래스 메소드를 사용할 수 있습니다.

단일 클래스나 상속받은 클래스의 변수를 조정할 때 유용하게 사용됩니다.

암묵적으로 @classmethod 데코레이터를 사용하면 첫 번째 인자로 cls 파라미터를 사용합니다. 인스턴스 메소드라 불리우는 self와 비슷한 개념입니다. 우리는 cls 파라미터를 통해 클래스 변수에 접근할 수 있게 됩니다.

위에서 언급하였듯이 @classmethod를 통해 클래스 변수를 조정할 수 있고, upgrade_os 메소드처럼 새로운 생성자를 만들어 줄 수도 있습니다.

class Computer:
    os = 'Linux' # 클래스 변수
    
class Personal_Computer(Computer):
    def __init__(self, c_id, pos):
        self.c_id = c_id
        self.partial_os = pos
        
    @classmethod
    def change_os(cls, this_os):
        if cls.os != this_os:
            cls.os = this_os
        else:
            print(f'{cls.os} Already up-to-date!')
            
    @classmethod
    def upgrade_os(cls, c_id, new_os):
        return cls(c_id, new_os)
            
computer_1 = Personal_Computer('2021', 'window11')
computer_2 = Personal_Computer('2020', 'window10')

# 변경 전, Linux -> Window
print(f'os change Before: {computer_1.os}, {computer_2.os}')
Personal_Computer.change_os(this_os = 'Window')

# 변경 후
print(f'os change After: {computer_1.os}, {computer_2.os}', end = '\n\n')

# upgrade os
print(f'os upgrade Before: {computer_2.partial_os}')
upgraded_computer_2 = computer_2.upgrade_os(computer_1.c_id, 'window11')
print(f'os change After: {upgraded_computer_2.partial_os}')

@staticmethod

정적 메소드입니다. 이 데코레이터를 사용하면 아래 코드처럼 객체 생성없이 바로 클래스를 통해 접근하여 사용이 가능합니다. 유용해보이지만, 많이 사용하지 않는 기능입니다.

class Computer:
    os = 'Linux' # 클래스 변수
    
class Personal_Computer(Computer):      
    ..생략..
    
    @staticmethod
    def print_os():
        print(Computer.os)

Personal_Computer.print_os() # Linux

@property

@property 데코레이터는 getter/setter를 떠올리면 쉽다. 아래 코드처럼 사용 가능합니다.

@property는 getter, @name.setter는 setter 역할을 합니다.

class ERP:
    def __init__(self):
        self._salary = 100
    
    @property
    def salary(self):
        return self._salary
    
    @salary.setter
    def salary(self, value):
        if self._salary < 500:
            self._salary = value
        else:
            raise ValueError('No!')
        
erp = ERP()
print(erp.salary) # 100
erp.salry = 200
print(erp.salry) # 200

물론 파이썬 특성상 이렇게 하지않아도 변수에 자유롭게 접근하거나 새로운 값을 할당할 수 있습니다.
다만 파이썬에서는 언더바('_')를 활용해서 private/protected 의미를 부여하는데요.

'_name' 은 private, '__name'은 protected를 암묵적으로 의미합니다.

언더바를 사용하면 우리가 흔히 사용하는 기능인 객체 메소드 참조(예를 들면 Tab키를 눌러서 어떤 함수가 확인하는)가 되지 않습니다. 참조하려면 언더바를 명시적으로 붙여주어야 합니다.

뿐만 아니라 @name.setter의 기능은 당연히 setter와 같지만, 위의 예제처럼 변수 할당에 조건을 편리하게 걸 수 있다는 장점이 있습니다.

@dataclass

Python 3.7이상 버전부터 사용할 수 있는 @dataclass 데코레이터는 __init____eq____repr__ 등 함수를 자동으로 등록해주는 기능입니다.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
        
person1 = Person('kim', 10)
person2 = Person('kim', 10)

print(person1.name, person1.age)
print(person1 == person2)

위처럼 별도의 함수를 정의해주지 않아도 바로 사용할 수 있습니다.

@lru_cache

@lru_cache는 메모라이제이션(memorization) 기능을 의미합니다.

이 데코레이터를 사용하면 함수의 반환값을 max_size 크기까지 값을 저장할 수 있습니다.

from functools import lru_cache
import urllib

@lru_cache(maxsize = 32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'https://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'
    
id_check = []
for i in [8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991]:
    pep = get_pep(i)
    
    if i == 8:
        id_check.append(pep)
    
print(get_pep.cache_info()) # 총 hits 3 -> 320 2번, 8 1번 hit
assert id_check[0] is id_check[1] # id 비교, @lru_cache 사용안할 시 에러 발생!

@lru_cache 기능으로 인해 id_check에 들어가는 객체 id가 동일해서(캐시를 이용하기 때문) AssertionError가 발생하지 않습니다

매번 찾아보기 번거로워 사용하는 것만 정리해서 올려두었습니다.


가장 먼저 정규표현식하면 필수적으로 나오는 이메일과 핸드폰 번호 예제입니다.

# 스타일이나 표현하고 싶은 방법에 따라 다름, 여기선 가장 기본적인 문법
# 경우에 따라 특수문자를 표현하기 위해 %, - 등을 집어넣을 수 있다
# ex) r'([\w.%-]+@ 등)'
re.findall(r'(\w+)@(\w+).(\w+)', 'test123@tistory.com')
# [('test123', 'tistory', 'com')]
re.findall(r'(\w+)-(\w+)-(\w+)', '010-1234-5678')
# [('010', '1234', '5678')]

 

아래 예제에서는 findall로 대부분 예제를 작성했지만, 주로 search, match, findall을 사용합니다.

  • []
    • [] 안에 정의한 문자들에 포함되는 것을 찾는다
    • 단, '^'가 []의 맨 앞에 올 경우, 해당 패턴이 아닌 것을 찾는다
re.findall('[#4]', '1#a2b3c4')
# ['#', '4']

re.findall('[A-Z]', '12a3B4DZ')
# ['B', 'D', 'Z']

re.findall('[^0-9]', '12a3B4DZ')
# ['a', 'B', 'D', 'Z']
  • . (점)
    • 한 개 chracter(모든 문자)와 일치하는 것을 찾는다, .. 이면 character 두 개가 이어진 것을 찾는다
    • 만약 '점' 자체를 찾고 싶다면, \.으로 표현해주면 된다
re.findall('[0-9].[a-z]', '1#a2b3c4')
# ['1#a']
  • \w
    • character를 의미, \w\w는 character 두 개가 이어진 것을 의미한다
    • \W는 character가 아닌 문자를 의미
re.findall('[0-9]\w', '1a2b3c4')
# ['1a', '2b', '3c']
  • \d
    • 숫자를 의미, \d\d는 숫자 두 개가 이어진 것을 의미한다
    • \D는 숫자가 아닌 문자를 의미
re.findall('\d[a-z]', '1a2b3c4')
# ['1a', '2b', '3c']
  • \s
    • 공백을 의미(space, tab 등)
    • \S는 공백이 아닌 문자를 의미
  • ^과 $의 의미
    • ^: 문자열의 시작부터 일치
    • $: 문자열의 끝이 일치
re.findall(r'^abc', 'abc') # 'abc' -> a로 시작해야함
re.findall(r'^abc', '@abc') # 못찾음

re.findall(r'abc$', 'abc') # 'abc' -> c로 끝나야함
re.findall(r'abc$', 'abc@') # 못찾음
  • 문자열 앞에 r을 붙임
    • \t, \n 등 escape 표현을 무시하고 그대로 표현
a = r'test\n'
print(a) # test\n
  • '+', '*', '?'
    • +: 패턴이 적어도 한번은 나와야 추출
    • *: 패턴이 0번 이상인 경우 추출, 즉 없어도 추출함(+와 차이점)
    • ?: 패턴이 아예 없거나(0) 한번만 나오거나(1)
re.findall(r'a1+dz+', 'a1dzzzc') # 'a1dzzz'
re.findall(r'a1+dz+', 'adzzzc') # 못찾음

re.findall(r'a1*dz*', 'a1dzzzc') # 'a1dzzz'
re.findall(r'a1*dz*', 'adzzzc') # adzzz 찾음

re.findall(r'a1?dz?', 'a1dzzzc') # 'a1dz' -> 1 한번, z 한번
re.findall(r'a1?dz?', 'adzzzc') # adz -> 1 없음, z 한번
  • {}
    • 패턴 횟수를 설정해줄 때 사용
re.findall(r'te+st', 'test,teest,teeest') # ['test', 'teest', 'teeest']
re.findall(r'te{3}st', 'test,teest,teeest') # ['teeest'] -> e가 3번 반복되는 것만 추출
re.findall(r'te{2,3}st', 'test,teest,teeest') # ['teest', 'teeest'] -> e가 2~3번 반복되는 것만 추출
  • {}?
    • 최소 기준으로 매칭
re.search(r't.{2,3}', 'test,teest,teeest') # test
re.search(r't.{2,3}?', 'test,teest,teeest') # tes
  • 예제) 특정 문자로 시작되는 패턴 찾기
    • 여기서 다루는 예는 매우 한정적일 수 있습니다~
    • 여기서는 w로 시작되면서 숫자로 끝나는 패턴을 찾아보겠습니다. 단, w와 숫자 사이에는 공백이 있을 수 있고, 없을 수 있습니다.
re.search(r'[^A-Za-z0-9]w[\s]?\d+\d$', 'benz sclass w212') # w212
re.search(r'[^A-Za-z0-9]w[\s]?\d+\d$', 'benz sclass w 212') # w 212

 

1. @staticmethod

함수 객체를 선언하지 않고, 바로 method에 접근해서 사용할 수 있습니다.

class Test:
    def __init__(self):
        pass

    @staticmethod
    def print_x(x):
    	return print(x)
        
Test.print_x(10) # 객체 선언하지않고 바로 사용        

 

2. list의 extend

주로 list 뒤에 값을 붙인다고 하면, append 함수만 생각나는 경우가 많습니다.
extend 함수는 익숙하지 않으면 잘 생각이...

[1, 2, 3]과 [10, 20]에 각각 함수를 사용했을 때 반환 결과는 다음과 같습니다.

  • extend 함수: [1, 2, 3, 10, 20]
  • append 함수: [1, 2, 3, [10, 20]]
a = [1, 2, 3]
b = [10, 20]

a.append(b)
print(a) # [1, 2, 3, [10, 20]]

a.extend(b)
print(a) # [1, 2, 3, 10, 20]

 

3. Pandas -> Numpy

Pandas 라이브러리는 데이터 분석과 관련한 다양한 함수를 제공해줌과 동시에 NumPy 라이브러리와 같이 빠른 성능을 보여줍니다.

하지만 만약 하고 있는 작업을 NumPy Array를 활용한 연산으로 수행할 수 있다면, 되도록이면 NumPy 연산으로 바꿔보는 것을 추천합니다.

생각치 못한 곳에서 속도 향상이 일어날 수 있고, 이후 처리 작업으로 전환하기에도 매우 편리합니다.

 

4. 속도가 정말 느린 것 같다면 배치 연산을 활용

전체 데이터에 전처리(preprocessing) 작업을 수행한 후, 처리된 데이터를 stack, concat과 같은 함수를 사용하여
새로운 저장 공간(ex: list or DataFrame etc.)에 쌓는 경우 속도 저하 현상이 발생할 수 있습니다.

이는 전체 데이터를 한번에 쌓고자하는 작업 때문에 병목 현상이 발생한 경우인데
특히, 우리가 자주쓰는 loc, iloc 함수와 같이 인덱싱 기능이 포함된 함수를 사용할 경우 자주 만나볼 수 있는 문제입니다.

속도 향상을 위해 전체를 한번에 쌓는 방법보다 분할하여 쌓은 뒤, 합쳐(merge)주는 순서로 변경해보세요.
신경망 모델 학습 시에 배치 연산을 하는 것처럼 바꿔보면 큰 속도 향상을 기대해볼 수 있습니다.
(ex: 1,000개 데이터를 100개씩 나누어 10번 처리한 뒤, 한번에 합쳐주는 방법을 의미)

매우 쉬운 방법이지만 한번에 떠오르기 쉽지 않습니다.

 

5. Class Config 설정

클래스를 정의할 때 여러 가지 하이퍼파라미터 정의가 필요할 수 있습니다.

__init__함수내 변수로 선언하여 관리할 수 있지만, 다루는 함수가 많아지거나 복잡해질수록 무언가 실수로 하나씩 변경하지 못하는 실수가 발생하기 마련입니다.

이를 방지하고자 관리하는 스타일에 따라 (1) Config 클래스 (2) 외부 파일 저장 (3) Config dict를 인자로 넘기기 등 방법을 사용할 수 있는데 여기서 볼 것은 (3)번 방법입니다.

**kwargs(dict type)를 사용해서 정의된 하이퍼파라미터를 넘겨준 후, 클래스 변수로 등록하는 과정입니다.
(+ *args는 tuple type)

class Test:
    def __init__(self):
        pass
    
    def set_config(self, **kwargs):
        self.add_entries(**kwargs)
    
    def add_entries(self, **kwargs):
        for key, value in kwargs.items():
            self.__dict__[key] = value
            
config = {
    'num_epochs': 10,
    'batch_size': 1024,
    'hidden_nums': 16,
}

t = Test()
t.set_config(**config)

# 결과는 __dict__로 확인할 수 있다
# {'num_epochs': 10, 'batch_size': 1024, 'hidden_nums': 16}
print(t.__dict__)

 

6. Jupyter Notebook으로 수식 쓰기

수학 공식을 표현하고 싶을 때, LaTeX 방법으로 수식을 만들 수 있지만 복잡하고 어렵습니다.

# RMSE 예시
# $$RMSE=\sqrt {\sum_{i} (Y_{i} - \hat Y_{i})^2}$$

handcalcs 패키지를 활용하면 매우 쉽게 수식을 렌더링할 수 있습니다.

pip install handcalcs로 쉽게 설치할 수 있습니다. 설치 후, 모듈을 임포트합니다.

import handcalcs.render

%%render magic 명령어로 아래와 같이 쉽게 사용할 수 있습니다.

%%render

r = sqrt(a**2 +b**2)
x = (-b + sqrt(b**2 -4*a*c))/(2*a)

실제 Jupyter Notebook Output

언더바(_)를 통해 밑 이름도 지정할 수 있습니다.

%%render

# under -> '_'
a_x = 1
b_under_underunder = 2

7. 파이썬 크롤링하기

파이썬으로 크롤링을 해야할 때, 스크래피(Scrapy) 등 다양한 방법이 있지만 기본적으로 우리가 사용하는 방법은 BeautifulSoupSelenium 라이브러리입니다.

특히, Selenium은 ChromeDriver을 활용해서 뷰(View)를 확인할 수 있는 장점이 있어 명확하고, 몇몇 함수를 사용해 더욱 편리하게 크롤링을 진행할 수 있습니다. 뿐만 아니라 버튼 클릭부터 페이지 변경까지 매우 편리합니다.

그런데 만약 버튼 클릭, 페이지네이션(Pagination), 링크 들어가서 또 링크를 들어가야하는 등 여러 가지 복잡한 액션이 포함된 크롤링을 진행해야할 때는 어떨까요? 뿐만 아니라 파싱해야할 정보량도 많다면?
이때, Selenium만 활용하면 속도가 매우 느리다는 단점을 느낄 수 있습니다. 음... 굳이 별다른 이유를 설명하지 않아도 일단 뷰를 우리에게 제공한다는 점을 생각해보면 일반적으로 납득이 가는 상황입니다.

이에 대한 해결방법으로 BeautifulSoup만 활용해보는 것입니다. BeautifulSoup은 HTTP 통신으로 시각적인 정보를 제공하지 않고 바로 파싱을 진행할 수 있기 때문에 크롤링 속도가 매우 빠르다는 장점이 있습니다.

만약, 여기서 페이지네이션이나 링크를 타고 들어가는 크롤링 과정을 뷰로 확인하고 싶다면, Selenium을 활용하면 되겠죠. 그런데 단점은 속도잖아요. 속도가 걱정된다면?

두 방법을 섞어서 활용해보세요. 여러 가지 액션은 Selenium, 파싱은 BeautifulSoup를 활용하면 빠른 속도로 크롤링을 진행할 수 있습니다.

특정 가상 환경에 설치되어있는 패키지만 복사하고 싶다던지, 아니면 자신의 환경을 다른 사람에게 그대로 전달해줄 작업이 있으면 다음 방법을 사용하면 편리합니다.

(Copy) 의존 패키지 복사하기

1. activate env 가상환경 활성화

2. pip freeze > requirements.txt
명령어를 실행시키면 현재 경로에 txt 파일이 생성됩니다.
requirements는 관례적인 이름이기 때문에 다른 이름을 사용해도 무방합니다.

3. type requirements.txt로 확인 또는 직접 경로에 들어가서 열어봐도 됩니다.

(Install) 의존 패키지 설치하기

freeze 명령어를 통해 생성된 파일이 있는 경로에서 다음 명령어를 실행

1. pip install -r requirements.txt