Hong의 모든 글

선형대수와 데이터과학

데이터과학을 하려면 선형대수를 꼭 배워야할까?

데이터과학, 기계학습, 데이터마이닝, 인공지능, 통계 등에 항상 따라붙는 수학이 선형대수학입니다. 이와 관련된 채용 공고에도 선형대수학에 대한 기본 또는 충분한 이해가 요건으로 붙는 경우가 많았습니다.

데이터과학을 하려면 선형대수를 꼭 배워야 하나?

이 질문에 대한 대부분의 전문가들의 답은 “그렇다”일 것입니다.

어쩌면 여러분은 “데이터과학을 하는데 선형대수학은 필요없다”는 말을 듣고 싶어서 이 포스트를 보고 있는지도 모르겠습니다.

솔직히 누군가 그렇게 말한 사람이 있다면 그건 “개뻥”입니다. 또는 속성 과정으로 금방 배우고 마스터링 할 수 있다고 말한다면 그것도 “개뻥”입니다.

“얼마만큼 깊이 또는 많이 하느냐”의 차이는 있겠지만 선형대수의 기초 지식은 반드시 필요하며 데이터과학, 데이터분석, 통계, 기계학습, 데이터마이닝과 같은 데이터 관련 고급 기술을 배울 때 선형대수는 필수적입니다. 못하면 멍텅구리가 되거나 결국에는 아예 아무것도 이해하지 못하게 됩니다.

선형대수학은 무엇인가?

무시무시한 어감을 가진 선형대수는 대수학의 한 갈래로 영어로는 “Linear Algebra”이고 “리니어 앨지브라”라고 읽습니다. 발음이 조금 까다롭기 때문에 한국인들은 “선형대수” 또는 “선대”라고 읽습니다.

선형대수학이 무엇인지 쉽게 설명하면

선형대수는 짧게 말하면 벡터와 행렬을 다루는 수학입니다.

  • 벡터는 관련이 있는 숫자들을 묶어 놓은 것이고
  • 행렬은 벡터 여러 개를 묶어서 사각형 깍두기로 만들어 놓은 것입니다.

이렇게 모양을 만들어 놓는 것은 안 중요한데 이렇게 서로 뭔가 관련이 있는 숫자들을 묶어서 뭉치로 만들고 이것들을 서로 곱하고 나누고 빼고 줄이고 바꾸고 이것저것 하면 숫자와 숫자의 관계나 숫자 압축, 숫자 분해이 가능하고 문제를 풀 수 있는 경우가 많아집니다.

즉 선형대수학과 미적분을 사용하면 현실에서 정답을 찾는 것이 아니라 쓸만한 결과를 거의 항상 얻어낼 수 있습니다.

선형대수의 큰 매력은 대부분의 경우에 문제를 해결할 수 있다는 것입니다. 정답이 있다는 것이 아니라 “문제를 해결”한다는 것을 이해해야 합니다.

선형대수는 수학적으로 답이 없는 문제가 있을지라도 현실적으로는 억지로라도 답을 찾거나 가장 그럴듯한 것을 찾아낼 수 있습니다.

답이 없다고 포기할 것이 아니라 가장 그럴듯하고 가장 쓸만한 것을 찾아낸다면 그것이 답인 것인데 선형대수는 그런 것을 하는데 가장 도움이 되는 수학입니다.

왜 이름이 선형대수인가?

선형은 “선의 형태”라는 뜻인데 벡터와 행렬을 다룬다고 하면서 여기에 갑자기 “선”은 왜 나오는가?

선형대수가 있으면 원형대수나 곡형대수 같은 것도 있는가? 이런 의문도 들텐데요.

쪼금 어렵게 말하면 벡터를 다차원공간상에 매핑하고 이 벡터를 여러 연산으로 바꾸게 되면 이 벡터가 차원안에서 직선으로만 움직이기 때문입니다. 벡터는 공간상의 점이라고 하면 이 벡터는 직선운동만 가능합니다. 회전해서 움직일 수 없고 직선이 아니게 바꾸는 유일한 방법은 내적이라는 계산밖에 없습니다.

쉽게 말하면 벡터를 공간에 있는 점으로 표현해서 그림을 그려놓고 이런저런 계산을 해서 이 벡터를 다시 공간상에 점을 찍어 놓고 보면 직선으로만 움직이더라는 것이 이렇게 직선으로만 움직이게 해서 숫자로 부터 어떤 의미나 관계를 찾아내기 때문에 선형대수라고 합니다.

그래서 삼각형과 원의 관계를 다루는 삼각함수가 선형대수에서는 잘 보이지 않습니다. 물론 수학자들이 하는 고등수학에는 선형대수나 삼각함수나 뭐 이것저것 섞여서 빈번하게 나오지만 그건 그 사람들 알아서 할 일이구요.

