2020/11/21 - [Python/Community Manager를 위한 Python] - 4. 저장된 txt 파일은 어떻게 정제해야 할까? 2편

위 링크인 이전 글에 이어서 이번 정제를 마무리를 짓도록 한다. 일단 전체 소스를 이야기 하기 전에 중요 로직을 확인해본다.

카카오톡 채팅 데이터는 line by line으로 한 메시지에 여러 문장을 작성할 경우 짤리기 때문에 같은 메시지라고 인식하고 이 메시지를 붙여주는 것이 중요하다. 먼저 1편에서 확인한 내용은 아래와 같다.

fig1.txt 파일 읽은 후 리스트로 변환한 결과 

 

이제 아래처럼 바꿔 줘야 한다. 물론 아래는 각 채팅날짜별로 돌기 때문에 for loop를 이용하여 모든 날짜에 적용할 것이다. 그리고 아래에서 작성하진 않았지만 각 리스트로 구성된 메시지를 문자열로 변환 작업도 필요하다. 

fig2. 특정 날짜에 발생한 채팅 메시지 리스트를 문자열로 변환한 결과

fig2 결과를 위해 사용한 함수는 아래와 같다. 채팅 날짜에 해당하는 전체 메시지를 작성자+작성시간에 맞춰 문자열로 변환 후 다시 리스트 객체에 담아준다. 크게 신경써야 하는 부분은 1) 메시지가 시작된 부분인가? 2) 이전 편에서 확인했던 관리자 행동이나 참여자의 인입인가? 이다.

def split_talk_by_user(context):
    whole_txt, merge_txt = [], []
    for index, element in enumerate(context):
        start_str = re.match(r'\[.*?\]\s\[[오전|오후].*?\]', element)
        if start_str:
            whole_txt.append(' '.join(merge_txt))
            merge_txt = []
            merge_txt.append(element)
            if index == len(context)-1:
                whole_txt.append(' '.join(merge_txt))
        else:
            is_contain = False
            for action in ACTIONS:
                if action in element:
                    is_contain = True
            if is_contain:
                if merge_txt:
                    whole_txt.append(' '.join(merge_txt))
                whole_txt.append(element)
                merge_txt = []
            else:
                merge_txt.append(element)
    return [e for e in whole_txt if e.strip()]

 

그래서 전체 소스 코드는 아래를 참고하길 바란다.

github.com/hyunkyungboo/kakaotalk_chat_analysis/blob/master/01_read_txt_and_data_preprocessing.py

 

hyunkyungboo/kakaotalk_chat_analysis

카카오톡 채팅방 분석하기. Contribute to hyunkyungboo/kakaotalk_chat_analysis development by creating an account on GitHub.

github.com

 

다음 편에서는 데이터를 탐색해볼 것이다.

2020/11/14 - [Python/Community Manager를 위한 Python] - 3. 저장된 txt 파일은 어떻게 정제해야 할까? 1편

위 링크인 이전 글에 이어서.. 같은 날에 발생한 채팅들을 어떻게 정제할 것인지 살펴보고 순서에 맞춰서 정제를 해볼 것이다. 그리고 추가적으로 생각해야 하는 것은 관리자의 행동과 참여자의 인입이다. 자세히 말하자면 오픈 채팅창을 이용하다 보면 광고가 들어오고, 그 광고를 방장이 가리기도 하고.. 사람을 내보내기도 한다. 그리고 사람들은 오픈 채팅장에 입장도 하고 나가기도 한다. 아 그리고 때때로 특정 게시글을 공지로 올릴 수도 있게 된다. 따라서 이 부분도 미리 플래그 값을 추가한다면 추후 채팅방 트렌드를 확인할 때 유용하게 사용될 것이다.

위 설명에 맞춘 정제 포인트는 아래와 같다.

  • 기준일 추출
  • 작성자, 작성 시간, 메시지 추출
  • 공지글 확인하기
  • 관리자 행동, 참여자 인입 확인
  • 공지글, 삭제 메시지, 샵 검색, 이모티콘, 사진 확인

 

 

1. 기준일 추출

따로 정규식은 쓰지 않을 정도로 기준일 추출은 아주 쉽다. 기준일 양옆에 붙은 기호(-)만 제거해주면 된다. 

import datetime

def get_date(line):
    date_sep = '---------------'
    full_date = line.replace(date_sep, '').strip()
    talk_date, day_name = full_date[:-4], full_date[-3:]
    talk_date = datetime.datetime.strptime(talk_date, '%Y년 %m월 %d일')
    talk_date = datetime.datetime.strftime(talk_date, '%Y-%m-%d')
    return talk_date, day_name
       
line = "--------------- 2020년 11월 1일 일요일 ---------------"
get_date(line)
('2020-11-01', '일요일')

 

2. 작성자, 작성 시간, 메시지 추출

