ML에서 데이터 타입들에 따라 다르게 전처리 한다.

 

예를 들어 범주형과 수치형으로 나눌수 있다.

범주형 수치형
숫자값으로는 의미를 안가짐 숫자값으로 의미를 가짐
소형, 중형, 대형 170cm, 164.5cm , ...

위와 같이 확실히 다른것을 할수 있다..

주로 데이터들이 범주형은 pandas에서 dtypes 함수를 호출했을때 결과값은 object이고

수치형은 float 등 숫자형으로 나올것이다. ( 물론 dataFrame.info() 와 dataFrame.describe() 를 통해 정확히 분류해야함 )

 

그러면 전처리시 범주형과 수치형을 나눠서 해야하는데 인덱스별로 즉 컬럼별로 다 코딩을 해야하는것은 아니다.

 

범주형 컬럼, 수치형 컬럼을 미리 구분해두고 저장해둔다음에 반복문을 돌려두면 된다.

 


범주형, 수치형 column 나누는 방법

 

나누기 위해서 우선 pandas에서 지원하는 dtypes에 대해서 알아볼 필요가 있다.

 

dataFrame.dtypes 의 결과값의 타입은 Series형태이다. 즉 column이 하나인 dataFrame으로 나온다고 봐도 상관없다.

 

Series 로 나오는것을 확인 할 수 있다.

 

 

그러면 이제 data라는 이름의 dataFrame에는 어떤 column이 들어있는지 확인하면 아래와 같다.

 

 

총 2개의 object 타입이 있고 나머지는 int와 float로 구성되어있다.

int나 float형에도 범주형이 있을수 있으므로 describe 함수로 좀더 자세히 확인해보자

 

column의 이름에서도 알수는 있지만 각 수치 그자체로 의미를 가지므로 범주형이라 할 수 없다.

 

그러면 이제 범주형과 수치형을 직접 나눠서 저장하는 방법도 있지만 자동으로 해보는 코드를 살펴본다.

 

아래의 코드를 먼저 확인해보면 float, int, object인지 판별하는 간단한 코드이다.

 

코드 결과

위 사진을 보면 Object를 판별 하는 방법은 'O' 인지 확인하면 된다.

 

그럼 아래의 코드들로 컬럼들을 구분할 수 있다.

 

먼저 인코딩이 있으면 디코딩도 있을거고 이 두개 차이가 뭔데..?

컴퓨터 구조 시간에 인코더랑 디코더를 배웠던 기억이 있는데 이게 머신러닝에서도 쓰인다고..?

 

간단하게 설명하자면

  • 인코더 : 원래있는 데이터를 사용자가 정의한 방식으로 다르게 표현하는 것  (  ex. 암호문 만들기 )
  • 디코더 : 인코더로 다르게 표현된 값을 다시 원상복구 시키는 것 ( ex. 암호문 해독기 )

그럼 머신러닝에서 인코더는 언제 쓰여..?

 

머신러닝에 있어 데이터는 수치형뿐만 아닌 범주형의 데이터들도 많아. 또는 string값들도 있지

머신이 교육을 하기 위해서는 당연히 string값들보단 int형으로 들어가는게 편하겠지 우리가 동작하는 과정을 이해 하기 쉽고.

 

그러기 위해서 범주형이나 string의 값들을 int로 다르게 표현하기 위해서 사용되는게 인코더야

 


Sklearn.preprocessing 패키지를 이용한 범주형 변수 인코딩

여기에서 범주형 변수란, 차의 등급을 나타내는 [소형, 중형, 대형] 처럼 표현되는 변수이다.

범주형 변수는 주로 데이터 상에서 문자열로 표현되는 경우가 많으며, 문자와 숫자가 매핑되는 형태로 표현되기도 한다.

 

위 패키지에서 다뤄 볼 것은 label encoding, one-hot encoding이다.

 


1. Label Encoding

라벨 인코딩은 n개의 범주형 데이터를 0~n-1 의 연속적인 수치 데이터로 표현한다.


소형 : 0
중형 : 1
대형 : 2

  • 라벨 인코딩은 간단한 방법이지만, '소형'과 '중형'이라는 범주형 데이터가 가지고 있는 차이가 0과 1의 수치적인 차이라는 의미가 아님을 주의해야 한다..

 

 


2. One-hot Encoding

원핫 인코딩은 n개의 범주형 데이터를 n개의 비트(0,1) 벡터로 표현한다.
예를 들어, 위에서 언급한 소형, 중형, 대형으로 이루어진 범주형 변수를 원핫 인코딩을 통해 변환하면 다음과 같이 표현할 수 있습니다.


소형 : [1, 0, 0]
중형 : [0, 1, 0]
대형 : [0, 0, 1]

  • 원핫 인코딩으로 범주형 데이터를 나타내게되면, 서로 다른 범주에 대해서는 벡터 내적을 취했을 때 내적 값이 0이 나오게 된다. 간단하게 말해서 중형에 1값이 있다고 다른 소형이나 대형에서 0이었던 값이 변하는것이 아닌 독립적으로 계산된다.
  • Sklearn의 One-hot Encoder는 numpy 행렬로 입력을 넣어줘야 정상적으로 작동하므로, pandas DataFrame에서 numpy-array로 추출하여 사용한다.

데이터에 null값이 존재할때 즉 데이터 프레임에 빵꾸 나있을때에 그대로 머신에 넣으면 오류가 난다.. ( 아닌가..? 해본적이없다.. )

이를 해결하기위해 우리가 임의로 데이터를 넣자니 안될거같고.. 어떤 방법들이 있는지 알아보자..