데이터과학을 위해서선형대수를 꼭 배워하는 이유?

선형대수학을 모르고 데이터과학, 통계분석, 데이터마이닝, 기계학습, 인공지능을 한다면 어떤 알고리즘이나 방법(프레임워크나 스킴)의 원리를 이해하지 못하게 됩니다.

이해하지 못하면 다룰 수 없고 알고리즘같은 것이 압도당하고 지배당하게 됩니다.
샘플 코드 복붙해서 실행하는 것인 모델 빌드나 데이터 과학의 전부가 아닙니다.

간단한 예로 선형회귀(Linear Regression)에 대해서 얘기해보면

선형회귀는 처음보면 단순하고 쉬워 보이지만 기저를 다 이해하기 상당히 까다롭고 어렵습니다. 기본을 이해하지 못하면 결국 제대로 다루지 못하고 그 보다 더 복잡하고 난해한 알고리즘은 사용하지 못합니다.

선형회귀를 이해하기 위해서는 OLS를 알아야하고 그러려면 행렬연산이나 이것저것 선형대수와 미적분을 알아야합니다. 제대로 이해하지 못한채 경험적으로 암기한 내용으로만 선형회귀를 하고 나서는 엉뚱한 선형회귀 모델을 만들어 놓고 “이게 왜이러지?”하고 고민하는 경우를 많이 봤습니다.

숫자묶음에서 특징을 추출하는 특이값분해 같은 기법도 선형대수를 배우지 않으면 1도 이해하지 못합니다.

인공지능에서 숱하게 사용하는 경사하강(Gradient Desecnt)과 역전파(Back propagation)같은 것은 선형대수와 미적분의 컬래버입니다.

어떻게 배우면 될까? 문과도 가능할까?

선형대수는 대수학 중에서 매우 쉬운 수학 중에 하나입니다.

대수학들 (미적분, 선형대수학, 위상수학 등…)중에서 쉽다는 말이지 쉽다는 것이 절대 아닙니다.

문과든 이과든 이 선형대수학은 쉬운것 같지만 그렇게 쉽지 않습니다.

그렇다고 수포자가 못할 정도의 것은 아닙니다. 이것도 역시 사람이 만든 것이기 때문입니다.

선형대수학 공부를 쉽게 접근하려면

온라인 강의를 많이 들으면 좋습니다. 요즘 유튜브에 강의가 참 많이 있습니다. 하지만 시간이 많이 든다는 것을 알아야 하고 이해가 될 때까지 반복해서 듣거나 이해가 안되면 다른 강의를 찾는 것이 좋습니다.

가장 유명한 강의이고 추천할 강의는 길버트 스트랭 MIT 교수님의 선형대수학 강의이고 자막이 있습니다.

온라인 강의가 싫으면 결국 대학교재 중에 좋은 것을 하나 읽어야 하는데 “공업수학”교재를 찾으면 됩니다. 크레이그 공업수학 책이 가장 잘 알려진 편이지만 기초가 없으면 독학하기에 조금 어렵지만 못할 것도 아닙니다.

이런 책은 이해가 안되더라도 처음부터 끝까지 다 읽어야 좋은데 고등학교 이과 수준의 수학을 다 알고 대학에서 배우는 고등수학도 일부 알아야 한다는 점입니다.

늦지 않았을까?

늦었다고 시작했을 때는 늦은 겁니다.
하지만 더 늦으면 계속 늦어지기만 할 뿐입니다.
조금씩 하다보면 언젠가는 선형대수가 만만해지는 시기가 옵니다.
물론 이럴때가 또 위험하 시기이긴 하지만 말이죠.

중요한 것은 “지금부터 해도 나는 안될꺼야” 라는생각과 “책 한 권만 떼면 금방 마스터 할 수있을꺼야” 라는 생각을 버리는 것입니다.

파이썬 문자열에서 특수문자 제거하는 3가지 방법 – python 3 ways to remove punctuations from a string

Python으로 특수기호 제거하는 예제입니다.

자연어처리, 크롤한 데이터 정제 등을 할 때 특수문자를 제거하거나 클린징을 해야 할 때 많이 하는 작업입니다.

특히 비정형 데이터 중에서 텍스트(문자열)을 다루다보면 계속 해야 하는 그런 작업입니다.

짧게 먼저 요약하면

  • 가장 빠른 것은 translate() 메서드를 사용하는 것이고
  • 그 다음은 string.replace() 메서드를 사용하는 것이고
  • 가장 느린 것은 정규표현식을 사용하는 것입니다.

속도가 문제되지 않으면 (느려도 되면) 정규표현식을 사용하는 것이 가장 유연하고 좋습니다. 특정 문자를 넣고 빼거나 숫자를 포함하거나 하는 여러가지 작업을 할 수 있습니다.

3가지 방법의 소스코드를 참고하세요.

