Bag of Words

Step 1. 주어진 텍스트 데이터를 unique한 단어장으로 구축한다.

Step 2. 각 단어를 Categorical Variable로 보고, One Hot Vector 형태로 변환한다.

    └ 이때 모든 단어쌍은 euclidean distance가 sqrt(2)이고, cosine similarity(내적)가 0이다.

Step 3. 단어에 해당하는 모든 vector를 더하여 문장을 표현한다. 

 

Naive Bayes Classifier

 

문서를 분류할 수 있는 카테고리가 C개 있다고 해보자.

특정한 문서는 d이고, 이 문서가 각 카테고리에 속할 확률 분포는 P(c|d)이다.

모든 카테고리 중 가장 높은 확률을 가지는 카테고리 c로 문서를 분류한다.

이를 MAP(Maximum A Posteriori)라고 한다.

 

P(c|d)는 naive bayes 공식에 따라 P(d|c)P(c) / P(d)로 변환 가능하다. 

P(d)는 다양한 문서 중 문서 d가 뽑힐 확률을 의미하지만, d는 고정이기 때문에 P(d)는 상수로 무시할 수 있다.

 

 

어떤 문서 d는 다양한 단어(word)의 집합(w1, w2, ..., wn)으로 표현할 수 있다.

그렇다면 P(d|c)는 P( w1, w2, ..., wn|c)로 전개가 가능하고, P(w1|c) * P(w2|c) * ... * P(wn|c)로 바꿀 수 있다.

즉, 위의 식에서 가장 우측 변의 다항식이 된다.

 

Naive Bayes Classifier - Example

 

위의 표처럼 4개의 문장이 주어져있고, 5번 문장의 Class를 예측하는 상황이다.

1번과 2번 문장은 CV(Computer Vision) 문장이고, 3번과 4번은 NLP(Natural Language Proccess) 문장이다.

그렇다면 CV 카테고리로 분류할 확률은 P(c_CV)로 표현하고, 2 / 4 = 1 / 2이다.

NLP 카테고리로 분류할 확률은 P(c_NLP)로 표현하고, 2 / 4 = 1 / 2이다.

 

5번 문장은 Classification, task, uses, transformer의 네 단어로 이루어져 있다.

이에 각 단어들이 각 카테고리에 어떤 확률 분포를 가지는지 구해야 한다.

CV는 1번과 2번으로 두 문장을 합쳐서 모두 14개의 unique한 단어를 가지고 있다.

NLP는 3번과 4번으로 두 문장을 합쳐서 모두 10개의 unique한 단어를 가지고 있다. 

해당 단어들에서 5번 문장의 단어가 몇 번 나왔는지 세면 된다.

 

예를 들어서 task라는 단어는 CV에서는 총 1번 등장하여, P(w_task | c_cv) = 1 / 14이다.

반면 NLP에서는 총 2번 등장하, P(w_task | c_NLP) = 2 / 10이다.

 

 

위에서 구한 모든 확률을 곱하면, 5번 문장이 각 카테고리일 확률을 구할 수 있다.

다만 이 방식은 '한 번도 등장하지 않은 단어'가 있다면, 확률이 0에 수렴한다는 맹점이 있다.

따라서 Regularization(정규화)나 MLE(Maximum Likelihood Estimation; 최대 우도법) 등의 방법으로 파라미터를 유도한다.

자연어 구분

ㆍ NLU(Natural Language Understanding)

컴퓨터가 주어진 단어나 문장, 보다 더 긴 문단이나 글을 이해하는 것

  NLG(Natural Language Geneartion)

상황에 따라 자연어를 적절히 생성하는 것

 

자연어를 다루는 분야

NLP - 자연어 처리

    (주요 학회: ACL, EMNLP, NAACL)

Text Mining - 단어 추출

    (주요 학회: KDD, The WebConf(WWW), WSDM, CIKM, ICWSM)

Information Retrieval - 정보 검색

    (주요 학회: SIGIR, WSDM, CIKM, RecSys)

    (자연어 처리나, 단어 추출 등에 비해서는 상대적으로 늦게 발전한 분야)

 

NLP(Natural Language Processing) - 자연어 처리

Low-level parsing

    Tokenization(문장을 이루는 단어 단위로 쪼개나가는 과정)

    Stemming(다양한 단어의 어미 변형에도 본질을 추출하는 과정)

 

Word and Phrase level

    NER(Named Entity Recognition; 단일 단어 혹은 여러 단어로 이루어진 고유명사를 인식하는 과정)

    ㆍ POS tagging(Part Of Speech; 문장 내에서 각 단어들이 어떤 품사에 해당하는지 알아내는 과정)

    그 외 Noun Phrase Chunking, Dependency Parsing, Coreference Resolution

 

ㆍ Setence level

    ㆍ Sentiment Analysis(주어진 문장이 긍정인지 부정인지 예측하는 감정 분석)

    ㆍ Machine Translation(기계 번역)

 

Multi Sentence and Paragraph level

    ㆍ Entailment Prediction(두 문장 간에 논리적인 내포 혹은 모순을 예측)

    ㆍ Question Answering(독해 기반의 질의응답)

    ㆍ Dialog Systems(챗봇과 같은 대화를 수행할 수 있는 기능)

    ㆍ Summarization(주어진 글을 요약)

 

Text Mining - 단어 추출

ㆍ Feature Extraction

    ㆍ BoW(Bag of Words; 단어 출현 빈도를 기반으로 문서를 벡터화)

    ㆍ TF-IDF(Term Frequency Inverse Document Frequency; 단어의 중요도를 반영하여 가중치를 계산)

    ㆍ Word Embedding(단어 간 의미적 유사성을 반영하는 기법, Word2Vec, GloVe, FastText 등이 있음)

 

ㆍ Text Classification

    ㆍ Sentiment Analysis(텍스트가 긍정인지 부정인지 판단)

    ㆍ Document Clustering(서로 다르지만 유사한 단어끼리 묶는 방법, Topic Modeling과 같은 개념)

    ㆍ Topic Modeling(문서에서 주요 주제를 자동으로 추출하는 방법, LDA, LSA, NMF 등이 있음)

    ㆍ Spam Detection(이메일이나 메시지에서 스팸 여부를 판별)

 

Information Retrieval - 정보 검색

ㆍ Indexing

    ㆍ Inverted Index(검색 속도를 높이기 위해 단어 기반의 색인 구조 구축)

    ㆍ Tokenization & Normalization(검색을 위해 텍스트를 일관된 형태로 변환)

 

ㆍ Query Processing

    ㆍ Boolean Retrieval(AND, OR, NOT 연산을 이용하여 단순 검색)

    ㆍ Vector Space Model(벡터 공간 모델, 문서와 질의를 벡터로 변환하여 유사도를 계산)

    ㆍ BM25(Best Matching 25; 검색 정확도를 높이기 위한 가중치 기반 검색 모델)

 

ㆍ Ranking

    ㆍ TF-IDF 기반 랭킹(검색어와 문서의 관련도를 반영하여 순위를 매김)

    ㆍ PageRank(웹 검색에서 중요한 페이지를 상위에 노출하는 알고리즘)

    ㆍNeural IR(신경망 기반 검색, 딥러닝을 활용하여 문서와 질의의 의미적 유사도를 학습)

 

ㆍ Question Answering

    ㆍ 사용자의 질문에 대해 정답을 추출하는 시스템

 

ㆍ Recommender Systems

    ㆍ 사용자가 할 검색을 분석하여 자동으로 해주는 새로운 검색 시스템의 개념

    ㆍ Content Based Filtering(콘텐츠 기반 필터링, 사용자가 관심을 가질 만한 정보를 제공)

    ㆍ Collaborative Filtering(협업 필터링, 다른 사용자의 행동을 기반으로 추천)

 

NLP의 발전 과정

이미지와 CV 관련한 CNN, GAN 등의 구조처럼 지속 발전 중

 

일반적으로 숫자 형태의 입출력을 필요로 하기에, 글을 단어 단위로 쪼개고 벡터 형태로 변형한다.

이를 벡터 공간에서 하나의 점으로 표현한다는 뜻에서 Word Embedding이라고 한다.

주어진 단어에는 Sequence(순서)가 존재하기 때문에, 자연어 처리 모델이 이를 인식한다.

이런 Sequence 데이터 처리에 특화 모델이 RNN(Recurrent Neural Network)이다.

    ㆍ LSTM(Long Short Term Memory) : RNN의 모델 중 하나로, 기울기 소실 문제를 방지한 모델

    ㆍ GRU(Gated Recurrent Unit) : LSTM을 간소화하여 성능 향상을 한 모델 

 

이후 2017년 인공지능 학회(NeurIPS)에서 Google이 논문(Attention Is All You Need)을 발표한다.

기존의 RNN 기반의 자연어 모델을 Self Attention 구조의 Transformers 모델로 완전히 대체하였다.

현재까지도 대부분의 자연어 모델은 Transformers다.

    ㆍ 최초의 Transformers 모델은 Rule-based 기반의 기계 번역 개선을 위해 제안되었다.

    ㆍ Transformers 모델 공개 전에는, 텍스트의 종류에 따라 각 모델이 따로 존재했다.

    ㆍ Transformers 모델에서 Self-Supervised Learning이나 Pre-trained model의 예시로 BERT나 GPT-3가 있다.

https://github.com/miny-genie/RTC-mariokart-8-AI/tree/main/playground-main

 

RTC-mariokart-8-AI/playground-main at main · miny-genie/RTC-mariokart-8-AI

RTC(RoofTopCat)'s Mariokat 8 deluxe(nintendo switch platform) predicting AI model - miny-genie/RTC-mariokart-8-AI

github.com

 

2024.05.26(일)

구현 사항
- 좌측 frame에 24 * 4만큼 버튼 삽입 성공
- 버튼마다 이미지를 설정하고 center로 위치 조절 성공
- garbage collector가 이미지를 지우던 문제 사항 해결
 
추가 예정
- separtor를 기준으로 좌측 frame에는 맵 코스를, 우측 frame에는 예측 기능을 추가할 예정 
- 스크롤뷰로 마리오카트 코스 볼 수 있게 구현해야 함
- 좌측 frame에 여백 없애야 함
- 상단에서 검색 기능 추가해야 함
- 내부 코드 깔끔하게 만들기(현재는 extract.py 파일에서 하나로 관리 중)
 

2024.05.27(월)

구현 사항
- 전체적인 기하 관리(rowconfigure, columnconfigure) 완료
- 좌측 frame에서 상단 부분에 Filters로 맵 검색 프레임 추가 완료
- 이미지 버튼들을 하나의 Course frame에서 관리하도록 변경
- Course Frame을 스크롤 뷰로 볼 수 있게끔 변경
 
추가 예정
- 맵 검색 기능은 미구현, trie 자료구조를 활용하여 이미지 버튼 보이는 기능 추가하기
- 이미지 버튼 아래에 맵 이름에 해당하는 label 추가하기
- 우측 frame에서 예측할 수 있는 frame 구조 설계하기

 

2024.05.29(수)

구현 사항

- ConsoleWindow가 프로그램에 나오지 않던 문제를 해결

- PlaygroundUI의 Config 값을 잘못 던져주어 ValueError가 ConsoleWindow에 나오던 문제를 해결

- 우측 부분에 예측에 사용할 Prediciton frame과 결과에 사용할 Reuslt frame 추가 완료

- 버튼을 눌렀을 때, 맵 이름 Entry에 맵 번호가 들어가도록 command=launch 기능 추가 완료

 

추가 예정

- 맵 검색 기능은 미구현, trie 자료구조를 활용하여 이미지 버튼 보이는 기능 추가하기
- 이미지 버튼 아래에 맵 이름에 해당하는 label 추가하기

- ConsoleWindow에 현재 발생하는 에러 해결하기

    └ config_path 문제인데, 이는 cli.py에서 PlaygroundUI를 시작할 때 값을 주어야 함. 최종 부분에서 해결해야 하는 상황

- Prediction과 Result frame이 현재 play.py에 모여있는데, SRP에 따라 코드 쪼개기

- 맵 번호에 따른 입력값 자동으로 넣을 수 있게 추가하기

- 불안전한 놀이터! button의 기능(command) 추가하기

- 데이터베이스 추가하기

 

2024.06.05(수)

구현 사항

- Entry 텍스트 입력에 따른 이미지 버튼 filtering 기능 추가 완료

- 이미지 버튼을 filtering함에 따라 grid를 재배치하도록 작성(공백없이 왼쪽 위에서부터 채움)

- Course frame과 Prediction frame과 Result frame을 각각 다른 파일로 분리 완료

 

추가 예정

- 이미지 버튼 filtering 키워드 다양화하기

    └ 경기장 이름 국문/영문, 그랑프리 종류 국문/영문, 컵 종류 국문/영문

    └ 국문 검색 방법은 unicode 변환, kmp 알고리즘, trie 자료구조를 복합적으로 응용할 예정

- 이미지 버튼 이름(넘버링)을 filtering 키워드로 해싱하기

- Entry에서 텍스트를 입력할 때 IME 문제로 한글 하나 입력이 아닌 한 글자 단위로 입력되는 현상 해결

    └ <<CompositionEnd>> 이벤트에 대해서 알아보기
- 이미지 버튼 아래에 맵 이름에 해당하는 label 추가하기

- ConsoleWindow에 현재 발생하는 에러 해결하기

    └ config_path 문제와 packs_frame(render_frame) 함수 문제

- 불안전한 놀이터! button의 기능(command) 추가하기

- 데이터베이스 추가하기

 

2024.06.06(목)

실제 이미지(X), 그림판으로 그려본 예상 도안(O)

예시 도안

- 옥냥이의 마리오카트 그동안의 기록을 확인할 수 있는 데이터베이스 탭

- 검색 탭에서는 Playground와 같은 검색 알고리즘 도입

 

추가 예정

- 이미지 버튼 filtering 키워드 다양화하기

    └ 경기장 이름 국문/영문, 그랑프리 종류 국문/영문, 컵 종류 국문/영문

    └ 국문 검색 방법은 unicode 변환, kmp 알고리즘, trie 자료구조를 복합적으로 응용할 예정

- 이미지 버튼 이름(넘버링)을 filtering 키워드로 해싱하기

- Entry에서 텍스트를 입력할 때 IME 문제로 한글 하나 입력이 아닌 한 글자 단위로 입력되는 현상 해결

    └ <<CompositionEnd>> 이벤트에 대해서 알아보기
- 이미지 버튼 아래에 맵 이름에 해당하는 label 추가하기

- ConsoleWindow에 현재 발생하는 에러 해결하기

    └ config_path 문제와 packs_frame(render_frame) 함수 문제

- 불안전한 놀이터! button의 기능(command) 추가하기

- 데이터베이스 추가하기

 

2024.06.07(금)

구현 사항

- 예시 도안에 따른 Database Tab의 Frame 구축

- SRP에 따라 Filter Frame, Database Frame, test Frame으로 분해하여 생성

 

추가 예정

- 이미지 버튼 filtering 키워드 다양화하기

    └ 경기장 이름 국문/영문, 그랑프리 종류 국문/영문, 컵 종류 국문/영문

    └ 국문 검색 방법은 unicode 변환, kmp 알고리즘, trie 자료구조를 복합적으로 응용할 예정

- 이미지 버튼 이름(넘버링)을 filtering 키워드로 해싱하기