일단 아래 나온 방법들이 가장 좋다라는 것은 아니다 언제나 데이터에 따라 머신에 따라 어떤 방법을 써야할지는 엔지니어가 결정해야한다.

 


pandas로 null값 확인하기

  • 먼저 imputation을 하기전에 null이 있는지부터 확인을 해야한다.
  • 방법은 isna 또는 isnull이 있다. 둘다 같은 함수.. 이름만 다른..
  • pd.isnull( dataFrame ).sum() 을 통해 결측치의 갯수를 확인 할 수 있다.
    • pd.isnull 을 통해 True / False로만 구성된 dataFrame생성
    • sum()을 통해 열별로 계산 ( True +1, False +0 )
    • sum()을 한번 더 하게 된다면 행으로도 계산하여 데이터 프레임 전체의 null값을 확인할 수 있다.

 

pd.isna( dataframe ) 을 통해 true/false 데이터프레임 생성
sum() 을 통해 열별 합 계산

 

sum().sum()을 통해 전체 null갯수 확인

 


Sklearn.impute.Simplelmputer 을 통한 대체

simpleimputer을 통해서는 평균, 최빈, 중앙값등 간단한 대체가 가능하다.

라이브러리 사용을 위해 import

1. 평균값으로 대체 (Mean Imputation)

  • 결측치가 존재하는 변수에서 결측되지 않은 나머지 값들의 평균을 내어 결측치를 대체하는 방법이다.
  • 해당 값으로 대체 시 변수의 평균값이 변하지 않는다는 장점이 있지만, 많은 단점이 존재한다. 

strategy 변수에 mean을 줌으로 평균으로 대체하게해줌

 

2. 중앙값으로 대체 (Median Imputation)

  • 중간값은 데이터 샘플을 개수에 대해서 절반으로 나누는 위치의 값을 말한다.
  • 데이터 샘플의 수가 짝수개일 때에는 중간에 위치한 두 값의 평균을 사용한다.
  • 모든 관측치의 값을 모두 반영하지 않으므로 지나치게 작거나 큰 값(이상치)들의 영향을 덜 받는다.

 

 

 

3. 최빈값으로 대체 (Most-Frequent Imputation)

  • 최빈값은 가장 많이 나온 값이다.

 


 

1. 새로운 값으로 대체 (Substitution)

아예 해당 데이터 대신에 샘플링 되지 않은 다른 데이터에서 값을 가져온다. (그렇다면 validation set에서도 쓰지 않고 아예 버리게 되는 셈인 건가?)

2. Hot deck imputation

 다른 변수에서 비슷한 값을 갖는 데이터 중에서 하나를 랜덤 샘플링하여 그 값을 복사해오는 방법. 이 방법은 결측값이 존재하는 변수가 가질 수 있는 값의 범위가 한정되어 있을 때 이점을 갖는다. 또한 random하게 가져온 값이기 때문에 어느 정도 변동성을 더해준다는 점에서 표준오차의 정확도에 어느 정도 기여를 한다는 점이다.

3. Cold deck imputation

 Hot deck imputation과 유사하게, 다른 변수에서 비슷한 값을 갖는 데이터 중에서 하나를 골라 그 값으로 결측치를 대체하는 방식이다. 다만 cold deck imputation에서는 비슷한 양상의 데이터 중에서 하나를 랜덤 샘플링하는 것이 아니라 어떠한 규칙 하(예를 들면, k번째 샘플의 값을 취해온다는 등)에서 하나를 선정하는 것이다. 이 경우 hot deck imputation 과정에서 부여되는 random variation이 제거된다.

4. Regression imputation

 결측치가 존재하지 않는 변수를 feature로 삼고, 결측치를 채우고자 하는 변수를 target으로 삼아 regression task를 진행하는 것이다. 데이터 내의 다른 변수를 기반으로 결측치를 예측하는 것이기 때문에 변수 간 관계를 그대로 보존할 수 있지만 동시에 예측치 간 variability는 보존하지 못한다. (회귀분석을 생각해보면 regression line은 random component가 존재하지 않는다. regression 값 그 자체로 존재한다. )

5. Stochastic regression imputation

regression 방법에 random residual value를 더해서 결측치의 최종 예측값으로 대체하는 방식. regression 방법의 이점을 모두 갖는데다 random component를 갖는 데에서 따르는 이점 또한 갖는다. 

6. Interpolation and extrapolation (보간법, 보외법)

같은 대상으로부터 얻은 다른 관측치로부터 결측치 부분을 추정하는 것이다. 이 경우는 longitudinal data의 경우(어린이의 성장 과정을 추적하는 과정에서 얻은 키 데이터라든지 하는 경우)에만 가능할 것이다. 

 

 


결론

위와 같이 많은 방법들이 있다. 물론 이 방법들로 진행해야 하는것은 없다.

 하지만 서로 다른 방법에 따라 결과의 차이는 분명히 나타난다.

간단한 아래의 예시들을 비교해보면

  • 타이타닉에서 나이의 null 값을 나이의 평균으로 넣는방법
  • 타이타닉에서 나이의 null값을 이름 열을 통해서 대략적인 성별을 판단해 성별의 평균 나이로 넣는방법

분명 둘다 mean을 통한 imputation이지만 조금만 더 자세하게 다룰수 있다..

결국 쓰는사람이 데이터를 좀더 자세하게 다룬다면 더 좋은 결과가 나올수 있다.

스케일링 하는 이유

  • 데이터간 범위가 다르다면 머신이 동작하면서 오류값이 가장 낮은 곳으로 수렴하는 과정에서 속도 차이가 발생
  • 예를 들어 나이, 몸무게, 키 데이터가 있다면 나이와 몸무게는 키 데이터보다 숫자가 작아서 오류 역전파 과정에서 가중치가 크게 변하지 않음 하지만 키 데이터는 숫자가 커서 크게 변함..
  • 이를 해결하기 위해 모든 데이터들의 범위를 똑같이 맞춰주는 과정이 scaling
  • 분포의 모양을 바꾸는것은 아니다.

 