translate() 함수 사용하기

# strings 패키지의 translate() 함수를 사용하여 특수기호를 제거하는 예제
import string
input_string = '!hi. wh?at is the weat[h]er lik?e. !@##$%%^^&*)_+{}|?"'
output_string = input_string.translate(str.maketrans('', '', string.punctuation))
print(output_string)
# Returns: hi what is the weather like
# 제거되는 특수기호는 아래와 같다.
print(string.punctuation)
# Returns: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

정규표현식 regular expression 사용하기

# 정규표현식을 사용하여 특수기호를 제거하는 예제
import re
input_string = '!hi. wh?at is the weat[h]er lik?e. !@##$%%^^&*)_+{}|?"'
output_string = re.sub(r'[^\w\s]', '', input_string)
print(output_string)
# Returns: hi what is the weather like

# 좀더 빨리 하려면 정규표현식을 컴파일하는 것이 좋다.
pattern_punctuation = re.compile(r'[^\w\s]')
output_string = pattern_punctuation.sub('', input_string)
print(output_string)

string.replace() 사용하기

# string.replace() 함수를 사용하여 특수기호를 제거하는 예제
import string
input_string = '!hi. wh?at is the weat[h]er lik?e. !@##$%%^^&*)_+{}|?"'
for character in string.punctuation:
    input_string = input_string.replace(character, '')
print(input_string)
# Returns: hi what is the weather like

소스 파일

github에 노트북으로도 올려놨습니다.

https://github.com/euriion/python-exams/blob/main/remove-punctuations.ipynb

rsync – ‘cannot delete non-empty directory’ errors

로컬에 있는 파일과 모델, 데이터 등을 리모트의 어딘가로 싱크하는 것은 데이터사이언스 작업을 하면서 빈번하게 있는 일입니다. 복잡한 파이프라인을 작성할 것은 아니기 때문에 간단하게 rsync를 사용할 때가 많습니다.

제가 주로 사용하는 옵션은 이렇습니다.

rsync -avzrub --delete /tmp/source /tmp/target

그런데 이 옵션을 실행하다 보면 다음과 같은 에러가 나올 때가 있습니다.

rsync 'cannot delete non-empty directory' errors

–delete 옵션은 소스에 없는 것들은 타겟에서도 지우라는 명령입니다. 이때 소스에 없는 파일과 디렉토리는 타겟에서도 지워야 하는데 디렉토리를 지우지 못하는 것입니다.

원인은 -b 옵션 때문인데 -b 옵션은 타겟에서 파일을 지욱 때 만약을 대비하기 위해 파일을 ~가 끝에 붙은 이름으로 변경해서 백업하는 기능입니다. 이 옵션으로 인해 지우지 못하게 됩니다.

-b 옵션을 제거하면 문제가 해결됩니다.

만약 그래도 백업을 해야 하고 특정 디렉토리는 다르게 관리해야 한다면 –exclude 옵션으로 그 디렉토리를 제외하고 그 디렉토리는 다른 옵션으로 rsync를 수행해야 합니다.

Faiss – 고속 벡터 검색 엔진으로 유사도 검색하기, Vector Search Engine

Faiss는 Facebook Lab에서 만든 벡터 검색 엔진입니다.

Faiss는 벡터 갬색 엔진이고 유사도 검색을 하거나 추천, 기계학습로 만든 모델을 활용해서 응용 서비스를 만들 때 사용합니다.

별거 아닌거처럼 보이지만 불가능한 것을 가능하게 만들어 주는 매우 유용한 라이브러려입니다.

라이브러리이기 때문에 자체로 서비를 제공하는 것은 아니고 이 라이브러리를 이용해서 Backend, Frontend 서비스를 개발하거나 응용 프로그램에 넣을 수 있습니다.

벡터 검색 엔진

벡터 검색 엔진이 뭔지를 설명해야 하는데요. 보통 그래프 서치라고도합니다. 이것들은 주로 수치를 찾는 것을 말하는데 지도검색 같은데서도 사용하는 것으로 매우 쓸모가 많은 엔진입니ㅏㄷ.

일반적으로 검색 엔진이라고 말하면 흔히 텍스트를 검색하는 것을 생각합니다. 구글의 웹 검색, 네이버 검색, 다음 검색 같은 것은 검색 포털이요. 그게 아니면 Elastic Search나 Lucene갈은 검색 엔진을 생각할 텐데요.

하지만 벡터 검색은 텍스트가 아닌 벡터를 빠른 속도로 찾는 것을 말합니다. 벡터는 수열을 말합니다.

아래와 같이 10개의 숫자가 묶여 있으면 이걸 10차원 벡터라고 합니다. 숫자가 100개 있으면 100차원 벡터, 1000개면 1000차원 벡터입니다.