- Entry에서 텍스트를 입력할 때 IME 문제로 한글 하나 입력이 아닌 한 글자 단위로 입력되는 현상 해결

    └ <<CompositionEnd>> 이벤트에 대해서 알아보기
- 이미지 버튼 아래에 맵 이름에 해당하는 label 추가하기

- ConsoleWindow에 현재 발생하는 에러 해결하기

    └ config_path 문제와 packs_frame(render_frame) 함수 문제

- 불안전한 놀이터! button의 기능(command) 추가하기

- 데이터베이스 추가하기

 

2024.06.11(화)

구현 사항

- 영문 검색 기능 추가

 

2024.06.12(수)

구현 사항

- 이미지 버튼 label 추가

- korean exp-reg 구현 중

 

2024.06.17(월)

구현 사항

- korean exp-reg 구현 완료

- 초성 검색(initial_search), 유사 검색(fuzzy), 영문 검색(_eng_to_kor) 기능 포함

 

2024.06.18(화)

 

Korean IME problem in ttk.Entry

Hello! I’m a college student who wants to be a Python engineer, attending a Korean university. Working on the project, and I came here because there was a problem with themed tkinter. There is a problem with the Korean IME in the ttk.Entry object. Hangul

discuss.python.org

문제 상황

- ttk.Entry에 대한 한글 IME 문제 발생

- PyQt5의 QLineEdit에서 event(type: QInputMethodEvent)를 이용하면 event.preeditString()으로는 구현 가능

- tkinter(mainloop)와 PyQt5()의 이벤트 루프가 달라 혼용이 어려움

- 도저히 방법을 못 찾고 생각이 안 나서 discuss.python에 질문을 올려둔 상태

 

다음 개발 사항

1. Classification과 Regression 인공지능 모델 개발 완료 후 pkl로 프로그램에 탑재하기

2. Database 탭에서 그간 옥냥이가 한 마리오 카트 결과 삽입하기

 

2024.06.20(목)

구현 사항

- discuss.python에서도 답변을 듣지 못해, 현재 귀도 반 로섬에게 메일을 보내놓음

- 임시방편으로 eng_to_kor 함수를 사용하여 '영문 입력 > 국문 검색' 방법으로 검색 기능 구현 완료

 

구현 사항

- github api releases 이후 version.py에서 버전 및 업데이트 관리가 가능하도록  해당 링크 추가

 

2024.06.27(목)

Track 1 Frame 제작

Track 2 Frame 제작

Result 제작

 

2024.07.02(화)

테스트용 pkl 예측 모델 탑재

 

2024.08.10(토)

일부 한글화 진행

database 추가 완료

초기화 버튼 추가 완료

 

2024.08.13(화)

Tab name 변경

CustomTable 클래스 구현

key bind으로 데이터 셀 클릭 시 우측에 그래프 그리기 구현

어떤 그래프를 그릴지 명확하게 알려주기

그래프 사이즈 커지지 않게 조절하기

 

2024.08.18(일)

전체적인 프로그램 흐름 및 방향성 잡기

tab은 총 다섯 개(토토 예측, 옥냥이 기록, 개인 토토 기록, 개인 토토 기록으로 성향 파악하기, 설정)

토토 예측 - tkinter GUI 익히기, AI 모델에 대한 전반적인 이해도 높이기 및 구현하기

옥냥이 기록 - 데이터 관리 및 SQL, pandas 문법 익히기, 데이터 시각화 익히기

성향 파악 - 금융권 도메인 지식 이해하기, 데이터 시각화 및 정리하기

개인 토토 기록, 설정, 전체 -  Python OOP에 대한 공부 및 구현 익히기

 

Track 1과 Track 2 프레임을 하나로 통합(필수, 선택을 골라서 그에 맞는 모델을 골라서 결과 예측)

Result Save 프레임을 추가하여 본인이 행한 결과를 저장하게끔 구현 예정

'불안전한 놀이터' tab과 '개인 투자 기록' tab 연동 성공

 

2024.08.19(월)

'투자 성향' 파악하기 탭 frame 작성

cli.py, pyinstaller-cli.py, build-exe.py, config.py 파일 수정 및 제작

(exe 파일을 만들기 위해 필요한 파일들)

해당 파일에서는 API 서버와 통신을 위한 비동기 라이브러리 async, hypercorn 사용, 공부 중

 

5690 INFO: Copying bootloader EXE to C:\Users\kjmin\Desktop\desktop\졸업 이후\옥냥이_마리오카트\playground-main\build\playground\playground.exe
5764 INFO: Copying icon to EXE
Traceback (most recent call last):
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\win32ctypes\pywin32\pywintypes.py", line 33, in pywin32error
    yield
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\win32ctypes\pywin32\win32api.py", line 209, in BeginUpdateResource
    return _resource._BeginUpdateResource(filename, delete)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\win32ctypes\core\ctypes\_util.py", line 39, in check_null
    raise make_error(function, function_name)
OSError: [WinError 225] 파일에 바이러스 또는 기타 사용자 동의 없이 설치된 소프트웨어가 있기 때문에 작업이 완료되지 않았습니다.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\runpy.py", line 86, in _run_code      
    exec(code, run_globals)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\Scripts\pyinstaller.exe\__main__.py", line 7, in <module>
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\__main__.py", line 228, in _console_script_run
    run()
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\__main__.py", line 212, in run
    run_build(pyi_config, spec_file, **vars(args))
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\__main__.py", line 69, in run_build
    PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\building\build_main.py", line 1186, in main
    build(specfile, distpath, workpath, clean_build)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\building\build_main.py", line 1126, in build
    exec(code, spec_namespace)
  File "C:\Users\kjmin\Desktop\desktop\졸업 이후\옥냥이_마리오카트\playground-main\playground.spec", line 19, in <module>
    exe = EXE(
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\building\api.py", line 643, in __init__
    self.__postinit__()
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\building\datastruct.py", line 184, in __postinit__
    self.assemble()
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\building\api.py", line 756, in assemble
    self._retry_operation(icon.CopyIcons, build_name, self.icon)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\building\api.py", line 1018, in _retry_operation
    return func(*args)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\utils\win32\icon.py", line 212, in CopyIcons
    return CopyIcons_FromIco(dstpath, [srcpath])
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\PyInstaller\utils\win32\icon.py", line 144, in CopyIcons_FromIco
    hdst = win32api.BeginUpdateResource(dstpath, 0)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\win32ctypes\pywin32\win32api.py", line 208, in BeginUpdateResource
    with _pywin32error():
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\contextlib.py", line 153, in __exit__ 
    self.gen.throw(typ, value, traceback)
  File "C:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\site-packages\win32ctypes\pywin32\pywintypes.py", line 37, in pywin32error
    raise error(exception.winerror, exception.function, exception.strerror)
win32ctypes.pywin32.pywintypes.error: (225, 'BeginUpdateResourceW', '파일에 바이러스 또는 기타 사용자 동 의 없이 설치된 소프트웨어가 있기 때문에 작업이 완료되지 않았습니다.')
Traceback (most recent call last):
  File "c:\Users\kjmin\Desktop\desktop\졸업 이후\옥냥이_마리오카트\playground-main\build-exe.py", line 81, in <module>
    main()
  File "c:\Users\kjmin\Desktop\desktop\졸업 이후\옥냥이_마리오카트\playground-main\build-exe.py", line 69, in main
    run_pyinstaller(args.debug)
  File "c:\Users\kjmin\Desktop\desktop\졸업 이후\옥냥이_마리오카트\playground-main\build-exe.py", line 46, in run_pyinstaller
    subprocess.check_call(pyinstaller_args)
  File "c:\Users\kjmin\AppData\Local\Programs\Python\Python310\lib\subprocess.py", line 369, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['pyinstaller.exe', 'C:\\Users\\kjmin\\Desktop\\desktop\\졸업 이 후\\옥냥이_마리오카트\\playground-main\\pyinstaller-cli.py',
'--add-data', 'src/playground/VERSION;.', '--add-data', 'src/playground/static;static', '--name=playground',
'--icon=C:\\Users\\kjmin\\Desktop\\desktop\\졸업 이후\\옥냥이_마리오카트\\playground-main\\src\\playground\\static\\images\\icon.ico', '--clean', '--onedir', '--noconfirm', '--noconsole']'
returned non-zero exit status 1.

build-exe.py로 exe 파일 생성 시 발생하는 오류

원인 예측 1. OSError [WinError 225] 바이러스 취급 및 사용자 권한 부족

원인 예측 2. win32ctypes.pywin32.pywintypes.error 파일 리소스 업데이트 시도 실패

 

2024.08.20(화)

최상위 경로에서 실행했음에도 ModuleNotFoundError가 뜨는 문제 발생

에러 해결 중

 

2024.08.21(수)

https://github.com/spelunky-fyi/modlunky2/blob/main/CONTRIBUTING.md

 

modlunky2/CONTRIBUTING.md at main · spelunky-fyi/modlunky2

Contribute to spelunky-fyi/modlunky2 development by creating an account on GitHub.

github.com

github 정독 중......

 

2024.09.09(월)

데모 exe 생성 성공

하지만 실행 시 pyinstaller, cargo launcher, argparse, psycopg2, pysqlite2 등의 온갖 에러 발생

Rust 언어와 Pyinstaller 공부 중

0. PEP 8 - imports

# Correct
import os
import sys

# Wrong
import os, sys

# okay to say this though
from subprocess import Popen, PIPE

import하는 라이브러리는 줄을 구분해야 한다.

하지만 하나의 라이브러리에서 다른 패키지나 모듈을 적는 것은 허용한다.

 

# Standard library imports
import os
import sys

# Related third party imports
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

# Local application/library specific imports
from local_module import local_class
from local_package import local_function

import는 모듈의 주석이나 docstring 뒤, 최상단에 위치해야 한다.

import는 standard - third party - local 순서대로 그룹화 해야한다.

 

# Absolute imports
import mypkg.sibling
from mypkg import sibling
from mypkg.sibling import example

# Relative imports
from . import sibling
from .sibling import example

절대 경로를 사용하여 import하는 것을 권장한다.

가독성이 증가하고, 에러 발생 시 에러 메시지의 형태가 좋아지기 때문이다.

단, 절대 경로가 복잡해지면 상대 경로를 사용하는 것이 가능하다.

보통 표준 라이브러리 코드는 복잡한 경로를 피하고 절대 경로를 사용하는 것을 따른다.

 

# local name clashes
from  myclass import MyClass
from foo.bar.yourclass import MyClass

# class-containing module
import myclass
import foo.bar.yourclass

mc = myclass.MyClass
yr = foo.bar.yourclass.MyClass

클래스를 포함한 모듈에서 클래스를 가져올 때, from 모듈 import 클래스의 형태를 따른다.

이때 클래스명 충돌이 발생한다면, import만을 사용하여 모듈.클래스명의 형태로 명시하여 사용한다.

Wildcard imports(*)는 정확하게 무엇을 사용할지 헷갈리기 때문에, 사용하지 않아야 한다.

 

 

참고한 문서 자료: https://peps.python.org/pep-0008/

 

0. import library - logging

import logging

logger = logging.getLogger(__name__)

애플리케이션에서 발생하는 이벤트를 추적하고 기록하는 라이브러리다.

getLogger를 사용하여 로거를 불러와 레벨(단계)에 따른 수행을 진행할 수 있다.

정말 쉽게 말하면, 중간에 동작이 제대로 하는지 print문을 찍는 행위라고 생각하면 편하다.

 

logger.debug('debug text')
logger.info('info text')
logger.warning('warning text')
logger.error('error text')
logger.critical('critical text')

DEBUG, INFO, WARNING, ERROR, CRITICAL의 다섯 가지 레벨을 제공한다.

위처럼 원하는 레벨을 선택하고, 그 안에 출력하고자 하는 스크립트를 작성하면 끝이다.

아무 것도 설정하지 않는다면 terminal에, 설정해준다면 설정한 공간에 해당 스크립트를 출력한다.

 

레벨(단계, 수준) 사용하는 상황
DEBUG(10) 상세한 정보를 확인하거나, 문제를 진단할 때만 사용한다.
INFO(20) 예상대로 동작하는지 확인할 때 사용한다.
WARNING(30) 예상치 못한 일이 발생하거나 가까운 미래에 발생할 문제에 대해서 표시한다.
해당 이벤트가 발생하더라도 소프트웨어는 계속해서 동작한다.
logger method의 default 값이다.
ERROR(40) 더욱 심각한 문제로 인해 소프트웨어가 일부 기능을 수행하지 못할 떄 사용한다.
CRITICAL(50) 심각한 에러로 프로그램 자체가 계속 실행하지 않을 수도 있을 때 사용한다.

다섯 가지 레벨을 위와 같은 상황에 사용한다.

위의 메소드는 logging 라이브러리의 logger 구성 요소에 대한 설명이다.

로그 레벨을 설정하면, 해당 레벨 또는 그보다 높은 레벨의 메시지만 기록하도록 제한한다.

 

로깅 인터페이스를 제공하며 애플리케이션 코드에서 직접 사용 가능한 logger 외에,

파일, 콘솔, 네트워크 등으로 다양한 출력 대상을 골라 로그 메시지를 보내는 handler,

로그 메시지의 최종 출력 형식을 결정하는 formatter도 존재한다.

해당 사항은 python docs를 참고하자.

 

 

참고한 문서 자료: https://docs.python.org/ko/3/library/logging.html

참고한 문서 자료: https://docs.python.org/ko/3/howto/logging.html

참고한 웹 사이트: https://beenje.github.io/blog/posts/logging-to-a-tkinter-scrolledtext-widget/

 

0. absolute path and relative path

ModuleNotFoundError: No module named 'playground'

Absolute path(절대 경로)로 local 파일을 호출할 때 발생한 에러다.

아무리 찾아보고 해결할 수 없어 상대 경로로도 테스트 해보았다.

 

ImportError: attempted relative import with no known parent package

Relative path(상대 경로)로 local 파일을 호출할 때 발생한 에러다.

부모 패키지에 대해서 접근을 시도할 때 애초에 에러가 발생한다.

 

if __name__ == "__main__" and __package__ is None:
    __package__ = "expected.package.name"

이유는 간단했다. 파이썬 인터프리터가 자기보다 상위의 디렉토리를 인식할 수 없기 때문이다.

해결 방법도 간단하다. 실행 파일과 호출하려는 local 파일을 모두 감싸는 상위 폴더에서 호출하면 된다.

 

예를 들어 main 폴더 아래에 folder1 폴더와 folder2 폴더가 있다고 해보자.

그리고 folder1에는 sub1.py 파일이, folder2에는 sub2.py 파일이 있다.

sub1.py에서 sub2.py의 함수를 호출하면 에러가 발생한다.

이때 main에 main.py를 만들고 sub1.py를 import해서 실행하면 정상적으로 동작한다.

sub1.py 상에서는 여전히 warning 표시가 뜨지만, main.py에서는 두 폴더 모두 인식하기에 동작 가능하다.

이에 대한 자세한 사항으로는 PEP 366을 참고하자. 

 

 

참고한 문서 자료: https://peps.python.org/pep-0366/

참고한 웹 사이트: https://stackoverflow.com/questions/68960171/

 

0. tkinter sticky parameter

import tkinter as tk

root = tk.Tk()
widget = tk.Widget(root)
widget.grid(sticky="nsew")

grid 혹은 pack 배치 관리자로 위젯의 위치를 결정할 때,  tkinter의 sticky 파라미터를 사용한다.

Sticky를 통해 해당 셀 내에서 위젯의 위치를 정확하게 조절할 수 있다.

Sticky 옵션은 위젯이 부모 컨테이너 내에서 어느 방향으로 붙어야 하는지 지정한다.

각각 동(e), 남(s), 서(w), 북(n)의 방향을 나타내는 문자를 포함한다.

대소문자 여부는 상관 없으며, default는 빈 문자열이다. 

 

 

참고한 문서 자료: https://www.pythontutorial.net/tkinter/tkinter-grid/

 

0. contextlib.contextmanager

with open("example.txt", "r") as file:
    content = file.read()
    print(content)

파이썬에서 with 구문은 리소스 관리를 위해 사용한다.

with 구문은 파일, 네트워크 연결, 데이터베이스 세션 등의 리소스 사용이 끝난 뒤 자동으로 해제한다.

하지만 파일이 존재하지 않거나 세션에서 에러가 발생한다면, 에러 예외 처리 코드를 따로 작성해줘야 한다.

이렇게 되면 가독성이 나빠질 뿐더러, 무엇을 하고자 하는지 한 눈에 파악하기 힘들다.

이런 잡다한 코드(context)를 contextmanager를 통해 관리(manage)할 수 있다.

따라서 contextmanager는 주로 with 구문과 함께 사용한다.

 

import contextlib


@contextlib.contextmanager
def temp_chdir(new_dir):
    old_dir = os.getcwd()
    os.chdir(new_dir)
    try:
        yield
    finally:
        os.chdir(old_dir)
        
        
with temp_chdir(static_dir):
    self.root.call("lappend", "auto_path", f"[{themes_dir}]")
    self.root.eval("source themes/pkgIndex.tcl")

현재 해석 및 구현하고 있는 modlunky2의 실제 코드를 가져왔다.

자세한 해석은 밑에서 진행하고 우선 with 구문만을 보자.

temp_chdir 함수인 걸 보니, static_dir로 디렉터리 경로를 변경하는 코드같다.

이때 에러가 발생한다든지, 현재 경로를 따로 저장해두어야 한다든지의 처리를 해주어야 한다.

그런 처리를 contextmanager로 변환한 temp_chdir에서 하는 것이다.

 

동작 순서는 yield 이전 구문 > with 내부 구문 > yield 이후 구문 > with 이후 구문 순서다.

코드를 해석해보면, 현재 디렉터리를 old_dir에 저장하고 new_dir로 작업 디렉터리를 변경한다.

new_dir에서 self.root.call과 self.root.eval 구문을 수행하고, 다시 원래 디렉터리로 돌아온다.

이렇게 특정 부분에서만 역할을 바꾸는 느낌으로 사용할 수도 있다.

 

이때 try - finally 구문을 이용해서 에러 측면에서 신경 쓸 수 있다.

finally는 에러가 발생해도 반드시 실행을 하는 예외 처리 문법이다.

따라서 에러가 발생할 수도 있는 코드에서, 반드시 실행해야만 하는 코드가 있다면 try - finally를 사용하면 된다.  

 

import contextlib
import logging
import time

logger = logging.getLogger(__name__)


@contextlib.contextmanager
def timing():
    t0 = time.time()
    yield
    t1 = time.time()
    logger.debug(f"{t1-t0}")
    
    
with timing():
    ''' something code '''

그밖에도 시간 계산 용도로 사용할 수 있다.

코드를 따라가보면 timing()을 호출하고 t0에서 현재 시각을 저장한다.

그리고 yield로 timing()에서 나온 뒤 ''' something code ''' 부분을 수행한다.

그러고 yield 뒷부분 코드를 실행하는데, 이때 t1으로 현재 시각을 저장하고 그 차이를 logger로 찍는다.

 

import contextlib
import logging

logger = logging.getLogger(__name__)


@contextlib.contextmanager
def ignoring(*exceptions):
    try:
        yield
    except exceptions:
        logger.warning(exceptions)
        pass
        
    
list_ = [1]

with ignoring(IndexError, ZeroDivisionError):
    index10 = list_[10]
    indeterminate = 3 / 0

위에서 에러 발생 처리에 대한 try-finally를 짤막하게 언급했었다.

이 코드는 에러 무시 용도로 사용하는 또다른 예시이다.

 

ignoring() 함수를 contextmanager로 만들어서 예외를 받는다.

이때 exceptions를 여러 예외 타입을 매개변수로 받을 수 있게 *을 이용하여 선언한다.

IndexError, ZeroDivisionError를 인자로 건네주면 try에서 바로 yield로 빠져나온다.

list_[10]은 접근할 수 없는 위치이기에 except 구문으로 넘어간다.

발생한 예외 종류를 출력하고 pass로 다음 라인을 실행한다.

다음 라인은 3 / 0으로 0으로 나눌 수 없는 에러가 발생한다.

그러면 다시 except 구문으로 넘어가 ZeroDivisionError를 발생하고 pass를 실행한다.

 

결국 with 구문을 실행하되 ignoring()으로 넘겨준 에러는 무시하고 실행하라는 거구나!라고 해석할 수 있다.

하지만 넘겨주지 않은 에러, 가령 NameError 같은 경우에는 예외가 발생한다.

 

class ManagedFile:
    def __init__(self, file_name):
        self.file_name = file_name
        
    def __enter__(self):
        self.file = open(self.file_name, "r")
        return self.file
        
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
            

with ManagedFile("example.txt") as file:
    content = file.read()
    print(content)

손수 contextmanager의 역할을 구현할 수도 있다.

이때 __enter__와 __exit__의 두 가지 메소드를 구현해야 한다.

__enter__가 yield 이전 부분, __exit__이 yield 이후 부분에 해당한다.

 

__enter__ 메소드는 with 블록 진입 시 호출된다. 리소스를 할당하고 초기화하는 역할을 수행한다.

이 메소드의 반환 값은 with 구문의 as 부분에서 지정한 변수에 할당한다.

__exit__ 메소드는 with 블록을 벗어날 때 호출된다. 리소스를 해제하는 정리 작업을 수행한다.

이 메소드는 3가지의 인자(발생한 예외 타입, 예외값, 예외 traceback 정보)를 받는다.

만약 예외가 발생하지 않았다면 모든 인자는 None이다.

 

 

참고한 문서 자료: https://docs.python.org/ko/3/library/contextlib.html

참고한 문서 자료: https://docs.python.org/3/library/stdtypes.html#context-manager-types

참고한 문서 자료: https://docs.python.org/3/reference/datamodel.html#context-managers

참고한 문서 자료: https://docs.python.org/3/reference/compound_stmts.html#with

참고한 문서 자료: https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager

참고한 웹 사이트: https://www.youtube.com/watch?v=txDg45IsC9A

 

0. tk와 ttk

import tkinter as tk

root = tk.Tk()
button = tk.Button(root, text="Click me")
button.pack()
root.mainloop()

tk(Tkinter)

1. Tkinter의 기본 위젯 집합으로 Python을 설치할 때 기본적으로 포함하는 구성 요소

2. 비교적 간단하며 기본적인 GUI에 최적화, 사용자가 직접 세밀하게 스타일을 조정하는 것이 제한적

3. 따라서 기본 GUI 요소로 간단하게 구현할 때 적합

 

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
button = ttk.Button(root, text="Click me")
button.pack()
root.mainloop()

ttk(Themed Tk)

1. Themed Tk의 약자로 Tkinter의 확장 모듈, 표준 위젯보다 더 나은 일관된 스타일을 제공

2. tk에 비해 더 많은 스타일링 옵션(테마, 글꼴, 색상 등)을 제공하여 사용자가 조정하기 용이

3. 다양한 OS에서 비슷한 외관을 유지하며, 플랫폼 간 일관성을 갖추기에도 적합

 

0. tkinter widget - grid, pack, place

Tkinter에는 위젯을 배치하기 위한 기하 관리자(geometry manager)가 존재한다.

grid(), pack(), place()의 세 가지 관리자가 존재하는데, 각 관리자는 중복해서 사용할 수 없다.

 

import tkinter as tk

root = tk.Tk()

label1 = tk.Label(root, text="Label 1", bg="red")
label2 = tk.Label(root, text="Label 2", bg="green")

label1.grid(row=0, column=0, sticky="nsew")
label2.grid(row=0, column=1, sticky="nsew")

root.grid_columnconfigure(0, weight=1)
root.grid_columnconfigure(1, weight=1)
root.grid_rowconfigure(0, weight=1)

root.mainloop()

grdi() 기하 관리자는 위젯을 격자 형태로 배치한다.

행(row)과 열(column)을 지정하여 위치를 설정하고, 복잡한 레이아웃을 디자인할 때 유용하다.

 

row: 위젯을 배치할 행 번호

column: 위젯을 배치할 열 번호

sticky: 위젯을 셀 내에서 어떻게 확장할지 지정, NSEW를 조합하여 사용

padx, pady: 위젯 주위의 수평(padx) 및 수직(pady) 여백

 

import tkinter as tk

root = tk.Tk()

label1 = tk.Label(root, text="Label 1", bg="red")
label2 = tk.Label(root, text="Label 2", bg="green")

label1.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
label2.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=10, pady=10)