한 메시지에서 작성자, 작성 시간, 메시지 등 3개의 요소를 추출해야 하므로 다소 어려울 수 있다. 밑에 코드를 한 번 살펴보도록 한다. 

import datetime
import re

# 작성자, 작성 시간, 메시지 추출
def get_writer_and_wrote_at_and_msg(line):
    def convert_time(ko_wrote_at):
        import time

        if '오전' in ko_wrote_at:
            time_str = ko_wrote_at.replace('오전', 'am')
        elif '오후' in ko_wrote_at:
            time_str = ko_wrote_at.replace('오후', 'pm')

        time_str = datetime.datetime.strptime(time_str, '%p %I:%M')
        time_str = datetime.datetime.strftime(time_str, '%H:%M')
        return time_str
    
    split_line = re.split('\]|\[', line)
    split_line = [e for e in split_line if e.strip()]
    writer, wrote_at = split_line[0], convert_time(split_line[1])
    msg = line.split(split_line[1]+']')[1].strip()
    msg = msg if msg else None
    return writer, wrote_at, msg

line = "[포도] [오전 11:16] 안녕하세요!"
get_writer_and_wrote_at_and_msg(line)
('포도', '11:16', '안녕하세요!')

한 줄은 작성자와 작성 시간을 대괄호로 감싸고 있고 나머지는 메시지 부분으로 구성되므로 먼저, 대괄호 ([, ])를 기준으로 split()를 이용해 문자열을 분리해준다. 물론 이때 각 요소에 양쪽 공백도 없애준다. (19번째 행)

그러면 리스트에 [작성자, 작성 시간, 메시지]로 담기는데 이때 작성 시간은 오전 11:16의 형태로 오전/오후가 붙고 시간이 붙는다.  오전/오후 00 ~~ 12 표현을 00 ~ 24 표현으로 바꾸기 위해 convert_time() 함수를 만들어 주었다. 오전, 오후를 각각 am, pm으로 바꾸고 datetime 패키지를 활용하여 변경해주었다. 만약 오후였다면 어떻게 나오는지 아래 결과를 살펴보자.

line = "[BB] [오후 08:16] helloworld!"
get_writer_and_wrote_at_and_msg(line)
('BB', '20:16', 'helloworld!')

 아무래도 결과가 잘 나온것 같다. :D 

 

3. 관리자 행동, 참여자 인입 확인

이 부분은 생각해봐야 할 것이 꽤 있다. 여기서 정의하는 관리자의 행동은 1) 관리자가 메시지를 가리기 할 때 2) 참여자를 내보낼 때이고 참여자의 인입은 1) 참여자가 나갈 때, 2) 참여자가 들어올 때이다. 이때 카카오톡 메시지에는 다음처럼 나온다.

  • 채팅방 관리자가 메시지를 가렸습니다.
  • ~ 님을 내보냈습니다.
  • ~ 님이 나갔습니다.
  • ~ 님이 들어왔습니다.
import re

ACTIONS = ["채팅방 관리자가 메시지를 가렸습니다.", 
           "님을 내보냈습니다.",
           "님이 나갔습니다.",
           "님이 들어왔습니다."]

# 관리자, 참여자 행동 확인
def get_actions(line):
    global action_msg
    writer = '관리자'
    if ACTIONS[0] in line:
        action_msg = '게시글 공지'
    elif ACTIONS[1] in line:
        action_msg = '메시지 가리기'
    elif ACTIONS[2] in line:
        action_msg = '내보내기'
    elif ACTIONS[3] in line:
        writer, action_msg = line.replace(ACTIONS[3], ''), '나가기'
    elif ACTIONS[4] in line:
        writer, action_msg = line.replace(ACTIONS[4], ''), '들어오기'
    return writer, action_msg


msg_list = ["채팅방 관리자가 메시지를 가렸습니다.",
            "방장아니고반장님을 내보냈습니다.",
            "위너님이 들어왔습니다.",
            "위너님이 나갔습니다."]

for msg in msg_list:
    print(get_actions(msg))
('관리자', '메시지 가리기')
('관리자', '내보내기')
('위너', '들어오기')
('위너', '나가기')

 

4. 공지글 확인하기

이번에는 특정 값의 포함관계를 regex를 활용하여 확인해본다. 특정 게시글을 공지하게 되면 자동적으로 톡게시판 '공지': 라는 머리말이 붙고 이 부분이 있는지 re.match() 함수를 이용해 확인하였다,

def check_notice(text):
    start_str = re.match(r"톡게시판 '공지': ", text)
    is_notice_action = False
    if start_str:
        is_notice_action = True
    return is_notice_action
     
text = "톡게시판 '공지': 오늘도 평안하세요!"
check_notice(text)
True

 

5. 1~4 적용한 결과 도출하기

