글또9기:영국직장:데이터사이언티스트

글또9기 | Pandas와 Numpy

채유나 chaeyoonaaa 2023. 12. 10. 02:54
반응형

데이터 사이언티스트로 가장 많이 사용하는 기본 두 가지 라이브러리다: Pandas와 Numpy. 최근 Pandas를 좀 더 선호하게 되면서, 특히 Databricks에서 Pyspark로 프로그래밍 빈도가 늘어나면서, 그동안 두 가지 각각 장단점이 있다고만 생각했던 부분에 확실한 필요 조건을 알게되었다. 내용을 설명하기 위한 예제를 정의하고 관련 코드를 붙인다.

 

 

[문제와 조건 정의]

사이즈가 36x6인 어떤 행렬matrix을 가지고 있다고 가정하자. 각 성분component은 0 또는 1이다. 각 열마다 어떤 특정 조건 가중치weights를 적용한다고 한다. 즉, 1x6 벡터 weights를 36x6 matrix에 곱하는 연산이다. 이 때 binary matrix의 특성으로 곱의 결과 값 skew가 벌어지기 때문에 scale을 맞추기 위한 standardization 과정을 추가하자.

 

 

 

[풀이: Numpy vs Pandas]

import numpy as np

w, h = matrix.shape

component_total = (weights*matrix).sum(axis=1)
pct_increase = list((1-component_total)/component_total)

component_weightings = np.ones((w,h)) # intializing an array
for x in range(w):
    for y in range(h):
        component_weightings[x][y] = matrix[x,y]*(weights[y]+(weights[y]*pct_increase[x]))

 

연산 결과 값 component_weightings는 그러므로 36x6 ndarray다. 같은 방법을 Pandas로 접근해보자. 위에서의 input matrix와 weights는 모두 Pandas Dataframe으로 치환되었다.

import pandas as pd

cols = list(matrix.columns)

matrix *= np.array(weights)
matrix['component_total'] = matrix.sum(axis=1)
matrix['pct_increase'] = (1-matrix['component_total'])/matrix['component_total']

temp = matrix[cols].multiply(matrix['pct_increase'], axis='index')
matrix[cols] *= np.array(weights)

component_weightings = matrix[cols] + temp[cols]

 

행렬 matrix를 Numpy array로 표현했을 때와 Pandas Dataframe으로 표현했을 때 모두 각 성분component를 scalar값과 곱하는 연산 과정은 수월하게 할 수 있다. 벡터 요소elements를 ndarray에 곱할 때는 단순 * operator를 사용한 반면, Dataframe에 곱할 때는 .multiply() 메소드를 사용하여 함수 mul()을 적용하였을 뿐이다.

@Appender(ops.make_flex_doc("mul", "dataframe"))
def mul(self, other, axis: Axis = "columns", level=None, fill_value=None):
    return self._flex_arith_method(
        other, operator.mul, level=level, fill_value=fill_value, axis=axis
    )

multiply = mul

https://github.com/pandas-dev/pandas/blob/v2.1.4/pandas/core/frame.py#L8102-L8106

 

 

 

[장단점 비교]

Pandas가 Numpy library를 기반으로 작성되었기 때문에 연산 속도나 메모리 활용 측면은 논외로 한다.

 

그러면 Pandas 코드는 "내가 어떤 연산을 하는지 알아보기 쉽다"는 장점을 가진다. 열columns마다 header가 있기 때문이다. 다만 서로 다른 Dataframe을 곱할 때 index가 매치되지 않으면 Null값을 반환하므로 주의를 기울여야 하는 부분이 있다. 그렇지만 대체로는, 특히 새로운 값으로 열이 확장되었을 때 (예시에서 `component_total`과 `pct_increase`) slot-in된 구성 요소들이 각 행에 위치하면서 index slicing으로 범위를 다르게 가져가더라도 기존의 어떤 값에 새로운 어떤 값이 들어왔는지 파악하기 쉽다.

 

Numpy에서라면 우선 다른 변수로 각각 정의한 `component_total`과 `pct_increase`이 36x1의 벡터로 36x6 행렬과 매치되는 사실이 직관적이지 않을 수 있다. 때문에 후속 코드로 연계하다보면 (전반적인 프로그래밍의 특성이기도 하지만) Numpy variables들의 크기가 어떻게 정의되어 있고 변화하는지를 히스토리를 파악하는 게 중요한 미션이 된다.

 

