📝 불필요한 행과 열을 제거하기 위해 짧게 여러개씩 작성했던 코드들을 새로운 데이터에 적용하기 쉽도록 일괄처리하는 data_cleansing( ) 이라는 일괄처리용 함수를 만들어서 처리를 해보았다. 남산도서관에서 새로운 도서 데이터를 다운로드 했을때 dropna() 하고 groupby()등을 일일이 하기에는 너무 코드가 길었다. 따라서 필요한 코드만 파이썬 def함수로 만들어서 저장하면 간단하게 함수를 호출하거나 파이썬 스크립트를 실행하여 데이터 전처리를 하는 과정을 단순하게 만들 수 있다.
ns_book4.to_csv('ns_book4.csv', index=False) # 분석에 활용할 csv파일 (한줄한줄씩 전처리해서 만든 데이터프레임 (불필요한 데이터들 제거한거))
# 남산 도서관 장서 csv 데이터 전처리 함수
def data_cleaning(filename):
# 파일을 데이터프레임으로 읽음
ns_df = pd.read_csv(filename, low_memory=False) # 여기서 filename은 csv파일 이름을 말함
# NaN인 열을 삭제함
ns_book = ns_df.dropna(axis=1, how='all')
# 대출건수를 합치기 위해 필요한 행만 추출하여 count_df 데이터프레임을 만듦
count_df = ns_book[['도서명','저자','ISBN','권','대출건수']]
# 도서명, 저자, ISBN, 권을 기준으로 대출건수를 groupby함
loan_count = count_df.groupby(by=['도서명','저자','ISBN','권'], dropna=False).sum()
# 원본 데이터프레임에서 중복된 행을 제외하고 고유한 행만 추출하여 복사함
dup_rows = ns_book.duplicated(subset=['도서명','저자','ISBN','권'])
unique_rows = ~dup_rows
ns_book3 = ns_book[unique_rows].copy()
# 도서명, 저자, ISBN, 권을 인덱스로 설정
ns_book3.set_index(['도서명','저자','ISBN','권'], inplace=True)
# load_count에 저장된 누적 대출건수를 업데이트함
ns_book3.update(loan_count)
# 인덱스 재설정함
ns_book4 = ns_book3.reset_index()
# 원본 데이터프레임의 열 순서로 변경함
ns_book4 = ns_book4[ns_book.columns]
return ns_book4
📝 위에서 원본 데이터인 ns_202104.csv 파일을 전달하여 새로운 데이터프레임인 new_ns_book4를 만들고, 내가 개인적으로 한줄한줄씩 전처리해서 만든 ns_book4 데이터프레임과 같은지 비교를 해보았다. 다른 데이터프레임과 비교할때는 equals() 사용!!
(개인적으로 한줄씩 전처리를 해본 ns_book4는 너무 내용이 많아서 못넣었음)
new_ns_book4 = data_cleaning('ns_202104.csv')
ns_book4.equals(new_ns_book4)

👉 내가 개인적으로 한줄씩 전처리를한 ns_book4 와 위의 일괄처리 함수가 수행한 데이터프레임이 같다고 나왔다. 이를 통해서 남산 도서관을 제외한 새로운 장서 데이터를 분석하고 싶으면, 위의 일괄처리용 함수를 수행해서 전처리 작업을 간단하게 한번에 끝낼 수 있다.
📝 1)의 '불필요한 데이터 삭제하기' 과정에서도 말했듯이 2)의 과정도 한줄한줄씩 코랩에서 잘못된 데이터에 한해서 개인적으로 전처리를 해보았다. 그러나 코드가 너무 길어져서 1)과 마찬가지로 일괄처리 함수를 작성하여 그나마 간단하게 잘못된 데이터를 전처리를 해보았다.
ns_book6.to_csv('ns_book6.csv', index=False) # 한줄씩 전처리해서 만든 데이터프레임 (NaN이랑 잘못된거 처리한거)
def data_fixing(ns_book4):
# 도서권수와 대출건수를 int32로 바꿈
ns_book4 = ns_book4.astype({'도서권수':'int32', '대출건수': 'int32'})
# NaN인 세트 ISBN을 빈문자열로 바꿈
set_isbn_na_rows = ns_book4['세트 ISBN'].isna()
ns_book4.loc[set_isbn_na_rows, '세트 ISBN'] = ''
# 발행년도 열에서 연도 네 자리를 추출하여 대체, 나머지 발행년도는 -1로 바꿈
ns_book5 = ns_book4.replace({'발행년도':'.*(\d{4}).*'}, r'\1', regex=True)
unkown_year = ns_book5['발행년도'].str.contains('\D', na=True)
ns_book5.loc[unkown_year, '발행년도'] = '-1'
# 발행년도를 int32로 바꿈
ns_book5 = ns_book5.astype({'발행년도': 'int32'})
# 4000년 이상인 경우 2333년을 뺀다
dangun_yy_rows = ns_book5['발행년도'].gt(4000)
ns_book5.loc[dangun_yy_rows, '발행년도'] = ns_book5.loc[dangun_yy_rows, '발행년도'] - 2333
# 여전히 4000년 이상인 경우 -> -1로 바꿈
dangun_year = ns_book5['발행년도'].gt(4000)
ns_book5.loc[dangun_year, '발행년도'] = -1
# 0~1900년 사이의 발행년도는 -1로 바꿈
old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900)
ns_book5.loc[old_books, '발행년도'] = -1
# 도서명, 저자, 출판사가 NaN이거나 발행년도가 -1인 행을 찾음
na_rows = ns_book5['도서명'].isna() | ns_book5['저자'].isna() \
| ns_book5['출판사'].isna() | ns_book5['발행년도'].eq(-1)
# 교보문고 도서 상세 페이지에서 누락된 정보를 채움
updated_sample = ns_book5[na_rows].apply(get_book_info,
axis=1, result_type ='expand')
updated_sample.columns = ['도서명','저자','출판사','발행년도']
ns_book5.update(updated_sample)
# 도서명, 저자, 출판사가 NaN이거나 발행년도가 -1인 행을 삭제함
ns_book6 = ns_book5.dropna(subset=['도서명','저자','출판사'])
ns_book6 = ns_book6[ns_book6['발행년도'] != -1]
return ns_book6
👉 위의 일괄처리 함수도 엄~청 분량이 많은 편이지만, 한줄한줄 전처리를 했을 때보다는 그래도 적은편이었다. 이렇게 불필요한 데이터들과 결측치, 오류가 있는 데이터들을 처리하면 드디어 분석에 활용할 데이터가 만들어진 것이다.
📝 위에서 전처리해서 만든 ns_book6 에서 sum(ns_book['도서권수']==0) 으로 도서권수의 열의 값이 0인 행의 개수를 확인했더니, 3206개가 나왔다. 정확하지 않은 판단일수도 있지만 0권은 의미없다고 생각해서 삭제했다.
ns_book7 = ns_book6[ns_book6['도서권수']>0]
# 도서권수가 0인 데이터 제외한것을 ns_book7에 저장함
📝 이제 머신러닝에 활용할 데이터는 ns_book7이다.
# ns_book7 데이터를 다운받고 데이터프레임으로 불러옴
import gdown
gdown.download('https://bit.ly/3pK7iuu', 'ns_book7.csv', quiet=False)
import pandas as pd
ns_book7 = pd.read_csv('ns_book7.csv', low_memory=False)
ns_book7.head()