1,2는 작성시간이 존재하고 3은 작성 시간이 존재하지 않으므로 작성 시간 부분을 나눠 함수를 목적에 맞게 적용할 수 있다. 4번은 2번에서 추출된 메시지을 입력값으로 이용한다. 또한, start_regex에서 [ + (오전이나 오후)가 들어가는지 확인한 후 그다음에 각 목적에 맞춰 결과를 적용한다. 이때, is_talking_activity, is_notice_action을 추가하는데 3번에서 action_msg값이 None이 아니라면 is_talking_activity가 False이고 is_notice_action 또한, 4번 반환 값에 따라 결정될 것이다.  

def check_logic(content):
    content = content.strip()
    start_regex = r'\[.*?\]\s\[[오전|오후].*?\]'
    start_str = re.match(start_regex, content)
    is_talking_activity, is_notice_action = True, False
    wrote_at, action_msg = None, None
    if start_str:
        writer, wrote_at, msg = get_writer_and_wrote_at_and_msg(content)
        is_notice_action = check_notice(msg)
    else:
        writer, action_msg = get_actions(content)
        msg,is_talking_activity = content, False

    msg = re.sub(r'\s+', ' ', msg).strip()
    print(f"writer: {writer}, wrote_at: {wrote_at}, msg: {msg}")
    print(f"action_msg: {action_msg}, is_talking_activity: {is_talking_activity}, is_notice_action: {is_notice_action}")
    
content1 = '[hk] [오후 11:22] 심심해서 입사했다고 하던데..'
check_logic(content1)

content2 = 'hk님이 나갔습니다.'
check_logic(content2)
writer: hk, wrote_at: 23:22, msg: 심심해서 입사했다고 하던데..
action_msg: None, is_talking_activity: True, is_notice_action: False
writer: 고참참치님이 나갔습니다., wrote_at: None, msg: 고참참치님이 나갔습니다.
action_msg: 나가기, is_talking_activity: False, is_notice_action: False

 

6. 공지글, 삭제 메시지, 샵 검색, 이모티콘, 사진 확인

이 부분은 사후 처리로 추출된 메시지에서 확인을 한다. 다소 1~5 전개방식과는 다르지만 먼저 예제를 통해 내용을 살펴보자. 사실 1~5번 과정 전에 고려해야 할 부분이 있고, 이 결과를 데이터 프레임에서 람다식의 입력값으로 활용하여 각 목젝에 맞는 플래그 값을 줄 수 있다. 이 대, 데이터 프레임의 행으로 쌓을 것이다. 다음을 같이 확인해보자.

import pandas as pd

df = pd.DataFrame({'msg': ['이모티콘', '사진', '삭제된 메시지입니다.', '샵검색: #']})            
df['is_deleted_msg'] = df['msg'].apply(lambda x: True if x.strip() == '삭제된 메시지입니다.' else False)
df['is_emoji'] = df['msg'].apply(lambda x: True if x.strip() == '이모티콘' else False) 
df['is_picture'] = df['msg'].apply(lambda x: True if x.strip() == '사진' else False)
df['is_deleted_msg'] = df['msg'].apply(lambda x: True if x.strip() == '삭제된 메시지입니다.' else False)
df['is_search'] = df['msg'].apply(lambda x: True if x.startswith('샵검색: #') else False)
	msg	is_deleted_msg	is_emoji	is_picture	is_search
0	이모티콘	False	True	False	False
1	사진	False	False	True	False
2	삭제된 메시지입니다.	True	False	False	False
3	샵검색: #	False	False	False	True

람다식이 아직 어려울 수 있지만 하나만 기억하자. for문보다 간단하게 작성 가능함을!

 

다음 편에서 전체적으로 이용 가능한 소스를 소개하고 정제는 마무리 짓도록 하겠다. :D

먼저 저장된 txt 파일은 아래처럼 구성된다.

내보내기 된 카카오톡 오픈 채팅방.txt 내용 1
내보내기 된 카카오톡 오픈 채팅방.txt 내용 2

위 설명에 맞춘 정제 포인트는 아래와 같다.

  • 기본 헤더 제거
  • 일별 구분
  • 첫 입장 메시지 제거
  • 작성자/기준시간/작성 메시지 분리하기
  • 입장, 퇴장 확인하기

 

1. 정제 전 채팅 내용 데이터(.txt) 읽기

input_file_name = 'kakao_chat.txt'  # 텍스트 데이터 경로/이름 입력
with open(input_file_name, "r", encoding="utf-8-sig") as input_file:
    for line in input_file:
        line = line.strip()
        print(line)
데이터분석 QnA&네트워킹 님과 카카오톡 대화
저장한 날짜 : 2020-11-14 14:56:45

--------------- 2020년 11월 1일 일요일 ---------------
hk님이 들어왔습니다.운영정책을 위반한 메시지로 신고 접수 시 카카오톡 이용에 제한이 있을 수 있습니다.
[팬다 Jr.] [오전 10:27] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분
포도님이 들어왔습니다.
[팬다 Jr.] [오전 11:16] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분
[포도] [오전 11:16] 안녕하세요!
모모님이 들어왔습니다.

위 블록은 코드와 결과 일부 담고 있다. 그다음에 우선적으로 해야 하는 것은 기본 헤더 제거와 일자별 내용을 묶는 것이다.

2. 기본 헤더 제외한 일자별 내용 리스트로 담기

date_sep = '일 ---------------'
context_list, tmp_context_list = [], []
with open(input_file_name, "r", encoding="utf-8-sig") as input_file:
    index = 0
    for line in input_file:
        line = line.strip()
        start_index = 4
        if index == start_index:
            tmp_context_list.append(line)
        else:
            if line.endswith(date_sep):
                context_list.append(tmp_context_list)
                tmp_context_list = []
            tmp_context_list.append(line)
        index += 1
print("주어진 텍스트 파일 {}의 일자별 context는 {}건입니다.".format(input_file_name, len(context_list)))
주어진 텍스트 파일 kakao_chat.txt의 일자별 context는 11건입니다.

context_list의 내용은 아래와 같다. (일부)

[['데이터분석 QnA&네트워킹 님과 카카오톡 대화', '저장한 날짜 : 2020-11-14 14:56:45', ''],
 ['--------------- 2020년 11월 1일 일요일 ---------------',
  'hk님이 들어왔습니다.운영정책을 위반한 메시지로 신고 접수 시 카카오톡 이용에 제한이 있을 수 있습니다.',
  '[팬다 Jr.] [오전 10:27] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '포도님이 들어왔습니다.',
  '[팬다 Jr.] [오전 11:16] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '[포도] [오전 11:16] 안녕하세요!', (중략)

1번에 비해 다소 어려워진 것 같지만 때에 따라 어려워졌을 수도 있다... ㄴㅇㄱ

전반적인 코드 아래처럼 구성되어 있다.

  • 전체 내용을 담을 리스트, 일자별 내용을 담을 리스트 (각 context_list, tmp_context_list)
  • 일자별 구분을 위해 사용되는 date_sep과 .endwith()
  • 파일을 한 줄씩 읽는 for문, 그 한 줄에 대한 index

기본 헤더는 index 값이 0 ~ 3에 해당하므로 기준인을 index가 4로 시작된다. 그런데 기준일이 계속 늘어나기 때문에 기준일의 패턴인 [일 ---------------]을 활용하여 기준일 별 발생한 채팅 내용을 묶어줘야 한다. 그런데 첫 입장 메시지가 거슬리니 다음에서 첫 입장 메시지를 제거해본다. (단, 텍스트에 없는 경우가 있을 수 있다. 따라서 이 경우 다음을 넘어가도 된다.)

 3. 첫 입장 메시지 제거하기

if ~ continue를 활용하여 첫 입장 메시지, 즉 본문에 pass_msg에 해당하면 넘어가도록 한다.
(pass와는 달리 continue는 if ~ continue 이후 코드를 실행하지 않게 해 준다.)

 

date_sep = '일 ---------------'
pass_msg = '운영정책을 위반한 메시지로 신고 접수 시 카카오톡 이용에 제한이 있을 수 있습니다'
context_list, tmp_contenxt_list = [], []
with open(input_file_name, "r", encoding="utf-8-sig") as input_file:
    index = 0
    for line in input_file:
        line = line.strip()
        # 새로 추가한 부분 시작
        if pass_msg in line:
          continue 
        # 새로 추가한 부분 끝
        start_index = 4
        if index == start_index:
            tmp_contenxt_list.append(line)
        else:
            if line.endswith(date_sep):
                context_list.append(tmp_contenxt_list)
                tmp_contenxt_list = []
            tmp_contenxt_list.append(line)
        index += 1
print("주어진 텍스트 파일 {}의 일자별 context는 {}건입니다.".format(input_file_name, len(context_list)))
[['데이터분석 QnA&네트워킹 님과 카카오톡 대화', '저장한 날짜 : 2020-11-14 14:56:45', ''],
 ['--------------- 2020년 11월 1일 일요일 ---------------',
  '[팬다 Jr.] [오전 10:27] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '포도님이 들어왔습니다.',
  '[팬다 Jr.] [오전 11:16] 안녕하세요, 반갑습니다. 데분방입니다. 데분데분',
  '[포도] [오전 11:16] 안녕하세요!', (중략)

 

어느 정도 구조화 가능한 데이터를 확보했으므로 이제 본격적으로 다음 편에서 pandas를 활용해 데이터 정제 과정을 공유하고자 한다.

* 채팅방 우측 상단 더보기 버튼 -> 대화내용 -> 대화 내보내기 클릭 후 파일 저장 

카카오톡 오픈 채팅방을 운영을 하고 있는 분들이라면 카카오톡 채팅 내용에 대해 궁금해 하지 않을까? 라는 생각이 들었고 마침 학교 과제와도 겹치는 부분이 있어 과제 결과 도출까지 과정을 공유하고자 한다. :D

큰 틀의 목차는 아래와 같다.

  1. 카카오톡 대화는 어떻게 이쁘게 만들까?
  2. 추가적으로 도출가능한 데이터는 무엇이지?
  3. 이 데이터로 무엇을 확인해볼까?

다만 글 작성시에는 조금 더 상세한 목차로 진행하려고 한다.

 

 

 

항상 konlpy를 설치할 때면 오류가 난다..... 오늘은 새로 만난 오류이다.. 띠요옹... 먼저, Anaconda에서 가상 환경을 생성하였고 생성한 가상 환경에 konlpy 설치 시도를 하였다.

(gensim_packs) C:\Users\LG>pip install konlpy

그리고 발생한 오류. 해시값이 일치하지 않는다고....

Collecting konlpy
  Downloading konlpy-0.5.2-py2.py3-none-any.whl (19.4 MB)
     |████████████████████████████████| 19.4 MB 819 kB/s eta 0:00:01
ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated the package versions, please update the hashes. Otherwise, examine the package contents carefully; someone may have tampered with them.
    konlpy from https://files.pythonhosted.org/packages/85/0e/f385566fec837c0b83f216b2da65db9997b35dd675e107752005b7d392b1/konlpy-0.5.2-py2.py3-none-any.whl#sha256=65ab33ad28fb072306e44128ff3f23b5771d97d6501fa995511834ba8dd2bd40:
        Expected sha256 65ab33ad28fb072306e44128ff3f23b5771d97d6501fa995511834ba8dd2bd40
             Got        47430be567ed6661ac89c05c43f6d4a6ad61b9f6cead197ba92e083af906dd83

해결법은 jpype1를 먼저 설치해주는 것이다. (설치 결과 생략)

(gensim_packs) C:\Users\LG>conda install -c conda-forge jpype1
Collecting package metadata (current_repodata.json): done
Solving environment: done

다시 konlpy를 설치해준다.

(gensim_packs) C:\Users\LG>pip install konlpy
Collecting konlpy
  Downloading konlpy-0.5.2-py2.py3-none-any.whl (19.4 MB)
     |████████████████████████████████| 19.4 MB 327 kB/s
Collecting tweepy>=3.7.0
  Downloading tweepy-3.8.0-py2.py3-none-any.whl (28 kB)
Collecting beautifulsoup4==4.6.0
  Downloading beautifulsoup4-4.6.0-py3-none-any.whl (86 kB)
     |████████████████████████████████| 86 kB 578 kB/s
Collecting colorama
  Downloading colorama-0.4.3-py2.py3-none-any.whl (15 kB)
Requirement already satisfied: JPype1>=0.7.0 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from konlpy) (0.7.5)
Requirement already satisfied: numpy>=1.6 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from konlpy) (1.18.5)
Collecting lxml>=4.1.0
  Downloading lxml-4.5.1-cp37-cp37m-win_amd64.whl (3.5 MB)
     |████████████████████████████████| 3.5 MB 386 kB/s
Requirement already satisfied: PySocks>=1.5.7 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from tweepy>=3.7.0->konlpy) (1.7.1)
Requirement already satisfied: requests>=2.11.1 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from tweepy>=3.7.0->konlpy) (2.24.0)
Collecting requests-oauthlib>=0.7.0
  Downloading requests_oauthlib-1.3.0-py2.py3-none-any.whl (23 kB)
Requirement already satisfied: six>=1.10.0 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from tweepy>=3.7.0->konlpy) (1.15.0)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from requests>=2.11.1->tweepy>=3.7.0->konlpy) (2020.6.20)
Requirement already satisfied: chardet<4,>=3.0.2 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from requests>=2.11.1->tweepy>=3.7.0->konlpy) (3.0.4)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from requests>=2.11.1->tweepy>=3.7.0->konlpy) (1.25.9)
Requirement already satisfied: idna<3,>=2.5 in c:\users\lg\anaconda3\envs\gensim_packs\lib\site-packages (from requests>=2.11.1->tweepy>=3.7.0->konlpy) (2.10)
Collecting oauthlib>=3.0.0
  Downloading oauthlib-3.1.0-py2.py3-none-any.whl (147 kB)
     |████████████████████████████████| 147 kB 409 kB/s
