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

'파이썬 데이터 마이닝'에 해당되는 글 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 : 기상청에서 관리하던 데이터를 공공데이터포털에서 관리하는 걸로 변경이 되었습니다.


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

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


,

- 자세한 소스코드는 Github에 올렸습니다.

- 데이터 파일도 올려 드리고 싶지만 실제 카카오톡 대화내용이라 일부분만 올렸습니다.

- 소스코드는 짬짬히 업로드할 예정입니다.


오래 전부터 시도를 해보고 싶었지만, 카카오톡 대화 데이터를 이용해서 워드 클라우드부터 Word2Vector 등등 텍스트 마이닝에서 사용하는 기법을 사용해 볼려고 합니다.

통계적 지식은 거의 전무하지만, 1년간 우연히 텍스트 마이닝 프로젝트를 하면서 어깨 너머 보고 배운 것을 기반으로 소소한 프로젝트를 만들어 보았습니다.


프로젝트 폴더 구조


1. 카카오톡 대화를 Text 파일로 저장합니다.

컴퓨터 카카오톡을 켜서 아무 대화 방에서 Ctrl + S를 누르면 대화를 텍스트 파일로 저장이 가능합니다.

저는 여자친구와 나눈 대화를 실험 재료로 사용하고자 합니다.



그리고 프로젝트의 raw_data 폴더에 넣어 둡니다.

저는 raw_data/kko.txt 파일로 저장해두었습니다.



2. 정규표현식

정규표현식은 찾고자하는 텍스트가 일정한 패턴을 가지고 있을 때, 해당 패턴을 표현하기 위해 사용하는 언어입니다.

가독성이 썩 좋지 않아서 처음에 진입하기 꺼려지지만, 알아두면 텍스트에서 원하는 부분만 가져오기 편합니다.

카카오톡에 저장된 내용을 보면 다음과 같은 패턴을 알 수 있습니다.



언젠가 사용하리라 생각하면서, 일자 정보누가 몇 시어떤 말을하는지까지 패턴을 이용하여 추출하고자 합니다.

위의 패턴을 정규표현식으로 표현하면 다음과 같습니다.



꼼꼼하게 날짜의 패턴에서 숫자의 갯수를 지정하거나, 시간 부분도 표현이 가능하지만, 정형화된 패턴이라 너프하게 표현했습니다.  

이제 Python으로 정규표현식을 적용해봅시다.


regex.py의 일부분

1
2
3
4
5
6
7
8
9
def apply_kko_regex(msg_list):
    kko_pattern = re.compile("\[([\S\s]+)\] \[(오전|오후) ([0-9:\s]+)\] ([^\n]+)")
    kko_date_pattern = re.compile("--------------- ([0-9]+년 [0-9]+월 [0-9]+일) ")
 
    emoji_pattern = re.compile("["u"\U0001F600-\U0001F64F"  # emoticons
                               u"\U0001F300-\U0001F5FF"  # symbols & pictographs
                               u"\U0001F680-\U0001F6FF"  # transport & map symbols
                               u"\U0001F1E0-\U0001F1FF"  # flags (iOS)
                               "]+", flags=re.UNICODE)
cs


파이썬의 re package으로 정규표현식을 컴파일하여 문자열에서 패턴 추출을 할 수 있도록 제공합니다.

카카오톡 대화 부분과 날짜 패턴을 생성합니다. 그리고 추가적으로 이모지 패턴을 넣어줍니다. 

(이모지 패턴은 StackOverflow를 참고하여 작성하였습니다.)

이모지 패턴은 이후에 사용할 형태소 분석기 꼬꼬마(Kkma)에서 이모지를 만나면 에러가 발생해서, 이모지를 제거하기 위한 목적으로 만들었습니다.

