심심해서 하는 블로그 :: '데이터 마이닝' 태그의 글 목록

'데이터 마이닝'에 해당되는 글 2건

GitHub : https://github.com/ssooni/data_mining_practice


새로운 시리즈 시작!!

이전 카카오 시리즈에 이어서 미세먼지 데이터와 기상 데이터를 이용해서 데이터 마이닝 기법들을 적용하는 연습을 해보았습니다.

(그거 보다 궁금했어요.. 기상과 미세먼지와 밀접한 관계가 있는지 말이죠)

데이터 수집, 전처리, 상관 분석 등 적용 해보고 싶은 방법들을 적용하고 결과를 관찰하도록 하겠습니다.


미세먼지 데이터 수집



서울시 열린데이터광장(http://data.seoul.go.kr/)에서 시간별 평균 대기오염도 정보 데이터를 제공합니다.

서울의 구 단위로 시간단위 미세먼지 농도를 제공하고 2009년 데이터도 존재하길래 사용하기 적절하다고 판단하여 인증 키를 취득하였습니다.

회원가입 절차를 진행한 후에 간단하게 사용 목적을 명시하면 인증 키를 획득할 수 있었습니다.

데이터의 저작권은 출처를 밝히면 자유롭게 사용이 가능합니다.




기상 데이터 수집


열린데이터광장에 서울시의 과거 기상데이터도 제공하지만, 데이터가 썩 만족스럽지가 않아서 기상자료개방포탈(https://data.kma.go.kr/cmmn/main.do)에서 기상데이터를 수집하였습니다.

기상포탈에는 Open-API 서비스로 다양한 종류의 기상 데이터를 제공합니다.

저는 시간 단위로 측정된 미세먼지 데이터를 수집하였기 때문에 기상 데이터도 종관기상관측 시간자료 를 이용하였습니다.

마찬가지로 회원가입을 진행한 후에 API 사용 신청을 하면 별다른 승인 절차 없이 API 사용이 가능합니다.

API Key는 마이페이지 > 오픈 API 현황에서 확인 가능합니다.




API 호출 프로그램 작성

전체 소스코드는 GitHub를 참조바랍니다.

API Key 및 전체 데이터의 경우 공유가 힘든 점 양해바랍니다.


REST API는 Http URL의 요청을 처리하여 우리가 보통 사용하는 WEB 서비스는 Response를 HTML로 하는 것과 달리 JSON이나 XML 등의 형태로 Response하는 API 입니다.

Python에는 requests 모듈이 있어서 http url만 입력하면 reponse를 획득할 수 있습니다.


미세먼지 데이터 API

먼저 서울시 시간별 평균 대기오염도 정보 데이터 API를 호출하는 프로그램을 작성하였습니다.

URL에 특정일자를 넣어주면 사용자가 설정한 start_index에서 end_index까지 데이터를 불러오는 API 구조입니다.

list_total_count 라고 해당 호출의 최대 index 값을 제공하므로 아래의 절차로 프로그램을 구상하였습니다.


  1. 최초 호출에서 list_total_count를 획득한다.
  2. 다음 호출에서 end_index 값을 list_total_count까지 사용한다.
  3. 날짜별로 데이터를 CSV로 저장한다.

## callAPI.py
def call_api(api_name, start_date, end_date, dir_name):
    # API 키는 공개하기 힘든 점 양해 바랍니다.
    api_key = open("./raw_data/api_key").readlines()[0].strip()
    url_format = 'http://openAPI.seoul.go.kr:8088/{api_key}/json/{api_name}/1/{end_index}/{date}'
    headers = {'content-type': 'application/json;charset=utf-8'}

    for date in pd.date_range(start_date, end_date).strftime("%Y%m%d"):
        # 최초 1회 Call은 해당 일자의 데이터 수를 확인한다.
        url = url_format.format(api_name=api_name, api_key=api_key, end_index=1, date=date)
        response = requests.get(url, headers=headers)
        end_index = response.json()[api_name]["list_total_count"]
        print("Max Count(%s): %s" % (date, end_index))

        # 해당 일자의 모든 데이터를 불러온다.
        url = url_format.format(api_name=api_name, api_key=api_key, end_index=end_index, date=date)
        response = requests.get(url, headers=headers)
        result = pd.DataFrame(response.json()[api_name]["row"])

        # 수집된 데이터를 CSV로 저장합니다.
        result.to_csv("./raw_data/%s/dust_%s.csv" % (dir_name, date), index=False, encoding="utf-8")

        # API 부하 관리를 위해 0.5초 정도 쉬어 줍시다 (찡긋)
        sleep(0.5)





기상 데이터 API


다음은 기상데이터 API를 사용하는 함수를 작성하였습니다.

이 API는 조회 시작 일자와 시간 / 최종 일자와 시간, 가져올 데이터의 수를 Param으로 사용합니다.

안전하게 저는 1회 요청시 하루 단위로 넉넉하게 최대 100건 정도를 요청하였습니다.

매 시간 단위로 측정되는 데이터라 많아야 24건이기 때문이죠.

결과는 마찬가지로 CSV 파일로 저장하였습니다.


## callAPI.py
def call_weather_api(start_date, end_date):
    # API 키는 공개하기 힘든 점 양해 바랍니다.
    api_key = open("./raw_data/weather_api").readlines()[0].strip()
    url_format = 'https://data.kma.go.kr/apiData/getData?type=json&dataCd=ASOS&dateCd=HR&startDt={date}&startHh=00&endDt={date}&endHh=23&stnIds={snt_id}&schListCnt=100&pageIndex=1&apiKey={api_key}'

    headers = {'content-type': 'application/json;charset=utf-8'}
    for date in pd.date_range(start_date, end_date).strftime("%Y%m%d"):
        print("%s Weather" % date)
        url = url_format.format(api_key=api_key, date=date, snt_id="108")
        response = requests.get(url, headers=headers, verify=False)
        result = pd.DataFrame(response.json()[-1]["info"])
        print(result.head())
        result.to_csv("./raw_data/weather/weather_%s.csv" % date, index=False, encoding="utf-8")

        # API 부하 관리를 위해 0.5초 정도 쉬어 줍시다 (찡긋)
        sleep(0.5)



데이터 종합

미세먼지와 기상 데이터를 2009년 1월 1일부터 2019년 1월 1일 구간으로 수집하였습니다.

두 개의 데이터는 공통적으로 날짜 컬럼이 있기 때문에 날짜 컬럼을 기준으로 Join을 해주었습니다.

데이터의 양이 많아진 만큼 File I/O 속도가 느린 CSV 포멧이 아닌 HDF 포멧으로 변경하였습니다.


## callAPI.py
def concat_data():
    df_list = list()

    # ./raw_data/dust 아래의 모든 파일을 읽습니다.
    for root, dirs, files in os.walk("./raw_data/dust", topdown=False):
        for name in files:
            df_list.append(pd.read_csv(os.path.join(root, name)))

    dust = pd.DataFrame(pd.concat(df_list, sort=False))

    # Datetime 형태로 Index를 변경해줍니다.
    dust["MSRDT"] = dust["MSRDT"].apply(lambda x: dt.datetime.strptime(str(x), "%Y%m%d%H%M"))
    dust = dust.set_index("MSRDT")

    df_list.clear()

    # ./raw_data/weather 아래의 모든 파일을 읽습니다.
    for root, dirs, files in os.walk("./raw_data/weather", topdown=False):
        for name in files:
            df_list.append(pd.read_csv(os.path.join(root, name)))
    weather = pd.DataFrame(pd.concat(df_list, sort=False))

    # Datetime 형태로 Index를 변경해줍니다.
    weather["TM"] = weather["TM"].apply(lambda x: dt.datetime.strptime(x, "%Y-%m-%d %H:%M"))
    weather = weather.set_index("TM")

    # join() 함수는 같은 index 끼리의 join을 제공합니다.
    weather.join(dust, how="inner").to_hdf("./raw_data/data.hdf", "master")
    dust.to_hdf("./raw_data/data.hdf", "dust")
    weather.to_hdf("./raw_data/data.hdf", "weather")


제공받은 데이터에서 미세먼지에 해당하는 컬럼은 PM10 또는 PM25가 있습니다.

각각 10μm, 2.5μm 이하의 먼지의 농도라고 하는데 그 중에서 초미세먼지 농도에 해당하는 PM25를 사용하고자 한다.

데이터의 분포를 확인하고자 Box Plot을 그려서 확인하고자 한다.

이 때 지역별로 확인해 보는 것이 눈에 더 잘 보일거 같아서 확인해 보았는데..




아니 저 수치가 정말 나올 수 있는 수치인가요..??

서대문구는 무슨 일이 있었던 걸까요..??

노이즈라고 생각이 드는데 다음 포스팅에서 어느 정도 전처리를 하고 사용해야 할 거 같아요.

2021.01.06 : 기상청에서 관리하던 데이터를 공공데이터포털에서 관리하는 걸로 변경이 되었습니다.


긴 글 읽어 주셔서 감사합니다.

공감 버튼은 작성자에게 큰 동기부여가 됩니다.


,

Python2.x를 사용하여 구현하였습니다. 3.x버전을 사용하시는 분들은 참고하시길 바랍니다.


1.  k-NN 알고리즘

가장 가까운 이웃을 찾아라

k-NN 알고리즘은 기존의 데이터들은 각각의 라벨이 붙어 있고, 라벨이 없는 임의의 데이터 X가 과연 어떤 종류에 들어가는지 분류하는 알고리즘이다. 이때 X와 기존의 데이터들의 거리를 모두 구한 후 그중 가장 가까운 k 개의 데이터의 라벨들 중 가장 많은 수의 라벨이 X 데이터의 라벨로 결정하게 하는 알고리즘이다.


일반적으로 k-NN 알고리즘을 적용하기 위해서는 수치형 값을 사용하는 편인데, 임의의 데이터와 기존 데이간의 거리를 계산하기 위해서이다. 특별히 훈련하는 과정도 없는 특징도 바로 모든 기존의 데이터와 거리를 계산하는 특징으로 나타나는 것이다.


2.  NumPy로 구현


알고리즘의 구현 순서

1) 파일로부터 데이터를 수집 

2) 정규화(Normalization)