Installing collected packages: oauthlib, requests-oauthlib, tweepy, beautifulsoup4, colorama, lxml, konlpy
Successfully installed beautifulsoup4-4.6.0 colorama-0.4.3 konlpy-0.5.2 lxml-4.5.1 oauthlib-3.1.0 requests-oauthlib-1.3.0 tweepy-3.8.0

 

마지막으로 잘 설치가 되었는지 확인하기!

(gensim_packs) C:\Users\LG>python
Python 3.7.7 (default, May  6 2020, 11:45:54) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from konlpy.tag import Kkma
>>> from konlpy.utils import pprint
>>> kkma = Kkma()
>>> pprint(kkma.sentences(u'네, 안녕하세요. 반갑습니다.'))
['네, 안녕하세요.', '반갑습니다.']

'Python > 기본' 카테고리의 다른 글

[ERROR] Microsoft Visual C++ 14.0 is required.  (0) 2020.05.30
아래 링크를 따라 해 보았습니다.
>> medium.com/@gscheithauer/process-mining-in-10-minutes-with-r-1ab28ed74e81

 

1. 들어가기 전에

프로세스 마이닝은 프로세스 분석을 하는 것이고 워크숍, 인터뷰, 과거 문서들이 아닌 비즈니스 시스템에서 도출된 데이터를 기반으로 프로세스 마이닝을 적용시킬 수도 있죠! 프로세스 모델을 생성하고 이 모델을 활용해 프로세스 준수 문제를 발견할 수도 있습니다. 현재 프로세스 마이닝을 할 수 있는 많은 도구가 있지만 그중 하나인 오픈 소스 도구인 buparR(여러 R 패키지로 구성됨)을 이용합니다.

