SAS나 SQL에 익숙해져 있는 사람에게 가장 어려운 부분이 바로 pandas dataframe은 Series의 집합이므로 조건에 따라서 값을 변환하는 경우에는 정상적으로 작동하지 않는다. 아래 코드에서는 np.where() 가 SQL의 CASE WHEN TEN END 처럼 작동할 것이라고 생각했지만 그렇지 않다. 이렇게 row별로 컬럼의 값에 따라서 처리를 다르게 하기 위해서는 for row loop 문으로 처리해야 한다는 문제점이 있습니다. pandas dataframe 레벨에서 모든 문제를 해결하면 좋을텐데, 이게 그렇게 마음처럼 되지는 않으니까 루프문으로 데이터를 생성해야 하는 것입니다.

 np.where(), np.select() 함수는 사용자 정의 함수를 이용해서 작성하는 것보다 훨씬 더 빠릅니다. 예전에는 df.assign() 함수를 이용해서 코드를 변환했는데 이것도 모든 것을 해결할 수 없으므로 이렇게 별도의 사용자 함수를 사용할 수 밖에 없습니다.

 

1. 필요한 라이브러리를 불러옵니다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
In [2]:

2. temp 데이터 프레임을 생성합니다. rgs_dt는 등록일인데, NULL 값일 때 '99999999'로 넣어준다.

temp=pd.DataFrame({'id': [1, 2, 3, 4, 5, 6],
                   'rgs_dt': ['20230101', '20230208', '20221208', '20221021', '20230331', '99999999']})

3. 기준월을 넣어 줍니다.

In [3]:
temp['bas_ym']='202303'

4. np.where()함수를 넣으면 정상 작동하지 않는다는 것을 알 수 있습니다. 에러 코드를 보면 pd.to_datetime()이 조건이 아니라 시리즈 전체에서 작동한다는 것을 알 수 있습니다.

In [4]:
temp['diff']=np.where(temp.rgs_dt=='99999999',
                      99999999,
                      pd.to_datetime(temp.bas_ym, format='%Y%m') - pd.to_datetime(temp.rgs_dt, format='%Y%m%d')
                      )
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-d51adba5b206> in <cell line: 1>()
      1 temp['diff']=np.where(temp.rgs_dt=='99999999',
      2                       99999999,
----> 3                       pd.to_datetime(temp.bas_ym, format='%Y%m') - pd.to_datetime(temp.rgs_dt, format='%Y%m%d')
      4                       )

/usr/local/lib/python3.10/dist-packages/pandas/core/tools/datetimes.py in to_datetime(arg, errors, dayfirst, yearfirst, utc, format, exact, unit, infer_datetime_format, origin, cache)
   1066             result = arg.map(cache_array)
   1067         else:
-> 1068             values = convert_listlike(arg._values, format)
   1069             result = arg._constructor(values, index=arg.index, name=arg.name)
   1070     elif isinstance(arg, (ABCDataFrame, abc.MutableMapping)):

/usr/local/lib/python3.10/dist-packages/pandas/core/tools/datetimes.py in _convert_listlike_datetimes(arg, format, name, tz, unit, errors, infer_datetime_format, dayfirst, yearfirst, exact)
    428 
    429     if format is not None:
--> 430         res = _to_datetime_with_format(
    431             arg, orig_arg, name, tz, format, exact, errors, infer_datetime_format
    432         )

/usr/local/lib/python3.10/dist-packages/pandas/core/tools/datetimes.py in _to_datetime_with_format(arg, orig_arg, name, tz, fmt, exact, errors, infer_datetime_format)
    536 
    537     # fallback
--> 538     res = _array_strptime_with_fallback(
    539         arg, name, tz, fmt, exact, errors, infer_datetime_format
    540     )

/usr/local/lib/python3.10/dist-packages/pandas/core/tools/datetimes.py in _array_strptime_with_fallback(arg, name, tz, fmt, exact, errors, infer_datetime_format)
    471 
    472     try:
--> 473         result, timezones = array_strptime(arg, fmt, exact=exact, errors=errors)
    474     except OutOfBoundsDatetime:
    475         if errors == "raise":

/usr/local/lib/python3.10/dist-packages/pandas/_libs/tslibs/strptime.pyx in pandas._libs.tslibs.strptime.array_strptime()

ValueError: unconverted data remains: 99

5. 2가지 변수를 추가해 줍니다. 

 A. 날짜의 차이는 아래와 같이 간단하게 ().days()로 계산이 가능합니다. :) 네, 아주 쉽지요.

 B. 월의 차이를 계산하려고, #으로 막아 놓은 코드는 정상 작동하지 않습니다. ().days() 와 같은 쉬운 함수가 존재하지 않습니다.

 C. 그래서 어쩔 수 없이 .year(), .month()를 갖고 계산하는 방법 외에는 없습니다. 

In [5]:
temp['diff']=[99999999 if temp.loc[j, 'rgs_dt'] == '99999999'
              else (pd.to_datetime(temp.loc[j, 'bas_ym'], format='%Y%m') - pd.to_datetime(temp.loc[j, 'rgs_dt'], format='%Y%m%d')).days
              for j in temp.index
              ]

#temp['mcn']=[99999999 if temp.loc[j, 'rgs_dt'] == '99999999'
#              else (pd.to_datetime(temp.loc[j, 'bas_ym'], format='%Y%m').to_period('M')
#              - pd.to_datetime(temp.loc[j, 'rgs_dt'], format='%Y%m%d').to_period('M'))
#              for j in temp.index
#              ]


temp['mcn']=[99999999 if temp.loc[j, 'rgs_dt'] == '99999999'
              else (pd.to_datetime(temp.loc[j, 'bas_ym'], format='%Y%m').year -  pd.to_datetime(temp.loc[j, 'rgs_dt'], format='%Y%m%d').year)*12
                   + pd.to_datetime(temp.loc[j, 'bas_ym'], format='%Y%m').month - pd.to_datetime(temp.loc[j, 'rgs_dt'], format='%Y%m%d').month
              for j in temp.index
              ]
In [ ]:
temp
Out[ ]:
idrgs_dtbas_ymdiffmcnmcn1012345
1 20230101 202303 59 <2 * MonthEnds> 2
2 20230208 202303 21 <MonthEnd> 1
3 20221208 202303 83 <3 * MonthEnds> 3
4 20221021 202303 131 <5 * MonthEnds> 5
5 20230331 202303 -30 <0 * MonthEnds> 0
6 99999999 202303 99999999 99999999 99999999
In [ ]:
temp['mcn'].info()
<class 'pandas.core.series.Series'>
RangeIndex: 6 entries, 0 to 5
Series name: mcn
Non-Null Count  Dtype 
--------------  ----- 
6 non-null      object
dtypes: object(1)
memory usage: 176.0+ bytes
In [ ]:
pd.to_datetime('19010101', format='%Y%m%d')
 
Out[ ]:
Timestamp('1901-01-01 00:00:00')

 예전에도 이렇게 코드를 어떻게 해서든 가볍게 짜려고 했지만, 오늘 반나절을 보내고 나서 왜 문제였는지 겨우 알 수 있게 되었습니다. 몇 번이나 코드를 수정했지만 뭔가 개선되는 것은 그리 많지 않았습니다. 그래도 이렇게 파이썬하고도 친해질 수 있어서 다행입니다. 예전에는 SAS나 임팔라만 써서 불안한 점이 있었는데, 지금은 참 이것저것 하는 일도 많고 데이터도 많아서 좋습니다.

반응형

+ Recent posts