트랙 CC1180분

객체 중심 이벤트 데이터

2단계의 Orbit 로그는 업무 이슈 하나를 사례로 삼았다. 그런데 PR이 있는 업무 234건에 PR이 247개 걸려 있고, 13건은 PR이 두 개다. 리뷰 승인 사건 하나에는 리뷰어와 PR 작성자 두 사람이 걸린다. 업무를 사례로 삼으면 두 PR의 사건이 한 줄에 섞이고, PR을 사례로 바꾸면 업무 생성과 막힘이 로그에서 사라진다. 사건 하나가 여러 대상에 걸린 기록을 사례 하나로 줄이지 않고 담을 수 있는가. 이 모듈은 Orbit 로그를 객체 중심 이벤트 로그 표준인 OCEL 2.0 파일로 바꾸고, 객체 유형마다 흐름을 그려 사례 하나로 줄일 때 숫자가 어떻게 바뀌는지 잰다.

진행
설명 50분, 실습 110분, 정리 20분
결과
pm-lab/orbit-ocel.json, pm-lab/orbit-ocel.sqlite, 객체 유형별 흐름 보고(pm-lab/ocel-report.txt)
명령
ocel-export.py, ocel-notebook.py, PM4Py read_ocel2, ocel_flattening, discover_ocdfg
cd ~/gh-pm-lab
python3 -c "import pm4py, simpy, sklearn; print(pm4py.__version__, simpy.__version__, sklearn.__version__)"

세 패키지가 없으면 트랙 C 안내의 설치 명령을 먼저 실행한다. 이 모듈의 ocel-export.py는 Python 표준 라이브러리만 쓰고, ocel-notebook.py는 PM4Py 2.7.23.8을 쓴다. PM4Py를 불러오면 표준 오류로 라이선스 안내문이 먼저 나온다. 아래 출력에서는 그 안내문을 뺐다.

1사례 하나로 줄일 때 생기는 일

업무마다 PR이 몇 개인지 센다.

PR이 하나인 업무가 221건, 둘인 업무가 13건이고, 나머지 23건은 PR이 없는 QA 업무다. OB-005는 3월 12일에 PR #1005로 끝났다가 3월 17일에 다시 열려 PR #2005로 다시 끝났다. 업무를 사례로 삼은 로그에서 OB-005의 사건 13개는 한 줄로 이어지고, “PR 생성”과 “리뷰 승인”이 두 번씩 나온다. 두 리뷰는 서로 다른 PR에 대한 것이고 리뷰어도 다르지만(dev-a, dev-b), 사례 하나 안에서는 같은 활동의 반복으로만 보인다.

판 데르 알스트(W. M. P. van der Aalst)는 2019년 논문 “Object-Centric Process Mining: Dealing with Divergence and Convergence in Event Data”(SEFM 2019)에서 사례 하나를 고를 때 생기는 문제를 두 가지로 정리했다.

문제뜻GitHub 기록에서Orbit에서
수렴 (convergence)사건 하나가 여러 사례에 복제된다PR 하나가 이슈 두 개를 닫으면, 업무를 사례로 삼은 로그에서 그 PR의 생성, 리뷰, 병합이 두 업무에 한 번씩 들어간다. 리뷰 사건 하나를 사람별로 나누면 리뷰어와 작성자 두 사람에게 들어간다PR이 업무 둘을 닫은 경우는 없다. 리뷰 사건 322건은 사람 두 명에 걸린다
발산 (divergence)사례 하나 안에 같은 활동이 여러 번 나오는데, 서로 다른 대상에 대한 것이다업무 하나에 PR이 여럿이면 업무 사례 안에서 PR 생성, 리뷰, 병합이 섞인다. 두 PR이 동시에 열려 있으면 “PR 생성 → 리뷰 승인” 연결이 다른 PR의 리뷰를 이을 수 있다PR이 둘인 업무 13건. 두 PR이 차례로 열려 섞이지는 않지만, 다시 열림 뒤의 둘째 PR이 같은 활동 이름으로 이어진다