2. 데이터 소개 및 준비하기

이 글에서는 BPI Challenge 2017 [5]에서 제공 한 은행 신용 응용 프로그램 프로세스의 실제 데이터(익명)를 사용합니다. 분석 전 아래 깃 레포지토리를 다운로드합니다.
>> Clone GitHub project: https://github.com/scheithauer/processmining-bupaR

 

scheithauer/processmining-bupaR

Process Mining with bupaR. Contribute to scheithauer/processmining-bupaR development by creating an account on GitHub.

github.com

3. 분석

분석이지만 남의 코드 뜯어보기!! (다운로드한 레포에서 01-scripts 폴더의 00_pm_bupar_MAIN.R을 열어주세요!)

3.1 패키지 설치하기

이 분은 벡터 표시 (c)를 앞에 붙여주는군요! 추가적으로 namespace 'htmltools' 0.4.0 is being loaded, but >= 0.4.0.9003 is required 에러가 난다면.. 업데이트를 해도 똑같다면 그냥.. R을 지우고 새로 설치하세요.. 4 버전으로... 그럼 해결이 됩니다.. 

# check installed packages and install only necessary ones #### 
c_necessary_packages <- c( 
  'bupaR', 
  'edeaR', 
  'processmapR', 
  'eventdataR', 
  'readr', 
  'tidyverse', 
  'DiagrammeR', 
  'ggplot2', 
  'stringr', 
  'lubridate'   
) 
c_missing_packages <- c_necessary_packages[!(c_necessary_packages %in% installed.packages()[,"Package"])] 
if(length(c_missing_packages) > 0) install.packages(c_missing_packages)