1. Min - Max Scaling

 

  • 수치형 데이터를 0~1 사이로 변경시켜줌
  • 언제나 0과 1의 값은 존재하게된다.
  • 코드 예시

우선 필요한 라이브러리 import 스케일러는 sklearn.preprocessing에 있다

 

 

랜덤 정수값으로 dataFrame 생성해서 확인을 한다. ( 1~100사이의 정수로 이루어진 size [200,5] df )

 

scaler 불러와서 우선 fit을 한다. fit은 통계값을 계산하는 과정이라 생각하면된다 여기서는 최대 최소값 찾는정도. 이후 transform을 통해 minMax Scaling수행하여 dataframe으로 확인한다.

 

최대 최소가 모두 0,1 이 된것을 확인 할 수 있다.

 


2. Standard Scaling 

  • 표준 정규분포화 하여서 평균이 0이고 표준편차가 1이 되도록 scaling을 하는 방법이다.
  • z-score 정규화라고도 말한다.
  • 코드 예시

standardScaler는 sklearn.preprocessing에 있다.

 

위의 minMax scaler와 똑같이 사용하면 된다.

 

mean이 정확한 0이 아닌 아주 작은 수로 되긴했지만 이는 컴퓨터의 표현의 한계이므로 넘어가자 std인 표준편차도 1로 수렴한것을 볼 수 있다.

 

머신러닝과 데이터의 관계는 땔수 없다.

 

이때 데이터를불러와 데이터를 전처리하고 가공하는 작업을 python의 기본 문법으로 하기에는 쉽지않다..

 

애초에 엑셀 데이터를 기본 python 문법으로 불러오는것부터 막막하다.

 

이를 위해 numpy와 pandas는 머신러닝을 배우기 시작할때 가장먼저 배우게 된다.

 


파이썬 기본 라이브러리에는 numpy와 pandas가 없으므로 import문을 통해 가져와야한다.

 

numpy와 pandas가 뭔지 모르겠으면 아래의 링크를 확인...

 

[Python] Numpy와 Pandas 왜 둘 다 배워..? :: 음기의 공부노트 (tistory.com)

 

[Python] Numpy와 Pandas 왜 둘 다 배워..?

솔직히 csv읽어오고 데이터 전처리시 data frame만 사용하는거 같은데 왜 둘다 배움..? . .  pandas로 불러온 data frame에서 values로 계산 작업할때 결과물이 모두 numpy라서 numpy도 필요해 NumPy 특징 N-deme..

eomgisan.tistory.com

 


공통

1. 인덱싱과 슬라이싱

  • 파이썬의 기본 문법으로 타 언어보다 다양한 방법과 쉽게 인덱싱 그리고 슬라이싱을 할 수 있다.
  • 인덱싱
    • 앞부분 부터 할 경우에는 0부터 시작
    • 뒷 부분 부터 할 경우 에는 -1부터 시작

양수로 인덱싱  ( 앞 부분부터 인덱싱 )
음수로 인덱싱 ( 뒷 부분부터 인덱싱 )

  • 슬라이싱
    • [ 시작부분 : 끝나는 부분 : 스텝 ] 형식으로 끝나는 부분이 8 일경우 인덱스 8의 원소는 안들어가게된다.
    • 스텝은 진행할때마다 인덱스를 얼마나 바꿀건지 정하는 부분으로 만약 역순으로 리스트를 불러오고 싶으면 -1, 짝수나 홀수의 인덱스만 불러 오고 싶으면 2 로 설정한다.
    • 아래는 예시로 코딩 해보면서 연습하며 익숙해지도록 하자.
    • 아래의 예시중 A[-2:1] 은 결과가 안나온다 step이 default가 1이므로 -2의 인덱스부터 1까지 양의 방향으로 연결이 안되기 때문이다. 고로 이때는 step을 -1로 설정하면 잘 나오게 된다.

 

 

2. lamda함수 문법

 

 

3. 반복 중첩...


Numpy

 

1. 행렬의 형태 확인  : .shape  .reshape(m,n)

  • Numpy는 같은 타입의 데이터들로 이루어진 행렬간 연산에 자주 쓰인다. 연산을 하기 위해서는 필수적으로 행렬들간 크기들이 맞아야 한다.
  • 즉 연산전 행렬의 크기를 확인하고 그에 맞게 변경시켜주는 함수이다.
  • reshape는 진행시 원소의 갯수가 같은 행렬로만 변경 가능하다 ( ex. [3,4] -> [2,6] 가능 / [3,4] -> [1,2] 불가능 )
  • 또한 변경후 원소들은 이전 행렬에서의 순서대로 들어간다. 아래의 그림을 참고하면 이해하기 쉽다


 

2. 행렬간 연결하기 : .concatenate( [ array1, array2 ],  axis = 진행할 차원 )

  • 먼저 axis는 연결 시킬 차원을 지정한다. 쉽게 말해서 행을 기준으로 붙일건지, 열을 기준으로 붙일건지 지정하는 인자이다.
  • 하지만 numpy는 주로 n차원인 데이터를 다룰때 많이 사용하게 되므로 4차원만 넘어가도 쉽지 않다..
  • 나는 쉽게 이해하기 쉽게 그냥 shape의 결과로 나오는 tuple의 몇번째 원소를 바꿀건지를 지정한다고 이해하였다.


 