사례를 PR로 바꾸면 발산은 줄지만, 업무에만 걸린 사건(업무 생성, 작업 시작, 막힘, 결정 기록, 다시 열림)이 빠진다. 사례를 무엇으로 정하든 한쪽의 정보가 빠지거나 복제된다. 객체 중심 이벤트 로그(object-centric event log)는 사례를 하나로 정하지 않고, 사건마다 관련된 객체를 모두 적는다.

2OCEL 2.0의 구성

OCEL 2.0(Object-Centric Event Log 2.0)은 RWTH 아헨 대학의 판 데르 알스트 연구실이 중심이 되어 2023년에 낸 교환 형식 명세다. 명세서와 예시 로그는 ocel-standard.org에 있고, 2024년에 베르티(A. Berti) 등이 같은 명세를 arXiv(2403.01975)에 올렸다. 1.0판에 없던 것은 세 가지다. 사건과 객체의 관계에 붙는 한정자(qualifier), 객체와 객체의 관계, 시간에 따라 바뀌는 객체 속성이다.

요소뜻Orbit에 적용한 것
객체 유형 (object type)사건이 걸리는 대상의 종류task(업무 이슈), pr(풀 리퀘스트), person(사람)
사건 유형 (event type)활동 이름로그의 활동 12종 그대로
사건-객체 관계 (E2O)와 한정자사건 하나가 어떤 객체에 어떤 역할로 걸렸는가리뷰 승인 → task(task), pr(pr), 리뷰어(reviewer), PR 작성자(pr author)
객체-객체 관계 (O2O)객체 사이의 관계pr → task(closes), pr → person(author)
객체 속성과 그 변화속성 값과 그 값이 바뀐 시각task의 area(고정), state(OPEN, CLOSED), blocked(yes, no). pr의 number, state(OPEN, MERGED). person의 role
형식같은 내용을 담는 세 가지 파일 형식JSON, XML, SQLite. 이 모듈은 JSON과 SQLite를 만든다

GitHub의 리뷰(review)도 아이디가 있는 객체이고, 리뷰 하나에 댓글 여러 개가 달린다. Orbit 로그는 리뷰 하나를 사건 하나로만 적었으므로 review 객체를 만들지 않았다. 리뷰 댓글까지 모으는 저장소라면 review를 객체 유형으로 두고 “리뷰 하나에 댓글 몇 개, 몇 번 오갔는가”를 객체 단위로 잴 수 있다.

3OCEL 2.0 파일 만들기

ocel-export.py는 키트 형식의 CSV(case_id,activity,timestamp,resource,ref,area)를 읽어 OCEL 2.0 파일을 쓴다. 객체와 관계는 다음 규칙으로 만든다.

  • 사건마다 그 줄의 case_id를 task 객체로, ref가 있으면 그 PR을 pr 객체로, resource를 person 객체로 잇는다.
  • person의 한정자는 활동으로 정한다. 업무 생성은 creator, PR 생성은 author, 변경 요청과 리뷰 승인은 reviewer, PR 병합은 merger다. 리뷰 사건은 PR 작성자에게도 pr author로 잇는다.
  • 업무 완료가 같은 업무의 PR 병합 바로 뒤 60초 안에 기록되었으면 그 PR에도 closing pr로 잇는다. 닫는 키워드로 PR이 이슈를 닫으면 두 사건이 몇 초 간격으로 생기기 때문이다.

숫자를 검산한다. 사건-객체 관계 5,582개는 task 관계 1,975개(모든 사건), pr 관계 1,310개(PR 번호가 있는 사건 1,063개와 PR 병합에 이어진 업무 완료 247개), person 관계 1,975개, 리뷰 사건의 작성자 관계 322개를 더한 값이다. 객체-객체 관계 494개는 PR 247개마다 closes와 author가 하나씩이다. 속성 변화 1,106건 가운데 612건이 task의 state와 blocked, 494건이 pr의 state다.