[-0.00709967 -0.01956441  0.03790117 -0.00150699 -0.02145613 -0.06128123
  0.04965064 -0.05588596  0.08241648 -0.05128423]

이런 것들이 수억개가 있고 수억개 중에 어떤 벡터와 가장 가까운 벡터를 찾아야 한다면 문제가 어려워집니다.

가장 가까운 것을 주어진 입력 벡터와 수억개의 벡터를 모두 하나씩 연결해서 서로의 거리를 계산한 다음 가장 가까운 것을 찾아야 하기 때문입니다.

가장 가까운 것을 찾는데 수십분이 걸릴 수 있습니다. 이러면 실제 서비스에서는 쓸 수 없습니다.

어떤 사용자가 온라인 서적 판매사이트에 접속했을 때 그 사람에게 책을 추천해줘야 하는데 추천할 책 목록을 검색하는데 10분씩 걸린다면 서비스에 적용하지 못합니다. 다른 서비스도 마찬가지구요.

Faiss는 인덱싱 방식을 다르게 해서 데이터가 많아도 짧게는 밀리초 단위 길게는 수초 이내에 결과를 찾아 줍니다. 즉 온라인 추천 서비스에 빠르게 적용하는 추천 시스템 등을 개발하는데 사용할 수 있습니다.

Python Faiss library

Faiss는 Python wrapper를 공식 지원하고 있습니다. c++로 만들어졌으니까 다른 언어로도 연결해서 사용할 수 있습니다. Go lang이나 Node.js, Kotlin 같은 것을 쓰면 Python 보다는 성능이 더 좋을 것입니다.

깃헙 레파지토리: https://github.com/facebookresearch/faiss

레파지토리에 있는 것을 설치해도 되고 그냥 pip를 이용해서 설치해도 됩니다.

pip3 install faiss-cpu

gpu 버전을 설치하고 싶으면 gpu 버전ㅇ로 명시해서 설치하면 됩니다.

pip3 install faiss-gpu

사용법은 매뉴얼을 봐야 하겠지만 기본 사용법은 쉽습니다.

Faiss로 유클리디안 거리로 벡터 검색하기

아래 코드는 유클리디안 거리(Euclidean Distance)로 찾는 예제입니다.

이런 것은 KNN (K-nearest-neighbor) 와 같은 기계학습 모델에 사용하는 것입니다. KNN은 판별 모델에서 사용할때 매우 강력한 알고리즘이지만 검색할 때 너무 느리고 자원을 많이 사용하는 문제로 인해서 실제로는 거의 사용을 못하는 알고리즘이지만 Faiss를 이용하면 이걸 쓸 수 있습니다.

Faiss 색인을 생성할 때 벡터의 차원을 지정해주고, Index의 유형도 결정을 해줘야 하는 것이 중요합니다. 검색은 입력한 k의 갯수만큼 리턴하게 되어 있고 벡터의 색인 번호와 거리를 리턴하게 되어 있늡니다.

색인 번호는 그냥 입력한 입력한 벡터의 순번입니다.

import faiss
import numpy as np
import random

# Euclidean distance 기반으로 가장 가까운 벡터를 찾는다.

# 랜덤으로 10차원 벡터를 10개 생성
vectors = [[random.uniform(0, 1) for _ in range(10)] for _ in range(10)]
# 10차원짜리 벡터를 검색하기 위한 Faiss index 생성
index = faiss.IndexFlatL2(10)
# Vector를 numpy array로 바꾸기
vectors = np.array(vectors).astype(np.float32)
# 아까 만든 10x10 벡터를 Faiss index에 넣기
index.add(vectors)
# query vector를 하나 만들기
query_vector = np.array([[random.uniform(0, 1) for x in range(10)]]).astype(np.float32)
print("query vector: {}".format(query_vector))
# 가장 가까운 것 10개 찾기
distances, indices = index.search(query_vector, 10)
# 결과룰 출력하자
idx = 0
for i in indices:
    print("v{}: {}, distance={}".format(idx+1, vectors[i], distances[idx]))
    idx += 1

Faiss로 코사인 유사도로 검색하기

유클리디안 거리(Euclidean Distance)로 가장 가까운 벡터를 찾으면 특정 차원의 양적 수치에 따라는 거리가 가깝다고 판별되는 편향의 문제가 있습니다. 이게 문제가 될 때가 있고 그렇지 않을 때가 있는데 이것은 문제의 도메인에 따라 다릅니다. 그러니까 문제가 주어진 환경에 따라 그때그때 다르다는 뜻입니다.

이런 문제를 피하는 방법은 유사도를 계산할 때 거리측정 방법을 유클리디안 거리를 사용하지 않고 코사인 유사도를 사용해서 벡터의 방향이 가까운 것을 찾는 것입니다. 보통 검색엔진들도 이 방법을 기본으로 사용합니다.