3. 통계량 계산하기 : .sum(axis=0)    .mean(axis=0)    .var(axis=0)   .std(axis=0)    .max(axis=0)    .min(axis=0)   

.unique()

 

  • 행렬의 모든 원소에 대하여 합계산 한 값을 출력하는 경우는 거의 없을것이야..
  • 주로 통계량을 계산할때는 column별로 또는 한 차원을 기준으로 진행을 할텐데 위의 axis를 지정해주는 이유이다.
  • 모든 numpy함수의 문법은 다음과 같은 np.sum(행렬, 변수), 행렬.sum(변수) 두개의 형태를 모두 지원한다.
  • unique는 axis를 지정하지 않고 행렬 내 모든 원소를 확인해 중복값들을 제거한 후 [n,]의 형태로 결과를 반환한다.
sum 합계 계산
mean 평균 계산
var 분산 계산
std 표준편차 계산
max 최댓값 계산
min 최솟값 계산
unique 중복값 제거후 리스트 출력

 


 

 

4. 로그함수와 지수함수 적용 : np.log(대상)   /   np.exp(대상)

  • 통계나 확률분포를 배우게 되면 log함수나 지수함수는 필수적으로 계산식에 들어가게 된다.
  • 이때 사용되는 것이 log와 exp이다.
  • 위의 예시들과 달리 log와 exp는 행렬.log() 또는 행렬.exp()로 계산할 수 없으며 꼭 위의 문법들로 사용해야한다.

 

 


Pandas

판다스는 2차원의 여러가지 데이터 타입의 원소들을 저장하는 라이브러리이다.

판다스의 데이터 타입은 series와 dataframe 두 종류가 있다. series는 단순히 column이 1개인 2차원 배열이고 dataframe은 column이 여러개인 2차원 배열이다.


 

1. 데이터 불러오기 : pd.read_csv(경로)

  • 판다스의 주된 사용 목적은 데이터를 불러오고, 데이터에 대해 질의 등이다. 주로 수치 계산은 numpy
  • 이를 위해서는 사용자의 db에 있는 데이터를 python으로 불러와야하는데 이떄 사용되는 함수는 read_csv가 대표적이다.
  • db 환경에 따라 경로를 지정할때라던지 추가적인 라이브러리 설치가 필요 하니 이는 구글링으로 해결하자.. ( ex. kaggle 데이터셋 colab으로 불러올때, 컴퓨터 디렉터리 파일 불러올때 등등.. )

 

2. 데이터 프레임의 형태 확인하기 : .shape()

  • numpy와 동일하게 데이터를 불러오면 먼저 형태를 확인해야 한다.
  • 예를 들어 데이터들의 갯수는 몇개인지 ( 행 ), 데이터들의 column은 몇개인지 ( 열 ) 확인하여 데이터 분석 과정을 설계하여야한다.
  • 문법은 numpy와 같으므로 생략한다.
  • reshape은 지원 안함..

 


 

3. 데이터들을 몇개만 뽑아서 보기 : .head( 출력할 행의 갯수 )

  • 데이터 프레임이 가진 데이터를 몇개만 지정해서 출력해주는 함수이다.
  • default 값으로는 5가 지정되어있다.
  • 주로 데이터 불러온후 이 함수를 사용하여 변수 저장이 어찌 되어있는지 각 데이터가 어떤 의미를 가지는지 확인하기 위해 사용한다.
  • 이것을 통해 null값이 포함되어있는지도 운좋으면 확인 가능하지만 주로 이것 말고 다른 방법을 통해 null값을 확인한다.


 

4. 데이터의 정보 확인하기 : .info()

  • 데이터들은 수치형과 범주형이 있다.
  • 쉽게말해 수치형은 연속적인 데이터로 모든 숫자에 대해 의미를 가지는 값이다 ( ex. 몸무게 )
  • 범주형은 불연속 적인 데이터로 특정 값에 대해서만 의미를 가지는 값이다. ( ex. 등수 )
  • info() 함수는 이를 확인하기 위해 사용된다. 수치형 데이터와 범주형 데이터들에게 모두 같은 데이터 전처리 과정을 하지 않고 서로 다른 방법을 사용해야한다. ( 많은 방법이 있지만 정답은 없다. )

 


5. 데이터의 열별 기초 통계량 확인하기 : describe()

  • 열별로 즉 column별로 최솟값 최빈값 최대값 중위값등 기초적인 통계량을 보여준다.
  • 이것을 통해 대략적인 데이터의 분포를 확인 할 수 있고 null값이 포함되어있는지도 확인이 가능하다. ( count 값 )

 


 

6. 데이터 프레임의 데이터값만 가져오기 : values()

  • 특이하게 데이터 프레임에서 인덱싱으로 한 열을 지정하면 단순한 행렬이 나오는건이 아닌 series가 나오게 된다. 즉 dictionary처럼 키와 값의 쌍으로 이루어진 [n,2]짜리 데이터 프레임이 나온다 생각하면 된다.
  • 또한 (2,3)의 위치하여있는 값을 가져올때도 [2,2] 짜리 데이터 프레임이 나오게 된다.. 이게 진짜 화남..
  • 단순한 값만 가지고있는 행렬 즉 numpy의 계산을 지원하는 형태로 나오게 하기 위해서는 valuse() 함수를 사용하면된다.
  • 이는 numpy에서 지원하는 형태로 배열을 반환하게 된다.

 


 

7. 행렬간 연결하기 : .concat( [ df1, df2 ],  axis = 진행할 차원 )

  • numpy의 concatenate와 동일한 과정을 수행한다.

 


 

8. column 순서 변경하기 

 


 