e00005(OB-001의 리뷰 승인)는 객체 네 개에 걸린다. #2005 객체는 3월 18일에 OPEN, 3월 20일에 MERGED가 되었고 OB-005를 닫는다. 같은 파일이 키트의 samples/c-orbit-ocel.json으로 들어 있다.

출력 파일의 확장자를 .sqlite로 주면 OCEL 2.0의 관계형 형식으로 쓴다. 활동마다 표가 하나씩 생기고, 표 이름은 event_map_type 표가 활동 이름과 잇는다.

PM4Py의 관계형 스키마 점검(표의 존재, 기본 키, 외래 키 등 24개 조건)을 모두 통과했다. SQLite 파일은 SQL로 바로 질의할 수 있어 객체가 수십만 개인 로그에 맞고, JSON은 사람이 읽고 grep으로 찾기 쉽다.

내 저장소의 OCEL

실습 저장소나 팀 저장소에서는 실습 20의 event-log.sh로 같은 형식의 CSV를 만든 뒤 바꾼다. GitHub에 접속하는 명령이라 출력은 싣지 않는다.

bash github-pm-starter-kit/event-log.sh "$PM_REPO" --snapshots pm-lab/pm-data/snapshots > pm-lab/event-log.csv
python3 github-pm-starter-kit/ocel-export.py pm-lab/event-log.csv pm-lab/my-ocel.sqlite --roles github-pm-starter-kit/roles.tsv
python3 github-pm-starter-kit/ocel-notebook.py pm-lab/my-ocel.sqlite

event-log.sh는 업무 이슈를 닫는 PR만 따라가므로, PR 하나가 이슈 여러 개를 닫는 수렴이 실제로 얼마나 있는지는 따로 확인한다. gh pr list의 closingIssuesReferences 필드가 PR이 닫는 이슈 목록이다.

gh pr list -R "$PM_REPO" --state merged --limit 200 --json number,closingIssuesReferences \
  --jq '.[] | select((.closingIssuesReferences | length) > 1) | "#\(.number): \([.closingIssuesReferences[].number] | join(", "))"'

출력 한 줄이 이슈 둘 이상을 닫은 PR 하나다. 이런 PR이 있으면 event-log.sh의 로그에는 그 PR의 사건이 업무마다 한 번씩 들어가 있고, ocel-export.py는 같은 PR 번호의 사건을 사건 하나로 합치지 않는다. 이때는 PR 사건을 한 번만 적고 task 객체 여러 개에 잇도록 추출 단계를 고쳐야 수렴이 사라진다.

4PM4Py로 읽고 평탄화하기

ocel-notebook.py는 # %% 줄로 셀이 나뉜 스크립트다(실습 24의 pm4py-notebook.py와 같은 방식). 셀 [1]~[6]을 한 번에 실행하고 결과를 보고 파일로 남긴다.

평탄화(flattening)는 객체 유형 하나를 골라, 그 유형의 객체 하나를 사례 하나로 삼은 일반 로그를 만드는 일이다(PM4Py ocel_flattening). 셀 [3]의 표를 읽는다.

사례로 삼은 유형결과읽는 법
task사례 257, 행 1,975, 변형 202단계 로그와 같다. 모든 사건이 업무 하나에만 걸려 있어 복제도 누락도 없다
pr사례 247, 행 1,310, 빠진 사건 665, 변형 4PR에 걸리지 않은 사건 665건이 빠진다. 남은 흐름은 첫 커밋, PR 생성, 변경 요청 반복, 리뷰 승인, 병합, 업무 완료뿐이라 변형이 4개다
person사례 8, 행 2,297, 두 번 이상 들어간 사건 322리뷰 사건 322건이 리뷰어와 작성자 두 사람의 사례에 한 번씩 들어간다. 사람별 로그에서 리뷰 승인은 494번, 변경 요청은 150번으로 실제의 두 배다
평탄화한 로그의 숫자를 합치지 않는다