3.2 패키지 불러오기

자 패키지를 설치했으면 설치한 패키지들을 불러야겠죠?

library(bupaR)
library(edeaR)
library(processmapR)
library(eventdataR)
library(readr)
library(tidyverse)
library(DiagrammeR)
library(ggplot2)
library(stringr)
library(lubridate)

3.3 데이터 불러오기

자 이제 데이터들도 읽어봅시다.

# load BPI Challenge 2017 data set ####
data <- readr::read_csv('./00-data/loanapplicationfile.csv',
                         locale = locale(date_names = 'en',
                                         encoding = 'ISO-8859-1'))

# change timestamp to date var
data$starttimestamp = as.POSIXct(data$`Start_Timestamp`, 
                                 format = "%Y/%m/%d %H:%M:%S")

data$endtimestamp = as.POSIXct(data$`Complete_Timestamp`, 
                               format = "%Y/%m/%d %H:%M:%S")

# remove blanks from var names
names(data) <- str_replace_all(names(data), c(" " = "_" , "," = "" ))

3.4 이벤트 로그로 변경하기

프로세스 마이닝을 적용하기 위해 데이터를 이벤트 로그로 변환합니다.

# transform data into eventlog
events <- bupaR::activities_to_eventlog(
  data,
  case_id = 'Case_ID',
  activity_id = 'Activity',
  resource_id = 'Resource',
  timestamps = c('starttimestamp', 'endtimestamp')
)

참고: 이벤트 로그 변환 관련 bupaR

3.5 데이터 탐색하기

# statistics eventlog ####
events %>% 
  summary

events %>% 
  activity_frequency(level = "activity") 

events %>% 
  activity_frequency(level = "activity") %>% 
  plot()


# filter all cases where one specific activity was present
events %>% 
  filter_activity_presence(activities = c('A_Cancelled')) %>% 
  activity_frequency(level = "activity") 

 

3.6 프로세스 마이닝