9. 특정 조건의 행 또는 열을 추출하기 : loc[행조건, 열조건], iloc[행인덱스, 열인덱스]

  • loc과 iloc모두 특정 행또는 열을 추출한다. 특별히 iloc은 인덱스를 사용하므로 앞에 i가 붙었다 생각하면 둘 구분이 쉽다.
  • 주로 loc을 많이 사용한다. 조건을 줌으로 특정 범위의 값들만 데이터를 불러와 연속형을 범주형으로 바꿀수도 있고..
  • loc을 많이 써보면 이해하기 쉽다.. 아래는 간단한 예시이다.

age가 39인 데이터의 age,b,c,d값을 가져온다.


 

10. 데이터 열별 또는 전체에게 간단한 수식 계산 하기 : map, apply, applymap

  • map은 Series의 각 원소에 연산을 적용한다. 즉 한 열에 대해서만 적용할 수 있다. 각 원소의 값을 받아 단일값을 반환한다.

 

  • apply는 데이터프레임에서 사용할 수 있는 함수로 행 또는 열에 대해서 함수를 적용할때 사용한다.

 

  • applymap은 series의 map과 동일하게 단일값을 입력을 받아 단일값을 출력한다. 데이터 프레임에서 사용할 수 있다.

솔직히 csv읽어오고 데이터 전처리시 data frame만 사용하는거 같은데 왜 둘다 배움..?

.

.

 

 pandas로 불러온 data frame에서 values로 계산 작업할때 결과물이 모두 numpy라서 numpy도 필요해

 

 


NumPy

 

특징

  • N-demension의 Matrix에 대해 연산이 가능해!!
  • Matrix내의 모든 원소는 같은 데이터 타입이어야만 해!!
  • 난수 생성이 가능해
  • 선형대수학에서 배운 내용들을 적용하기가 쉽다!

 


Pandas

 

특징

  • csv와 같은 데이터 파일을 불러올때 종종 쓰여!
  • SQL처럼 데이터에 대해 질의를 할수 있어
  • col 별로 데이터 타입이 달라도 괜찮아!!
  • 2차원 데이터만 사용이 가능해! ( n차원 이상에서는 사용이 불가능해.. excel이 3차원으로 나오진 않잖아 )

 


정리

 

그니깐 그냥 Pandas로 데이터 전체적으로 확인하고 삽입, 삭제, 질의 등을 통해 지울거 지워

그리고 나서 Numpy를 통해 수식 계산이나 연산을 통해 데이터를 통계내고 싶으면 pandas -> values 를 통해 numpy배열 생성해서 거기서 계산 후 다시 pandas로 저장하자!

1. size()의 결과를 0과 비교할 생각이라면 차라리 empty를 호출하자

  • size()의 결과와 0을 비교하는 것은 empty()를 호출하는 것과 본질적으로 똑같습니다. 하지만 size()는 항상 상수 시간에 수행된다는 보장이 없고, empty()는 항상 상수 시간에 수행됩니다.이유는 list의 splice 함수와 밀접한 관련이 있습니다. splice 함수는 객체의 복사를 하지 않고, list의 특정 요소들을 다른 list로 옮길 수 있는 함수입니다.

    list의 size()가 상수 시간에 수행되도록 하려면, list 객체 내에 리스트의 요소가 몇 개 존재하는지를 담아두는 멤버를 하나 준비하고, 요소의 수를 변경시킬 수 있는 모든 경우에 이 멤버 변수의 값을 갱신 해야 합니다.

    물론 splice 함수도 마찬가지로 요소의 수를 담는 멤버 변수를 갱신 해야 하고, 이를 위해서 splice가 호출될 때마다 옮긴 요소의 수를 세어야 하므로, splice 함수가 상수 시간에 수행되도록 만들 수가 없게 됩니다.

    여러 STL 제품에서 list를 다르게 구현해 놓고 있습니다. 예상하고 있겠지만 구현을 맡은 개발자가 size와 splice 중 어디에 비중을 두고 있는지에 따라, splice가 상수 시간에 수행되는 대신 size가 상수 시간에 수행되지 않을 수도 있고 그 반대일 수도 있습니다.

    지금 쓰는 라이브러리의 list에서 size()가 상수 시간에 수행된다고 하더라도 나중에 라이브러리를 교체하거나, 다른 플랫폼으로 포팅할 일이 생길 지도 모릅니다. 그러므로 size()의 결과를 0과 비교하기보다는 empty()를 호출하는 것이 좋습니다.

 

 

 