사람별로 평탄화한 로그에서 “리뷰 승인 494회”를 세면 리뷰를 494번 한 것처럼 보인다. 수렴이 생긴 로그에서는 활동 횟수를 객체 유형마다 따로 읽고, 사건 수가 필요하면 OCEL의 사건 표에서 센다. PM4Py의 discover_ocdfg 결과에도 같은 이유로 사건 수(events)와 객체 수(total_objects)가 따로 있다.

5수렴과 발산을 숫자로

셀 [4]는 실제 분석에서 자주 만드는 로그 하나를 흉내 낸다. PR을 사례로 삼으면서 백로그 대기와 막힘도 함께 보려고, 그 PR이 닫는 업무의 사건을 모두 PR 사례에 붙인 로그다.

항목값읽는 법
PR 사례, 행247, 2,087업무에 걸린 사건 1,898건으로 행 2,087개를 만들었다
수렴: 두 PR 사례에 복제된 사건189PR이 둘인 업무 13건의 사건이 두 PR 사례에 한 번씩 들어갔다
발산: 다른 PR의 사건이 섞인 행136#1005 사례에 #2005의 첫 커밋, PR 생성, 리뷰, 병합이 들어 있다
#2005의 “업무 생성 → 이 PR 병합”16.9일#2005는 3월 17일 13시 첫 커밋부터 3월 20일 9시 병합까지 3일이 걸리지 않았다. 업무 생성 시각을 붙이면 첫 PR의 시간까지 이 PR의 리드 타임에 들어간다

이 로그로 “PR 리드 타임”을 재면 다시 열린 업무의 둘째 PR이 실제보다 길게 나오고, 업무 생성은 PR이 있는 업무 234건보다 13번 많은 247번 나온다. OCEL에서는 업무 생성이 task 객체에만 걸리므로 PR 리드 타임은 pr 객체의 사건만으로, 백로그 대기는 task 객체의 사건만으로 잰다.

6객체 중심 직접 이어짐 그래프

객체 중심 직접 이어짐 그래프(object-centric directly-follows graph, OC-DFG)는 객체 유형마다 따로 직접 이어짐 관계를 구해 한 그림에 겹친다. 연결의 색이 객체 유형이고, 연결의 EC는 그 유형의 객체에서 두 사건이 이어진 횟수(event couples), 활동 상자의 E는 사건 수다. 셀 [5]가 task와 pr 두 유형으로 그래프를 구하고 pm-lab/ocel-fig/ocdfg.dot로 저장한다.

Orbit OCEL의 객체 중심 직접 이어짐 그래프. 파란 연결은 task 객체, 주황 연결은 pr 객체의 흐름이다. task 흐름은 업무 생성, 작업 시작, 막힘, 첫 커밋, 결정 기록, PR 생성, 리뷰, 병합, 업무 완료, 다시 열림을 모두 지나고, pr 흐름은 첫 커밋, PR 생성, 변경 요청, 리뷰 승인, PR 병합, 업무 완료만 지난다. 첫 커밋에서 PR 생성으로 가는 연결은 task 204회, pr 247회다.
task(파랑)와 pr(주황)의 OC-DFG. ocel-notebook.py --out이 저장한 DOT를 Viz.js 3.30.0으로 그렸다. 읽기 쉽도록 객체 유형의 색과 글자 크기만 바꿨다.

셀 [5]의 출력에서 두 유형을 비교한다.

  • task에만 있는 연결 11개: 업무 생성, 작업 시작, 막힘, 막힘 해소, 결정 기록, 다시 열림 가운데 하나가 들어간 연결이다. 이 활동들은 PR 객체에 걸리지 않는다.
  • “첫 커밋 → PR 생성”은 task 204회, pr 247회: 업무 43건에서 첫 커밋과 PR 생성 사이에 결정 기록이 있다. task의 흐름에서는 “첫 커밋 → 결정 기록 → PR 생성”으로 끊기고, 결정 기록이 걸리지 않는 pr의 흐름에서는 247개 PR 모두 첫 커밋 다음이 PR 생성이다. 같은 두 활동의 관계가 어느 객체 유형을 기준으로 보느냐에 따라 다르게 나온다.
  • pr 흐름의 끝은 모두 업무 완료: closing pr 관계 덕분에 PR 247개가 모두 “PR 병합 → 업무 완료”로 끝난다. 이 관계가 없으면 pr의 흐름은 PR 병합에서 끝나고, PR이 업무를 닫았다는 사실은 로그에 남지 않는다.

