본문 바로가기

머신러닝/지도학습

[Machine Learning] Kaggle_데이터셋 분석_미 초소형 기업 밀도 예측

320x100

 

서론

 

이번 시간에는 현재 진행중인 미국 내 통계사의 산업통계 데이터를 통해

2022년 이후의 카운티 별(county) 초소형 기업(Microbusiness) 현황에 대해 간단한 분석을 해보고,

필자 나름대로의 데이터 전처리를 진행하려 합니다.

 

*county : 자치주, 자치군 등 영미권의 행정 구역을 뜻합니다.

(예컨대 미 주소에서 체로키 카운티, 달라스 카운티 등이 존재)

 

데이터셋의 목표이자 컴피티션의 목표는 북미 통계청에서 제공한 census 통계자료를 확인해보고,

이를 feature로 판단하여 train 세트에서 확인한 후 미래의 기업밀도를 예측하는 것입니다.

 

#  목표 설정

 

# 북미의 지역구 별 초소형 기업의 밀도 분석
# 타겟(종속변수) : microbuisiness_density
# 피쳐 : 밀도를 계산하기 위한 독립변수인데, 먼저 cifps는 소도시 별 "코드"를 뜻합니다.

 

필자가 분석한 feature는 다음과 같습니다.

 

#  핵심 피쳐 선정

 

* pct_bb : 도시의 인터넷 가입자 비율 (2017~2021)
* pc_college : 4년제 대학생 졸업자 비율(25세 이상, 2017~2021)
* pct_foreign_born : 외국인 비율(2017~2021)
* pct_it_workers : IT 종사자 인구비율(2017~2021)
* median_hh_inc : 1년간의 가계 소득 중앙값
 
 
 
※ 현재 진행중인 컴피티션이기 때문에 원본 전체는 미공개하고, 데이터프레임의 originality는
최대한 가리는 방향으로 전개했습니다.

※ 실제 분석결과는 아직 진행중으로, 결과값이 나오면 다시 수정하거나, 추가 게시물로 서술할 예정입니다.

※ 현재 영미권에서 쓰이는 용어와 번역상의 차이가 존재할 수 있습니다.
    허나 이 부분은 최대한 국내 정서와 행정 명칭과 유사하게 번역하고자 노력했습니다.

 

시작

 

 

# 알고리즘 선정

종속 변수는 특정한 지역구 안에 존재하는 기업 밀도(영세한 기업)이기 때문에

사용 알고리즘은 간단하게 Linear regression과 KNN(k 최근접 모델), boosting 계열에서는

LightGbm을 사용하려 합니다.

 

추후 제시하겠지만, 가계소득값으로 인해 데이터가 차이가 커졌는데요.

자릿수가 만 단위가 넘어가는 금액 vs 십의 자릿수 이하의 percent가 있기 때문에

표준화 스케일로 스케일링도 진행할 예정입니다.

 

 

# 학습시킬 파일 로드부터 시작
 
train = pd.read_csv('train.csv') # 훈련용 데이터
census = pd.read_csv('census_starter.csv') # 독립변수에 대한 설명

 

 

train은 학습용 데이터가 있는 csv였습니다.

현재 cfips가 각 자치구 별 코드를 의미하는 것으로, county는 제거하고 묶으려 합니다.

 

train.head()

 

census는 컴피티션 담당자가 참고사항으로 업로드한 파일로, 최근 5년간의 카운티의

인구 흐름이 담겨 있습니다. #핵심 피쳐 선정에서 미리 서술한 바 있습니다.

 

census.head()

 

train.info()
# train은 cfips가 x년x월별로 구분, census는 cfips가 연도 구분없이 존재함
# trian의 cfips를 총 기간동안 데이터의 평균으로 groupby할 것인지 vs 가장 최근 날짜의 데이터만 남길것인지
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14677 entries, 0 to 14676
Data columns (total 7 columns):
 #   Column                 Non-Null Count  Dtype  