# process map ####
events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  process_map(render = F) %>% 
  export_graph(file_name = './02-output/01_pm-bupar_process map.png',
               file_type = 'PNG')


# process map - performance ####
events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  process_map(performance(mean, "mins"),
              render = F) %>% 
  export_graph(file_name = './02-output/02_pm-bupar_process map performance.png',
               file_type = 'PNG')


# precedent matrix ####
precedence_matrix <- events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  precedence_matrix() %>% 
  plot()

ggsave('./02-output/03_pm-bupar_process precedence matrix.png', precedence_matrix)
rm(precedence_matrix)


# trace explorer
trace_explorer <- events %>%
  trace_explorer(coverage = 0.5)

ggsave('./02-output/04_pm-bupar_trace explorer.png', trace_explorer, width = 12)
rm(trace_explorer)

# idotted chart
chart <- events %>%
  dotted_chart()

chart

# resource map ####
events %>%
  filter_activity_frequency(percentage = .1) %>% # show only most frequent resources
  filter_trace_frequency(percentage = .8) %>%    # show only the most frequent traces
  resource_map(render = F) %>% 
  export_graph(file_name = './02-output/05_pm-bupar_resource map.png',
               file_type = 'PNG')


# resource matrix ####
resource_matrix <- events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  resource_matrix() %>% 
  plot()

ggsave('./02-output/06_pm-bupar_resource matrix.png', resource_matrix)
rm(resource_matrix)


# process map where one activity was at least once present ####
events %>%
  filter_activity_presence(activities = c('A_Cancelled')) %>% 
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  process_map(render = F) %>% 
  export_graph(file_name = './02-output/07_pm-bupar_process map cancelled.png',
               file_type = 'PNG')


# process map where one activity was at least once present in Feb 2016 ####
events %>%
  filter_time_period(interval = c(ymd(20160101), end_point = ymd(20160102)),
                     filter_method = 'start') %>% 
  filter_activity_presence(activities = c('A_Cancelled')) %>% 
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  process_map(render = F) %>% 
  export_graph(file_name = './02-output/08_pm-bupar_process map cancelled time intervall.png',
               file_type = 'PNG')


# Conditional Process Analysis ####
events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  throughput_time('log', units = 'hours')

events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  throughput_time('case', units = 'hours')

  
events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  group_by(`(case)_ApplicationType`) %>% 
  throughput_time('log', units = 'hours')
  
plot <- events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  group_by(`(case)_ApplicationType`) %>% 
  throughput_time('log', units = 'hours') %>% 
  plot()

plot

ggsave('./02-output/08_pm-bupar_throughput application type.png', plot)
rm(plot)

events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  group_by(`(case)_LoanGoal`) %>% 
  throughput_time('log', units = 'hours')

plot <- events %>%
  filter_activity_frequency(percentage = 1.0) %>% # show only most frequent activities
  filter_trace_frequency(percentage = .80) %>%    # show only the most frequent traces
  group_by(`(case)_LoanGoal`) %>% 
  throughput_time('log', units = 'hours') %>% 
  plot()

plot

ggsave('./02-output/09_pm-bupar_throughput loan goal.png', plot)
rm(plot)

 

음.. 따라 해 보았지만 여전히 뭔 말인지 모르겠다. 다음부터는 bupaR 함수 하나하나 봐야겠다.

오랜만에 R을 써서 그런지 많은 함수와.. 사용법을 잊어버렸다.. 이것은 파이썬 덕분....... 따라서 이를 정리해본다.

1. 데이터 불러오기

먼저 readr 패키지로 csv 파일을 불러오자.

library(readr)

root_path <- 'C:\\Users\\LG\\Desktop\\data_set'
df <- read_csv(file.path(root_path, 'word.csv'))

출력 결과이다.

Parsed with column specification:
cols(
  id = col_double(),
  sex = col_double(),
  bday = col_date(format = ""),
  overall = col_double(),
  supervisor = col_double(),
  conditions = col_double(),
  colleagues = col_double(),
  workplace = col_double(),
  tasks = col_double()
)
> df
# A tibble: 50 x 9
      id   sex bday       overall supervisor conditions colleagues workplace tasks
   <dbl> <dbl> <date>       <dbl>      <dbl>      <dbl>      <dbl>     <dbl> <dbl>
 1     1     1 1999-08-04      65         47         53         74        49    54
 2     2     0 2000-01-16      58         79         73         72        41    46
 3     3     1 1999-07-16       0         32         10         25         9    30
 4     4     0 1999-09-03      80         53         71         67        52    37
 5     5     1 1999-05-22      56         52         21         31        50    46
 6     6     0 1999-10-19      93         92         45         80       100    77
 7     7     1 2000-02-05      45         57         44         55        19    47
 8     8     1 1998-08-01      61         63         77         81        63    49
 9     9     0 2000-01-09      68         63         49         76        39    99