2. 단일 요소를 단위로 동작하는 멤버 함수보다 요소의 범위를 단위로 동작하는 멤버 함수가 더 낫다

  • 단일 요소를 단위로 동작하는 멤버 함수(이하 단일 요소 함수)보다 요소의 범위를 단위로 동작하는 멤버 함수(이하 범위 멤버 함수)가 성능 면에서 더 낫습니다. 만일 단일 요소 함수를 사용하여 여러 요소를 삽입하려면, 반복문을 사용할 수 밖에 없습니다. 반복문을 사용하여 요소를 하나씩 삽입하면 타자량도 많고, 가독성도 그다지 좋다고 할 수 없습니다.


    1. vector<Widget> v1, v2;                // v1과 v2는 Widget을 담는 벡터라고
    2.                                     // 가정합시다.
    3. ...
    4.  
    5. // v1의 내용을 v2의 뒷쪽 반과
    6. // 똑같이 만드는 가장 빠른 방법은
    7. // 무엇일까요?
    8.  
    9. // 정답은 assign을 사용하여
    10. // 다음과 같이 짧게 작성할 수 있습니다.
    11.  
    12. v1.assign(v2.begin() + v2.size() / 2, v2.end());
    13.  
    14. // 반복문을 사용하면
    15.  
    16. v1.clear();
    17. for (vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2;    ci != v2.end();    ++ci) {
    18.         v1.push_back(*ci);
    19. }
    20.  
    21. // 하지만 assign을 호출하는 것이
    22. // 손이 덜 간다는 것은 뻔합니다.
    23.  
    24. // 루프를 피하는 한 가지 방법은
    25. // 알고리즘을 사용하는 것입니다.
    26.  
    27. v1.clear();
    28. copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
    29.  
    30. // 타자량이 훨씬 많이 줄긴 했지만,
    31. // assign을 사용한 예제보다는
    32. // 여전히 타자량이 많습니다
    33. // (약간이지만...).
    34.  
    35. // 게다가 copy의 내부를 보면
    36. // 결국 루프를 사용하여 구현되어 있습니다.
    37.  
    38. // 삽입 연산자(inserter, back_inserter,
    39. // front_inserter)를 사용해서 복사 대상
    40. // 범위를 지정하는 copy는 거의 모두
    41. // 범위 멤버 함수로 바꿀 수 있고,
    42. // 바꿔야 합니다.
    43.  
    44. // 위의 copy는 insert의 범위 함수
    45. // 버전으로 대체할 수 있습니다.
    46.  
    47. v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
    48.  
    49. // copy는 복사의 의미에 초점이 맞춰진 반면,
    50. // insert는 삽입의 의미에 초점이 맞춰져
    51. // 좀 더 명확하게 의미 전달이 됩니다.


    단일 요소 멤버 함수보다 범위 멤버 함수가 더 좋은 이유를 이미 두 가지나 확인했습니다.
    • 범위 멤버 함수를 사용한 코드가 대개 짧다(즉, 프로그래머의 손이 덜 아프다).
    • 범위 멤버 함수는 훨씬 명확하고 간결한 의미를 전달한다.

 

 

 

  • 만일 배열에 있는 데이터를 벡터의 앞 부분으로 옮긴다고 생각해 봅시다. 이 경우 단일 요소 함수를 쓰는 것 보다 범위 멤버 함수를 쓰는 것이 좋은 이유가 3 가지나 있습니다. 첫째, 단일 요소 함수는 배열의 멤버 수 만큼 호출되어야 하지만, 범위 멤버 함수는 딱 한 번만 호출되면 되므로 함수 호출 비용이 적게 듭니다. 물론 인라인 함수인 경우에는 차이가 없지만 모든 단일 요소 함수가 인라인인 것은 아닙니다.
  • 둘째, 벡터에 들어 있던 기존의 데이터들을 미는 횟수에서 차이가 납니다. 단일 요소 함수는 배열의 요소들을 하나 씩 삽입하기 때문에 총 복사 횟수는 (배열의 총 요소 수) * (벡터에 들어 있던 요소의 수) 만큼이 됩니다. 반면 범위 멤버 함수의 경우 몇 개가 삽입되는지를 미리 알 수 있기 때문에, (벡터에 들어 있던 요소의 수) 만큼만 복사(딱 한번만 밉니다)가 일어납니다.
  • 셋째, 메모리 할당에 관한 것입니다. 대부분의 경우 벡터는 메모리가 꽉 찰 때마다 자신의 용량을 두 배로 늘리도록 구현이 되어 있습니다. 즉 n개의 새 데이터 요소를 하나씩 삽입하려고 하면 메모리 할당을 log2n번이나 하게 되는 셈입니다. 반면 범위 멤버 함수를 쓰면 삽입할 요소의 수를 미리 알 수 있으므로 딱 한 번 필요한 메모리를 할당하면 됩니다.
  • vector에 대해서 설명드린 내용은 string에서도 동일하게 적용됩니다. deque의 경우에는 비슷하긴 하지만 vector나 string과는 다른 메모리 관리 방식을 취하고 있어서 "반복적인 메모리 재할당"에 관한 이야기는 맞지 않습니다. 하지만 불필요하게 빈번한 컨테이너 내 요소의 이동이나, 불필요한 함수 호출에 관한 이야기는 일반적으로 맞습니다.
  • list 역시, 범위 멤버 함수가 단일 요소 함수보다 수행 성능에서 우수합니다. 되풀이되는 함수 호출에 있어서는 역시 범위 버전이 좋습니다. 하지만 list는 노드 기반으로 동작하기 때문에 메모리 할당에 관한 사항은 딱 맞지 않습니다. 그 대신에 리스트의 노드를 연결하는 next 포인터와 prev 포인터 값이 불필요하게 되풀이해서 세팅되는 문제가 생깁니다.
  • 최소한 표준 시퀸스 컨테이너에 대해서는, 단일 요소 버전의 삽입이냐, 범위 버전의 삽입이냐를 선택하는데 있어서 "프로그래밍 스타일"을 압도하는 많은 요인들을 내새울 수 있게 되었습니다. 그렇다면 연관 컨테이너에 대해서는 어떨까요? 단일 요소 버전의 insert에서 여전히 반복 함수 호출의 오버헤드가 있긴 하지만 딱 부러지게 효율이 어떻다라고는 말씀드리기 힘듭니다. 게다가 몇 가지 특수한 종류의 범위 삽입 함수들의 최적화의 여지를 가지고 있지만 이론적으로만 이러한 최적화가 존재합니다.하지만 연관 컨테이너에서 범위 멤버 함수를 쓴다고 해서 효율이 뒤진다든지 하는 것은 없으니 지금 쓰셔도 잃는 것은 없습니다.

    굳이 효율 문제가 아니더라도, 타자수를 줄여주고 나중에 읽기도 편해 이해하기 좋기 때문에 연관 컨테이너에서도 범위 멤버 함수를 쓰는 것이 좋습니다.
  • 범위를 지원하는 멤버 함수는 어떤 것인지 미리 알아놓고 정리해 두면, 나중에 이것들을 사용할 기회를 포착하기가 매우 쉬울 것입니다. 
  •  


    1. // 다음에 나온 시그너쳐(signature)에서,
    2. // 매개 변수 타입인 iterator는 말 그대로
    3. // 컨테이너의 반복자 타입, 즉 container::iterator
    4. // 란 뜻입니다.
    5.  
    6. // 한편 InputIterator는 어떤 입력
    7. // 반복자도 받아들일 수 있다는 뜻입니다.
    8.  
    9.  
    10. //-----------------------------------------------------------------------------
    11. // ※ 범위 생성(Range Construction)
    12. //   모든 표준 컨테이너는 다음과 같은
    13. //   형태의 생성자를 지원하고 있습니다.
    14.  
    15. container::container(InputIterator begin,        // 범위의 시작
    16.                     InputIterator end);            // 범위의 끝
    17.  
    18. // 이 생성자에 넘겨진 반복자가
    19. // istream_iterator 혹은
    20. // istreambuf_iterator이면
    21. // C++에서만
    22. // 볼 수 있는 가장 황당한 분석
    23. // 결과(parse)가 생깁니다.
    24.  
    25. // 이것 때문에 컴파일러는 이것을
    26. // 컨테이너 객체의 정의로 보지 않고
    27. // 함수 선언으로 이해하고 말지요.
    28.  
    29.  
    30.  
    31. //-----------------------------------------------------------------------------
    32. // ※ 범위 삽입(Range Insertion)
    33. //   모든 표준 컨테이너는 다음과 같은
    34. //   형태의 insert를 지원하고 있습니다.
    35.  
    36. void container::insert(iterator position,        // 범위를 삽입할 위치
    37.                     InputIterator begin,        // 삽입할 범위의 시작
    38.                     InputIterator end);            // 삽입할 범위의 끝
    39.  
    40. // 연관 컨테이너는 자신이 가지고 있는
    41. // 비교 함수를 사용하여 삽입될 요소가
    42. // 놓일 위치를 결정하기 때문에 위치
    43. // 매개 변수를 가지고 있지 않은
    44. // 시그너쳐를 제공합니다.
    45.  
    46. void container::insert(InputIterator begin, InputIterator end);
    47.  
    48. // 단일 요소 버전의 insert를
    49. // 범위 버전으로 교체할 부분을 찾을 때
    50. // 잊지 말아야 할 것이 있습니다.
    51.  
    52. // 바로 단일 요소 함수 중에 몇몇은
    53. // 다른 함수 이름으로 위장하고
    54. // 있다는 사실입니다.
    55.  
    56. // 예를 들어, push_front와
    57. // push_back은 하나의 요소를
    58. // 컨테이너에 넣는 함수이지만 "삽입"
    59. // 류로 불리지 않지요.
    60.  
    61.  
    62. //-----------------------------------------------------------------------------
    63. // ※ 범위 삭제(Range Erasure)
    64. //   역시 표준 컨테이너에서 범위 버전의
    65. //   erase를 제공하고 있지만, 반환 타입은
    66. //   시퀸스 컨테이너와 연관 컨테이너에
    67. //   대해서 각각 다릅니다.
    68.  
    69. // 시퀸스 컨테이너에선 다음과 같은
    70. // 형태를 쓸 수 있고,
    71.  
    72. iterator container::erase(iterator begin, iterator end);
    73.  
    74. // 반면에 연관 컨테이너에서는 다음과
    75. // 같은 형태를 쓸 수 있습니다.
    76.  
    77. void container::erase(iterator begin, iterator end);
    78.  
    79. // 반환 타입이 다른 이유는
    80. // 연관 컨테이너 버전의 erase에서
    81. // 지워진 요소의 바로 뒤에 있는
    82. // 요소를 가리키는 반복자를
    83. // 반화하게 하면 납득하기 힘든
    84. // 수행 성능 저하가 생길 수 있다고
    85. // 합니다.
    86.  
    87. // vector와 string의 insert에 대한 이야기가
    88. // erase에서 통하지 않는 부분은 반복되는
    89. // 메모리 할당에 있습니다.
    90.  
    91. // vector와 string의 메모리는
    92. // 새 데이터 요소를 넣을 때에는 자동으로
    93. // 커지지만 내부의 요소 수가 줄어들
    94. // 때에는 자동으로 작아지지 않기
    95. // 때문입니다
    96.  
    97.  
    98. //-----------------------------------------------------------------------------
    99. // ※ 범위 대입(Range Assignment)
    100. //   모든 표준 시퀸스 컨테이너는
    101. //   범위 버전의 assign을 제공하고
    102. //   있습니다.
    103.  
    104. void container::assign(InputIterator begin, InputIterator end);
    105.  



 