root.mainloop()

pack() 기하 관리자는 위젯을 부모 컨테이너 내에서 수직이나 수평으로 스택하는 형태로 배치한다.

각 위젯을 순서대로 배치하며, 간단한 레이아웃을 만들 때 유용하다.

 

side: 위젯을 부모 컨테이너 내에서 배치할 방향(tk.TOP, tk.BOTTOM, tk.LEFT, tk.RIGHT)

fill: 위젯을 할당한 공간을 어떻게 채울지 지정(tk.NONE,tk.X, tk.Y, tk.BOTH)

expand: 위젯이 부모 컨테이너의 남은 공간을 차지하도록 확장하는 여부(True는 전부 채움)

padx, pady: 위젯 주위의 수평(padx) 및 수직(pady) 여백

 

import tkinter as tk

root = tk.Tk()

label1 = tk.Label(root, text="Label 1", bg="red")
label2 = tk.Label(root, text="Label 2", bg="green")

label1.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=10, pady=10)
label2.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=10, pady=10)

root.mainloop()

place() 기하 관리자는 위젯을 정확한 위치에 배치하는 절대 배치 관리자이다.

x, y 좌표를 사용하여 위젯의 위치를 지정한다. 크기(width, height)도 설정할 수 있다.

 

x, y: 위젯의 좌상단 모서리 좌표

width, height: 위젯의 너비와 높이

relx, rely: 부모 컨테이너의 너비와 높이에 대한 상대적인 위치(0.0부터 1.0까지)

relwidth, relheight: 부모 컨테이너의 너비와 높이에 대한 상대적인 너비와 높이(0.0부터 1.0까지)

 

 

참고한 공식 문서: https://www.astro.princeton.edu/~rhl/Tcl-Tk_docs/tk/winfo.n.html

참고한 웹 사이트: https://edukoi.tistory.com/108

 

0. winfo_width(), winfo_reqwidth()

import tkinter as tk

root = tk.Tk()

label = tk.Label(root, text="Hello, Tkinter!")
label.pack()

print("Actual width:", label.winfo_width())

root.mainloop()

winfo_width()

현재 위젯의 실제 너비를 반환한다.

바꾸어 말하면 위젯이 화면에 표시된 지금, 할당된 실제 크기를 의미한다.

 

import tkinter as tk

root = tk.Tk()

label = tk.Label(root, text="Hello, Tkinter!")
label.pack()

print("Requested width:", label.winfo_reqwidth())

root.mainloop()

winfo_reqwidth()

위젯이 요청하는 너비를 반환한다. 즉 위젯의 내용이나 구성 요소에 따라 바뀐다.

바꾸어 말하면 위젯이 정상적으로 모든 내용을 표시하기 위해 필요한 최소 너비를 의미한다.

 

import tkinter as tk

def print_width(widget):
    print("Requested width:", widget.winfo_reqwidth())
    print("Actual width:", widget.winfo_width())

root = tk.Tk()

label = tk.Label(root, text="Hello, Tkinter!")
label.pack()

# 1. update_idletasks()를 사용하여 레이아웃 업데이트 강제
root.update_idletasks()
print_width(label)

# 2. after() 메서드를 사용하여 이벤트 루프 이후에 호출
root.after(100, print_width, label)

root.mainloop()

종종 winfo_width()나 winfo_height()를 사용하는 경우에 return이 1인 경우가 있다.

이는 크게 2가지 이유로 발생할 수 있다.

1. 위젯을 아직 배치하지 않았다.

2. 이벤트 루프를 아직 실행하지 않았다.

 

이를 위한 해결 방법도 크게 2가지가 존재한다.

1, update_idletasks(): Tkinter가 대기 중인 모든 작업을 처리하고 레이아웃을 업데이트한다.

2. after(ms, func, *args): 이벤트 루프 실행 후, ms밀리초 후에 func에 해당하는 함수를 호출한다.

 

 

참고한 웹 사이트: https://stackoverflow.com/questions/34373533/

참고한 웹 사이트: https://tkinter-discuss.python.narkive.com/8qi5jSjA/winfo-width-problem

 

0. Optional

ㅇㅇ

 

0. descriptor '__init__' requires a 'super' object but received a 'Frame'

# Wrong code
super.__init__(parent)

# Correct code
super().__init__(parent)

말그대로 super() 함수를 호출해야 하는데, 잘못 호출한 경우에 발생하는 오류다.

recevied a 'Frame' 오류가 뜬 이유는 던져준 parent 매개변수가 Frame Class이기 때문이다.

 

0. korean-regex

ㅇㅇ

 

0. "" in text is True

text = ['ㅏ', 'ㅑ' ,'ㅓ', 'ㅕ']
find = ""
print(find in text)	# False

text = 'ㅏㅑㅓㅕ'
find = ""
print(find in text)	# True

와 이건 뭐지...?

 

0. SyntaxError: f-string: unmatched '('

FUZZY = "__{}__".format(int("fuzzy", 36))

 

한국어 검색 기능을 위한 korean-reg-exp 구현을 위해 코드를 작성 중이었다.

위 해당 코드를 작성하고 나서 발생한 오류다.

 

OK --> f"hello ' this is good"
OK --> f'hello " this is good'
ERROR --> f"hello " this breaks"
ERROR --> f'hello ' this breaks'

큰따옴표와 작은따옴표를 구분하여 f-string 문법을 작성하여야 한다.

MySQL에서 문자열 내에 따옴표를 작성하는 느낌과 비슷하다.

f-string을 큰따옴표로 작성한다면, 내부에는 작은따옴표만을 혹은 그 반대로 써야 한다.

그렇지 않으면 같은 따옴표끼리 묶여 뒷부분의 문자열은 인식하지 못할 수 있다.    

 

참고한 웹 사이트: https://stackoverflow.com/questions/67540413/f-string-unmatched-in-line-with-function-call

 

0. Python GUI IME

import sys
from PyQt5.QtWidgets import QApplication, QLineEdit, QVBoxLayout, QWidget
from PyQt5.QtGui import QInputMethodEvent