3) 가시화(Visualization)

4) 테스트 데이터와 모든 데이터 간의 거리 계산 후 가장 근접한 k개 데이터 선정후 다수결로 결정

   (k-NN 알고리즘)


1) 파일로부터 데이터 수집

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from numpy import *
import operator
 
def file2matrix(filename):
    # 데이터에 저장된 Iris의 종류는 총 3개입니다.
    # 각각을 숫자로 매핑한 Dictionary를 구현합니다.
    Iris_dict = {"Iris-setosa" : 1"Iris-versicolor" : 2"Iris-virginica" : 3}
    
    # 파일을 열고 라인 단위로 파일을 읽어 드립니다.
    fr = open(filename)
    TextLine = fr.readline()
    
    # numOfData  : 데이터의 수를 의미
    # featMatrix : 파일로 읽어드린 데이터를 행렬로 저장하기 위해 영행렬로 초기화합니다.
    #              이 때 Feacture의 수가 총 3개이므로 Data의 수 X 3의 행렬을 만듭니다.  
    numOfData = len(TextLine)
    featMatrix = zeros((numOfData, 3))
 
    # labelVector : dataMatrix의 각 행에 대응하는 lebel을 저장합니다.
    labelVector = []
    index = 0
    
    for line in TextLine:
        # 각각의 데이터는 ','으로 구분되어 있습니다.
        # 따라서 ','을 기준으로 분리하여 줍니다.
        splitData = line.split(',')    
        # 파일에 데이터가 "종류,특징1,특징2,특징3"으로 저장되어 있으므로
        # 두번째 열부터 끝까지가 Feature입니다. 
        featMatrix[index, : ] = splitData[1:]
        labelVector.append(Iris_dictionary.get(splitData[0]))
        index += 1
    
    return featMatrix, labelVector   