10    10     0 1999-09-19      71         66         69         81        53    62
# ... with 40 more rows

2. 컬럼 추가

성별이 0일 경우 여성, 성별이 1일 경우 남성으로 나타낸 컬럼을 추가해본다.

먼저 dplyr 패키지를 설치한다.

install.packages("dplyr")

이제 데이터 프레임에 컬럼을 추가해보자. 컬럼 생성은 mutate() 함수를 사용한다. 함수 사용시 mutate(새로 생성할 컬럼명 = 값/조건)으로 입력하는데 이 때, 조건일 경우 조건 설정에 if_else, case_when 2가지 방법을 사용할 수 있다.

library(dplyr)

buff = 0
if(buff == 0) {
  new_df <- df %>%
    mutate(sex_desc = if_else(sex == 0, 'Female', 'Male'))
} else {
  new_df <- df %>%
    mutate(sex_desc = case_when(sex == 0 ~ 'Female',
                                TRUE ~ 'None'))
}
new_df

컬럼 추가 결과이다.

> new_df
# A tibble: 50 x 10
      id   sex bday       overall supervisor conditions colleagues workplace tasks sex_desc
   <dbl> <dbl> <date>       <dbl>      <dbl>      <dbl>      <dbl>     <dbl> <dbl> <chr>   
 1     1     1 1999-08-04      65         47         53         74        49    54 male    
 2     2     0 2000-01-16      58         79         73         72        41    46 Female  
 3     3     1 1999-07-16       0         32         10         25         9    30 male    
 4     4     0 1999-09-03      80         53         71         67        52    37 Female  
 5     5     1 1999-05-22      56         52         21         31        50    46 male    
 6     6     0 1999-10-19      93         92         45         80       100    77 Female  
 7     7     1 2000-02-05      45         57         44         55        19    47 male    
 8     8     1 1998-08-01      61         63         77         81        63    49 male    
 9     9     0 2000-01-09      68         63         49         76        39    99 Female  
10    10     0 1999-09-19      71         66         69         81        53    62 Female  

그런데 혹시라도 3, 4 등과 같은 이상치가 발생될 수 있으므로 조건을 정확히 설정하고 예외 숫자를 다르게 표현할 수 있게 하는 것을 추천한다. 아래는 case_when을 사용한 예제이다. (이 때, 0일 때는 여성, 1일 때는 남성, 나머지는 None으로 처리한다.)

new_df <- df %>%
  mutate(sex_desc = case_when(sex == 0 ~ 'Female',
                              sex == 1 ~ 'male',
                                TRUE ~ 'None'))

4. 컬럼 순서 재정렬하기

이제 새롭게 생성된 컬럼를 성별 바로 뒤로 올 수 있도록 컬럼 재정렬을 해본다.
직접 컬럼 벡터 사용, select()함수를 사용하는 2가지 방법으로 컬럼을 재정렬할 수 있다.
**위 2가지 방법 모두 컬럼명을 이용해 재졍렬 순서를 입력하면 된다.

# library(dplyr)
buff = 0
if(buff == 0) {
  new_columns_order = c('id', 'sex', 'sex_desc', 'bday',
                        'overall', 'supervisor', 'conditions',
                        'colleagues', 'workplace', 'tasks')
  new_df <- new_df[new_columns_order]
} else {
  new_df <- new_df %>%
    select(id, sex, sex_desc, bday,
           overall, supervisor, conditions,
           colleagues, workplace, tasks)
}
new_df

아래는 재정렬된 결과이다.

> new_df
# A tibble: 50 x 10
      id   sex sex_desc bday       overall supervisor conditions colleagues workplace tasks
   <dbl> <dbl> <chr>    <date>       <dbl>      <dbl>      <dbl>      <dbl>     <dbl> <dbl>
 1     1     1 male     1999-08-04      65         47         53         74        49    54
 2     2     0 Female   2000-01-16      58         79         73         72        41    46
 3     3     1 male     1999-07-16       0         32         10         25         9    30
 4     4     0 Female   1999-09-03      80         53         71         67        52    37
 5     5     1 male     1999-05-22      56         52         21         31        50    46
 6     6     0 Female   1999-10-19      93         92         45         80       100    77
 7     7     1 male     2000-02-05      45         57         44         55        19    47
 8     8     1 male     1998-08-01      61         63         77         81        63    49
 9     9     0 Female   2000-01-09      68         63         49         76        39    99
10    10     0 Female   1999-09-19      71         66         69         81        53    62

5. 데이터 저장하기

아래 코드는 저번 포스트와 동일

#library(readr)
write_csv(new_df, file.path(root_path, 'word_add_sex_desc.csv'))

'R > 기본' 카테고리의 다른 글

R로 .csv 형식 저장하기  (0) 2020.06.06
R로 .sav 파일 읽기  (0) 2020.06.06

+ Recent posts