Faiss도 이걸 지원하는데 예제는 아래 코드를 보시면 되고 앞서 설명했던 유클리디안 거리 기반의 검색과 다른 점은 index를 생성할 때 타입을 다르게 생성해야 하고 벡터를 노말라이즈 해줘야 한다는 것입니다. 벡터가 이미 노말라이즈되어 있다면 안해도 됩니다.

import faiss
import numpy as np
import random

# 코사인 유사도 (Cosine Similarity) 를 이용해서 가장 가까운 벡터를 찾으려면 몇가지를 바꿔줘야 한다.
# 코사인 유사도 (Cosine Similarity) 를 사용하려면 벡터 내적으로 색인하는 index를 만들면 된다.
# 코사인 유사도를 계산하라면 벡터 내적을 필연적으로 계산해야 하기 때문이다.

# 랜덤으로 10차원 벡터를 10개 생성
vectors = [[random.uniform(0, 1) for _ in range(10)] for _ in range(100)]
# 10차원짜리 벡터를 검색하기 위한 Faiss index를 생성
# 생성할 때 Inner Product을 검색할 수 있는 index를 생성한다.
index = faiss.IndexFlatIP(10)
# 아래는 위와 동일하다.
# index = faiss.index_factory(300, "Flat", faiss.METRIC_INNER_PRODUCT)

# Vector를 numpy array로 바꾸기
vectors = np.array(vectors).astype(np.float32)
# vectors를 노말라이즈 해준다.
faiss.normalize_L2(vectors)
# 아까 만든 10x10 벡터를 Faiss index에 넣기
index.add(vectors)
# query vector를 하나 만들기
query_vector = np.array([[random.uniform(0, 1) for x in range(10)]]).astype(np.float32)
print("query vector: {}".format(query_vector))
# 가장 가까운 것 10개 찾기
distances, indices = index.search(query_vector, 50)
# 결과룰 출력하자.
idx = 0
for i in indices:
    print("v{}: {}, distance={}".format(idx+1, vectors[i], distances[idx]))
    idx += 1

노트북 코드

위 코드의 노트북은 깃헙 레파지토리에 올려 두었습니다.

https://github.com/euriion/python-exams/blob/main/faiss/faiss-exam.ipynb

다음 번에는 기회가 되면 Faiss를 이용한 간단하고 빠른 추천 엔진을 만드는 예제를 올려보겠습니다.

R로 스타벅스 지역별 매장수 분석, 데이터 크롤, 데이터 분석

R언어로 , 스타벅스 매장 데이터를 크롤해서 분석하는 간단한 예제 스크립트입니다.

전체 코드는 글 아래 쪽에 있습니다.

코드 설명

예제에서 하려는 것

간단하게 재미 삼아하는 토이(toy) 분석입니다. 실제 분석 프로젝트에서는 이 보다는 더 심도가 깊게 해야 합니다.

이 분석의 실제 목적은 R로 데이터 크롤을 해서 분석은 어떻게 하는 것인지 설명하는 것입니다. 분석 리포트에서는 크롤한 데이터를 이용해서 인사이트를 도출하고 탐색적데이터분석(EDA)를 어떻게 하는지 예제로 보여줍니다.

이 분석에서 해보려고 하는 것은 스타벅스의 매장 목록을 가져와서 각 시도별, 구군별로 어느 지역에 스타벅스 매장이 가장 많은지 어떤 인사이트가 있는지 살펴보고 또 지역별 인구수와 스타벅스 매장 수와 관계가 있는지를 확인해 보는 간단한 분석입니다.

분석에 들어가기 전에 상식적으로 식음료 판매 매장들은 유동인구가 많은 곳에 자리를 잡는 것이 일반적이기 때문에 당연히 지역별 인구수와 스타벅스 매장 수는 관련이 있을 것이라고 추측할 수 있습니다.

그래서 서울이라면 강남 지역에 스타벅스가 가장 많을 것이라고 추측해 볼 수 있습니다. 서울은 강남에 오피스가 가장 많아서 직장이들도 많고 유동 인구도 많습니다. 정말 그런지 확인은 해봐야 겠지요.

그래서 여기서 확인해 볼 가설은 “스타벅스 매장의 위치는 지역의 인구수 또는 유동인구와 관련이 있을 것이다.” 라는 것입니다.

너무 뻔한 것이어서 굳이 세심하게 분석할 필요가 없다고 생각할지 모르지만 하지만 이미 알고 있거나 너무 뻔한 사실도 실제로 그런지 확인하는 것도 유의미합니다.

의외성이 많아지는 요즘 세상은 알고 있던 상식이 실제와는 다른 경우도 많기 때문입니다.

먼저 알아야 할 것