cs


2) 정규화

정규화를 하는 이유는 각각의 특징(Feature)들이 서로 다른 단위를 가진다는 점에서 시작합니다. 
만약 달리기를 하는 사람의 데이터가 아래와 같이 있다고 가정하여 봅시다

Feature 1 : 지칠 때까지 최대한 달린 거리
Feature 2 : 100m 달리기 기록

Data 1 :  (10,300m, 13.40초, 장거리 선수)
Data 2 :  (5,000m, 11.30초, 단거리 선수)

Data 1 ~ Data의 거리 :

     


kNN은 Feature 간의 거리를 계산하는 알고리즘입니다. Data 1 ~ Data 2간의 거리 값에서 가장 큰 비중을

차지하는 것은 Feature 1이 되어 버리겠죠. 왜냐면 Feature를 구성하는 단위의 크기가 어마하게 차이가

나기 때문입니다. 100m 달리기 기록을 아무리 단축하여도 거리에는 큰 영향을 못 준다.


데이터를 분석하는 사람의 판단으로 Feature 1이나 Feature 2나 동일하게 중요하다고 판단을 내리는 경우

위의 같은 사례는 썩 좋아하는 상황이 아닙니다. 따라서 데이터를 일정 범위로 줄여 버리는 과정인 정규화 과정을 거칩니다.