class KoreanInputLineEdit(QLineEdit):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def inputMethodEvent(self, event: QInputMethodEvent):
        super().inputMethodEvent(event)
        commit_string = event.commitString()
        preedit_string = event.preeditString()
        text = self.text()
        # print(f"Commit: {commit_string}, Preedit: {preedit_string}, Current Text: {text}")
        print(f"{text}{preedit_string}")
        
    def keyPressEvent(self, event):
        super().keyPressEvent(event)
        text = self.text()
        print(f"Key Pressed: {text}")

class KoreanInputWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.layout = QVBoxLayout()

        self.line_edit = KoreanInputLineEdit(self)
        self.layout.addWidget(self.line_edit)

        self.setLayout(self.layout)
        self.setWindowTitle('Korean Input with PyQt')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = KoreanInputWidget()
    ex.show()
    sys.exit(app.exec_())

ㅇㅇ

 

0. tkinter widget - command

self.game_count_label = ttk.Label(self, text="0판")
        self.game_count_label.grid()
        self.game_count = ctk.CTkSlider(
            self, from_=1, to=5, number_of_steps=5, command=self.update_label
        )
        self.game_count.grid()
        
        self.game_count.bind("<Motion>", self.update_label)

command 옵션을 사용한 함수는 슬라이더의 값을 매개변수로 받는다. 만약 이벤트 매개변수를 사용하고 싶다면, bind 메서드를 사용. 하지만 실시간 업데이트를 위해서는 command 옵션을 사용하는 것이 더 적합하다

 

def update_label(self, value):
    self.game_count_label.config(text=f"{str(int(value))}판")

def update_label(self, event):
    value = self.game_count.get()
    self.game_count_label.config(text=f"{value}판")

self update label 함수 매개변수

 

0. async, hypercorn library

00

 

0. WinError 225, Rust rc.exe Not Found

ㅇㅇ

 

ㅇd

 

dd

 

https://github.com/fluent/fluent-bit-docs/issues/427

 

Windows compilation error: Missing rc.exe · Issue #427 · fluent/fluent-bit-docs

Hi. I'm trying to follow https://github.com/fluent/fluent-bit-docs/blob/master/installation/windows.md#compile-from-source, but I think there's a setup step missing. When I follow the instructions ...

github.com

dd

 

dd

 

dd

 

ㅇㅇ

 

0. WARNING: lib not found

C:\Users\kjmin\AppData\Roaming\Python\Python310\site-packages\pywin32_system32에 있는

pythoncom310.dll 파일과 pywintypes310.dll 파일을

C:\Users\kjmin\AppData\Roaming\Python\Python310\site-packages\win32 폴더로 복사

 

참고한 웹 사이트 : https://stackoverflow.com/questions/65573140/importerror-no-system-module-pywintypes-pywintypes39-dll

 

0. Pyinstaller Module Not Found

00

 

ㅇㅇㅇㅇㅇㅇ : https://catloaf.tistory.com/66

ㅇㅇㅇㅇㅇㅇ : https://blog.naver.com/blueqnpfr1/221582265508

ㅇㅇㅇㅇㅇㅇ : https://stackoverflow.com/questions/53299868/pyinstaller-hidden-imports-not-included

ㅇㅇㅇㅇㅇㅇ : https://stackoverflow.com/questions/65573140/importerror-no-system-module-pywintypes-pywintypes39-dll

ㅇㅇㅇㅇㅇㅇ : 

 

0. Pyinstaller Hidden Import

00

 

검색어 : pyinstaller hidden-import not found

dddddd : https://nashorn.tistory.com/entry/PyInstaller에서-sklearn-연관-문제-해결-방

 

 

0. Pyinstaller 'VERSION' FileNotFoundError

00

 

0. AttributeError, argparse library

00

 

0. 00

00

 

(모드렁키 코드 해석 중)

(개요 및 아이디어 작성 후 수정 중)

 

AI 모델링이 전부가 아니었다.

AI는 역시나 분석을 하기 위한 도구 중 하나라는 것을 확실히 짚고 간다.

모델링과 학습은 프로젝트의 극히 일부였고, 최종 목표는 exe 배포다.

정확하게 말하자면 내가 쓰려고 만드는 김에, 겸사겸사 exe를 만들어서 배포 및 업데이트 관리를 하려는 거지만.

 

아무튼 tkinter 기반으로 쉽게 예측할 수 있게끔 만드려고 한다.

언제까지 불안전한 놀이터 예측을 위해서, anaconda 열고 jupyter notebook 열고 ipynb 파일 열고... 할 수 없지 않은가.

모델이야 pkl 파일로 저장해서 사용하면 문제가 없고,

주어지는 변수들이야 data leakage 확인해서, 데이터별 모델을 따로 만들면 해결 가능한 문제다.

 

하지만 정확하게 어떤 모델을, 어떤 변수를 학습해서, 얼마나 만들지 구체화 해야 한다.

(AI에 비유하자면 threshold를 찾아야 하는 느낌이랄까)

동시에 입력할 변수들을 어떻게 보기 좋게 구상할 것이고,

exe의 전체적인 GUI는 어떻게 만들지 이게 가장 핵심이자 걸림돌이 되는 부분이었다.

살짝 구상을 해봤는데... 이게 상당히 규모가 커지는 느낌이다.

이래서 어떤 프로젝트든 첫 구상, 기획이 가장 중요하다고 말하는구나 체감하는 중이다.

 

다른 프로젝트를 할 때는 어느 정도 개요를 잡고 시작한 것에 비해,

이번 프로젝트는 냅다 데이터 수집 및 AI 모델링부터 들이박았더니 확실히 돌아가는 느낌이다.

SDLC의 나선형 모형으로 개발한다고 좋게좋게 생각하려 한다. 이런 방법에서는 나름대로 배울 게 있는 법이다.

 

아 가장 중요한 것. 이번 프로젝트는 각잡고 OOP로 짠다.

언제까지고 전역 변수(global)에다가 함수만 작성해서 짤 수 없다. 도전을 해야 발전이 있는 법이다.

Overlunky 파일도 뜯어보면서, class, function을 적재적소에 구분해서 잘 짜보자.

생각보다 장기 프로젝트에 진입할 것 같다는 느낌이 왔다.

목표는 네이버가 치지직 포인트를 개발하기 전까지.

언제 추가해줄지 모르니 최대한 빨리 개발하라고 몰아붙이는 중이다.

 

링크 : https://www.youtube.com/watch?app=desktop&v=e7eRonTN8DI

검색 : gamble project using tkinter

아이디어 : 경기장(track)을 텍스트가 아니라, 사진도 같이 제공?

 

링크 : https://gagbestov.life/product_details/116021835.html

검색 : gamble project using tkinter

아이디어 : 기존 경기 내역을 확인할 수 있는 DB도 같이 제공?? > 결과 예측에 도움이 될 수도

 

링크 : https://github.com/topics/soccer-matches?l=python

검색 : sport betting project using tkinter

아이디어 : 값을 입력하는 게 아니라, 직접 선택하는 형식으로? > 그럼 더 빨리 고를 수 있지 않나?

 

링크 : https://www.si.com/fannation/soccer/futbol/video/epl-table-based-on-100-year-football-manager-2024-simulation

검색 : sport betting project using tkinter

아이디어 : 맵 DB를 이런 식으로 승률순으로 보여주는 것이 더욱 직관적

 

검색 : sport betting project using tkinter

아이디어 : 와 이건 진짜 토토 사이트 같은데, 가져올 UI/UX가 있을지도

 

기능 : 사용자 입력에 반응하여 아이템을 필터링하는 실시간 검색 기능

알고리즘 : 트라이 자료 구조 기반 검색 기능 구현

 

기능: 컵, 그랑프리 검색 기능 구현

 

아이디어 : 그려본 화면(24.04.01)

 

아이디어 : Database 화면을 구상(24.06.06), 그동안의 옥냥이 경주 기록(흔적)을 확인할 수 있는 데이터베이스

아이디어 : 텍스트와 숫자가 잔뜩 있으면 보기 힘드니, 우측에 3개의 중요한 그래프를 띄울 예정

아이디어 : (위에서부터) N판 합계 R등 성공/실패 그래프, 토토 결과 정배/역배 그래프, 최근 5판 게임 등수 그래프

 

링크 : https://www.youtube.com/watch?v=KmWndUSbUMo

검색 : betting tracker dashboard

아이디이 : Tracker 화면 참고(24.06.06), 개인이 어떻게 투자했는지에 대한 결과 tracking 탭(추가할지는 미지수)

 

0. 개발 우선 순위

- 프로토타입 GUI 구상

     └ 사용 툴 : Adobe Illutrator CS6 64bit (mock-up 방식으로 진행)

     └ 상단 카테고리 구분하기: 예측(불안전한 놀이터) / DB(일대기?) (옥냥이식으로 직관적인 이름 짓기)

- AI 모델링

     └ prefix rank, odds 데이터 등이 있는 버전과 없는 버전 구분하기 (data leakage 고려)

     └ 분류, 회귀 모델 구분하기

- exe 파일 제작

     └ OOP와 design pattern 준수

     └ 맵 및 데이터 검색 기능 구현 (trie 자료 구조)

     └ 등수나 맵을 입력하는 방식이 아니라 선택하는 방식 (check button)

- 옥냥이 마리오카트 방송 데이터 실시간 DB 추가 (장기 목표, 서두르지 말 것)

     └ 방송 데이터를 엑셀이나 csv로 만들기

     └ 만든 데이터를 자동으로 모델에 학습시켜서 업데이트하기 (RPA)

     └ 깃허브 코드 수정, 모델 학습, exe 업데이트까지 한 번에 자동화 (최종 유지 관리 목표)

0.  수정 사유 및 기존 값

생각해보니 학습을 잘못 시켰다

data leakage 이

(가장 원본 데이터 feature importance 점수)

 

drop_columns = [
    'index',
    'date',
    'round',#
#    'game_count',
#    'game_goal',
#    'cur_game_count',
    'win_odds',#
    'lose_odds',#
    'track_E',
#    'cc',
#    'part_people',
#    'rank',
#    'prefix_rank',#
    'odds_result',
    'significant',
#    'RESULT',
#    'track_E_encoded',
    'odds_result_SU',#
    'odds_result_UD',#
]

 

0. PCA 이전

scoring method별 clf 모델 점수 평가

 

clf 모델별 scoring method 점수 평가

 

가장 성능이 좋았던 모델들 점수 확대

 

scoring method별 reg 모델 점수 평가

 

reg 모델별 scoring method 점수 평가

 

가장 성능이 좋았던 모델들 점수 확대

 

feature_importance

 

0. PCA = 2

ㅇㅇㅇ

 

ㅇㅇ

 

ㅇㅇ

 

ㅇㅇ

 

0. PCA = 3

ㅇㅇ

 

ㅇㅇ

 

ㅇㅇ

 

ㅇㅇ

 

RTC-mariokart-8-AI/3_select_model.ipynb at main · miny-genie/RTC-mariokart-8-AI

RTC(RoofTopCat)'s Mariokat 8 deluxe(nintendo switch platform) predicting AI model - miny-genie/RTC-mariokart-8-AI

github.com

현재 작성하는 코드들은 위의 깃허브 링크에 커밋해두었다.

수정 및 개발을 진행하다보니 일부 코드가 달라진 점이 있을 수 있다.

 

1. 모델 선정

# "clf_lgbm": LGBMClassifier() > 성능 문제로 인한 제외
# "clf_svc": SVC(probability = True) > 성능 문제로 인한 제외

clf_model_dict = {
    "clf_ridge": LogisticRegression(penalty='l2', solver='lbfgs'),
    "clf_lasso": LogisticRegression(penalty='l1', solver='liblinear'),
    "clf_logistic_regression": LogisticRegression(),
    "clf_logistic_regression_": LogisticRegression(solver='liblinear'),
    "clf_adaboost": AdaBoostClassifier(),
    "clf_gradient_boosting": GradientBoostingClassifier(),
    "clf_random_forest": RandomForestClassifier(),
    "clf_xgb": XGBClassifier(use_label_encoder=False, eval_metric='logloss'),
    "clf_catboost": CatBoostClassifier(verbose=0),
    "clf_decision_tree": DecisionTreeClassifier(),
}

테스트할 분류 모델들을 전부 모아놨다.

Regression의 Ridge와 Lasso에 해당하는 모델은 각각 LogisticRegression의 penalty 매개변수로 설정했다.

penalty를 l2로 설정하면 Ridge와 유사하게, penalty를 l1으로 설정하면 Lasso와 유사하게 사용할 수 있다.

LogisticRegression은 solver 매개변수를 사용하여 소규모 데이터셋에 적합한 liblinear 유무에 따라 2가지를 사용한다.

 

LGBMClassifier 모델과 SVC 모델은 테스트한 결과 모든 점수가 0에 가깝게 나왔다.

두 모델은 우선 주석 처리를 해놓았다. 추후 상황에 따라 모델 선정을 결정한다.

 

# "reg_lgbm": LGBMRegressor() > 성능 문제로 인한 제외

reg_model_dict = {
    "reg_ridge": Ridge(),
    "reg_lasso": Lasso(),
    "reg_linear_regression": LinearRegression(),
    "reg_adaboost": AdaBoostRegressor(),
    "reg_gradient_boosting": GradientBoostingRegressor(),
    "reg_random_forest": RandomForestRegressor(),
    "reg_xgb": XGBRegressor(),
    "reg_catboost": CatBoostRegressor(verbose=0),
    "reg_decision_tree": DecisionTreeRegressor(),
    "reg_svr": SVR(),
}

테스트할 회귀 모델들을 전부 모아놨다.

분류처럼 회귀에서도 성능이 0에 가깝게 나온 모델은 우선 제외하고 계산했다.

 

2. 모델 학습

def fit_model(
    model: BaseEstimator,
    X: pd.DataFrame,
    y: pd.Series,
    test_size: float = 0.2,
    random_state: int = 42
) -> tuple[BaseEstimator, pd.DataFrame, pd.Series, pd.DataFrame, pd.Series]:

    X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size = test_size,
        random_state = random_state
    )
    model.fit(X_train, y_train)
    
    return model, X_train, X_test, y_train, y_test

모델과 종속 변수, 독립 변수를 매개변수로 입력한다.

그리고 학습한 모델, train과 test를 나눈 데이터셋들을 반환하는 fit_model() 함수 선언 후 사용한다.

이때 PEP, type hint에 유의하면서 함수를 작성한다.

 

def eval_clf_model(model: BaseEstimator, X: pd.DataFrame, y: pd.Series) -> tuple[float]:
    y_pred = model.predict(X)
    y_proba = model.predict_proba(X)[:, 1]

    accuracy = accuracy_score(y, y_pred)      # 정확도
    precision = precision_score(y, y_pred)    # 정밀도
    recall = recall_score(y, y_pred)          # 재현율
    f1 = f1_score(y, y_pred)                  # F1 점수
    roc_auc = roc_auc_score(y, y_proba)       # ROC-AUC 점수
    
    return accuracy, precision, recall, f1, roc_auc

분류(classifier) 모델들을 평가하는 함수의 수도(pseudo) 코드다.

당연하게도 회귀(regression) 모델 점수 평가는 다르기 때문에 함수를 따로 작성해야 한다.

 

추가 주의사항으로 CatBoost 코드는 따로 작성해야 한다.

CatBoost는 기본적으로 카테고리형 데이터를 처리하도록 설계한 모델이다.