---  ------                 --------------  -----  
 0   row_id                 14677 non-null  object 
 1   cfips                  14677 non-null  int64  
 2   county                 14677 non-null  object 
 3   state                  14677 non-null  object 
 4   first_day_of_month     14677 non-null  object 
 5   microbusiness_density  14676 non-null  float64
 6   active                 14676 non-null  float64
dtypes: float64(2), int64(1), object(4)
memory usage: 802.8+ KB

 

지역코드가 train에서는 2000년 1월 1일~2010년 1월 1일과 같은 방식으로

연속적으로 되어 있었으나, census에서는 단 하나씩만 존재했습니다.

이때문에 groupby로 전부 묶을지, 혹은 가장 최신 연도만 빼고 모두 지울지 고민했습니다.

 

현재 공유되어 있는 코드에서는 시계열별로 분석하는 경우가 많았으나,

도메인 지식이 완벽하지 않은 상태에서는 너무 많은 흐름까지 고려하기 어려울 것이라고 판단하여

나머지는 제거하려고 계획했습니다.

 

census.describe(include='all') # 가장 최근의 통계치가 영향이 있을 것이라고 판단하여 cfips는 가장 최근 날짜의 데이터로 남기도록 함
# 그러나 그 자체만으로 의미도 있을 것

 

nan값을 확인했습니다만, 많진 않았습니다.

 

# 결측치부터 확인
census.isnull().sum().sort_values(ascending=True)
pct_bb_2017              0
median_hh_inc_2019       0
median_hh_inc_2017       0
pct_it_workers_2019      0
pct_it_workers_2017      0
pct_foreign_born_2019    0
pct_foreign_born_2017    0
pct_college_2019         0
pct_foreign_born_2018    0
pct_bb_2018              0
pct_college_2017         0
cfips                    0
pct_bb_2019              0
pct_college_2018         0
pct_college_2020         1
median_hh_inc_2018       1
pct_it_workers_2021      1
pct_it_workers_2020      1
pct_it_workers_2018      1
pct_bb_2021              1
pct_foreign_born_2021    1
pct_foreign_born_2020    1
pct_college_2021         1
pct_bb_2020              1
median_hh_inc_2020       2
median_hh_inc_2021       2
dtype: int64

 

위와 아래 내용이 필자가 originality를 최대한 보전하려고 한 이유입니다.

 

공적인 기관이나, 주최측에서 상당히 엄밀한 조사가 있었는지 결측치가 정말 없습니다.

혹은, 이미 오류검사를 한번 끝내고 업로드했을 가능성도 있습니다.

 

train.isnull().mean() # 결측치 없음
row_id                   0.000000
cfips                    0.000000
county                   0.000000
state                    0.000000
first_day_of_month       0.000000
microbusiness_density    0.000068
active                   0.000068
dtype: float64

 

참고자료의 결측치는 → 평균값으로 처리했습니다.

 

census.pct_bb_2020 = census.pct_bb_2020.fillna(census.pct_bb_2020.mean())
census.pct_bb_2021 = census.pct_bb_2021.fillna(census.pct_bb_2021.mean())
census.pct_college_2020 = census.pct_college_2020.fillna(census.pct_college_2020.mean())
census.pct_college_2021 = census.pct_college_2021.fillna(census.pct_college_2021.mean())
census.pct_foreign_born_2020 = census.pct_foreign_born_2020.fillna(census.pct_foreign_born_2020.mean())
census.pct_foreign_born_2021 = census.pct_foreign_born_2021.fillna(census.pct_foreign_born_2021.mean())
census.pct_it_workers_2018 = census.pct_it_workers_2018.fillna(census.pct_it_workers_2018.mean())
census.pct_it_workers_2020 = census.pct_it_workers_2020.fillna(census.pct_it_workers_2020.mean())
census.pct_it_workers_2021 = census.pct_it_workers_2021.fillna(census.pct_it_workers_2021.mean())
census.median_hh_inc_2020 = census.median_hh_inc_2020.fillna(census.median_hh_inc_2020.mean())
census.median_hh_inc_2018 = census.median_hh_inc_2018.fillna(census.median_hh_inc_2018.mean())
census.median_hh_inc_2021 = census.median_hh_inc_2021.fillna(census.median_hh_inc_2021.mean)

 