셀 [6]은 객체 속성의 시간 변화를 쓴다. state가 두 번 이상 CLOSED가 된 업무는 13건으로 다시 열린 업무 수와 같다. 활동을 세지 않고 상태 변화만으로 같은 답을 얻는다.

7이벤트 추상화와 로그 결함

GitHub의 원래 기록은 활동 12종보다 훨씬 잘다. 이슈 타임라인에는 라벨 붙임(labeled), 담당자 지정(assigned), 리뷰 요청(review_requested), 리뷰(reviewed), 커밋(committed), 병합(merged), 닫힘(closed), 다시 열림(reopened), 다른 이슈에서의 언급(cross-referenced) 같은 사건이 쌓인다. 잘게 기록된 사건을 분석 질문에 맞는 활동으로 묶는 일을 이벤트 추상화(event abstraction)라고 한다. 키트의 event-log.sh는 이미 몇 가지 추상화를 했다.

원래 기록키트 로그의 활동버린 정보
PR의 커밋 여러 개첫 커밋 하나커밋 수, 커밋 사이 간격(작업이 끊긴 시간)
blocked 라벨 붙임과 뗌막힘, 막힘 해소다른 라벨의 변화
리뷰 하나와 그 댓글들변경 요청 또는 리뷰 승인댓글 수, 댓글만 남긴 리뷰(COMMENTED)
Project의 Status 변경작업 시작(18시 스냅숏)In Progress가 된 정확한 시각

수리아디(S. Suriadi) 등은 2017년 Information Systems 논문에서 실제 로그에 자주 나오는 결함을 패턴 11개로 정리했다. 그중 GitHub 기록에서 볼 수 있는 것을 Orbit과 키트에 대어 본다.

패턴뜻GitHub와 Orbit에서
양식 기반 기록 (form-based event capture)여러 사건이 한 번의 저장으로 같은 시각에 기록된다작업 시작이 모두 18:00 스냅숏 시각이다 (실습 22)
뜻하지 않은 시간 역행 (inadvertent time travel)기록 시각 때문에 순서가 실제와 뒤집힌다작업 시작보다 첫 커밋이나 막힘이 먼저인 사례 33건 (실습 22에서 고침)
부수 사건 (collateral events)한 번의 행동이 사건 여러 줄로 남는다PR 병합 5초 뒤의 업무 완료. ocel-export.py가 둘을 같은 PR에 잇는 근거다
찾기 어려운 사례 (elusive case)사건에 사례를 잇는 값이 없다닫는 키워드 없이 만든 PR은 업무에 이어지지 않아 로그에서 빠진다
동음이의 이름 (homonymous label)같은 활동 이름이 다른 뜻으로 쓰인다다시 열린 업무의 둘째 “첫 커밋”은 둘째 PR의 첫 커밋이다. 업무 전체의 첫 커밋과 이름이 같다
기준 없는 시각 (unanchored event)시각의 형식이나 시간대가 섞인다event-log.sh --tz를 다르게 준 파일을 합치면 생긴다 (실습 22의 막히면 표)

OCEL로 바꾸면 동음이의 이름 문제는 줄어든다. 둘째 “첫 커밋”은 #2005 객체에 걸리므로, pr 객체를 기준으로 보면 언제나 그 PR의 첫 커밋이다.

8사람 객체와 개인정보