참조 : http://ajwmain.iptime.org/programming/book_summary/%5B02%5Deffective_stl/effective_stl.html#I04

 

Effective STL 정리

매우 긴 문자열을 담을 수 있는 string 류의 컨테이너(string-like container for very large strings) 이 컨테이너는 rope(로프) 라고 불립니다. SGI는 로프를 이렇게 규정하고 있습니다. 로프는 확장성을 갖춘 문

ajwmain.iptime.org

 

C++ 기초를 복습하고 나서 코딩테스트 문제를 풀다보면 직접 구현할수도 있지만 이미 구현되어있는 자료구조, 알고리즘을 불러오면 쉽다는걸 느꼈다..

 

처음에는 STL 왜쓰는지.. 그냥 직접 구현하면 되는거 아닌가 했더니 사용해보니 진짜 편하긴하다..

 

그래도 쓰기전에는 꼭 직접 모든것을 구현해보자... 어떻게 동작하고 어떤 원리인지는 알고 써야지..

 


STL에는 많은 컨테이너가 있다.  ( 자료구조라 생각하면 편함 )

  • 표준 STL 시퀸스(sequence) 컨테이너: vector, string, deque, list.
  • 표준 STL 연관(associative) 컨테이너: set, multiset, map, multimap.
  • 비표준 시퀸스 컨테이너: slist(단일 연결 리스트), rope(대용량 string).
  • 비표준 연관 컨테이너: hash_set, hash_multiset, hash_map, hash_multimap.
  • STL에 속하지 않은 표준 컨테이너: 배열(C++ 배열), bitset, valarray, stack, queue, priority_queue. 

 