regex.py의 일부분

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
def apply_kko_regex(msg_list):    
 
    """ 상단 생략 """
 
    kko_parse_result = list()
    cur_date = ""
 
    for msg in msg_list:
        # 날짜 부분인 경우
        if len(kko_date_pattern.findall(msg)) > 0:
            cur_date = dt.datetime.strptime(kko_date_pattern.findall(msg)[0], "%Y년 %m월 %d일")
            cur_date = cur_date.strftime("%Y-%m-%d")
        else:
            kko_pattern_result = kko_pattern.findall(msg)
            if len(kko_pattern_result) > 0:
                tokens = list(kko_pattern_result[0])
                # 이모지 데이터 삭제
                tokens[-1= re.sub(emoji_pattern, "", tokens[-1])
                tokens.insert(0, cur_date)
                kko_parse_result.append(tokens)
 
    kko_parse_result = pd.DataFrame(kko_parse_result, columns=["Date""Speaker""timetype""time""contents"])
    kko_parse_result.to_csv("./result/kko_regex.csv", index=False)
 
    return kko_parse_result
cs

이후의 코드는 파일에서 읽은 메세지를 정규식을 이용하여 패턴을 추출하고 이모지의 경우 삭제하는 코드입니다.


line 14

문자열 Type str의 내장 함수 findall()은 정규식 패턴에 해당하는 모든 문자열을 추출해줍니다. 


line 18

특정 패턴을 가진 문자열을 치환하는 경우 re.sub()를 사용합니다.

re.sub(패턴, 치환 문자열, 대상 str) 

지금의 경우에는 모든 이모지 패턴의 문자를 메세지 텍스트에서 삭제하기 위해 사용하였습니다.


line 22-23

추출된 결과를 pandas.DataFrame에 저장하고 csv 파일로 저장합니다.





3. 한글 형태소 분석기 적용

우리가 글을 배울 때, 단어를 익히고 문장을 배우고 문장들을 이어서 문맥을 완성합니다. 

이처럼 텍스트를 분석할 때 처음부터 문장 전체를 사용하지 않습니다. 

작은 단위로 쪼개는 과정을 Tokenizing 라고 하는데, 형태소 단위까지 쪼개는 경우도 있고, 단어 중심으로 쪼개는 경우도 있습니다.


저는 한글 형태소 분석 모듈 KoNLPy를 이용하여 형태소 단위로 쪼개는 것을 선택하였습니다.

설치는 Anaconda Prompt에서 아래의 명령어를 입력하면 설치가 가능합니다.


>> pip install konlpy


KoNLPy에는 Hannanum, Kkma, Komoran 등의 형태소 분석 패키지를 제공하는데 저는 Kkma(꼬꼬마)를 사용했습니다.

특별한 이유는 없습니다.. 꼬꼬마라는 네이밍이 귀여워서 선택했을 뿐..


kkma_token.py의 일부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re
import pandas as pd
from konlpy.tag import Kkma
 
 
def get_noun(msg_txt):
    kkma = Kkma()
    nouns = list()
    # ㅋㅋ, ㅠㅠ, ㅎㅎ  
    pattern = re.compile("[ㄱ-ㅎㅏ-ㅣ]+")
    msg_txt = re.sub(pattern, "", msg_txt).strip()
 
    if len(msg_txt) > 0:
        pos = kkma.pos(msg_txt)
        for keyword, type in pos:
            # 고유명사 또는 보통명사
            if type == "NNG" or type == "NNP":
                nouns.append(keyword)
        print(msg_txt, "->", nouns)
 
    return nouns
cs


line 10 - 11
카카오톡 데이터로 분석하다보니 ㅋㅋ ㅎㅎ 등을 제거하는 로직을 추가하였습니다.
ㅋㅋ, ㅎㅎ, ㅜㅜ는 그래도 꼬꼬마에서 무난하게 이모티콘으로 해석해 줍니다.
하지만 "ㅁㄹ데ㅂ제더레ㅐ벚대ㅏ에"와 고양이가 타이핑한 게 들어가니까 성능이 매우 느려져서 제거하는 것을 추가 했습니다. 

line 14
물론 kkma.nouns() 라는 명사만 따로 추출하는 함수를 제공해주는데, 모든 조합 가능한 명사를 제공하는 건지 마음에 안들어서 kkma.pos()를 사용하여 문장에 속하는 것들의 품사를 분석한 뒤 그 중에 명사나 고유 명사에 해당하는 것만 추출했습니다.

결과물


카카오톡 특징상 맞춤법, 띄어쓰기가 전혀 안되있다보니 정확성은 떨어집니다.

서로간의 애칭이나 신조어가 들어가는 경우에 더욱 이상하게 분리를 하는데 이 경우에는 사전에 추가를 해주면 됩니다.



4. Kkma 사전에 키워드 추가

Anaconda 설치경로/Lib/site-packages/konlpy/java 경로로 이동합니다.

kkma-2.0.jar의 압축을 풀어 준 후 폴더로 이동합니다.

폴더 안에 dic 폴더에 사전들이 있습니다.


해당 사전에 마지막에 추가하고자 하는 내용을 넣어 줍니다.

명사만 저는 추가하였습니다.




이전 폴더로 이동해서 jar 파일로 다시 아카이브한 후 덮어쓰면 사전이 적용 됩니다.


카카오톡 대화 내용에 형태소 분석기를 적용하여 보았습니다.

다음 포스팅에서는 워드클라우드를 생성하는 것을 주제로 찾아 뵙겠습니다.


도움되셨다면 공감버튼 눌러주세요.

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



,