from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(ns_book7, random_state=42)
print(len(train_set), len(test_set))

📝 코드 실행결과 전체에서 75%가 훈련 데이터, 25%가 검증 데이터로 나누어졌다. 이제 사이킷런에 있는 선형회귀모델을 위의 데이터로 훈련해본다.
X_train = train_set[['도서권수']]
y_train = train_set['대출건수']
print(X_train.shape, y_train.shape)

📝 X_train은 282577개의 행과 1개의 열로 이루어진 데이터프레임이고, y_train은 '시리즈 객체' 즉 282577개 원소를 가진 1차원 배열이다. (사이킷런이 입력으로는 2차원 배열, target으로는 1차원 배열을 기대하기 때문에 이런식으로 해줬다.)
📝 이제 사이킷런의 linear_model 모듈의 LinearRegression 클래스를 불러와서 선형회귀모델을 훈련시켜본다.
from sklearn.linear_model import LinearRegression
lr = LinearRegression() # LinearRegression 클래스의 객체 lr 만들었음
lr.fit(X_train, y_train) # fit() 메서드를 호출해서 모델을 훈련함

X_test = test_set[['도서권수']]
y_test = test_set['대출건수']
lr.score(X_test, y_test)

📝 위의 결과를 보면 점수가 0.1이므로 점수가 안좋다. 도서권수로 대출건수를 예측하는건 어렵다고 본다.
print(lr.coef_, lr.intercept_)
# lr.coef_ : 학습된 기울기, lr.intercept_ : 절편

📝 위의 결과를 보면 기울기는 1, 절편은 0에 매우 가까운 음수이다. 따라서 선형회귀모델 : y = x 이다.
📝 로지스틱회귀 모델을 만들기 전에, 먼저 타겟 y_train과 y_test를 이진 분류에 맞게 바꿔야한다. 즉, 음성 클래스에 해당하는 0과 양성 클래스에 해당하는 1로 바꿔야한다. 아래 코드는 도서권수로 대출건수가 평균보다 높은지 아닌지를 예측하는 이진분류를 하는 코드이다.
borrow_mean = ns_book7['대출건수'].mean()
y_train_c = y_train > borrow_mean
y_test_c = y_test > borrow_mean
from sklearn.linear_model import LogisticRegression
logr = LogisticRegression()
logr.fit(X_train, y_train_c) # 훈련 세트로 fit() 메서드를 호출함
logr.score(X_test, y_test_c) # 검증 세트로 score() 메서드를 호출함

📝 실행 결과를 보면 71%정도를 맞췄다. 나름 괜찮은 결과가 나왔다. 그러나, y_test_c에 있는 음성 클래스와 양성 클래스의 비율이 비슷하지 않다는 문제점이 있다.
y_test_c.value_counts()

📝 음성 클래스가 69% 정도이고 양성 클래스는 31% 정도이다. 이제 더미모델로 score() 메서드의 결과를 확인해본다.
from sklearn.dummy import DummyClassifier
dc = DummyClassifier()
dc.fit(X_train, y_train_c)
dc.score(X_test, y_test_c)

📝 69% 정도로 정확도가 나왔다. 이 값이 모델을 만들때 기준점이 되는 점수이다. 만약 이 점수보다 낮으면 좋은 모델이라고 하기 어렵다.