특히 타겟 변수의 데이터 타입을 기반으로 예측 결과의 타입을 결정한다.

즉 이진 분류에서 타겟 변수의 값이 문자열('True', 'False')이라면,

CatBoostClassifier predict() 메소드는, 예측 결과를 동일한 문자열 타입으로 반환한다.

그래서 문자열 처리에 대한 코드를 작성해주어야 한다.

 

def clf_cross_validation(
    model: BaseEstimator,
    X: pd.DataFrame,
    y: pd.Series,
    cv: int = 5
) -> tuple[list[float]]:
    
    # No1 cv
    cross_val_score()
    
    # No2 cv
    skfolds = StratifiedKFold(n_splits = 5, random_state = 42, shuffle = True)
    cross_val_score(cv=skfolds)
    
    # No3 cv
    skfolds = StratifiedKFold(n_splits = 5, random_state = 42, shuffle = True)
    skfolds.split(X, y)
    eval_clf_model(model, X_val, y_cal)
    
    return scores1, scores2, scores3

cross validation으로 모델 점수를 평가한다.

이때 사용하는 cross validation은 총 3가지 방법으로 진행한다.

1. sklearn.model_selection 라이브러리에서 지원하는 cross_val_score() 함수로 계산한다.

2. cross_val_score() 함수에 StratifiedKFold로 나눈 데이터를 집어넣어 계산한다.

3. StratifiedKFold로 나눈 데이터로 sklearn.metrics 라이브러리에서 지원하는 함수로 직접 계산한다.

 

함수 이름이 clf_cross_validation()인 이유는 분류와 회귀의 점수 측정이 다르기 때문이다.

분류(clf, classifier)는 accuracy, precision, recall 같은 방법으로 점수를 측정하지만,

회귀(reg, regression)는 r2, mae, mse, rmse 같은 방법으로 점수를 측정한다.

역할이 다르다고 판단하여 두 함수를 쪼개두었다.

 

LogisticRegression 모델을 clf_cross_validation로 평가한 점수를 시각화하면 이런 느낌이다.

점수의 최저점과 최고점을 보기 쉽게, 정렬한 뒤 그래프로 그렸다.

확실히 같은 cross_validation이라 하더라도, 계산 방식에 따라 미세한 점수 차이를 보인다.

 

종류에 따라서 시각화한 LogisticRegression 모델 점수이다.

상단의 꺾은선그래프는 한 fold의 전체 평균이라면,

바로 위의 히스토그램은 scoring method별 평균 점수에 해당한다.

 

cvs와 sfk는 각각 cross_val_score의 약어, stratifiedkfold의 약어로써 사용했고,

mix는 stratifiedkfold를 cross_val_score에 혼합하여 적용했다는 의미로 사용했다.

 

보기 좋게 scoring method끼리 묶어서, cross_validation 방법에 따라서 색상을 달리 해보았다.

전반적으로 StratifiedKFold로 나눈 데이터를 cross_val_score에 넣은 게 점수가 높다.

 

확실히 데이터의 불균형이 높아 accuracy와 roc_auc는 비교적 높게 나온다.

그에 비해 양성을 정확히 예측해야 하는 precision과 recall은 비교적 낮게 나온다.

물론 이 모델이 LogisticRegression이라는 것과 불균형에 대한 어떠한 전처리도 하지 않았다는 걸 감안해야 한다.

 

3. 분류(clf) 모델 점수 평가 및 시각화

각 모델별로 15개의 점수를 확인하여 데이터프레임을 작성했다.

위에서부터 5개씩 cvs, mix, skf 점수순이고, scoring method는 accuracy, precision, recall, f1, roc_auc 순이다.

 

scoring method별 clf 모델 점수 평가 그래프다.

전반적으로 상단에 분포한 직선이 좋은 성능을 발휘하는 모델이다.

당장 보기에 GradientBoosting, RandomForest, CatBoost, DecisionTree가 좋아보인다.

아무래도 트리 기반의 모델들이, 현재 데이터에서 좋은 성능을 보여주는 것 같다.

 

clf 모델별 scoring method 점수 평가 그래프다.

세로 grid를 기준으로 상단에 몰려있는 모델이 좋은 성능을 발휘하는 모델이다.

GradientBoosting, CatBoost, DecisionTree가 비교적 상단에 몰려있다.

 

성능이 좋게 나온 모델들 점수만을 뽑아서 그래프를 그려보았다.

상위권 모델 중에서도 GradientBoosting이 압도적인 예측 성능을 보인다.

DecisionTree와 CatBoost는 비등비등한 성적을 보인다.

recall 점수는 DecistionTree가 더 높지만, roc_auc 점수는 CatBoost가 더 높다.

 

4. 회귀(reg) 모델 점수 평가 및 시각화

마찬가지로 회귀(regression) 모델에 대한 점수 생성하여 데이터 프레임으로 만들었다.

위에서부터 4개씩 cvs, mix, skf 점수순이고, scoring method는 r2, mae, mse, rmse 순이다.

 

scoring method별 reg 모델 점수 평가 그래프다.

r2는 높을수록 좋고, mae, mse, rmse는 오차이기에 낮을수록 좋다.

하단에 꽤 많은 모델이 몰려있지만, LinearRegression, GradientBoosting, RandomForest가 성능이 좋다.

 

reg 모델별 scoring method 점수 평가 그래프다.

clf처럼 무언가 도드라지는 특징을 얻을 수 있을까해서 한번 그려보았다.

하지만 4가지의 scoring method가 같은 방향성을 지닌 게 아니기에 한 눈에 알아보기는 어렵다.

그나마 전반적으로 낮은 곳에 몰려있는 GradientBoosting, RandomForest가 눈에 띈다.

 

r2는 높으며 mae, mse, rmse가 낮은 모델 2개를 선정했다.

두 개의 그래프에서 공통적으로 보이는 RandomForest와 GradientBoosting 점수를 비교했다.

성능 면에서는 RandomForest가 미세하게 좋지만,

분류(classifier)와의 일관성을 생각했을 때 GradientBoosting도 괜찮아 보인다.

 

성능이 가장 좋았던 2개의 모델에 실제로 값을 예측해보았다.

hyphen(-)을 기준으로 왼쪽이 모델 예측값, 오른쪽이 실제 결괏값이다.

왼쪽 모델 예측값 중 왼쪽이 RandomForest 예측값, 오른쪽이 GradientBoosting 예측값이다.

예측과 실제 결괏값이 상당히 유사함을 알 수 있다. 이정도면 쓸만하다.

 

참고로 실제 결괏값으로는 train_test_split으로 나눈 test 셋을 사용하지 않았다.

2024년 2월 27일 마리오카트 유튜브본의 데이터를 새롭게 수집하여 예측에 사용했다.

불안전한 놀이터 결과는 없었지만, 순전히 등수를 예측함에 있어서는 더 좋으리라고 생각했다.

 

5. 추후 방향 및 유의점

 

- 함수들의 type hint 제대로 작성하기

- 그래프로 시각화하기

 

- track_E 숫자, 정도의 차이를 학습하지 않는 방법 모색

- 성능 평가 점수말고, feature_importance도 반드시 확인하기 > 불균형 정도

 

- K-fold에서 K 값 조절하기

- 시각화: K에 따라서, 점수에 따라서 > 꺾은선그래프

- 시각화: 각 모델별 평균치 > 산점도

 

- GridSearchCV로 모델 선정 후 하이퍼 파라미터 튜닝하기

- C(강도) 범위 최솟값, 최댓값 판단하기

- 각 파라미터 가중치 산출하기 

0. Import

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import warnings

warnings.filterwarnings(action='ignore')
plt.rc('font', family='Malgun Gothic')

시각화나 계산에 필요한 라이브러리를 호출했다.

가끔 가다가 폰트 인식 오류, 버전 관리 주의, 함수 삭제 등등의 '주의(warning)' 구문이 나온다.

jupyter notebook 환경에서 실행하였고, 이는 filterwarnings으로 주의 메시지를 무시할 수 있다.

 

또한 plt에서 한글 폰트를 인식하지 못하는 오류로, 직접 폰트를 지정해주었다.

해당 프로젝트에서 사용하는 한글 폰트는 '맑은 고딕'이다.

쓸모없는 꺽새나 문양 없이 깔끔한 고딕체이기에 사용했다.츄

 

1. Checking columns info

 

작성한 mariokart 데이터를 csv로 변환 후 데이터프레임의 정보를 확인했다.

컬럼들의 전체 크기(개수)가 동일한지, 의도한 대로 data type이 들어갔는지 확인하기 위함이다.

의도한 바로는 특이사항(significant) 같은 컬럼을 제외하고는 전부 int나 float여야 하는데 아니다.

또한 null 데이터, 즉 결측치 때문에 데이터 개수도 일정하지 않다.

데이터 전처리를 하면서 유심히 확인해보도록 하자.

 

2. Preprocessing - game_count

전처리 하기 전 game_count(좌), 전처리 하고 난 후 game_count(우)

전처리 하기 직전의 game_count 컬럼의 데이터 분포다.

under 3와 under 4 데이터 때문에 object 타입으로 들어간 모습이다.

under N이라는 것은 결국 N등 이내로 들어와야 한다는 말과 같다.

따라서 under가 있는 경우 under를 제거하는 방향으로 전처리를 진행했다.

astype으로 형변환하는 것도 잊지 말자.

 

전체적으로 데이터를 살펴보면서 발견한 문제점이 있다.

데이터 불균형이 극심하다는 것이다.

대부분의 놀이터가 그러하듯... 역배가 계속해서 터지면 그건 역배라고 부르지 않는다.

이 점을 유의하면서 우선 데이터를 계속해서 전처리 해보자.

 

3. Preprocessing - game_goal

전처리 하기 전 game_goal(좌), 전처리 하고 난 후 game_goal(우)

game_goal도 데이터 불균형이 상당히 크다.

거의 대부분의 상황에 '3판 합계 15등 이내'라는 놀이터가 열린다.

또한 마찬가지로 int를 예상했지만, object의 타입을 가진 game_goal 컬럼이다.

under와 유사하게 fix라는 데이터가 있었던 것을 까먹었다.

fix(N)의 경우 N등을 정확하게 맞추는 놀이터가 열린 경우다.

 

물론 방향성이 아주 조금 다른 느낌이기는 하다만...

해당 프로젝트의 목적은 '성공/실패를 판단하는 이진분류기'에 가깝다.

따라서 정확한 N등에 대한 정보는 필요 없어 drop 해주었다.

정확한 등수 예측 기능도 추가하긴 할 거다만, fix는 선택지가 3개 이상이기에 drop이 맞다고 판단했다.

해당 프로젝트에서 필요한 데이터는 성공/실패라는 2개의 선택지다.

 

내 예상과 다르게 흘러가는 게 상당히 마음에 들었다.

이래야 진짜 실전처럼 데이터를 전처리하고 다룰 수 있기 때문이다.

오히려 좋아.

 

4. Preprocessing - win_odds, lose_odds

columns info에서 win_odds와 lose_odds만 631개로 데이터가 1개? 2개? 적었던 것을 확인했다.

이를 근거로 어딘가에 결측치가 있으리라고 판단하여, isna()로 결측치 개수를 세주었다.

아니나 다를까 win_odds와 lose_odds에서 각각 1개의 결측치를 확인했다.

실제로 위의 1개의 결측치는 옥냥이가 유튜브에 올리지 않은 새벽에 달린 놀이터였다.

하지만 나는 그때 생방송을 보지 않고 있었고, 사관님께서도 방송 도중 놓쳐 생겨버린 결측치다.

 

배당에 의해 성공/실패 여부를 판가름하는 게 아닌,

성공/실패에 따라서 사람들의 투표가 몰려 배당이 정해진다고 생각한다.

즉 배당은 독립변수라고 판단하여, fill하지 않고, row를 제거하는 방향으로 처리했다.

 

X['win_odds'] = X['win_odds'].str.replace("..", "." , regex=False)

win_odds에서는 EDA 할 때 발견했던 missing value가 하나 있다.

실제 csv 파일을 만들 때, 12.20이 아니라 온점(.)을 2번 눌러버려서 12..20이라는 데이터가 생겼다.

이 떄문에 float로 형변환을 하지 못 하고 계속 에러가 떴었다.

이런 실수는 환영이다. 실제 데이터 오류이지 않은가? 이 또한 연습이다. 

 

replace를 사용하여 문자열을 대체했다.

하지만 replace의 문자열 대체는 정규표현식(RegEx)을 따라 동작한다.

정규표현식에서 온점(.)은 '어떤 문자든 상관없다'는 의미로써 사용한다.

즉 \.\.로 온점을 온점으로 사용하든지, regex=False로 정규표현식이 아님을 명시해야 한다.

 

X['win_odds'] = X['win_odds'].astype(float)
X['lose_odds'] = X['lose_odds'].astype(float)

win_odds와 lose_odds도 info에서 object임을 확인했었다.

이에 object 자료형을 float로 변경하는 것이 필요하다.

처음에 다른 컬럼들이 전부 int이니 int로 처리했는데, 이 컬럼은 소수 데이터가 있는 float다.

사소한 점 하나하나 주의하자.

 

5. Preprocessing - track_K, track_E

X = X.drop(["track_K"], axis=1)

track_K는 track_E가 있기에 크게 필요없는 컬럼이다.

따라서 지금 drop하든 나중에 feature_to_drop 과정에서 drop하든 상관이 없다.

사용자에 따라 처리하되, drop 해야 한다는 것만 잊지 말자.

 

from sklearn.preprocessing import LabelEncoder

label_encoder = LabelEncoder()
encoded = label_encoder.fit_transform(X['track_E'])
X['track_E_encoded'] = encoded

track_E 데이터는 LabelEncoder를 사용하여 전처리했다.

track_E_encoded에 인코딩한 데이터를 넣고, 나중에 track_K와 함께 drop하기로 했다.

track_E 데이터를 살려놓은 이유는 두 가지이다.

1. 어떤 문제나 변경점이 생길지 모르기에, 원본 데이터를 살려놓을 필요가 있다고 생각했다.

2. LabelEncoder로 인코딩 해버리면 숫자만이 남는데, 숫자로는 어떤 맵인지 즉각 알 수 없다.

    encoder.classes_로 살펴볼 수 있지만, 데이터프레임에서 직접 눈으로 보는 게 더 와닿아 살려두었다.

 

track_E_encoded 컬럼에 대한 데이터 분포를 시각화했다.

다른 컬럼들에 비해서 의외로 데이터 불균형은 적어보인다는 느낌이 들었다.

동시에 뭔가 고르게 나온다..? 그래프가 계단 형태를 띄는데 계단의 폭이 어느 정도 일정하다는 느낌이 있다.

이런 데이터는 불균형이 있다고 봐야하는지 없다 봐야하는지 아직은 잘 모르겠다.

 

데이터 불균형보다 실은 더 문제는 따로 있다.

문자열의 경기장 데이터를 숫자로 바꾸는 순간, 숫자들 간의 상관관계가 생길 우려가 있다.

우선 진행을 해보고, 문제가 생긴다면 다른 방법으로 처리하는 것도 고려해야 한다.

 

track_2_num = dict()
num_2_track = dict()

for num, track in enumerate(label_encoder.classes_):
    track_2_num[track] = num
    num_2_track[num] = track