이 포스트에서 하려는 것을 할 때 꼭 알아야 할 정보와 필요한 것은 3가지입니다.

  • 매장의 위치 데이터 제공처
  • 매장의 위치데이터 데이터를 크롤(스크랩)하는 법
  • 크롤한 데이터를 잘 정제, 정돈하는 법

R언어 사용법 등은 당연히 알아야 하는 것이라 목록에는 안 적었습니다.

Python이나 다른 언어로 해도 되지만 여기서는 R언어로 하겠습니다.

데이터를 얻어 오는 곳

이런 정보가 어디 있는지는 검색을 해서 알아내야 합니다. 검색해보니 대충 아래와 같은 것이 제일 만만해 보입니다.

먼저 스타벅스 매장 주소 데이터는 스타벅스 웹사이트에서 가져오면 되는데 csv나 엑셀 파일로 다운로드하게 지원하지 않으니 웹페이지의 내용물을 읽어서 파싱(parsing)해야 합니다.

이 작업은 웹 스크랩이라고 하는데 흔히 크롤이라고 부릅니다. 사실 원래 크롤과 스크랩은 서로 차이가 많습니다만 이 글에서는 그냥 널리 알려진 대로 크롤이라고 하겠습니다.

또 스타벅스 웹페이지는 지도상에 매장의 위치를 표시하기 위해서 Ajax로 데이터를 호출하는 방식을 사용하는데 이것의 방식을 알아내서 어떻게 데이터를 가져와야 하는지도 알고 있어야 합니다.

보통 크롬이나 파이어폭스의 개발자도구를 사용해서 알아내는데 연습과 방법을 익히는데 시간과 노력이 필요합니다. IT활용능력, 웹개발능력과 관련이 있는 것이라서 이것까지 설명하려면 설명이 길어지므로 이것을 알아내는 방법은 여기서는 생략합니다. 다음 기회에 다른 포스트에서 하겠습니다.

이 글에 소스코드를 넣어 두었으니 이미 완성된 코드를 참고 참고하세요.

인구데이터는 국가통계포털 웹사이트에서 가져오면 됩니다. csv나 excel로 다운로드 할 수 있게 지원하므로 매우 편합니다만 그래도 역시 데이터 정제, 정돈 작업은 좀 필요합니다.

인구데이터는 여기 말고도 받을 수 있는 곳이 더 있습니다. 더 편한곳이 있으면 그냥 거기를 이용하세요.

데이터를 크롤하는 법

R언어는 텍스트 파일을 파싱하거나 json 데이터를 처리하는 것이 조금 복잡한 편에 속합니다. R언어가 수치와 벡터 계산에 중점을 두기 때문에 이런 텍스트 처리에는 매우 약합니다.

하기가 까다롭다는 말입니다.

이런 것을 하려면 Python이나 Javascript가 더 나은 선택이지만

R언어로 이걸 하면 한 코드에서 데이터 크롤, 정제, 분석, 시각화까지 한 번에 할 수 있어 관리가 편하고 나중에 코드를 다시 볼 때 전체 흐름을 이해하기 편하다는 장점이 있습니다.

어쨌든 R언어에서 httr, urltools, jsonlite 패키지의 사용법을 익히면 됩니다.

데이터 정제, 정돈

데이터랭글링이라는 것을 할 텐데 이런 작업은 tidyverse 패키지에 포함된 여러 패키지를 이용하는 것이 가장 세련되고 좋은 방법입니다.

그리고 시각화는 ggplot2를 사용하겠습니다.

결과

스타벅스 전체 매장수: 1658개

1658개로 나옵니다. 스타벅스는 모두 신세계에서 운영하는 직영점이라고 알려져 있고 관리가 웹사이트 관리가 잘 되고 있을 것이라고 생각돼서 이 수치는 맞을 것입니다.

수천개는 될 것 같지만 제 예상보다는 적습니다.

시도별 매장수

   sido               n
   <chr>          <int>
 1 서울특별시       571
 2 경기도           383
 3 부산광역시       128
 4 대구광역시        71
 5 인천광역시        67
 6 경상남도          66
 7 광주광역시        59
 8 대전광역시        59
 9 경상북도          48
10 충청남도          36
11 울산광역시        29
12 전라북도          29
13 강원도            28
14 충청북도          26
15 전라남도          25
16 제주특별자치도    22
17 세종특별자치시    11

시도별 매장수는 역시 서울이 가장 많고 그 다음은 경기도, 부산 순입니다. 인구수가 많은 시도에 매장이 더 많은 것을 알 수 있습니다.

네 당연하죠. 물론 지역별로 로컬 카페나 다른 프렌차이즈, 브랜드의 커피매장이 더 인기가 있는 지역이 있을 수도 있으니 이 가설이 틀렸을 여지는 있습니다.

차트로 보며 시도별 매장수의 규모를 더 직관적으로 확인할 수 있는데 서울특별시와 경기도를 합치면 다른 시도를 다 합친 것 보다 더 많습니다.