person 객체를 두면 사람마다 평탄화한 로그(셀 [3]의 person 행)가 바로 나온다. 한 사람의 6개월 활동이 시각까지 한 줄로 이어진 기록이고, 개인을 평가하거나 감시하는 데 그대로 쓸 수 있는 형태다. 파일을 팀 밖으로 주기 전에 세 가지 중 하나를 고른다.

방법명령남는 것과 한계
가명화--pseudonymize --map 파일아이디를 person-01 같은 가명으로 바꾼다. 대응표는 따로 보관한다
사람 객체 빼기--no-persontask와 pr의 흐름만 남는다. 리뷰 집중도 같은 자원 분석은 할 수 없다
집계만 내보내기(파일 대신 보고서)역할별, 주별 건수와 대기 시간만 준다

가명은 처음 나온 순서로 붙는다. 그런데 person-01은 role이 PM인 유일한 사람이고, QA도 한 명뿐이다. Orbit처럼 여덟 명인 팀에서는 역할 속성 하나와 활동 유형만으로 가명이 누구인지 알 수 있다. 개인정보 보호법 제2조 제1호의2는 가명처리를 “추가 정보가 없이는 특정 개인을 알아볼 수 없도록 처리하는 것”으로 정의한다. 작은 팀의 가명 파일은 이 조건을 만족하지 못할 수 있으므로, 외부에 줄 때는 사람 객체를 빼거나 집계만 준다. 대응표(pm-lab/person-map.tsv)는 저장소에 커밋하지 않는다.

막히면
증상확인과 해결
오류: 출력 파일의 확장자는 .json 또는 .sqlite다XML 형식은 이 스크립트가 쓰지 않는다. 필요하면 JSON을 PM4Py로 읽어 pm4py.write_ocel2_xml로 저장한다
오류: 객체 아이디가 두 유형에 겹친다사람 아이디가 업무 키와 같거나(OB-001이라는 계정), PR 번호 칸에 업무 키가 들어갔다. CSV의 ref 열을 확인한다
ModuleNotFoundError: No module named 'pm4py'ocel-export.py는 PM4Py 없이 돈다. 노트북과 검증 명령만 PM4Py가 필요하다. 트랙 C 안내의 설치 명령을 실행한다
노트북의 변형 수가 4와 20이 아니다평탄화한 로그는 사례와 시각 순서로 정렬해야 변형을 바르게 센다. 셀 [3]의 flat 함수가 정렬을 한다. 직접 코드를 쓸 때도 sort_values를 빼지 않는다
SQLite 파일을 다시 만들 때 오류가 난다스크립트가 같은 이름의 파일을 지우고 새로 만든다. 다른 프로그램(DB 도구)이 파일을 열고 있으면 닫는다
  • 수렴과 발산을 Orbit의 예(리뷰 사건 322건, PR이 둘인 업무 13건)로 설명할 수 있다.
  • pm-lab/orbit-ocel.json과 pm-lab/orbit-ocel.sqlite를 만들고 관계형 스키마 점검을 통과했다.
  • task, pr, person으로 평탄화한 로그의 행, 빠진 사건, 복제된 사건을 비교했다.
  • “첫 커밋 → PR 생성”이 task와 pr에서 다르게 나오는 이유를 설명할 수 있다.
  • 가명화한 파일이 작은 팀에서 다시 식별되는 이유와 대안을 적었다.

마치며...

사례 하나를 고르는 일은 분석 질문을 고르는 일이고, 고르지 않은 객체의 정보는 빠지거나 복제된다. OCEL 2.0은 사건마다 걸린 객체를 모두 적어 이 선택을 분석 단계로 미루고, 평탄화는 그 선택을 다시 하는 단계다. 이 모듈에서는 Orbit의 업무, PR, 사람을 객체로 나누어 같은 기록이 객체에 따라 다른 흐름과 숫자를 내는 것을 확인했다. 다음 모듈에서는 업무 로그 하나로 여러 발견 알고리즘과 선언적 제약을 비교하고, 6월 1일의 변화를 통계 검정으로 찾는다.