위에서 언급한 대로 숫자만을 보고 어떤 경기장인지,

반대로 경기장 이름만을 보고 어떤 숫자인지 바로바로 나오기 어렵다.

따라서 dictionary로 찾을 수 있게끔 값을 저장해주었다.

 

6. Preprocessing - cc

전처리 하기 전 cc(좌), 전처리 하고 난 후 cc(우)

실제로 cc는 자동차의 배기량을 뜻하는 단위이다.

마리오 카트에서도 cc로 맵의 빠르기를 구분짓고 있다.

mirror는 Nintendo Switch 플랫폼의 mario kart 8 deluxe 기준으로 150cc 고정이다.

정확하게는 실제 인게임에서 맵 대칭만 적용이고, 속도만 150cc으로 동일하다.

따라서 mirror 데이터는 전부 150으로 전처리해주었다.

이번 컬럼이 아마 데이터 불균형이 정말 큰 경우가 아닐까...하는 생각이 든다.

 

7. Preprocessing - odds_result

win_odds, lose_odds 전처리 할 때 찾은 4개의 NaN 데이터가 있다.

 

뭔지 직접 찾아보니 실제 데이터에서는 ACC, CANCEL, NOT OPEN에서 결측치가 발생했다.

그 중 ACC 데이터는 정확하게 맞추는 fix 전처리, 즉 game_coal에서 이미 걸러졌다.

따라서 본래는 9개였지만, CANCEL, NOT OPEN의 4개만 남은 상황이었다. 

 

X = X.loc[~X['odds_result'].isna()]

배당 결과가 없다는 의미는 토토가 무산되었다는 말이다.

이런 경우는 넣은 포인트를 그대로 돌려받아 회수가 가능하다.

따라서 해당 row는 drop하여 처리한다.

 

from sklearn.preprocessing import OneHotEncoder

X['odds_result'] = X['odds_result'].replace("straight up", "SU").replace("underdog", "UD")

encoder = OneHotEncoder(sparse=False)
encoded = encoder.fit_transform(X[['odds_result']])

df_encoded = pd.DataFrame(encoded.astype(int), columns=encoder.get_feature_names_out())
X = pd.concat([X.reset_index(), df_encoded], axis=1)

 

전처리 이후 정배/역배를 적은 데이터는 OneHotEncoding으로 처리한다.

정배는 straight up을 줄여서 SU로, 역배는 under the dog를 줄여서 UD로 많이 쓴다.

OneHotEncoding을 해서 컬럼명이 길어지는 것을 방지하기 위해 2글자 약어로 바꿔주었다.

 

SU와 UD는 단 두 가지의 데이터이기 때문에 OneHotEncoding이어도 차원이 크게 늘어나지 않는다.

따라서 숫자와 상관관계가 생길 수 있는 LabelEncoding이 아닌 OneHotEncoding으로 처리했다.

 

이제는 빠지면 섭한 데이터 불균형이다.

그래도 불균형이 심한 편은 아니라고 생각한다.

 

8. Preprocessing - significant

X = X.loc[X['significant'].isna()]

significant의 특이사항들(CANCEL, MISS, NOT OPEN 등)을 제외한다.

isna()로 공백이 있는(특이사항이 없는) 데이터들만 추려낸다.

 

9. Preprocessing - RESULT

최종적으로 확인한 True/False 데이터 분포이다.

데이터 불균형 정도는 odds_result와 유사하다.

 

10. Checking corr and VIF

일반적인 Pearson 상관계수로 두 변수 간의 선형 관계의 강도를 측정한다.

가장 많이 사용하는 상관계수 방법이지만, 이상치에 민감하다.

그래서 데이터가 정규 분포를 따르지 않는다면 해석을 잘못할 가능성이 존재한다.

 

그래서 추가로 Spearman 상관계수와 Kendall 상관계수를 이용한 상관관계를 살펴보았다.

Spearman 상관 계수는, 비선형 관계를 포함해 두 변수 간의 순위 기반의 연관성을 측정한다.

이상치의 영향을 덜 받으며, 데이터가 비정규 분포일 때 유용한 상관 계수이다.

Kendall 상관 계수는, spearman 상관 계수와 유사하게 순위 기반의 상관관계를 측정한다.

데이터 셋이 작을 때나 불균형한 데이터셋에 적합하며, 이상치에 강건하다.

 

수집한 데이터는 비선형이면서 데이터 불균형이 높고 데이터 셋이 작아, Kendall 계수가 가장 적합하다고 생각한다.

Kendall 계수의 상관 관계 점수에 기반하여 관련 있는 컬럼들을 확인한다.

또한 Kendall 외에 나머지 두 상관 계수에서도 드러나는 높은 연관성들이 있다.

다중공선성(VIF)을 고려해서 추후 특정 컬럼은 drop하는 걸 고려해야 한다.

 

1. (win_odds, round)
2. (lose_odds, game_goal)
3. (prefix_rank, cur_game_count)
4. (prefix_rank, rank)
5. (odds_result_UD, RESULT)

 

11. Modeling - Data subset

X, y = X.drop('RESULT', axis=1), X['RESULT']

개략을 알기 위해 간단한 모델링 후 결과를 살펴보려고 한다.

학습을 위한 X와 결과 예측을 위한 y로 데이터셋을 분리해주었다.

 

테스트 하다가 에러가 발생했다.

RESULT에 TRUE와 FALSE만 넣어주어 bool 타입이라고 생각했는데 아니었다.

"TRUE"와 "FALSE" 데이터로 들어가 object 타입이 된 것이다.

이럴 때 발생하는 에러여서 astype으로 형변환을 진행했다.

 

타입 해결 후 LogisticRegression으로 학습하여 테스트했다.

이때 new_data를 임의로 넣어주었는데 데이터프레임이 아니라 1차원 리스트로 넣어주었다.

이떄 차원이 맞지 않는다는 에러가 발생했다. 즉 1차원을 2차원으로 변형해야 한다.

그 후 predict_proba의 shape가 np.array(1, 2)라서 다시 1차원으로 변형했다.

 

차원을 변경하는 게 익숙치 않아, 관련 함수를 찾아보았다.

2차원 데이터 프레임(2차원의 np.array)을 1차원으로 바꾸는 함수는 2가지가 있다.

flatten()과 ravel()인데 이게 느낌이 약간 sorted()와 sort()의 차이 같은 느낌이다.

flatten()이 sorted()고, ravel()이 sort()에 해당한다.

둘 다 1차원으로 평탄화 해주지만, flatten()은 기존 값은 유지한 채 새로운 값을 반환한다.

ravel()은 기존 값을 바꾼다. 그렇다고 반환 값이 없는 건 아니고 id(주소)가 같은 배열을 반환한다.

 

그리하여 최종적으로 테스트를 해주었더니 결과가 이상하다...

(중간에 RESULT 외에 등수 예측도 필요하다고 생각하여 rank 컬럼도 따로 빼주었다.)

생각해보니 rank는 1위부터 12위까지 존재하는 회귀(regression) 문제다.

LogisticRegression은 분류(classifier) 문제라서 학습이 안 되는 문제가 발생했다.

처음 배울 때도 그랬는데 이름에 Regression이 들어가서 종종 회귀 모델로 착각하는 경우가 있다.

또한 RESULT는 중간에 변수가 꼬여서 accuracy가 0으로 뜨는 문제가 발생했다.

다시 깔끔하게 리모델링을 진행하기로 했다.

 

12. reModeling - Data subset

X, y_rank, y_result = X.drop(['rank', 'RESULT'], axis=1), X['rank'], X['RESULT'].astype(bool)

데이터 셋을 X와 y 2개로 나누어준다.

y_rank는 등수 예측에 사용할 회귀(regression) 모델용 데이터,

y_result는 결과 예측에 사용할 분류(classifier) 모델용 데이터로 넣는다.

y_result에는 결괏값 자료형 때문에 오류가 발생하지 않도록 astype을 한다. 

 

13. reModeling - rank column

특별한 파이프라인 설정 없이 LinearRegression으로 진행했다.

LogisticRegression이 아니다. 헷갈리지 말자.

 

 

학습 후 train 데이터셋과 test 데이터셋에 대한 RMSE 점수 측정했다.

회귀(regression) 모델에서 손실 함수로 MSE를, 평가 함수로 MAE를 보통 사용한다.

그래서 MAE를 하려고 했지만, 손실 함수를 평가 함수로써 사용하면 어떻게 될까?라는 생각에 MSE로 진행했다.

이때 MSE는 점수가 너무 커져서 RMSE로 진행했다.

R2, MAE, MSE, RMSE에 대한 비교와 특징 설명은 링크를 참고하면 좋다.

 

14. reModeling - rank prediction

예측한 결과 예시 사진이다.

'1판 이내 12등 이내'라는 토토가 열렸고, 150cc 71번 맵에 12명이 참가했다.

이 경우에 옥냥이는 6등을 할 거라는 결괏값이다.

 

15. reModeling - RESULT column

특별한 파이프라인 설정 없이 LogisticRegression으로 진행했다.

 소규모 데이터셋에 적합한 매개변수로 solver에 liblinear를 설정했다.

 

 

학습 후 train 데이터셋과 test 데이터셋에 대한 accuracy_score 점수 측정

여기서는 간단하게 어떤 느낌으로 나오는지 확인하기 위해 acc 점수만 확인했다.

마치 신뢰 수준을 95%로 설정한 것처럼, 다른 prediction, recall 등의 점수는 우선 보류한다.

 

16. reModeling - RESULT prediction

위에서와 마찬가지로 '1판 이내 12등 이내'라는 토토가 열렸고, 150cc 71번 맵에 12명이 참가했다고 가정한다.

문제점이 발생했다. 마리오 카트에서는 12등이 꼴지다.

즉 1판 이내 12등 이내는 무조건 성공인데, True 확률이 57%밖에 안 된다.

 

이상함을 느껴 model의 coef(feature importances)를 확인해봤는데 너무 치우쳐져있다.

이 부분은 모델링을 할 때 주의깊게 살펴보고 고쳐야 할 듯하다.

 

17. 추후 방향

- track_E 숫자, 정도의 차이를 학습하지 않는 방법 모색

 

- 첫 판은 rank 데이터 정보가 없지만, 두 번째 판부터는 rank 데이터가 있다.

- 따라서 같은 가중치(weight)를 기반으로 2가지 모델을 만든다?

- 그래서 다른 모델로, 결과를 따로 구하는 방법으로 가야 할 듯

 

- 회귀와 분류의 2가지 방안에서 고려해야 함

- 데이터셋이 적기 때문에, 이에 특화된 모델을 골라서 학습해야 할 듯하다

- 회귀 측면) LinearRegression, Ridge & Lasso Regression, ElasticNet, SVR

- 분류 측면) LogisticRegression, DecisionTree, SVM

 

- 문제점) 데이터 불균형이 꽤 높다.

- catboost를 사용해야 할지도?

 

- 우선 가중치나 micro tunning 없이, 가장 단순한 형태로 전처리를 진행

- 그 후 각 모델에 대해서 학습하여 cross validation 진행

- cross validation으로 가장 높은 모델 평가 후 선정

 

- pyinstaller로 exe 파일을 만들어야 하기 때문에, 이 기간도 고려해서 프로젝트 진행

- 프로그램 img 파일도 생각하기

- modlunky를 참고해서 OOP로 코드 작성하기

본격적인 EDA를 하기에 앞서 추후에 쓸 자료를 분석해보고 싶었다.

그러니까 무슨 말이냐면, 옥냥이한테 "당신 마리오 카트 전적을 분석해보니 이렇게 나왔어!"

라고 팬카페에 쓸 건덕지 좀 마련하고 싶다는 말이다.

 

ㆍ 맵별 평균 순위

ㆍ 승률이 높은(혹은 낮은) 맵들

ㆍ 평균 승률

이런 것들을 가볍게 확인해보고자 한다.

 

df_td = df_td.sort_values(['win_ratio', '성공'], ascending=False)

df_td.head(10).plot.bar(
    y=['성공', '실패'],
    color=['lightcoral', 'royalblue'],
    figsize=(10, 5),
    rot=20,
)

plt.title("성공률이 높은 경기장 TOP 10")
plt.ylabel("판 수")
plt.grid(True, linestyle='--', linewidth=0.3, axis='y')

우선 가장 보편적으로 성공률이 가장 높은 경기장 TOP 10이 궁금했다.

df_td는 track_distribution 데이터를 갖는 데이터프레임이다.

track_K와 RESULT로 groupby한 다음에 각 경기장마다 성공/실패 여부가 몇 개인지 담긴 데이터프레임이다.

df_td를 win_ratio(승률)과 성공 컬럼을 기준으로 내림차순 정렬했다.

이때 승률은 (성공 판 수 / 전체 판 수)로 계산했다.

 

이때 경기장이 성공했다는 건 무슨 의미일까?

'3판 합계 15등'으로 놀이터가 열렸다고 해보자.

그리고 3판을 '비틀어진 맨션(3등)', 'Tour 마드리드 그란데(7등)', '미끌미끌 트위스터(4등)'으로 들어왔다고 하자.

그럼 놀이터 배팅 결과가 '성공'일 것이다.

그럼 위에서 언급한 3개의 경기장은 '성공' 데이터가 하나 쌓인 것이다.

 

정리하면 해당 경기장(track)을 달릴 시, 배팅을 성공할 확률이 높다는 말이다.

왜 다들 게임할 때, 손에 맞는 캐릭터가 있고 뭘 해도 안 되는 캐릭터가 있지 않은가.

옥냥이가 '빅 블루'는 못하고, 'N64 요시 밸리'는 잘하는 그런 느낌이다.

 

물론 전체 게임 횟수가 632개 뿐이고, 맵의 전체 개수는 96개밖에 없다.

단순 계산만 해봐도 평균 한 맵당 6.58번 플레이한 데이터 뿐이다.

즉 데이터 표본이 상당히 적어, 정말 참고용(팬카페 올릴 용도)임을 감안하자.

 

df_td = df_td.sort_values(['win_ratio', '실패'], ascending=[True, False])

df_td.head(10).plot.bar(
    y=['성공', '실패'],
    color=['lightcoral', 'royalblue'],
    figsize=(10, 5),
    rot=20,
)

plt.title("성공률이 낮은 경기장 TOP 10")
plt.ylabel("판 수")
plt.grid(True, linestyle='--', linewidth=0.3, axis='y')

마찬가지로 성공률이 낮은 경기장도 살펴보자.

높은 경기장의 정렬과는 조금 다른 정렬이 필요하다.

몰랐는데 단 한 번도 성공하지 못 한 경기장이 꽤 많이 있었다...

 

똑같이 승률이 0%라고 하더라도, 10판 승률 0%와 1판 승률 0%는 다르다.

그래서 'win_ratio' 컬럼은 오름차순, '실패' 컬럼은 내림차순으로 정렬해야 한다.

win_ratio는 말그대로 승률이기에 오름차순으로 해야, 가장 낮은 값이 처음에 온다. 

 

이런... 성공률 하위 TOP 10은 전부 성공률 0%라니

그래도 옥냥이가 선샤인 공항을 할 때, 나름 잘했다고 생각했는데... 요시 아일랜드도...

여기서 이 방식의 문제점을 생각해볼 수 있다.

 

만약 '선샤인 공항'을 1등으로 들어왔다고 해보자.