우리나라 스타벅스 매장은 수도권에 크게 집중되어 있습니다.

구군별 매장수

   sido       gugun        n sido_gugun           
   <chr>      <chr>    <int> <chr>                
 1 서울특별시 강남구      88 서울특별시   강남구  
 2 서울특별시 중구        52 서울특별시   중구    
 3 서울특별시 서초구      48 서울특별시   서초구  
 4 경기도     성남시      47 경기도   성남시      
 5 경기도     고양시      42 경기도   고양시      
 6 서울특별시 영등포구    40 서울특별시   영등포구
 7 서울특별시 종로구      39 서울특별시   종로구  
 8 경기도     수원시      35 경기도   수원시      
 9 경기도     용인시      34 경기도   용인시      
10 서울특별시 송파구      34 서울특별시   송파구  

구군별로 매장수를 살펴보면 역시 서울 강남구가 가장 많습니다. 한국에서 아마 인구밀도가 가장 높은 지역이며 유동인구와 일과시간에 직장인이 가장 많이 있는 곳입니다.

그 다음은 역시 유동인구가 가장 많고 오피스밀집 지역이며 인구밀도가 높은 서울 중구입니다. 중구에는 명동, 충무로, 을지로가 있습니다.

그 다음은 서울 서초구인데 서초구는 강남구와 인구밀도는 비슷하지만 오피스공간 보다는 주거지역이 훨씬 많기 때문에 강남구나 중구에 비해서 낮시간에 직장인이 아주 많지는 않습니다. 그래도 괘나 많습니다.

그리고 그 다음은 경기도 성남시인데 성남에는 분당과 판교가 있으며 성남전체의 인구도 많으므로 이것도 당연합니다.

서울-강남구에 스타벅스가 압도적으로 많으며 다른 대형 지역구들도 제법 많습니다. 앞서 말했듯이 이 지역은 직장인들이 주중에 많이 머무는 지역들입니다. 물론 여기 나온 인구수는 거주 인구수이기 때문에 직장인이 주중에 많다는 것과의 상관관계를 다시 확인해야 할 필요가 있습니다만 알려진 바로는 매우 특별한 경우를 빼면 거주인구가 많은 지역에 직장도 많습니다.

인구수와 매장수의 상관관계

	Pearson's product-moment correlation
data:  sido_tbl$n and sido_tbl$total
t = 7.6707, df = 15, p-value = 1.439e-06
alternative hypothesis: true correlation is not equal to 0
95 percent confidence interval:
 0.7216524 0.9609928
sample estimates:
      cor 
0.8926678 

시도별 인구수와 스타벅스 매장수의 상관관계를 보면 상관계수가 0.89로 강한 상관이며 검정에서 대립가설 채택으로 상관이 있다는 결과가 나옵니다.

더 해볼 것

상관관계를 볼 때 단순 인구수가 아닌 면적을 인구수로 나눠서 인구밀집도를 구한 다음에 상관관계를 보는 것이 더 합리적이겠지만 면적데이터를 가져와서 붙여야 하니 귀찮아서 생략하겠습니다.

또 구군별로도 면적데이터를 가져와서 구군별로도 인구수와 스타벅스 매장수가 상관이 있는지 확인해 보는 것도 필요합니다만 이것도 생략하겠습니다.

혹시 해보고 싶으시면 아래 소스를 보시고 고쳐서 직접 한 번 해보세요.

결론 및 인사이트

매장수와 지역의 인구밀집도는 관계가 있습니다.

시도별, 구군별로 모두 상관관계가 있습니다.

따라서 스타벅스매장이 많은 곳은 인구밀집도가 높은 지역일 가능성이 큽니다.

인구밀집도는 유동인구수와 상관관계가 있는 것으로 알려져 있습니다.

스타벅스 매장이 많은 곳(있는 곳)은 유동인구가 많은 곳입니다.

마약 누군가 많은 유동인구가 필요한 점포를 개업하려고 한다면 스타벅스 부근에 개점하면 됩니다.

길거리 음식을 팔거나 판매하려면 패션 팟업스토어, 푸드 트럭을 열려면 스타벅스 근처에 하면 잘 될 것입니다. 물론 그게 가능하다면 말이죠.

전체 스크립트

소스 파일

https://github.com/euriion/r-exams/blob/main/starbucks_analysis.R

소스 내용