이렇게 많은것중 한가지만 알면 당연히 안된다.. 각각의 특징을 이해하고 언제 어떤것을 사용할지를 정리하는것이 이 글의 목표....   

 

우선, STL 컨테이너는 연속 메모리(continuous-memory) 컨테이너와 노드 기반(node-based) 컨테이너로 나눌 수 있다.

 

  • 연속 메모리 컨테이너( 배열 기반 컨테이너 )
    동적 할당된 하나 이상( 대개 하나 )의 메모리 단위( chunk )에다가 데이터 요소를 저장해 두는 컨테이너입니다. 새 요소가 삽입되거나 이미 있던 요소가 지워지면(erase), 같은 메모리 단위에 있던 다른 요소들은 앞 혹은 뒤로 밀려나면서 새 요소가 삽입될 공간을 만들던지, 지워진 공간을 메웁니다. 이러한 "밀어내기" 때문에 수행 성능의 발목을 잡을 수 있고, 예외 안전성(exception safety)에도 영향을 미칩니다.
    여기에 속하는 컨테이너는 vector, string, deque입니다. 비표준 컨테이너인 rope 역시 연속 메모리 컨테이너입니다.

 

  • 노드 기반 컨테이너
    동적 할당된 하나의 메모리 단위에다가 하나의 요소만을 저장합니다. 컨테이너 요소를 삽입 혹은 삭제했을 때 노드의 포인터만이 영향을 받지, 노드의 내용은 그대로입니다. 따라서, 삭제나 삽입이 일어났다고 해도 나머지 요소들이 밀려난다든지 하는 일이 없습니다.

    연결 리스트를 나타내는 컨테이너, 즉 list와 slist가 노드 기반이고, 표준 연관 컨테이너 모두가 노드 기반 입니다(이것들은 전형적으로 균형 트리(balanced tree)로 구현되어 있습니다).

 

  • 이제는 "어떤 상황에 어떤 컨테이너를 쓰면 가장 좋을까?"에 관련된 문답을 수월하게 정리할 수 있을 것입니다.
    • 인덱스를 통한 요소 삽입 가능해야한다. -> 시퀀스 컨테이너
    • 요소들의 순서에 관심 없다 -> 해쉬 컨테이너
    • 반복자 타입에 대한 구분
      • 임의 접근 반복자 : vector, deque, string
      • 양방향 반복자 : slist와 해쉬 컨테이너는 쓸 수 없다
    • 요소 삽입 삭제시 다른 요소가 밀려나는일 없어야한다 : 연속메모리 컨테이터는 불가
    • C의 데이터 타입과 메모리 배열 구조적으로 호환되어야 한다 : vector 밖에 쓸 것이 없다.
    • 탐색 속도가 중요하다 : 해쉬 컨테이너, 정렬된 vector, 그리고 표준 연관 컨테이너
    • 컨테이너의 참조 카운팅이 신경 쓰이나요? 그렇다면 string 가까이에는 가지 않는 것이 좋습니다. 많은 string 코드가 참조 카운팅이 되도록 구현되어 있습니다. 이럴 때 vector<char>를 쓰는 것입니다.
    • 삽입 삭제가 안정적 : 노드 기반 컨테이너를 고려해 보시기 바랍니다.
    • 트랜잭션적인 삽입이 여러 개의 요소(범위로 주어집니다)에 대해 이루어져야 할 경우에는 list를 선택합니다. 
    • 반복자, 포인터, 참조자가 무효화(포인터가 가리키고 있던 메모리의 실제 내용이 없어지는 일을 뜻한다)되는 일을 최소화해야 하나요? 이런 경우에는 노드 기반 컨테이너를 사용하기 바랍니다. 노드 기반 컨테이너는 노드 삽입과 삭제가 일어나도 기존의 반복자나 포인터 혹은 참조자가 무효화되지 않기 때문입니다(가리키고 있는 요소를 삭제하지 않는 한 말이죠). 반대로 연속 메모리 컨테이너는 전체적인 메모리 재할당이 빈번하게 일어나기 때문에 반복자나 포인터, 참조자가 무효화되기 쉽습니다.
    • 임의 접근 반복자를 지원하는 시퀸스 컨테이너가 필요한데, 요소 삭제가 일어나지 않고 요소 삽입이 컨테이너의 끝에서만 일어나는 한, 포인터와 참조자가 무효화되지 않는 것이 필요한가요? 아주 특별한 경우이긴 하지만, 어쩌다 이런 경우를 만난다면 deque가 정답입니다. deque는 요소 삽입이 끝에서 일어날 때 반복자만 무효화되는 재미있는 컨테이너입니다(STL 컨테이너 중 포인터와 참조자를 무효화시키지 않고 반복자만 무효화되는 것은 deque가 유일합니다).

 

참조 : http://ajwmain.iptime.org/programming/book_summary/%5B02%5Deffective_stl/effective_stl.html#I04

+ Recent posts