제거 확인

 

census.isnull().sum() # 결측치 제거
pct_bb_2017              0
pct_bb_2018              0
pct_bb_2019              0
pct_bb_2020              0
pct_bb_2021              0
cfips                    0
pct_college_2017         0
pct_college_2018         0
pct_college_2019         0
pct_college_2020         0
pct_college_2021         0
pct_foreign_born_2017    0
pct_foreign_born_2018    0
pct_foreign_born_2019    0
pct_foreign_born_2020    0
pct_foreign_born_2021    0
pct_it_workers_2017      0
pct_it_workers_2018      0
pct_it_workers_2019      0
pct_it_workers_2020      0
pct_it_workers_2021      0
median_hh_inc_2017       0
median_hh_inc_2018       0
median_hh_inc_2019       0
median_hh_inc_2020       0
median_hh_inc_2021       0
dtype: int64

 

언급했듯이, state자체는 일대일 대응하는 숫자형 변수가 있기 때문에

제거 예정입니다.

 

# object 확인
train.info() # cfips가 있기 때문에 county, state는 삭제해도 무방
# first_day_of_month는 날짜로 변환
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14677 entries, 0 to 14676
Data columns (total 7 columns):
 #   Column                 Non-Null Count  Dtype  
---  ------                 --------------  -----  
 0   row_id                 14677 non-null  object 
 1   cfips                  14677 non-null  int64  
 2   county                 14677 non-null  object 
 3   state                  14677 non-null  object 
 4   first_day_of_month     14677 non-null  object 
 5   microbusiness_density  14676 non-null  float64
 6   active                 14676 non-null  float64
dtypes: float64(2), int64(1), object(4)
memory usage: 802.8+ KB

 

복구는 파일을 바로 로드하면 되서 매우 위험한 것은 아니지만

무의식적으로 그냥 실행할 때를 고려해서 삭제에는 항상 주의를 표시했습니다.

 

# 열 삭제
# 주의
train.drop(labels = ['county','state'], axis=1, inplace =True)
train.head()

 

train.first_day_of_month
0        2019-08-01
1        2019-09-01
2        2019-10-01
3        2019-11-01
4        2019-12-01
            ...    
14672    2020-04-01
14673    2020-05-01
14674    2020-06-01
14675    2020-07-01
14676            20
Name: first_day_of_month, Length: 14677, dtype: object

 

지역 코드를 가장 앞으로 오도록, 열의 순서를 통일했습니다.

참고사항으로, 지역 코드(cfips)는 일반적인 북미의 FIPS라는 지역코드와

카운티별 고유 숫자를 합친 것입니다. (4~5자리)

col1=census.columns[5:].to_list()
col2=census.columns[:5].to_list()
new_col=col1+col2
census=census[new_col]
census

 

순서 바뀐것 확인

census.columns
Index(['cfips', 'pct_college_2017', 'pct_college_2018', 'pct_college_2019',
       'pct_college_2020', 'pct_college_2021', 'pct_foreign_born_2017',
       'pct_foreign_born_2018', 'pct_foreign_born_2019',
       'pct_foreign_born_2020', 'pct_foreign_born_2021', 'pct_it_workers_2017',
       'pct_it_workers_2018', 'pct_it_workers_2019', 'pct_it_workers_2020',
       'pct_it_workers_2021', 'median_hh_inc_2017', 'median_hh_inc_2018',
       'median_hh_inc_2019', 'median_hh_inc_2020', 'median_hh_inc_2021',
       'pct_bb_2017', 'pct_bb_2018', 'pct_bb_2019', 'pct_bb_2020',
       'pct_bb_2021'],
      dtype='object')

 