# 스타벅스 목록을 읽어서 간단한 통곗값을 출력하는 R 스크립트
# install.packages(c("httr", "urltools", "jsonlite"))
library(httr)
library(urltools)
library(jsonlite)
# 데이터를 긁어올 사이트: https://www.starbucks.co.kr
# 시도 목록을 가져오는 부분
url <- "https://www.starbucks.co.kr/store/getSidoList.do"
res_content <- POST(url)
res_object <- fromJSON(content(res_content, "text"))
sido_items <- tibble(sido_nm=res_object$list$sido_nm, sido_cd=res_object$list$sido_cd)
# 구군 목록을 가져오는 코드. 구군은 분석에 필요하지 않으므로 리마킹
# url = "https://www.starbucks.co.kr/store/getGugunList.do"
# post_params = "sido_cd=01&rndCod=4X93H0I94L"
# res_content <- POST(url, body=parse_url(sprintf("%s?%s", url, post_params))$query)
# res_object <- fromJSON(content(res_content, "text"))
# 각 시도별 주소를 가져오는 부분
url <- "https://www.starbucks.co.kr/store/getStore.do"
payload <- "in_biz_cds=0&in_scodes=0&search_text=&p_sido_cd=08&p_gugun_cd=&in_distance=0&in_biz_cd=&isError=true&searchType=C&set_date=&all_store=0&whcroad_yn=0&P90=0&new_bool=0&iend=1000"
post_params <- parse_url(sprintf("%s?%s", url, payload))$query
res_content <- POST(url, body = post_params)
res_object <- fromJSON(content(res_content, "text"))
# 시도 코드와 이르을 받아서 주소목록이 들어 있는 data.frame을 리턴하는 함수
get_addrs <- function(sido_cd, sido_nm) {
  url <- "https://www.starbucks.co.kr/store/getStore.do"
  payload <- sprintf("in_biz_cds=0&in_scodes=0&search_text=&p_sido_cd=%s&p_gugun_cd=&in_distance=0&in_biz_cd=&isError=true&searchType=C&set_date=&all_store=0&whcroad_yn=0&P90=0&new_bool=0&iend=10000", sido_cd)
  post_params <- parse_url(sprintf("%s?%s", url, payload))$query
  res_content <- POST(url, body = post_params)
  res_object <- fromJSON(content(res_content, "text"))
  cbind(rep(sido_cd, length(res_object$list$addr)), rep(sido_nm, length(res_object$list$addr)), res_object$list$addr)
}
# 데이터랭글링
# install.packags(c("tidyverse", "stringi"))
library(tibble)
library(dplyr, warn.conflicts = FALSE)
library(stringi)
df <- sido_items |> rowwise() |> mutate(addr=list(get_addrs(sido_cd, sido_nm)))
tbl <- do.call(rbind.data.frame, df$addr)
names(tbl) <- c("sido_cd", "sido_nm", "addr")
tbl <- tbl |> rowwise() |> mutate(addrsplit=stri_split_fixed(addr, " ", 4))
tbl <- as_tibble(do.call(rbind.data.frame, tbl$addrsplit))
names(tbl) <- c( "sido", "gugun", "dong", "bunji")
tbl |> count()  # 전체 매장 개수
stat_sido <- tbl |> count(sido) |> arrange(desc(n))  # 시도별 매장 수
stat_gugun <- tbl |> count(sido, gugun) |> arrange(desc(n)) |> mutate(sido_gugun=paste(sido, " ", gugun))  # 시도별 매장 수
# 시각화
library(ggplot2)
p <- ggplot(stat_sido, aes(x =reorder(sido, n), y = n)) +
  geom_col(aes(fill = n), width = 0.7)
p <- p + coord_flip()
p <- p + xlab('시/도')
p
p <- ggplot(head(stat_gugun, 20), aes(x =reorder(sido_gugun, n), y = n)) +
  geom_col(aes(fill = n), width = 0.7)
p <- p + coord_flip()
p <- p + xlab('구/군')
p
# 데이터 출처: # https://kosis.kr/statHtml/statHtml.do?orgId=101&tblId=DT_1B040A3&checkFlag=N
pop_sido_text_data <- "sido,total,male,female
서울특별시,9505926,4615631,4890295
부산광역시,3348874,1638207,1710667
대구광역시,2383858,1174667,1209191
인천광역시,2949150,1476663,1472487
광주광역시,1441636,713037,728599
대전광역시,1451272,724026,727246
울산광역시,1121100,575939,545161
세종특별자치시,374377,186907,187470
경기도,13571450,6830317,6741133
강원도,1538660,774315,764345
충청북도,1597097,810548,786549
충청남도,2118638,1083242,1035396
전라북도,1785392,888291,897101
전라남도,1832604,922190,910414
경상북도,2624310,1322509,1301801
경상남도,3311438,1666968,1644470
제주특별자치도,676691,339071,337620
"
pop_sido_tbl <- read.csv(textConnection(pop_sido_text_data))
sido_tbl <- pop_sido_tbl |> inner_join(stat_sido, by = "sido")
# 상관계수
cor(sido_tbl$n, sido_tbl$total)
# 결괏값: 0.8926678
cor.test(sido_tbl$n, sido_tbl$total)

여기까지 입니다.