그리고 나머지 2개의 경기를 12등 12등으로 들어와서 실패를 했다.

그럼 선샤인 공항은 실패 데이터가 하나 쌓이는 거다.

그렇다 선샤인 공항 데이터 입장에서는 억까를 당한 것이다.

근데 데이터가 적다고 하더라도, 선샤인 공항 실패가 10회라면 이건 실력 아닐까?

 

이항 분포 그래프 예시

이런 문제점을 해결하면서, 경기장별 '진짜 승률'이 높은 순으로 시각화를 하고 싶었다.

df_td에서 사용한 win_ratio 컬럼은 데이터 개수를 고려하지 않았다.

10판 중 7판 승리한 경기장의 승률(70%)보다, 1판 중 1판 승리한 경기장의 승률(100%)이 당연히 더 높다.

이러면 안 된다고 생각헀다.

따라서 '신뢰구간'을 활용하여 데이터 표본에 따른 공정한 승률을 구하고자 헀다.

 

현재 살펴본 그래프들은 '성공과 실패'라는 두 가지 기준으로 평가했다.

성공과 실패라는 결과는 공존할 수 없는 독립적인 시행, 즉 베르누이 시행이다.

베르누이 시행 기반의 이항 분포 그래프는, 특정 조건을 만족하면 정규 분포로 근사할 수 있다.

특정 조건은 시행이 충분히 크거나, 확률 p가 극단적이지 않은 경우이다.

반대로 말하면 수집한 데이터가 너무 적거나, 확률이 너무 편향될 경우 잘 동작하지 않는다.

이를 위한 방법이 Wilson score confidence interval이다.

 

Wilson score confidence interval

 

"R%의 신뢰 수준에서, 긍정적인 평가를 받을 확률은 최소 얼마에서 최대 얼마인가?"

라는 질문에 답을 얻을 수 있는 게 wilson score confidence interval이다.

이 방법은 긍정 평가 수, 전체 평가 수, 원하는 신뢰 수준을 입력으로 받는다.

그리고 긍정 평가가 가능한 범위(bound)를 반환한다.

 

시행(데이터 표본 수)이 적을수록 예측은 모호해진다.

따라서 wilson score는 적은 데이터에 대해 넓은 bound를 반환하고,

충분히 신뢰 가능한 만큼 데이터가 쌓이면 점차 bound를 좁혀나간다.

 lower bound와 upper bound 중에서 lower bound를 보통 점수로 사용한다.

그러면 믿을 만한 데이터에 상대적으로 높은 점수를 주고, 충분하지 못 한 데이터에는 낮은 점수를 준다.

이렇게 불확실성에 대한 밸런스를 잡을 수 있다.

 

그렇게 wilson score 점수가 높은 순서대로 TOP 10 경기장을 산출해보았다.

쉽게 말하자면 '위의 10개의 경기장을 달린다면 그래도 좋은 순위를 기대할만하다!'라는 의미다.

이때 주의할 점은 min이 긍정이고, max가 부정이다.

min은 최소 순위로 1등에 가까운 데이터고, max는 최대 순위로 12등에 가까운 데이터다.

 

  성공률 등수 wilson-score 등수
비틀어진 맨션 1 2
미끌미끌 트위스터 3 1
SFC 도넛 평야 3 4 3
Tour 시드니 스프린트 5 6
Tour 도쿄 블러 7 4
Wii 음매음매 컨트리 8 8
N64 요시 밸리 10 5

wilson score에서 설명할 때, 입력을 3가지 받는다고 했다.

그중 하나가 신뢰 수준으로, 편의상 가장 많이 사용하는 95%의 신뢰 수준으로 계산한 결과이다.

신뢰 수준에 따라 결과가 달라지겠지만, 95%로 잡은 현재로써는 '성공률이 높은 경기장 TOP 10'과 상당수 겹친다.

즉 위의 7개의 맵에 대해서는, 95%의 신뢰 수준으로 좋은 결과를 낸다고 볼 수 있겠다.

 

궁금해서 살펴본 성공/실패 판수다.

놀이터 성공 데이터는 180번, 실패 데이터는 443번으로 약 2.5배 차이가 난다.

생각보다 엄청 차이가 나지는 않는다!? 거의 5배는 날 줄 알았는데 역배가 꽤 터진다.

 

맵별 평균 승률과 최소ㆍ최대 승률은 모델링을 할 떄 다뤄보기로 하고,

여기서는 날짜별 평균 ㆍ최소 ㆍ최대 승률을 살펴본다.

하나의 사진으로 첨부하기에는 세로로 너무 길어서 임의로 2개의 데이터프레임으로 나눴다.

그러다가 든 생각이 날짜별 평균 승률을 나열해본다면, 에이징커브 그래프가 나올 수 있지 않을까? 생각이 들었다.

 

다행히(?) 아직 에이징커브 이슈가 있는 모양새는 아니다.

 

조금 더 보기 쉽게 가로 형태의 꺾은선 그래프를 그려보았다.

중간중간 (안 좋은 쪽으로) 튀는 데이터가 있기는 하지만 전반적으로 현재 상승 중이다.

옥냥이는 컨디션에 영향을 많이 받는 성장형 주인공인 걸로.

 

데이터 추가를 위해 무엇을 기준으로 삼을지 고민해보았다.

옥냥이가 포인트 예측(이하 불안전한 놀이터)을 연다는 건, 지금의 실력을 고려해야 한다는 말과 같다.

그래서 마리오카트 점수 변화를 그래프로 그려보았다.

 └ score 점수는 옥냥이와 올냥이 유튜브 기준, 게임 입장 시 처음 뜨는 점수를 기반으로 시각화했다.

붉은 색으로 보이는 점 부분은 '사관 데이터'의 첫 기록 날짜다.

 

가장 첫 날인 2021년 6월 9일의 데이터를 지금 넣는 게 과연 의미가 있을까?

저때는 정말 뉴비였고, 지금 존재하는 DLC 맵이 없는 경우도 있었다.

그렇게 죽죽 가파른 기울기로 점수가 올라갔고, 저 빨간 포인트.

저 빨간 점이 7000을 가기 직전의 점수로, 딱 기준으로 삼기 적절해보인다.

 

2022년 2월 4일을 기준으로, 점수 기울기가 완만해지기 시작한다.

그리고 점수도 7000대 ~ 8000대로 어느정도 안착한 모습을 보여준다.

(마치 주식 시장의 횡보 구간처럼 보인다.)

심지어 7000대까지는 계속 상승만 하다가 8000대 도달하니 하락하는 모습을 처음 보인다.

즉 옥냥이는 8000대의 실력이 딱이라는 이야기다.

 

현재로써 2021년의 데이터는 무의미하다.

따라서 아래의 7달치 데이터를 최종적으로 추가하고 마무리하기로 결정했다.

 

ㆍ 2022년 4월 12일

ㆍ 2023년 8월 21일, 9월 21일, 10월 23일, 12월 5일, 12월 22일

ㆍ 2024년 2월 27일

 

이제 해야 할 건... 저 1시간 반가량 있는 영상을 보며 직접 데이터를 다시 노가다하자.

물론 7달치를 말이다.

 

결국 학습하고자 하는 모든 데이터를 확인했다.

놓치고 있던 점이 하나 있었는데, 2024년 2월 27일부로 트위치가 한국 사업에서 빠졌던 점이다.

즉, 2024년 2월 27일 마리오 카트 방송은 놀이터가 열리지 않았다...!

따라서 2023년 데이터까지만 추가해줬다. 총 632 * 17의 데이터프레임을 완성했다.

 

혹시라도 course(경기장)이나 위의 csv 데이터가 필요하다면 아래를 참고하자.

https://github.com/miny-genie/RTC-mariokart-8-AI

혹시 모를 상황을 대비하여 사관님께 허락을 받았다...!

사실 일일이 실제 상황에서 데이터 수집을 하는 소요 기간이 반 이상 먹고 들어간다고 생각한다.

힘들게 데이터를 수집해주셨을 텐데... 흔쾌히 저렇게 얘기해주셔서 감사합니다!

이제 데이터를 가공할 시간이다.

 

위 데이터에 팬카페 사관님이 기록해주신 데이터(이하 사관 데이터) 중 일부이다.

사관님도 유튜브를 보고 기록했기 때문에 결측치나 이상치가 있을 수 있다고 적어놓으셨다.

그래서 데이터를 수기로 직접 옮기며 이중으로 확인하고 있다.

유튜브도 보면서 모든 데이터에 대해 확인하는 것은 사실상 어렵기 때문에, 뭔가 이상하다고 느끼는 부분만 확인한다.

 

수기로 데이터를 정리하는 것은 사실 고집이다.

데이터를 다뤄보니 얼마나 깨끗한지(clean)가 왜 중요한지 알게 되었다.

그래서 오류가 나지 않게끔 직접 수작업으로 옮기는 중이다.

(종종 알바 공고에서 '데이터 노가다 아르바이트'가 있는 이유를 이해했다...)

 

또한 엑셀 데이터를 불러와서 파이썬 코드로 RPA를 만들까도 고민했다.

하지만 예외가 있으면 문제가 발생하고, 예외 코드를 또 작성하고 하면 시간이 분명 오래 걸릴 것이다.

차라리 그걸 만들 시간에 직접 옮기는 게 더 빠르다고 판단했다.

하나하나 데이터를 보면서 '도메인 지식'이라는 것을 체감할 수도 있지 않을까? 하는 생각도 있다.

 

csv 파일의 대략적인 느낌은 위와 같다.

사관 데이터에 있는 정보를 최대한 자세하고, 모든 정보를 다 옮겨놓고 싶었다.

필요없다면 drop column을 하면 그만이지만, 그때 가서 필요한 정보가 생긴다면 골치가 아파진다.

또한 독립으로 존재하면 의미가 없을지 몰라도, 다른 column과 조합하면 의미가 생길 수도 있으니 말이다.

 

예를 들어서 round 컬럼을 살펴보자.

밑에서 설명하겠지만 round 컬럼은 '몇 번째 게임인지 나타내는 데이터'이다.

첫 판이라면 손이 덜 풀려서 질 수도 있고, 게임을 오래 하면 승률이 올라갈 수도 있다.

역으로 게임을 오래 하면 피로가 쌓여 승률이 오히려 내려갈 수도 있다.

현재 시점에서는 모르기에 csv 파일에 넣을 필요가 있다고 판단했다.

필요 없으면? 위에서 말한대로 drop하면 그만이다. 

 

index 데이터를 구분하기 위한 인덱스, 데이터프레임화하면 자동으로 인덱스가 생기지만 혹시 몰라 추가
date 옥냥이가 '언제 마리오카트를 했는지' 알 수 있는 날짜
round 사관 데이터의 1회차 2회차에 해당한다. 몇 번째 게임인지 나타내는 데이터
game_count N판 합계 R등 이내에서, N판 부분을 나타내는 데이터
game_goal N판 합계 R등 이내에서, R등 부분을 나타내는 데이터
cur_game_count N판을 진행한다면 현재 몇 번째 판인지 나타내는 데이터, 1번째 판인지 2번째 판인지 등
win_odds 성공(승리) 배당률
lose_odds 실패(패배) 배당률
track_K 주행한 마리오 카트 맵(경기장), 한글 데이터
track_E 주행한 마리오 카트 맵(경기장), 영문 데이터
cc 실제로는 배기량을 나타내는 단어이지만, 마리오 카트에서는 난이도를 표현
part_people 해당 경기에 참가한 인원 수
rank 해당 경기에서 기록한 등수
prefix_rank 한 round에서 rank의 누적 합
odds_result 경기(배당) 결과, straight up(SU)은 정배를 의미하고 underdog(UD)는 역배를 의미
significant 특이사항, 옥냥이의 정산 실수라든가 경기 무효라든가
RESULT 불안전한 놀이터(토토)의 결과, N판 합계 R등을 충족했다면 TRUE 아니라면 FALSE

각 컬럼에 대한 설명은 위와 같다.

track_K, track_E, siginificant 등 csv 파일을 만들 때 눈여겨 볼(?) 컬럼들이 몇 개 있다.

track의 이름은 반드시 필요하다고 생각했다.

실제로 옥냥이는 빅 블루를 진짜 못한다. 그래서 빅 블루가 선택되면 종종 괴성을 지른다.

하지만 인공지능 학습에 있어서는 영문 데이터가 수월하다.

동시에 맵은 '어떠한 고정 값'이기 때문에 Categorical data로 인코딩 할 수 있다.

그래서 mario_kart_8_course라는 시트를 따로 만들어 맵 이름을 저장했다.

 

(왼쪽의 엑셀 컬럼을 보면 알겠지만, 11번째 행부터 89번째 행까지는 숨기기를 해서 사진을 캡처했다.)

(별 의미는 없고 그냥 97개의 전체 맵 시작과 끝을 한 사진에 담고 싶어서 중간 부분은 숨겨놨다.)

이렇게 경기장(맵)에 대한 데이터만 따로 빼둔다면 오탈자를 찾기 쉬울 거라고 생각했다.

왜냐하면 한글로 경기장 이름을 작성하면 vlookip 함수로 영문 경기장 이름을 가져온다.

즉 영문 경기장 이름이 안 뜨면, 내가 쓴 한글 경기장이나 mario_kart_8_course의 데이터 중 하나는 오타가 있다는 거다.

 

동시에 시간 단축 효과도 있다.

본래라면 '마리오 스노 마운틴'이라고 쓰고 옆 칸에 'Mount Mario'라고 또 써야 한다.

하지만 함수로 데이터를 입력하면 '마리오 스노 마운틴' 한 번만 입력하면 된다.

 

vlookup_column 엑셀 vlookup 함수를 사용하여 데이터를 찾기 위해 앞으로 빼낸 컬럼(열)
idx 인덱스
grand_prix_K 그랑프리 구분, 한글 데이터
grand_prix_E 그랑프리 구분, 영문 데이터
cup_K 컵 구분, 한글 데이터
cup_E 컵 구분, 영문 데이터
track_name_K 경기장 이름, 한글 데이터
track_name_E 경기장 이름, 영문 데이터
significant 특이사항, 위키와 nintendo 공식 사이트의 이름이 다른 점을 기록하는 칸

각 컬럼에 대한 설명은 위와 같다.

나중에 어떻게 사용할지 모르기 때문에, 경기장에 대한 정보도 최대한 자세하게 저장하고 싶었다.

그래서 한글과 영문 이름을 전부 기록해 두었고, 언제 추가된 건지(컵 구분)도 기록했다.

 

significant 컬럼에는 크게 중요하지 않지만 혹시 모를 특이사항을 적어두었다.

예를 들어서 '뻐끔 신전'은 nintendo 공식에서는 'Piranha Plant Cove'라고 쓰여있다.

하지만 위키와 한국 닌텐도 홈페이지에는 'Tour Piranha Plant Cove'라고 쓰여있다.

한 가지로 통일만 하면 되기에 큰 문제는 없지만, 그래도 이 차이점을 남겨두고 싶었다.

또한 위키에는 GC로 적힌 게 공식 홈페이지에는 GCN으로,

위키에 SFC로 적힌 게 공식 홈페이지에는 SNES로 적혀있다는 점을 확인했다.

이런 특이사항을 적은 컬럼이 significant다.

 

사관 데이터를 csv로 제작하다보니 문제가 발생했다.