이것은 마치 엑셀 테이블에서 정보를 제공할 때 적절한 헤딩을 함께 볼 수 있는가(i.e., Pandas Dataframe) 아닌가(i.e., Numpy ndarray)로 비교할 수 있다. 이 때 "알아보기 편하다"는 것은 절대적으로 우월한 장점이 되진 않는다. 다루는 데이터의 정보 공개 민감도에 따라 보안 이슈가 될 수도 있기 때문이다. 단순 binary값일지라도 raw 데이터가 0, 1로 기록된 이면에 존재하는 배경 데이터에 따라, 예를 들어 특정 시험의 불합 여부라면, 코드를 공개하는 엔지니어의 목적 이상으로 다른 중요한 관심이 집중될 수 있다. 때문에 불필요 이상 정보가 노출되지 않게  라벨링을 좀 더 일반적인 이름으로 바꾸거나 아에 a, b, c 등 다른 익명화된 인코딩으로 별도 anonymised mapping을 해야하기도 한다. 직관적이지 않은 코드를 알아보기 쉽게 주석(혹은 필요한 별도 docs) 작성을 잘 하는 노력이 많이 들지, 지나친 데이터 공개를 방지하기 위해 인코딩(혹은 필요한 별도 config) 연산을 모델링에 추가하는 노력이 많이 들지에 판단은 필요 조건과 경험치에 따라 다를 수 있다 생각한다.

 

 

 

[장단점 응용 문제]

Pandas Dataframe에서 특정 열에만 선택적으로 연산을 적용하기가 불편한 점을 다뤄보자.

예를 들어 위의 표에서 붉은 바탕으로 표시된 6개의 행만 위의 기본 문제에서처럼 weighting 연산이 필요하다고 하겠다. 그러면 중간에 위치한 `HI` 열을 건너뛰게 slicing하고 벡터와 곱하는 것보다, `HI` 정보를 제외한 임시temp을 만드는 것이 수월할 것이다. 기존에 가지고 있던 추가 정보 `Professional group`에 대한 정렬을 연산 결과와 도로 붙이는 것도 품이 들 것이다. 간단하던 위의 코드에 몇 줄을 추가 적용하여 해결한 코드가 아래와 같다. 

# Scaling component weightings to sum at 1
temp = matrix.drop(['profession', 'hi'], axis=1)

temp_cols = list(temp.columns)
temp *= np.array(weights)
matrix['component_total'] = temp.sum(axis=1)
matrix['pct_increase'] = (1-matrix['component_total'])/matrix['component_total']

temp = matrix[list(temp_cols)].multiply(matrix['pct_increase'], axis='index')
matrix[list(temp_cols)] *= np.array(weights)

component_weightings = matrix[list(temp_cols)] + temp[list(temp_cols)] 
frame_weights = pd.concat([matrix['profession'], component_weightings], axis=1)

 

Numpy 예제는 처음부터 matrix 구성할 때 별도 input으로 두거나 (위의 코드에) 반복문for-loop에서 `HI`에 해당하는 순서를 skip하는 정도로 커스터마이징 리소스가 덜 들어가는 장점이 있다.

 

반면 Numpy 방식으로 계산이 어려운 부분이 Pandas에서 간단히 해결되는 차이가 생길 수도 있다. 예시에서 `Professional group`은 다시 어떤 카테고리로 묶여야 하는데 Pandas Dataframe에서라면 .groupby() 메소드를 활용해 집계함수를 적용하기 쉽기 때문이다. 예를 들어 카테고리별 백분율을 계산해야 한다고 하자. `Medical` 아래 2개 그룹의 합을 구해 각 그룹에 나눠주는 계산은  Pandas에서 해당 df열에 df.groupby(['category']).sum()을 나누고 100을 곱하는 것으로 (`Medical`이외 다른 카테고리까지 확장하여) 쉽계 계산해 낼 수 있다. 반면 Numpy matrix연산에는 카테고리에 맞게 reshpae을 하고 index구분을 개별 설정하는 과정부터 쉽지 않다. 모델링에 컨트롤해야 하는 변수가 너무 많아지는 것 또한 지양해야 할 것이다. 

 

 

 

[글을 마치며]

여기까지 데이터 사이언티스트가 자주 활용하는 가장 기본 두 가지 라이브러리 장단점을 예제를 들어 비교하였다. 사내 데이터 사이언스 팀에서 공유하는 분석 코드들 중 Pyspark Dataframe을 Pandas Dataframe으로 바꾸곤 하는 (기존 작업자들이 선호하는) 코드 전개 방식을 공부하다보니 여기까지 내용을 스스로 정리할 수 있게 되었다. 누군가 좀 더 경험이 많고 효율적으로 코드를 작성할 능력이 되는 분이 본다면 특히 마지막 코드를 나와 다르게 작성하실 수도 있겠다. 나 또한 현재까지의 결과를 개선하기 위해 좀 더 고민해야 할 부분으로 앞으로 과제로 남겨두겠다.

반응형