train 데이터프레임도 열순서를 바꾸겠습니다.

 

col1=train.columns[2:].to_list()
col2=train.columns[:2].to_list()
new_col=col1+col2
train=train[new_col]
train

 

안전성을 높이기 위해서 복사합니다.

 

train2 = train.copy()#복사
train2

 

아까 있었던 날짜데이터, first_day_of_month가 있었죠?

datetime 형식으로 바꿔줍니다

 

new_train["first_day_of_month"] = pd.to_datetime(new_train["first_day_of_month"]) # first_day_of_month 날짜형 데이터로 처리

 

현재 census 데이터프레임이 cfips를 인덱스로 하여 정렬되어 

존재하고 있었습니다.

 

train 데이터프레임도 기준을 맞춰주기 위해, 정렬해줍니다.

 

new_train = train.iloc[:,:5].sort_values(by='cfips')
new_train

 

※참조. 현재 census 상태

 

 

자세히 보면 cfips별 행의 갯수가 3142 - 3135개로, 서로 데이터프레임이

7개 정도 차이가 납니다. 이 부분은 통계청에서 조사한 county 수와

주최측이 조사한 county수가 차이가 났던것으로 판단했습니다.

 

갯수의 차이는 merge로 묶어서 조정해줍니다.

the_new_train = pd.merge(new_train, census, left_index=True, right_index=True, how='left') #1 조합 성공
the_new_train

 

좋습니다. 이제 필자가 필요한 변수들은 모두 한손안에 들어왔습니다.

 

 

여기서 흥미로운 인사이트를 제시하려 합니다.

 

현재까지 도시의 대졸자 비율, 해외 입주자 비율, 가계소득 중앙값, 인터넷 가입자 비율,

IT업계 종사자들을 독립변수로 결정하고 있었습니다.

 

그렇다면, 이 다섯가지 변수 중 기업의 밀도에 높은 상관관계를 갖고 있는 것은

어떤 것일까요? 대졸자 비율일까요? 도시의 가계소득 일까요?

 

대형 그래프를 그려봤습니다.

 

plt.figure(figsize = (18,18))
sns.heatmap(the_new_train.head(10).corr(), vmax = 1, vmin = -1, annot = True)

 

가장 왼쪽열(microbusiness_density)를 주목해봅니다.

 

active는 현재 활동중인 기업 수이기 때문에, 특별히 주목할 필요는 없습니다.

절대값 1, 즉 1이나 -1에 가까운 변수들이 높은 상관관계를 갖게 되는데요,

이부분에서는 대졸자 비율(pct_college), 가계소득 중앙값(median_hh_inc) 등이

상대적으로 높은 관계성을 갖고 있다고 추론할 수 있습니다.

 

다만 대졸자 비율이나 가계소득이 높다고 해서, 무조건 초소형 기업의 수가 

높다고 볼수는 없습니다. 강한 상관관계가 인과관계로 연역된다고 볼 수 없기 때문입니다.

 

# cfips 를 기준으로 나머지는 추후 test에서 데이터 전처리

 

자, 이제 전처리를 끝내려고 했으나 한가지 문제점이 있습니다.

 

the_new_train.info() # object발견. 2021의 가계소득 중앙값
# the_new_train.median_hh_inc_2021.dtype
the_new_train['median_hh_inc_2021']
<class 'pandas.core.frame.DataFrame'>
Int64Index: 3135 entries, 1001 to 56045
Data columns (total 29 columns):
 #   Column                 Non-Null Count  Dtype         