정규화


분모에는 각 Feature의 범위가 들어가고 분자에는 data Set에 저장되어있는 Feature 값과 최소값의 차이값이 들어갑니다. 이 식을 활용해서 파이썬으로 마찬가지로 구현하면 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 위의 함수에서 이어서 사용합니다.
def normarlization(dataSet):
    # dataSet 중의 최소 값과 최대 값을 구합니다
    # 그리고 두 값의 차이로 범위를 구해줍니다.
    minVal = dataSet.min(0)
    maxVal = dataSet.max(0)
    ranges = maxVal - minVal
 
    # 정규화 결과를 저장하는 행렬을 준비합니다.
    normalDataSet = zeros(shape(dataSet))
    
    # dataSet의 행의 갯수를 저장합니다.
    rowCnt = dataSet.shape[0]
 
    # 정규화 공식에 맞게 적용하였습니다.
    normalDataSet =(dataSet - tile(minVal, (rowCnt, 1))) / tile(ranges, (rowCnt, 1)) 
    
    return normalDataSet
cs


3) 가시화

보기에도 좋은 떡이 맛도 좋다는 말이 있죠? 데이터도 숫자로 되어 있으면 뭐가 뭔지 의미를 쉽게 알 수 없을
겁니다. 파이썬에는 matplotlib라는 좋은 그래프 그리는 라이브러리가 있으니까 그것을 적극적으로 활용해 봅시다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 위의 함수에서 이어서 사용합니다.
def visualizeData(dataSet, labels):
    import matplotlib
    matplotlib.use('Agg')
 
    import matplotlib.pyplot as plt
 
    fig, ax = plt.subplots(1,1)
    ax.scatter(dataSet[:,0], dataSet[ :, 1], 15.0*array(labels), 15.0 * array(labels))
    fig.savefig('plot1.svg')
    
    fig, ax = plt.subplots(1,1)
    ax.scatter(dataSet[:,0], dataSet[ :, 2], 15.0*array(labels), 15.0 * array(labels))
    fig.savefig('plot2.svg')
    
    fig, ax = plt.subplots(1,1)
    ax.scatter(dataSet[:,1], dataSet[ :, 2], 15.0*array(labels), 15.0 * array(labels))
    fig.savefig('plot3.svg')
 
cs


짜잔.. 숫자로 봤을 때 보다 훨씬 좋죠?


4) k-NN 알고리즘

이제 마지막으로 k-NN 분류기를 구현해보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 위의 함수에서 이어서 사용합니다.
def classify0(testDataSet, trainDataSet, labels, k):
    trainDataSetSize = trainDataSet.shape[0]
    
    # 테스트 데이터와 훈련 데이터간의 거리를 계산하는 과정입니다.
    diffMat = tile(testDataSet, (trainDataSetSize, 1)) - trainDataSet 
    sqDiffMat = diffMat ** 2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances ** 0.5
    
    # 오름차순으로 argsort를 합니다.
    # 거리가 짧은 순서대로 정렬이 됩니다.
    sortedDistIndex = distances.argsort()     
    
    # 다수결의 결과를 저장하는 곳입니다.
    classCount={}
    
    for i in range(k):
        # 본격 개표 방송
        # 오름차순으로 정렬한 데이터 중 k번째 데이터까지 라벨을 확인한 후
        # 투표 결과를 저장합니다.
        votelabel = labels[sortedDistIndex[i]]
        classCount[votelabel] = classCount.get(votelabel,0+ 1
    
    # 내림차순으로 정렬합니다
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    
    return sortedClassCount[0][0]   
 
cs


3.  마치면서..

k-NN은 단순하지만 데이터를 분류하는데 효과적인 알고리즘입니다. 하지만 데이터를 전부다 순회하면서

거리를 측정해야 하기 때문에 대규모의 데이터에 적용할 때는 시간이 오래걸리는 단점이 있습니다.

앞으로 여러 가지 알고리즘을 공부해서 포스팅 하겠습니다.


정말 긴 글 읽어 주셔서 감사합니다. 밑에 하트 한 번만 눌러주시면 저에게 큰 힘이 됩니다 ㅠㅠ



,