옥냥이가 성공/실패로 이진 분류를 한 게 아니라, 등수 예측하기로 놀이터를 연 것이다.

 

해당 경우의 win_odds, lose_odds는 위처럼 기록해두었다.

또한 odds_result, significant, RESULT는 우선 비워두었다.

significant에 특정 순위 정확하게 예측하기란 의미로 ACC를 기록해두고, 나중에 행을 drop할까 고민 중이다.

 

내가 만들고 싶은 예측기는 가능/불가능 이진 분류기지, 단판 특정 순위 예측이 아니다.

동시에 단판 예측 기록기도 만들어둘까? 라는 생각이 든다.

어쨋든 이건 어떻게 처리할지 조금 더 고민해봐야 겠다.

 

사관 데이터를 전부 입력했다. 490 * 17 크기의 csv 파일을 만들었다.

그중에서 발생한 예외 데이터들은 다음과 같다.

 

1. MISS: 옥냥이가 정산을 실수한 경우

2. ACC: 성공/실패 여부를 배팅하는 것이 아닌, 정확한 등수를 배팅하는 경우

3. CANCEL: 약관을 어겨 불안전한 놀이터를 재개장한 경우

4. game_count - under n: 성공/실패 여부는 맞지만, 합계가 아닌 N판 이내 R등 가능인 경우

5. NOT OPEN: 옥냥이가 놀이터를 개장하지 않은 경우

6. NaN: 다시보기 혹은 유튜브에 올라오지 않은 영상인데, 사관님께서 생방송 도중 놓쳐 결측치가 된 경우

 

바이럴 아닙니다. 뒷광고도 아닙니다. 앞광고도 아닙니다. 진짜 제가 좋아서하는 데이터 수집입니다.

사관 데이터를 보다가 문득, 모든 기록을 적었다기에는 적다는 생각이 들었다.

내 야심한 밤 숙면을 책임져줬던 그 영상들이 고작 이정도 데이터로 끝날리가 없다고 느꼈기 때문이다.

그래서 올냥이에 들어가서 직접 사관님의 데이터와 누락된 부분을 비교해보았다.

 

최초의 사관 데이터 날짜 이전(2021년 영상들 기준)으로

6월 - 9일, 11일, 16일, 17일, 22일, 23일

7월 - 8일, 12일, 13일, 19일, 27일, 28일, 30일

8월 - 2일, 3일, 9일, 23일

9월 - 2일, 6일, 23일

10월 - 8일

12월 - 17일, 30일,

추가로 2022년 4월 12일의 영상이 없었고,

 

마지막 사관 데이터 날짜 (2023년 8월 22일) 이후로는

2023년 8월 21일, 9월 21일, 10월 23일, 12월 5일, 12월 22일

2024년 2월 27일의 영상들이 없었다.

 

이를 보면 2022년부터 사관님께서 영상을 기록하신 것을 알 수 있다.

2022년부터 2024년까지의 데이터는 그래도 그렇게 많지는 않은데...

2021년의 데이터는 너무 방대해서 추가하기까지 시간이 너무 오래 걸릴 듯하다.

누락된 몇몇의 데이터들을 어디까지 추가할지는 고민해봐야 겠다.

최근 한 기업에서 진행한 AI 해커톤에 참여했다.

2월 26일부로 온라인 대회가 끝이 났고, 온라인 대회 진출에 실패했다.

하지만 인공지능 활용에 대한 감을 잡기 위해 참여한 대회였기에 만족했다.

어떤 상황에 어떤 방법으로 접근해야 할지 감각이 생겼다.

그러다 문득 든 생각이 '이 경험을 이용해서 나만의 사이트 프로젝트를 만들 수 없을까?'였다.

 

트위치(twitch)라고 하는 라이브 스트리밍 플랫폼이 있다.

여러 사정 때문에 한국에서는 사업을 철수하여 2024년 2월 27일부로는 더 이상 운영하지 않는다.

그러면서 대다수의 스트리머들이 네이버의 치지직(chzzk)으로 옮겨갔다.

 

치지직은 완성형으로 내놓은 스트리밍 서비스가 아니었다.

현재진행형(2024년 2월 29일)으로 스트리머와 시청자의 피드백을 받아가며 업데이트를 하고 있다.

그런데 정말 아쉬운 점이 있다. 포인트 제도가 없다는 것이다.

 

위는 트위치에서 방송을 하는 '옥냥이'라는 스트리머의, 내가 모은 채널 포인트다.

채널 포인트로는 이모티콘을 해금하여 사용할 수도 있는데, 가장 큰 묘미는 '채널 포인트 예측'이다.

스트리머와 시청자들끼리 즐기는 요소 중에 하나로, 이해하기 편하게 토토 시스템이라고 보면 된다.

오늘은 내가 벌었네~ 어제는 너가 벌었네~ 하면서 주인장 왜 이렇게 못하냐~ 하며 즐기는 용도다.

 

이 포인트 제도가 치지직에는 아직 없다. 

하지만 스트리머들의 건의를 받아 긍정적으로 검토하겠다는 이야기가 있다.

그렇다면 이번 치지직에서는 더 많은 포인트를 모은다는 목표가 생기지 않겠는가?

나왔을 때 준비를 하면 늦는다.

AI가 이를 예측해준다면, 이번에는 정말로 포인트 대기업이 될 수도 있을 것이다.

 

 

어떻게 진행할까 고민하다가 방향을 잡아보았다.

스펠렁키라는 게임도 참 좋아하는데, 유저가 만든 모드를 적용할 수 있는 프로그램이 따로 있다.

그것이 좌측에 있는 모드렁키(modlunky)라는 exe 파일이다.

여기서 주목할 점은 모드렁키는 오픈소스라는 것이다.

 

궁금증에 뜯어보았더니 이럴수가! 파이썬이다 파이썬!

나도 저런 프로그램을 만들어볼 수 있다는 것이다!!

현업에서 사용하는 코드를 배울 수 있다고 생각하여 자세하게 살펴보았었다.

그러다 pyinstaller를 사용해서 exe파일로 만들었다는 걸 알 수 있었다.

 

그래서 별 건 아니지만,  pyinstaller를 사용해서 간단하게 exe 파일을 만든 적이 있다.

친구의 권유로 메이플을 잠깐 했던 때가 있었다.

빅뱅 이전의 그 감성은 없어졌지만, 레벨업이 쑥쑥 된다는 시원함은 있었다.

코어 강화?라는 시스템이 새로 생겼던데, 이게 상당히 복잡했다.

유니온을 3000까지 키우는데 캐릭마다 스킬은 또 왜 그렇게 많은지...

그래서 내가 쓸 목적으로 '코어 계산기'를 만들어서 썼었다.

 

여기서 생각이 들었다.

단순하게 AI 모델을 만드는 것에 그치지 않고, 이걸 옥냥이 시청자와 공유해볼까?

내가 하나의 서비스를 만들어서 배포한다면 어떨까?

애초에 내 바람은 많은 사람에게 도움이 되는 게 아닌가?

 

잡다하게 이것저것 구현해본 경험,

10년차 인터넷 방송 시청자의 경험,

인공지능 해커톤의 경험,

삼위일체, 결합의 시간이다.

 

가장 중요한 데이터 확보다.

정말 감사하게도 옥냥이 팬카페에는 마리오 카트 기록을 해주시는 사관님이 한 분 계시다.

해당 사관님께 데이터 사용 요청 문의를 드렸으나 아직 답이 없다.

허락만 받는다면 csv 파일로 가공하여 사용하기에는 정말 최고의 데이터다.

 

대충 pyinstaller의 그림을 그려보았다.

보통 옥냥이가 '3판 합계 00등'을 기반으로 예측을 연다.

그래서 한 판이 끝날 때마다, 현재 상황을 반영하여 승률을 예측하게끔 할까...했지만

보통 첫 번째 판이 끝나기 전에 포인트 배팅 시간이 끝나버린다.

또한 세 번째 판은 끝나는 순간 결과가 판가름 나기에 예측이 의미 없어진다.

어떻게 구현할지에 따라서 인공지능 학습 방향이 결정된다.

저부분은 고민이 더 필요해보인다...

# ---------- Import ----------
from sys import stdin
input = stdin.readline


# ---------- Function ----------
def mulMatrix(A: list, B: list) -> list:
    N = 2
    result = [[0]*N for _ in range(N)]
    
    for m in range(N):
        for n in range(N):
            for k in range(N):
                result[m][n] += A[m][k] * B[k][n]
            result[m][n] %= MOD
                
    return result


def power(base, exponent):
    if exponent == 1:
        return base
    
    tmp = power(base, exponent // 2)
    
    if exponent % 2:
        return mulMatrix(mulMatrix(tmp, tmp), base)
    else:
        return mulMatrix(tmp, tmp)


def fibonacci(N):
    result = power([[1, 1], [1, 0]], N)
    return mulMatrix(result, [[1, 0],[0, 0]])[1][0] % MOD


# ---------- Main ----------
small, big = map(int, input().split())
MOD = 1_000_000_000

big = fibonacci(big+2)
small = fibonacci(small+1)
print((big - small) % MOD)

# ---------- Comment ----------
# big = fibonacci(big+2) - 1
# small = fibonacci(small+1) - 1
# big - small   = fibonacci(big+2) - 1 - (fibonacci(small+1) - 1)
#               = fibonacci(big+2) - fibonacci(small+1)
# so can skip intercept

1. 개념

그래프(Graph)는 원소 사이의 다대다 관계를 표현하는 자료 구조이다.

버스 노선도, 전철 노선도, 인간 관계도, 수도 배관 시스템, 물질의 분자 구조 등 다양한 관계를 표현할 수 있다.

위와 같은 예시들은 선형 자료 구조나 트리 자료 구조로는 표현할 수 없다.

 

그래프는 연결할 객체를 나타내는 정점(Vertex)과 객체를 연결하는 간선(Edge)의 집합으로 정의한다.

어떤 그래프가 있을 때, 얘는 항상 정확하게 이거!라고 부르기보다는, 조합에 가깝다고 생각한다.

그래프를 어떤 분류로 나누냐에 따라 다양하게 정의할 수 있기 때문이다.

따라서, 해당 글에서는 분류에 초점을 맞추어 살펴보려고 한다.

또한, 트리(tree)도 그래프의 일종이지만, 트리 하나로도 종류가 다양하기에 추후 자세히 살펴본다.

 

2. 용어 설명

차수(Degree) : 정점에 부속되어 있는 간선의 수

진입 차수(In-Degree) : 정점을 머리로 하는 간선의 수

진출 차수(Out-Degree) : 정점을 꼬리로 하는 간선의 수

경로(Path) : 기점부터 종점까지 간선으로 연결된 정점을 순서대로 나열한 리스트

경로 길이(Path length) : 경로를 구성하는 간선 수

단순 경로(Simple Tree) : 모두 다른 정점으로 구성된 경로

사이클(Cycle) : 단순 경로 중 기점과 종점이 같은 경

DAG(Directed Acyclic Graph) : 방향 그래프이면서 사이클이 없는 그래프, 트리도 DAG의 일종

 

3. 방향성에 따른 분류

 

무방향 그래프(Undirected Graph) : 간선에 방향이 없는 그래프

방향 그래프(Directed Graph) : 간선에 방향이 있는 그래프

 

4. 연결성에 따른 분류

 

연결 그래프(Connected Graph) : 모든 정점 쌍 사이에 경로가 있는 그래프

비연결 그래프(Disconnected Graph) : 일부 정점 쌍 사이에 경로가 없는 그래프

 

5. 순환성에 따른 분류

 

순환 그래프(Cyclic Graph) : 하나 이상의 순환을 포함하는 그래프

비순환 그래프(Acyclic Graph) : 순환을 포함하지 않는 그래프

 

6. 특별한 속성에 따른 분류

 

가중치 그래프(Weighted Graph) : 간선에 가중치가 있는 그래프

이분 그래프(Bipartite Graph) : 정점을 두 개의 그룹으로 나눌 수 있는 그래프

완전 그래프(Complete Graph) : 모든 정점 쌍이 서로 연결된 그래프

 

 

다중 그래프(Multigraph) : 두 정점 사이에 여러 간선이 있는 그래프

단순 그래프(Simple Graph) : 두 정점 사이에 최대 하나의 간선만 있는 그래프

정규 그래프(Regular Graph) : 모든 정점의 차수가 동일한 그래프

 

7. 트리와 숲

 

트리(Tree) : 연결되어 있으며 순환 없는 비순환 그래프

숲(Forest) : 하나 이상의 트리로 이루어진 비순환 그래프

 

8. 응용

 

위와 같은 그래프는 어떤 그래프일까? 보는 방법에 따라서 달라질 수 있다.

방향성 기준에서 본다면, 무방향 그래프이다.

연결성 기준에서 본다면, 연결 그래프이다.

순환성 기준에서 본다면, 순환 그래프이다.

동시에 최대 하나의 간선만 가지므로 단순 그래프이면서,

모든 정점의 차수가 같아 정규 그래프이기도 하다.

 

 

이 그래프는 방향 그래프면서 비연결 그래프고, 비순환 그래프면서 단순 그래프다.

동시에 AC, DBE는 각각 트리이면서, 전체적으로 보면 숲을 이룬다.

 

 

조금 특이한 정점이 하나 뿐인 이것은 그래프라고 부를 수 있을까?

처음에 그래프는 '정점과 간선의 집합'이라고 정의했다.

정점이 1개, 간선은 0개인 집합이다. 따라서 하나 뿐인 정점도 그래프가 맞다.

공집합도 집합이지 않은가?

 

간선이 없기 때문에 방향, 무방향 판별은 무의미하다.

그렇지만 비연결 그래프이고, 비순환 그래프면서

간선이 0개이기에 '최대 하나의 간선'을 가져야 하는 단순 그래프도 만족한다.

또한, 모든 정점의 차수가 0인 정규 그래프도 마찬가지로 만족한다.

무엇보다 이분 그래프도 될 수 있다.

 

9. 결론

백준이라는 알고리즘 트레이닝 사이트에서 열심히 문제를 풀어보고 있다.

그러다보니 어느덧 단계별로 풀어보기는 31단계까지 왔다.

그렇게 '31단계 그래프와 순회'의 마지막 문제를 풀다가 의문이 들었다.

'내가 그래프의 개념을 잘못 알고 있었나?'

 

그래프를 '사이클이 존재하는 간선과 정점의 모임'으로 알고 있었다.

문제를 해석하다가 '모든 정점이 다 이어진 입력만 주어지나?'

'어라? 그래프가 입력으로 주어진다고 했는데, 정점이 끊어진 것도 그래프가 될 수 있나?'

하는 의문에 휩싸였고... 전공책을 다시 꺼내들었다.

그리고 놀랍게도 개념을 잘못 알고 있었다는 사실을 깨달았다.

 

문제를 풀수록 '정확한 개념'을 바탕으로, '유연한 사고'를 생각하는 게 중요하다.

특히 그래프 같은 경우, 어떻게 바라보는냐에 따라서 달라질 수 있다.

이를 바탕으로, 비연결(단절)인 경우, 정점이 하나인 경우 등 다양하게 고려해야 한다.

문제를 다양하게 바라보고 해석할 줄 알아야 한다.

'Computer Science > 자료 구조(Data Structure)' 카테고리의 다른 글

비트마스크(Bitmask)  (0) 2023.08.13

+ Recent posts