---  ------                 --------------  -----         
 0   microbusiness_density  3135 non-null   float64       
 1   active                 3135 non-null   int64         
 2   row_id                 3135 non-null   object        
 3   first_day_of_month     3135 non-null   datetime64[ns]
 4   pct_college_2017       3135 non-null   float64       
 5   pct_college_2018       3135 non-null   float64       
 6   pct_college_2019       3135 non-null   float64       
 7   pct_college_2020       3135 non-null   float64       
 8   pct_college_2021       3135 non-null   float64       
 9   pct_foreign_born_2017  3135 non-null   float64       
 10  pct_foreign_born_2018  3135 non-null   float64       
 11  pct_foreign_born_2019  3135 non-null   float64       
 12  pct_foreign_born_2020  3135 non-null   float64       
 13  pct_foreign_born_2021  3135 non-null   float64       
 14  pct_it_workers_2017    3135 non-null   float64       
 15  pct_it_workers_2018    3135 non-null   float64       
 16  pct_it_workers_2019    3135 non-null   float64       
 17  pct_it_workers_2020    3135 non-null   float64       
 18  pct_it_workers_2021    3135 non-null   float64       
 19  median_hh_inc_2017     3135 non-null   int64         
 20  median_hh_inc_2018     3135 non-null   float64       
 21  median_hh_inc_2019     3135 non-null   int64         
 22  median_hh_inc_2020     3135 non-null   float64       
 23  median_hh_inc_2021     3135 non-null   object        
 24  pct_bb_2017            3135 non-null   float64       
 25  pct_bb_2018            3135 non-null   float64       
 26  pct_bb_2019            3135 non-null   float64       
 27  pct_bb_2020            3135 non-null   float64       
 28  pct_bb_2021            3135 non-null   float64       
dtypes: datetime64[ns](1), float64(23), int64(3), object(2)
memory usage: 799.3+ KB
cfips
1001     62660.0
1003     64346.0
1005     36422.0
1007     54277.0
1009     52830.0
          ...   
56037    76668.0
56039    94498.0
56041    75106.0
56043    62271.0
56045    65566.0
Name: median_hh_inc_2021, Length: 3135, dtype: object

 

한가지 object로 되어있는 변수가 있습니다.

 

이 피쳐는 오류가 섞여 있는것으로 판단했습니다. 

분명 숫자로만 구성되어 있는데 object에서 int로 치환이 안됩니다.

 

the_new_train = the_new_train.astype({"median_hh_inc_2021":"int"})
t = the_new_train.iloc[:,23:24]
# the_new_train['median_hh_inc2021'] = the_new_train['median_hh_inc_2021']
# the_new_train.info() # median_hh_inc_2021는 어째서인지 object에서 변경이 안됨. 
# 직접 보기에는 숫자들인데, method로 확인됨
#더미데이터로 제작은 열이 3000개로 늘어나서 불가
TypeError                                 Traceback (most recent call last)
<ipython-input-88-1d83a56d7ff4> in <module>
----> 1 the_new_train = the_new_train.astype({"median_hh_inc_2021":"int"})
      2 t = the_new_train.iloc[:,23:24]
      3 # the_new_train['median_hh_inc2021'] = the_new_train['median_hh_inc_2021']
      4 # the_new_train.info() # median_hh_inc_2021는 어째서인지 object에서 변경이 안됨.
      5 # 직접 보기에는 숫자들인데, method로

8 frames
/usr/local/lib/python3.8/dist-packages/pandas/_libs/lib.pyx in pandas._libs.lib.astype_intsafe()

TypeError: int() argument must be a string, a bytes-like object or a number, not 'method'

 

median_hh_inc_2021 열을 비롯하여, 몇가지 더 삭제할 예정입니다.
# 열 삭제
# 주의
the_new_train.drop('median_hh_inc_2021', axis = 1, inplace = True)
the_new_train.isnull().sum()
microbusiness_density    0
active                   0
row_id                   0
first_day_of_month       0
pct_college_2017         0
pct_college_2018         0
pct_college_2019         0
pct_college_2020         0
pct_college_2021         0
pct_foreign_born_2017    0
pct_foreign_born_2018    0
pct_foreign_born_2019    0
pct_foreign_born_2020    0
pct_foreign_born_2021    0
pct_it_workers_2017      0
pct_it_workers_2018      0
pct_it_workers_2019      0
pct_it_workers_2020      0
pct_it_workers_2021      0
median_hh_inc_2017       0
median_hh_inc_2018       0
median_hh_inc_2019       0
median_hh_inc_2020       0
pct_bb_2017              0
pct_bb_2018              0
pct_bb_2019              0
pct_bb_2020              0
pct_bb_2021              0
dtype: int64

 

# row_id drop한 버전

the_new_train.drop('row_id', axis = 1, inplace = True)

 

이제 훈련셋을 분리하여 모델링할 예정입니다.

 

# 훈련셋 분리 후 스케일링 →학습 → 시작
X = the_new_train.drop(['microbusiness_density', 'first_day_of_month'], axis = 1)
y = the_new_train['microbusiness_density']
# 분리. 날짜 제외

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 10)

 

독립변수 확인했습니다.

 

X # 확인

Standard 스케일링을 진행합니다.

 

scale = StandardScaler() # 에러 위험을 낮추기 위해 고려
X_trainst = scale.fit_transform(X_train)
X_testst = scale.transform(X_test)

 

이제 선형회귀 모델을 적용합니다.

LinearRegression은 초기에 패키지로 다 호출했습니다.

model = LinearRegression()
model.fit(X_trainst, y_train)
LinearRegression()

 

 

예측값도 구합니다.

 

pred = model.predict(X_testst)

 

필자는 점수를 계산하기 전에, 일단 추상적으로 떠올리던 것을

실체화해서, 시각적으로 구현하는 것을 선호합니다.

눈으로 보이면 무엇이 안된것인지, 무엇이 잘된것인지 더 잘보입니다.

 

그 예측값이 완전히 틀린 것이라고 하더라도요!

 

# 테이블로 평가
comparison = pd.DataFrame({
    'actual': y_test, # 실제값
    'pred': pred
}) 
comparison
	actual	pred
cfips		
8043	3.623784	3.407006
53013	2.580258	7.674203
51017	2.736318	1.896151
26157	2.078809	2.228772
26119	2.754960	1.227988
...	...	...
26127	2.666148	2.870470
55095	4.640566	3.946307
37109	5.263868	4.783580
29159	2.219227	3.034277
55101	3.818579	4.896052
627 rows × 2 columns

 

# scatterplot 제작
plt.figure(figsize=(10, 7))
sns.scatterplot(x = 'actual', y= 'pred', data = comparison)
plt.show() # 1,2개의 아웃라이어

 

안타깝게도 아웃라이어가 남아있습니다.

두 세개정도 완전히 커진 수치값이 보이네요.

 

평가 점수로 넘어갑니다.

 

from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error

# mse 평가
mean_squared_error(y_test, pred)
10.444502825135869

 

스크롤이 너무 길어져서, mse, mae를 다시 호출했습니다.

 

# rmse 평가
mean_squared_error(y_test, pred, squared=False)
3.2317956038610904

 

# mae 평가

mean_absolute_error(y_test, pred)
1.6267654123898432

 

결정계수도 확인해봅니다.

 

model.score(X_trainst, y_train) # R²
# 매우 낮은 결정계수
0.19693808002268087

 

변동이 강해서인지, 뚜렷하게 높은 점수를 보이지는 않게 되었습니다.

 

이렇게 해서, 첫번째로 데이터 분석과 전처리 과정을 종료하겠습니다.

선형회귀의 식과 정확도 평가를 진행했고, 다음 시간에는

KNN 모델과 점수를 높여보기 위한 LightGBM 알고리즘을 진행하며,

추후 파일 업로드까지 예정되어 있습니다.

 

끝.😎

728x90