윈도에 보면 스티커 노트인가 있을텐데 그거 비슷한겁니다. 실행하면 화면에 오늘 할 일이나 다른 메모같은 걸 띄워둘 수 있는데
저 메모 레이아웃을 개별로 바꿀수도 있고 몰아서 바꿀수도 있음. 저기다가 오늘 할일이나 장볼거 목록같은거 정리해두시면 됩니다. 메모 이동이랑 크기 조절도 가능했던걸로 기억함.
Clementine
음악 재생기를 윈도우에서는 AIMP 쓰고 맥에서는 Pine tree 썼고 리눅스에서는 Clementine 씀. 음악 재생기입니다.
우분투도 기본적으로 음악폴더가 다 있어요... 그래서 그 폴더 안에 음악을 때려박아두면 클레멘타인에서 저렇게 내가 갖고 있는 파일들이 다 뜹니다. AIMP처럼 테마가 있는 건 아닌데 배경이랑 그 음악 재생할때 움직이는 바 색깔정도는 커마 가능함. 저 배경은 좀 오래전에 해둔거긴 한데 카카오에서 배경이미지 쓰라고 준거 적용한겁니다.
Gnome-sushi
이냥반이 먼 초밥을 깔라 하나 싶겠지만 얘를 깔아두면 뭐가 되냐...
파일 아이콘에 커서 딱 올리고 스페이스바를 딱 누르면 미리보기가 가능합니다. 쟤는 py파일이라 저렇게 나오는거고 리눅스에 들어갈 수 있는 모든 파일들을 저런식으로 미리보기가 가능함.
Mozila Thunderbird
얘는 내가 깔은 게 아니라 우분투 처음 깔면 깔려있는거지만 아무튼.
본인은 보통 국내용에는 네이버, 해외용에는 gmail을 알려주는데 문제가 뭐냐... 내가 네이버는 매일 로그인하니까 메일이 오면 봐요. 근데 구글은 gmail을 들어가야 볼 수 있거든요? 뭐 크롬 띄우면 메일 몇통 왔는지 알려주고 이딴게 없습니다 구글은. 근데 브라우저 켜고 들어가기는 귀찮고 아웃룩은 가끔 메일 수 이상하게 뻥튀기하고(메일 막 몇만통 되어있고 그럼) 그래... 지금까지 썼던 것들 중에서는 얘가 그나마 쓸만했음.
며칠 전에 화이자 지원했는데 메일 와서 뭐지 서류 광탈인가 했는데 알고보니 '아 님 지원해줘서 ㄳㄳ 검토해보고 연락드림'이었음... 딱히 그걸 위해서 주식을 산 건 아니지만 나도 엄연히 니네 주주다...
GitKraken
애를 내가 우분투에서만 쓰는 건 아닌데... 정확히 말씀드리자면 깃 클라이언트 중에서 리눅스까지 커버치는 애가 얘밖에 없음. 윈도랑 맥밖에 안쓰시면 선택지가 많은데 내가 리눅스 쓴다...
이거 쓰든가 터미널에서 하시든가 직접 만드시든가... 선택지 세갭니다.
VLC 미디어 플레이어
아이콘이 공사장 파일런(그 꼬깔 있음)모양이지만 이 친구는 팟플레이어같은 친구입니다. 동영상 플레이어죠. 이렇게 얘기하면 리눅스에서 뭔 동영상을 보냐 하실 수도 있는데 유튜브는 추천이 그놈이 그놈이라 쉽게 질립니다... 그러면 음악 듣거나 내가 갖고있는거 보거나 두개밖에 없음.
이 코드의 장점은 일단 구현은 했다. 근데 나중에 글꼴 첨삭하거나 순서 바꿀때 사람이 한떨기 저승갓숭이 됩니다. 왜냐고? 저거 번호를 일일이 바꿔야돼... 맨 밑에께 빠지는건 양반입니다. 가운데꺼 빠지면 번호 다땡겨야되고 가운데에 뭐가 추가되면 또 번호 다 밀고 해야되는데 이거 하다보면 사람이 진짜 저승갓숭에 빙의해요. 화병걸려. 그래서 에미나이한테 물어봤더니 어떤 방법을 줬느냐...
fontSelection.addEventListener('change', (e) => {
const selectedFont = e.target.value; // 선택된 option의 value 값을 가져옵니다.
fontSelection.style.fontFamily = selectedFont;
textTitle.style.fontFamily = selectedFont;
textArea.style.fontFamily = selectedFont;
});
JS단에서는 옵션태그에 있는 value만 가져와서 CSS 바꾸게 하고 옵션태그 안에 import에 있는 font-family명을 고대로 복붙하면 HTML이랑 CSS만 수정하면 된다. 참 쉽죠?
ERD는 Entity-Relationship Diagram이다. 데이터 모델잉에 짬좀 차보신 분들이거나 본인이 SQLD를 치셨다면 뭔지 알 수도 있는데, 엔터티와 걔들간의 관계, 그리고 엔타티가 갖고 있는 속성에 대해 서술하는 도면입니다. 이거 그려주는게 되게 많은데 대표적으로는 dbdiagram이랑 ERDcloud가 있습니다. 둘다 써보긴 했는데 개인적으로는 전자가 더 편하더라고...
그러던 와중에 인별을 보다가 VScode 추천 익스텐션으로 ERD Editor라는 게 나온 걸 보게 되었던 것이다.
이거 말하는 거 맞습니다.
익스텐션을 설치하고 다이어그램명.vuerd.json으로 이름을 붙여주면 ERD를 그릴 수 있게 된다.
밑에 빈 화면이 떴다고요? 우클릭 하세요. 메뉴 있음.
여기 PK가 있습니다. 이거는 시험 사용이라서 걍 하는건데, 실제로 모델링 하실때는 무작정 그리지 마시고 엔터티 뭐 할지, 속성 뭐 할지랑 관계정도는 미리 계획해두시길 바랍니다. 물론 그거 정하면서 시간 진짜 많이 갈거임… 생각보다 고려할 게 많더군…
아, PK는 alt+k 누르면 됩니다.
이렇게 지지고 볶으면 일단 엔터티 하나가 나온다.
다른 제약조건이나 적어둘 거 있으시면 메모하십쇼. alt+m 누르면 메모 가능합니다. 그런데 우리가 이거나 하자고 ERD를 그린건 아니잖아요? 관계 어케그림??? 을 하기 위해서는 엔터티 하나를 더 그려야 한다. 근데 엔터티임 엔티티임 뭐가맞는겨 대체?
여기 두 개의 엔터티가 있다. 사실 포챔스 속성은 좀 간소화해서 저렇게 만든거고 기술 PP, 스테이터스(종족값 말고 실스텟)나 개체값같은 것도 있는데 넘어가자. 언제 체공방특공특방스피드 다 파고 앉아있음... 일단 이 두 엔터티를 보니 FK로 갈만한 게 뭐가있나요? 그죠, 도감번호랑 폼 코드 있죠. 이거 공통된 엔터티라고 무조건 다 FK박는거 아닙니다.
테이블 사이에서 우클릭하면 Relationship이라고 있는데 거기서 적절한 관계 하나 묶으시면 옆집으로 넘어간다. 지금은 비식별자 관계라서 점선으로 나오는데 알고보니 얘네가 식별자 관계다? 그러면 그 키들을 alt+k로 프라이머리 키 만들면 파란 실선으로 나온다.
테이블에서 우클릭-table properties 가시면 이런것도 볼 수 있습니다. …근데 제약조건 다른건 어따줌?
여러분. 세계 3대 고민은 거창한 게 아닙니다. 오늘 뭐입지 오늘 뭐먹지 오늘 뭐하지예요. 거창한거 없습니다. 회사에서 제일 중요한 안건? 물론 회의 안건도 있겠지만 가장 중요한거 뭐임? 오늘 뭐먹지예요. 짜장이랑 짬뽕은 걍 짬짜면 드시고 탕수육은 찍먹들거 좀 덜고 부먹해주시면 중용을 지킬 수 있지만(아니면 사는 사람 마음대로...) 점심 메뉴? 이거 어려워... 고르는거 진짜 밀레니엄문제 못지 않은 난제임...
랜덤 점메추
const randomBtn = document.querySelector('#button1'); // 랜덤랜덤 (여기 있는 리스트들 중)
const randomLunch = document.querySelector('#jeommechu1'); // 랜덤 표시구역
const ourBtn = document.querySelector('#button2'); // 입력받은 것들 중 하나 랜덤
const ourLunch = document.querySelector('#jeommechu2'); // 선택 표시구역
점메추가 왜 두개임? 하나는 뭐 딴거 있음? 예. 이 점메추 추첨기는 아 나 뭐먹을지 몰라 니가 골라주셈 하나랑 제육이랑 김치찌개중에 뭐먹을지 추천해주는거 하나 있습니다. 그 중에서도 이건 아 뭐먹을지 골라주셈이다. 메뉴 풀은 우리가 갖고있고 버튼을 누르면 그 중에서 하나를 뽑는 방식 맞다.
// 랜덤 밥추천
randomBtn.addEventListener('click',()=>{
let selectedLunch = document.createElement('p'); // 당첨된 밥
let lunchList = ['백반', '국밥', '찌개', '분식', '중국집', '빵', '라면', '편의점', '햄버거', '샐러드', '파스타']; // 밥 후보 리스트
});
아 브랜드 골라주는게 아니냐고요? 이건 브랜드가 아니라 말 그대로 '뭐 먹을지' 골라주는겁니다. 쟤가 햄버거 먹으라 하면 맥날갈지 버거킹갈지 프랭크버거를 갈지 노브랜드 버거를 갈지는 당신이 정하셔야죠. 브랜드도 정해주는거 아니냐고? 그럼 2번 기능 쓰셔야됩니다.
// 랜덤 밥추천
randomBtn.addEventListener('click',()=>{
let selectedLunch = document.createElement('p'); // 당첨된 밥
let lunchList = ['백반', '국밥', '찌개', '분식', '중국집', '빵', '편의점', '햄버거', '샐러드', '파스타']; // 밥 후보 리스트
const randomIndex = Math.floor(Math.random() * lunchList.length); // 몇번째 주소가 당첨됐는가
const selectedMenu = lunchList[randomIndex]; // 응 너야
selectedLunch.innerText = selectedMenu;
// 3. 기존에 결과가 있다면 지우고 새로 붙이기 (하나만 나오게 하려면 필요)
randomLunch.innerHTML = '';
// 4. #jeommechu1 영역에 붙이기
randomLunch.appendChild(selectedLunch);
});
여기까지만 하면 추첨은 다 됐는데 문제가 하나 있음.
글자가 너무 작죠? 그래서 클래스 붙인 다음 CSS에서 크기랑 색상같은 걸 제어할거다.
그러면 얘는 됐고... 이제 입력받아서 해보자.
입력받아서 점메추
그 있잖아요. 햄버거 먹을건데 맥날갈래 맘터갈래 프랭크버거갈레 수제버거집갈래 롯리갈래 버거킹갈래 노브랜드갈래 KFC갈래 이거. 이거는 여기서 할 일이다.
// 선택 밥추천
ourBtn.addEventListener('click',()=>{
let selectedLunch = document.createElement('p'); // 당첨된 밥
// 여러분이 입력한 밥을 리스트로 만들거예요
const randomIndex = Math.floor(Math.random() * lunchList.length); // 몇번째 주소가 당첨됐는가
const selectedMenu = lunchList[randomIndex]; // 응 너야
selectedLunch.innerText = selectedMenu;
selectedLunch.classList.add('selectedlunch');
// 3. 기존에 결과가 있다면 지우고 새로 붙이기 (하나만 나오게 하려면 필요)
ourLunch.innerHTML = '';
// 4. #jeommechu1 영역에 붙이기
ourLunch.appendChild(selectedLunch);
});
DOM 생성하고 추첨하고 갖다 붙이는 건 똑같은데 리스트 안에 있는 걸 우리가 입력받아서 추첨할거다. 그러면 이거 걍 입력창 만들면 되는거 아니냐고? 입력을 어떻게 받고 어떻게 처리하고 입력 어떻게 그만 받을거임? 그걸 정해야 할 거 아닙니까.
// [이벤트] 엔터키로 메뉴 추가
lunchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const value = lunchInput.value.trim();
if (value === "") return; // 빈 값 방지
if (ourList.includes(value)) {
showToast('이미 있는 메뉴예요!');
} else {
ourList.push(value);
showToast(`${value} 추가 완료!`);
updateLunchView(); // 리스트 갱신
}
lunchInput.value = "";
}
});
일단 인풋 텍스트 하나 만들고 엔터키를 누르면 메뉴 리스트에 추가가 되는 구조이다. 그럼 그거 관련해서 사이드로 추가되는 기능이 있겠죠? 그렇습니다.
// [기능] 토스트 메시지 생성 함수
function showToast(message) {
const toast = document.createElement('div');
toast.innerText = message;
toast.style.position = 'fixed';
toast.style.bottom = '20px';
toast.style.padding = '10px 20px';
toast.style.background = 'var(--main-color)';
toast.style.color = 'var(--white)';
toast.style.borderRadius = '5px';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000); // 2초 뒤 삭제
}
// [기능] 입력한 메뉴 리스트 화면에 다시 그리기
function updateLunchView() {
lunchView.innerHTML = ''; // 기존 리스트 초기화
ourList.forEach(menu => {
const li = document.createElement('li');
li.innerText = menu;
lunchView.appendChild(li);
});
}
토스트 메시지 만드는 코드하고 추가한 메뉴 보여주는 코드 만들었음. 물론 지우는 버튼도 따로 있습니다.
// 선택 밥추천
ourBtn.addEventListener('click',()=>{
let selectedLunch = document.createElement('p'); // 당첨된 밥
// 여러분이 입력한 밥을 리스트로 만들거예요
// 있다면 말이지
if (ourList.length === 0) {
alert("추가된 메뉴가 없어요! 먼저 메뉴를 입력해주세요.");
return; // 함수 종료
}
const randomIndex = Math.floor(Math.random() * ourList.length); // 몇번째 주소가 당첨됐는가
const selectedMenu = ourList[randomIndex]; // 응 너야
selectedLunch.innerText = selectedMenu;
selectedLunch.classList.add('selectedlunch');
// 3. 기존에 결과가 있다면 지우고 새로 붙이기 (하나만 나오게 하려면 필요)
ourLunch.innerHTML = '';
// 4. #jeommechu1 영역에 붙이기
ourLunch.appendChild(selectedLunch);
});
저 에미나이가 전역 좋아하길래 리스트 밖으로 빼버리고 목적 따라서 분리함... 그래서 어떻게 됐냐고요?
기존 글꼴에서 이야기 굵은체와 온글잎 콘콘을 들어내고(그래서 기본글꼴이 프리텐다드임) 서궁, 탕바, 문막초 별반체를 추가함. 사실 추가하고 싶은 글꼴이 따로 있는데 눈누에 임포트가 안올라옵니다…
내용 삭제 버튼 추가
다운로드 버튼 바로 왼쪽에 있는 게 내용 삭제 버튼이다. 기존에 이 버튼이 없었을때는 내용 지우고 싶으면 F5키를 눌렀거든요? 그러면 어떻게 된다? 글꼴이 기본글꼴이 됩니다. 오호 통재라. 대신에 저 버튼을 누르게 되면 내용만 쏙 지우는거라 글꼴 설정이 유지됩니다.
줄바꿈 정리 버튼
저기 빗자루가 그거임.
전에 글쓰는 프로그램들 얘기하면서 몇몇 프로그램들 메모장에 복붙하면 줄바꿈 이슈 있어서 못 쓴다고 했죠? 단락이랑 단락 한줄 띄었는데 붙여넣기 했더니 갑자기 한줄 추가되는 그런거 있잖아요. 그거 처리하는 기능입니다. 에디터에 따라서는 저걸로 한방에 되는 경우도 있고 쓰다에서 복붙한거는 대화체 줄간격을 수정해줘야 한다는 단점이 있음.
일단 본인은 도커가 뭔지는 모름. 근데 써봤습니다. 프롤로그에서 SQLD 준비할 때 리눅스에서 오라클 썼다고 했는데, 리눅스에서 오라클 돌리려면 도커를 설치해야 합니다. 그리고 도커는 컨테이너를 실행하고 뭘 해야 하기 때문에 컨테이너 키고 오라클 키고 으아악 오타 멈춰 가라 지에딧! 하면서 쿼리를 짰어요…
도커는 컨테이너를 활용하여 소프트웨어를 배포 및 구동하기 위한 오픈 소스 플랫폼입니다. 내가 맞게 이해한건지는 모르겠는데, 오라클을 도커에서 돌릴때 도커 컨테이너를 먼저 켰던것도 오라클이 들어있는 컨테이너를 열어서 적재된 오라클(과 관련된 모든 것들)을 다 꺼내는 절차를 거쳐야 하는거고, 여기에 서술할 과정들이 컨테이너에 내가 코딩한거랑 뭐뭐 필요한지를 차곡차곡 적재하는 뭐 그런건가봄.
이거 한다고 어제 새벽까지 밤샜음… 그리고 여기는 아예 해본적이 없어서 채찍피티의 도움을 받아서 진행했음.
극한적재
저 깃이그노어는 딱히 도커에서만 쓰는 건 아니고, 이런이런 파일들은 바뀌어도 무시해~ 라고 적어두는거라고 이해하시면 되겠습니다. 여기서 봐야 할 건 DockerFile이랑 requirements.tst랑 docker-compose.yml 세개임.
저 도커파일을 만들라고 했을 때 처음 든 생각이 아니 뭔 파일? 파일이면 확장자가 있어야되는데 확장자는 없고 걍 만들라는데 뭐 어쩌라는겨? 였음... 근데 이름을 강 DockerFile로 하면 VScode에서는 아이콘이 고래(도커 아이콘이 고래 위에 컨테이너 있는거임)로 바뀝니다. docker-compose 저친구는 뭐 하는 앤지 모르겠는데 쟤 들어가면 좀 더 간단하다고 하더라고... 채찍피티 피셜이라 한번 찾아보기는 해야겠지만.
저 리콰이얼먼츠는 우리 이거 돌릴때 이거 필요해~ 하는 것들을 다 적어두는거다. 저기에는 matplotlib만 있지만 내가 씨본 썼으면 Seaborn, 플롯틀리 썼으면 Plotly, 오리클 썼으면 oracledb(맞나?)… 뭐 이런 것들 쓰면 된다. 아무튼 저렇게 구성하고 컨테이너를 만들려고 했는데 안돼! 이 컴퓨터에 도커가 안깔렸어! (리눅스에는 오라클때문에 도커 깔려있음)
그래서 설치를 다 하고 만들었는데 또 안돼! wml인가 뭔가 업데이트를 하래! 그걸 또 따로 깔고 재부팅을 또 해야돼! 그래서 도커 깔고 한번 쟤 깔고 한번 재부팅을 했죠... 그러고 나서 이제 컨테이너에 적재를 다 끝내고 깃헙에 db 지우고 올린 다음(어차피 돌리면 생성됨) 리눅스에서 실행을 했는데 이게 한번에 됐으면 제가 세시까지 밤을 안 샜습니다.
극한실행
아니 무사히 패킹하고 올렸으면 잘 된 거 아니예요? 아니죠. 이걸 받아서 실행했을 때 이상이 없는지를 확인해야 합니다. 컨테이너에 화물을 개판으로 실으면 그게 잘 적재되겠음? 개판으로 적재됐는데 그게 무사히 갈까요? 받아봤더니 포장 개박살나있고 화물 개작살나있고 썩어있거나 할 수도 있잖아요.
여러분들 쿠팡에서 뭐 시켰는데 박스 찌그려져서 온 적 있죠? 포장 개박살나서 온 적은? 난 있어... 근데 박스 찌그러진거는 내용물에 문제 없으면 그 허브 컨테이너에 낑겨서 그렇게 되는겁니다. 내 경험담임... 사람이 옮기는 속도가 화물 쌓이는 속도를 못 따라잡아요. 특히 내 라인에 무거운거 걸리면... 아무튼 그런 사태가 일어나지 않을런지 리눅스로 미리 테스트를 해보다가 문제가 터진겁니다... 물론 버그가 터진다거나 하면 디버깅도 해야겠죠. 그 디버깅하고 아 이제 끗 제출하자까지 하느라 새벽 3시까지 밤샌거임...
일단 우분투 노트북을 켤 일이 잘 없습니다... 원래는 백준 풀 때나 분석 돌릴때(혹은 공부할때) 간간이 켰었는데 애가 할배노트북이라 공부하러 밖에 나가질 못해요... 켜놓고 숨쉬면 배터리 2%씩 빠짐. 완충해놓고 충전기 안 꼽고 3시간 글쓰면 2~30%까지 방전됩니다. 그거는 충전기를 들고 다니면 해결은 되는데 스카 가는것도 돈이잖아... 집에서 하면 그것도 해결은 되는데, 사양 문제때문에 분석 돌리면 뻗어요 애가... 그래서 캐글 데이터분석 할 거 남아있는것도 아예 올스톱됐음...
그리고 리눅스는요… 윈도우처럼 친절하지 않아… 업데이트 있으면 당신이 터미널 가서 있나 봐야됨… 근데 쟤를 거진 한달인가 두달만에 켰거든요? 밀린 업데이트가 쌓였어요. 그거 까는데도 시간이 들어요… 아니 그럼 그거 설치하면 되는거 아니예요? 그게 거기서 끝났으면 내가 새벽 3시까지 밤을 안 샜어요…
우분투에 도커를 작년에 깔았습니다. 그래서 버전이 낮아요. docker compose v2가 있어야되는데 우분투에 v1이 있었어. 아니 그럼 딸깍 업데이트 하면 안돼요? 리눅스에 그런 친절함 바라시면 안돼요. 애초에 리눅스는 뭐 깔때도 터미널에서 sudo 안 붙이면 권한 없다고 빠꾸먹이는 OS예요. 파이참도 윈도우 맥에서는 딸깍하면 열릴때 압축 푼 폴더 경로 찾아가서 직접 쉘 스크립트 열어야 되는 OS예요...
그럼 업데이트를 안 하면요? 컨테이너가 신식 도어록인데 적재된거 풀어야되는 애가 구식 열쇠 갖고있으면 열려요? 도어락을 망치로 부실거야? 그래서 업데이트를 해야되는데 또 문제가 뭐냐... 리눅스에서 apt update && upgrade 돌릴 동안 dpkg가 잠깁니다. 그래서 업데이트 다 끝나고 잠김 풀릴때까지 또 대기해야돼요. 그 업데이트가 다 끝나고 도커 업데이트를 마무리했음... 그래서 v2로 바꿨습니다.
그러고 한 3~40분동안 디버깅했음... 업데이트는 됐으니까 적재하는데 문제는 없다, 그런데도 실행이 안되는건 화물... 그러니까 적재된 코드에 문제가 있다 해서 디버깅하고 올리고 실행하고 어씨 에러났다 디버깅하고 올리고 실행하고 뭐야 왜안돼 디버깅하고 올리고 실행하고 오씨 할렐루야 됐다 근데 그래프 글꼴을 왜 못찾지(그거 빼고 다 잘 돌아갔음) 에이씨 날려 걍 하고 끝난겁니다... 이 작업때문에 세시까지 밤샌거예요...
글꼴 아...
본인이 제일 싫어하는게 기본글꼴하고 고딕입니다. 손글씨나 도트체 이런거 좋아하는데… (마비옛체 좋아함) 그렇다고 포폴용 그래프에 마비옛체나 손글씨체를 쓸 수는 없으니까 그나마 타협 보는 폰트가 나눔스퀘어랑 프리텐다드입니다. 그래서 본인이 데이터 분석 한 글 보시면 글씨가 다 손글씨 마비옛체 픽셀체 이런데 BI쪽으로 한건 다 나눔스퀘어죠? 포폴로 쓸라고 일부러 나눔스퀘어로 한 겁니다.
그럼 뭐가 문제예요? 리눅스에 글꼴이 안 깔렸음? 깔려있습니다. 근데 인식을 못해요. 저장된 이름이 다른건지 경로때문인건지 모르겠는데 디버깅이 필요하기도 하고, 리눅스에서 글꼴 깔 때는 윈도우처럼 딸깍... 이거 개별 설치(ttf 하나)는 되는데 보통은 터미널에서 특정 경로에 폴더 만들고 거기다가 ttf파일을 복사합니다. 안그랬다가 피본 적 있음...
그래서 글꼴 설정을 왜 뺐냐고요? 나눔스퀘어가 안 깔려있는 다른 컴퓨터에서도 이런 오류가 나면 어떡함? 막말로 사측 컴퓨터로 적재된 코드 내리면서 돌리다가 그 컴퓨터에 나눔스퀘어가 없어서 뭐여 이 폰트 몰라요 하고 중간에 오류메시지만 줄줄이 소시지마냥 한 열댓줄 떠봐요. 어유… 세상천지 그런 대참사도 없어…
전에도 얘기했지만 SQLite는 파이썬 깔 때 알아서 따라오는 친구라 설치를 할 필요가 없습니다. 저 conn… 저거 내가 오라클 할 때도 봤던 것 같음… 이게 오라클이요… 리눅스에서 SQLD 할 때는 터미널로 돌렸습니다. 근데 이 터미널이 한글 쓰다가 오타가 나면 아주 괴랄한 문자가 나오기도 하고, 다른 입력기처럼 뭐 위로 가서 수정해야징 이런게 안 돼서 따라가기 힘든 것도 있어서 파이썬이랑 연결해서 했었음…
저거 그리고 MySQL 파이썬에서 돌릴때도 나옵니다.
create_table_query = """
CREATE TABLE IF NOT EXISTS events (
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
search_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
event_type TEXT NOT NULL,
query TEXT,
result_count INTEGER,
compound_id INTEGER,
error_type TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
cursor.execute(create_table_query)
conn.commit()
근데 쟤가 파이썬 설치할 때 따라온거지 엄연히 언어라 다르기때문에 쿼리 막 갖다 쓰면 VScode가 님아 이거 틀렸어염 합니다. 착한 코더 여러분들은 파이썬에서 SQL 쓸 때 쿼리를 따옴표 세 개로 감싸주시길 바랍니다.
엥? 근데 저 NOT EXISTS는 뭐예요? 테스트를 한번 하고 끝이 아니라 디버깅하고 돌리고 하면 여러번 돌릴 수도 있음. 근데 도커 패킹하면서 쟤를 같이 패킹하면서 걍 CREATE 주면 두번째부터는 아이 있는데 왜 또 만들래! 하면서 에러를 퉤잇합니다. 그러니까 표가 '없으면' 만들라는 얘기를 하는거다. 저 밑에 커밋이요? 저거 안 하면 CREATE문 실행만 하고 기껏 만든 테이블이 반영 안 돼서 있었는데 없어질 수 있음.
table_view_query = 'PRAGMA table_info(events);'
cursor.execute(table_view_query)
rows = cursor.fetchall()
for row in rows:
print(row)
뭐야 셀렉트문 어디감? 저 테이블은 테이블만 있고 인스턴스들이 없어서 셀렉트문 써도 암것도 안 나옵니다. 근데 테이블이 만들어졌으면 정보는 어쨌든 있을 거 아니예요? 그러니까 정보 주십쇼 하는거다.
이벤트 생성
def __init__(self):
self.conn = conn
self.cursor = cursor
self.search_id = 0 # 일단은 0
# 여기 있는 분자들은 약으로 쓰고 있는 분자들입니다. (진세노사이드는 인삼에 들어있는 사포닌)
# 오타는 일부러 넣은겁니다. 대신 오타의 경우 후속 이벤트도 없고, 검색 결과 수도 0입니다.
# artemisinin: 오타입니다. artemisinin이 맞아요.
# nafroxen: 얘도 오타입니다. naproxen이 맞아요.
# acetaminopen: acetaminophen이 맞습니다.
# gentamycin: gentamicin이 맞습니다.
self.compounds = ["aspirin","ibuprofen","acetaminopen","acetaminophen","arteminisin","artemisinin","imatinib","nafroxen","ginsenoside","salicylic acid","naproxen","fluoxetine","thalidomide","duloxetine","gentamycin","gentamicin","rifampicin"]
self.compound_results = { # 검색 결과 고정해야 합니다. 똑같은 검색어로 똑같은 DB 뒤져서 누구는 100건 누구는 200건 이건 에바입니다.
"aspirin": 120,
"ibuprofen": 85,
"acetaminophen": 143,
"artemisinin": 67,
"imatinib": 41,
"ginsenoside": 23,
"salicylic acid": 98,
"naproxen": 74,
"fluoxetine": 56,
"thalidomide": 12,
"duloxetine": 37,
"gentamicin": 29,
"rifampicin": 44
}
self.typo_corrections = { # 룰 7. 오타의 경우 검색 결과가 0개/룰 8. 오타때문에 결과가 0이 나왔다면, 그 사용자는 제대로 된 철자를 사용해서 다시 검색한다. (하다가 추가됨)
"acetaminopen": "acetaminophen",
"arteminisin": "artemisinin",
"nafroxen": "naproxen",
"gentamycin": "gentamicin"
}
self.current_search_id = 1
전체 코드 올려드리고 싶은데 이거 전체 코드 올리면 5000자 넘어가서 네이버에서는 짤립니다...
일단 저 오타는 일부러 넣은거고… 쟤들 다 뭐냐고? 일단 다들 알법한 아세트아미노펜, 이부프로펜, 아스피린은 생략하고… 아르테미시닌은 개똥쑥(풀때기 이름이 그럼)에 들어있는 성분인데 말라리아 잡는 데 쓰는 성분이고, 이마티닙은 글리벡이라고 하면 어? 그거? 하는 분들도 있을거다. 나프록센은 진통제인데 NSAID 계열이고, 플루옥세틴(=프로작)은 항우울제고, 탈리도마이드는 예전에 입덧에 직방이라고 해서 먹었는데 이성질체가 혈관 생성을 억제하는 효과가 있어서 기형아를 유발했던 약이다. 지금은 이 성질을 이용해서 제한적으로 항암제로 쓰는 중이다. 둘록세틴(=심발타)은 항우울제고, 젠타마이신이랑 리팜피신은 항생제. 진세노사이드는 사포닌 중에서도 특히 인삼에 들어있는 사포닌을 이르는 말이고(인삼 들어간거 광고할때 많이 나옴), 살리실산… 아스피린이랑 다릅니다. 아스피린은 '아세틸'살리실산이고 살리실산은 식물 호르몬임. 각질 없애는데 들어갑니다.
저게 사실은 그냥 코드 쭉 쓰면서 했다가 OOP 서타일로 클래스 안에 함수 넣어서 걍 이벤트 찍어내게 만든거다. 이렇게 하면 뭐 문제 생겼을 때 클래스만 건들면 걔가 찍어내는 객체가 바뀌잖아요.
이건 이벤트 시간 랜덤 생성기인데, 문제가 하나 있다. 전에 룰 얘기하면서 모든 이벤트는 검색이 선행되고 그 다음에 후행으로 진행된다고 했잖아요? 근데 시간을 랜덤으로 짜면서 거기에 대한 처리를 안 했다... ㅋㅋㅋㅋㅋㅋ 순서는 후행인데 시간이 먼저 나올 수도 있음... 하...
이게 이벤트 생성하는 애다. 밑에 매개변수까지 만드는 애는 따로 있고 얘는 매개변수 입력받아서 이벤트 찍어내는 애라고 보시면 됩니다. 그 INSERT문 밑에 또 커밋이 있죠? 이거 원래 이렇게 하면 안됩니다... 지금이야 뭐 한 만개 나오고 땡이지만 실제로는 한 백만개 천만개 쌓일수도 있는데 이걸 일일이 커밋한다?
상사한테 이 소리 들을 수 있음.
# 검색에서 후행 이벤트 파생시킬 애
def generate_search_flow(self):
user_id = random.randint(1, 999999) # 사용자 아이디: 랜덤입니다. 마치 포켓몬 게임의 트레이너 아이디같은 것... (랜덤임)
query = random.choice(self.compounds) # 사용자의 검색어
search_id = self.current_search_id # 검색 아이디
self.current_search_id += 1
if query in self.typo_corrections: # 오타났으면
result_count = 0 # 결과 0개 (룰 7에 의거함)
else:
result_count = self.compound_results[query] # 아니면 개수 출력하시고
event = self.insert_event(search_id,user_id,"search",query=query,result_count=result_count) # 이벤트 생성
events = [event]
if result_count > 0:
self.generate_followup_events(events, search_id, user_id, query, result_count) # 후행 이벤트 따로 뺐습니다
else:
result_view_event = self.insert_event(search_id, user_id, "result_view", query=query, result_count=0)
events.append(result_view_event)
# 룰 8. 오타때문에 결과가 0이 나왔다면, 그 사용자는 제대로 된 철자를 사용해서 다시 검색한다.
if query in self.typo_corrections:
corrected_query = self.typo_corrections[query] # 오타때문에 결과가 안 떴으니 철자를 교정해서 재검색을 해줍니다
corrected_search_id = self.current_search_id
self.current_search_id += 1 # 사용자는 같지만 검색은 다시 하는거라서 아이디는 다름
corrected_result_count = self.compound_results[corrected_query] # 철자 정정했으니까 다시 결과가 나오겠죠?
corrected_event = self.insert_event(corrected_search_id, user_id, "search", query=corrected_query, result_count=corrected_result_count) # 그럼 다시 생성해야겠죠?
events.append(corrected_event)
self.generate_followup_events(events, corrected_search_id, user_id, corrected_query, corrected_result_count)
return events
주석이 잘못됐는데 얘가 모든 이벤트의 시발점같은 애임. 욕 아님.
주석에 포켓몬 아이디는 뭔 소리냐고요? 포켓몬 게임에 보면 트레이너 아이디라는 게 있습니다.
이 이미지에서 IDNo. 라고 쓰여있는 게 트레이너의 아이디인데, 구세대는 5자리였고(쟤는 리프그린이라 5자리) 6세댄가 7세대부터 6자리로 늘어났어요. 저기서 세이브파일을 날리고 다시 처음부터 시작하면 저 아이디 바뀝니다. 저게 랜덤이라 저 아이디 번호 12345나 23456같은 걸로 맞추려고 리셋하는 사람도 있어요.
근데 나는 검색 아이디도 난수로 하려고 했는데 채찍피티가 순차적으로 생성하게 만들었네... if query in self.typo_corrections 밑에 있는 건 오타났으니 정해서 찾는거다. gentamycin으로 찾았으면 0일거고, 아 철자 틀렸구나 하고 gentamicin으로 다시 검색한다는 얘기.
# 원래 한 덩어리였던 후행 이벤트를 따로 뺐습니다.
def generate_followup_events(self, events, search_id, user_id, query, result_count):
if random.random() < 0.95: # 5% 확률로 에러가 당신을 반깁니다
# 검색 결과 조회
result_view_event = self.insert_event(search_id, user_id, "result_view", query=query, result_count=result_count)
events.append(result_view_event)
# 검색 결과에서 개별 화합물 결과를 보겠죠?
chemical_view_count = random.randint(1, min(result_count, 5))
for _ in range(chemical_view_count):
# 하지만 1번만 보라는 법은 없죠?
compound_id = random.randint(1, result_count)
chemical_event = self.insert_event(search_id, user_id, "chemical_view", query=query, compound_id=compound_id)
events.append(chemical_event)
# 다운로드(CSV로)->다운로드는 한번만 합니다.
if random.random() < 0.7: # 실패할 확률 있음(30%)
csv_event = self.insert_event(search_id, user_id, "csv_download", query=query)
events.append(csv_event)
else:
error_event = self.insert_event(search_id, user_id, "error", error_type="csv_download_failed")
events.append(error_event)
else:
error_event = self.insert_event(search_id, user_id, "error", error_type="result_page_timeout")
events.append(error_event)
원래 한 덩어리였는데 왜 뺐냐를 먼저 말씀드리겠음... 이벤트 순서상 검색이 시발점이고 그 뒤에 후행 이벤트가 온다고 했는데, 이게 검색어 오타나서 정정하고 다시 검색한 검색어에도 적용이 돼야 합니다. 근데 안되는거야. 되게 하려면 저 코드부분을 if 오타 고 투 정정 밑에 또 넣으라는데 이렇게 되면 문제가 뭐게요? 100줄이 넘는 코드에서 위쪽 수정하면 아래쪽도 찾아서 수정해야됩니다. 그리고 내가 제일 싫어하는게 쓸데없이 복잡한거거든...
어쨌든 검색 이벤트에서 분기가 있다뿐이지 후행 이벤트는 같으니까 걍 저거 밖으로 빼서 덩어리 만들고 검색 이벤트에서 그 덩어리를 연결한거다. 그러면 저 덩어리만 수정하면 되니까 편하죠. 코드 줄 수가 줄어드는 건 아니겠지만.
일단 데이터 관련해서도 직무가 크게 데이터 분석이랑 엔지니어링 있습니다. 둘이 하는 일이 아예 다른데 분석은 주어진 데이터를 지지고 볶아서 어떤 인사이트를 내는거고 데이터 엔지니어링은 데이터 분석, AI 모델링, 의사결정에 필요한 데이터를 안정적으로 수집, 저장, 처리, 변환하는 시스템(파이프라인)을 설계 및 구축하는거임. 그리고 내가 이력서 준비한거나 부트캠프 들었던건 다 분석쪽인데... 오 여기 근무조건 개꿀 이러고 이력서랑 포폴 냈던 직무가 엔지니어링인겁니다.
그러니까 데이터 분석 이력서랑 데이터 분석 포폴로 데이터 엔지니어링 직무에 냈는데 서류합격을 해서 과제를 받고 오늘 새벽 3시까지 해서 제출 완료한겁니다… 예… 둘이 하는 일이 아예 다르잖아요? 해보니까 분석이랑 엔지니어링이랑 둘이 공통점은 파이썬을 쓴다밖에 없는거같음. 물론 분석쪽에서도 전처리를 하긴 하는데 보통은 결측값이나 이상치같은거 분석하기 전에 쳐내는거지 이렇게 본격적으로 한 적은 없었음… 물론 쳐내고 다듬을게 많아서 골아팠던 적은 있습니다만.
니콜라스형... 미안...ㅋㅋㅋㅋㅋㅋ
그래서 메일 받고나서 본인 표정은 이거였음. 사측에서도 내 이력서 보고 저 표정 지었을거임... 솔직히 두 직무가 뭔 차인지 몰라서 오 근무조건 개꿀 하고 넣은거긴 한데, 그렇다고 해서 또 과제를 대충 할 순 없잖아요? 그러니까 풀라고 준 건 최대한 성심성의껏 풀었음... 하던게 아닌것도 있고, 아예 처음 보는것도 있어서 아마 사측에서 깃헙 보면서 이건 또 뭐여 하고 계실겁니다...
과제를 제출하고 문의 메일을 보냈을 때 회사명이랑 문제를 공개해도 된다고 답변을 받았습니다. 근데 사실 허락을 받았어도 회사명은 원래 공개 잘 안해서… 문제도 100% 공개하는 건 아니고 큰 틀만 공개할거임.
문제
Step 1. 이벤트 생성기 작성
Python, Go, Java 중 하나를 이용해 웹 서비스에서 발생할 수 있는 이벤트를 랜덤하게 생성하는 스크립트를 작성하는 것. 단, 이벤트는 최소 두가지 이상이어야 한다.
Step 2. 로그 저장
생성한 이벤트를 본인이 적합하다고 생각하는 저장소에 자유롭게 저장할 것. JSON을 통째로 저장하지 말고, 필드를 구분하여 저장해야 한다. (JSON: 그 파이썬 딕셔너리같이 생긴거 있음)
Step 3. 데이터 집계 분석
저장된 데이터를 분석하는 쿼리(또는 코드)를 최소 2개 작성할 것. 아래 예시에서 해도 되고, 다른거 해도 된다.
1) 이벤트 타입별 발생 횟수 2) 유저별 총 이벤트 수 3) 시간대별 이벤트 추이 4) 에러 이벤트 비율
Step 4. Docker로 실행 가능하게 만들기
docker-compose.yml을 작성해서 docker compose up 한 번으로 전체 스택이 실행되게 구성할 것. (앱+DB까지)
Step 5. 결과 시각화
SQL 집계 결과를 시각화하면 됩니다. 차트 생성해서 그걸 이미지로 저장하거나 BI툴(태블로)을 쓰거나 편한걸로 하시면 됨.
이게 문제인데… 일단 보자마자 대뇌 커널패닉이 온 것도 있고… 문제가 하나 더 있었음. 제가 몇 번 얘기하지 않았습니까? 리눅스 노트북이 할배노트북이라(램 8기가임…) 분석 돌리다가 뻗는다고… 윈도우에도 파이썬이 깔려는 있는데, 딱 백준 풀 용도로 파이썬만 깔아둔거라 넘파이 판다스 폴라스 플롯틀리 이딴거 하나도 안 깔려있다. 그래서 JS는 윈도우에서 하고 파이썬은 리눅스에서 돌렸다가 컴퓨터 사양 문제로 스톱했어요… 맥북은 집으로 갔고.
그니까 선택지가 할배노트북에서 사리적립하면서 하기 vs 게이밍 놋북(윈도)에서 최소한의 설치만으로 진행하기 두개였고 데드라인이 3일이라 나는 후자를 골랐음. 그래서 이거 도커 돌리면서 까는거 딱 하나 있습니다. matplotlib. SQL 돌리긴 돌리는데 SQLite는 파이썬 깔 때 딸려오는 친구라 별도로 설치는 안 해도 됨. 근데 내가 예전에도 한번 얘기했던... 이놈들 표준을 합치긴 했는데 그럼에도 불구하고 지네들꺼에만 되는 기능이 있다고 했죠? 오라클의 롤업 큐브 그루핑셋 이런거. 그런게 SQLite에도 좀 있으니까 문법은 한번 찾아보십셔... 합칠거면 아예 통일하든가 해야되는데 이놈들이 논의를 어떻게 한 건지 모르겠음.
아무튼 그래서 사만팔천가지 모듈이 다 깔렸지만 돌리다가 뻗을지도 모를 리눅스는 나중에 도커 패킹한거 테스트하는 용도로만 썼습니다. 도커 얘기는 나중에 또 털어드리겠음…
서비스 구상
서비스를 구상하는 과정에서도 뭐하지… 하다가 그래도 많이 접했던+익숙한거 하는 게 좋을 것 같아서 ChEMBL같은 데이터베이스로 하자 했음. 내가 개인적으로 다뤄봤던 데이터는 캐글 데이터 말고는 NCBI 창고 탈탈 털어서 DNA 이런거 가져온게 다였음… 그리고 마케팅 이런쪽은 아예 문외한이라(본인 뼛속까지 이과) 전에 팀플할때도 GA4 다루면서 진짜 스트레스 엄청 심했거든… 쇼핑몰 이런게 무난하긴 한데 내가 그런거 잘 몰라서 놓치는 부분도 분명 있을거란 말이죠. 그래서 DB로 하자! 하고 정했다. 그리고 웹 서비스에서 발생할 수 있는 이벤트라고 했지 뭐 하라고 딱 정해주진 않았잖아요?
데이터베이스에서 이벤트를 뽑아낼 수 있냐고? ChEMBL에서는 일단 1) 검색하고 2) 결과 보고 3) CSV파일로 받는것만 해도 3개 나온다. 거기다가 하나 더 추가해서 개별 화합물 정보 조회까지 하면 일단 네 개 나온다. 근데 이 이벤트를 그냥 막 갖다가 생성하면 안된다. 이 이벤트들에는 일련의 규칙들이 존재해야 한다. 생각해봐요. 화합물 검색도 안 했는데 CSV부터 받을거임? 검색 결과 띄우다가 502떴는데 CSV파일 다운로드가 되겠어요? 안됨. 이런 일련의 규칙들을 정해야 하고, 생성할 때 저장할 데이터들도 정해야 한다. 모델링 드가자
Rule
1. 각 아이디는 6자리 정수형으로, 6자리가 안 될 경우 앞에 비는 만큼 0으로 채운다. (예: 1234->001234) 단, 저장할때는 걍 저장함. 2. 화합물 정보 확인, 검색 결과 조회, csv 다운로드는 검색이 반드시 선행되어야 한다. 3. 같은 검색 아이디 내에서 csv 다운로드는 한 번만 하는 것으로 한다. (보통 필요한 것만 한번에 받죠...?) 단, 화합물 조회는 검색 결과가 여러개라면 여러 번 진행될 수 있다. 4. 같은 사용자가 여러 개의 화합물을 검색할 수 있다. 예를 들어서, 한 사용자가 아세트아미노펜과 이부프로펜을 검색할 수 있다. 물론 검색 결과와 후행 이벤트 역시 다른 검색 ID에 대해서는 별개이다. 5. 검색 ID가 같다면 검색어도 같다. 6. 에러가 생겼을 때는 이벤트 아이디와 검색 아이디, 유저 아이디, 이벤트 타입, 에러 타입만 기록된다. 7. 검색하다가 오타가 났을 경우, 결과 개수가 0이 된다. 8. 오타때문에 결과가 0이 됐을 경우, 결과를 확인하고 재검색한다. 9. 검색어별 검색 결과 개수는 항상 일정해야 한다.
원래는 6번까지 있었는데 구현하면서 7~9번이 추가됐다. 이 규칙들이 이렇게 놓고 보면 이게 뭐여 싶지만 사실 하나하나 보다보면 어 이거구나 할 것들이긴 합니다. 아이디... 사용자랑 검색 아이디는 6자리 되는데 이벤트 ID는 안되데...
화합물 정보 확인, 검색 결과 조회, csv 다운로드는 검색이 반드시 선행되어야 한다. ->검색했더니 이만큼 있더라를 보려면 검색을 먼저 해야 한다. 그리고 이만큼 있었군 하고 나서 개별 화합물 정보를 보거나(안 볼 수도 있지만) 정보를 받을 화합물을 선택하고 CSV 파일을 받는다.
검색 ID가 같다면 검색어도 같다. ->이건 3, 4, 5를 같이 설명해드림. 일단 3번... 물론 선택을 다르게 해서 다운로드를 여러번 할 수는 있겠지만, 기본적으로 여기서는 한 번의 검색당 한 번의 다운로드를 하는 걸로 정했다. 4번은 구글 검색을 예로 들어보자... 코바늘로 텀블러백 도안도 찾고 코스터 도안도 찾고 액막이 명태 도안도 찾고 할 수 있잖아요? 인공지능 크레딧은 한도가 있겠지만 검색은 하루에 하나만 되는거 아니잖음. 5번은 2번이랑 연결해서 보면 바로 이해가 되실건데, 일련의 이벤트들이 검색이 먼저 선행되어야 합니다. 그리고 A라는 유저가 이부프로펜을 검색하고(이부프로펜의 검색 아이디 생성) 이부프로펜의 결과를 보고 이부프로펜의 결과를 다운로드 받는거니까 한 사용자가 한 검색어에 대해서 진행하는 후행 이벤트의 검색어는 모두 동일하다.
7번~8번은… 사실 이건 생각 못 했던건데 구현하다가 추가했음… 여러분 젠타마이신은 gentamicin인 거 아십니까? 항생제는 대부분 어쩌고마이신 어쩌고실린 이렇게 끝나는데, 마이신 들어가면 보통 접미어가 mycin입니다. 반코마이신도 Vancomycin이고 카나마이신도 Kanamycin이잖아요. 근데 젠타마이신은 Gentamicin입니다. 이런 식으로 철자를 헷갈린다거나, 모 게임의 모 대장장이마냥 손이 미끄러진다던가, 키보드가 뻑난다던가 하는 등의 이유로 오타가 날 수 있잖아요? 그러면 결과가 제대로 안 나오니까(여기서는 결과 개수가 0) 뭐여? 아 철자 틀렸네 하고 다시 검색한다 이겁니다.
9번은 어떻게 보면 당연한거다. 누구는 이부프로펜 찾았는데 결과 100개 나오고 누구는 똑같이 이부프로펜 찾았는데 200개 나오면 안되잖아요? 그거다. 그래서 얘는 구현단에서 아예 검색 결과 수를 매핑했다.
스키마
콜아웃 배경을 노란색으로 해서 저런거니 양해 바람... 그리고 저 에러타입은 구현단에서 값 바뀐다.
이거 뭐 여기 들어오시는 분들은 대부분 뭔지 알테니까 기본적인 건 생략하겠음. AI는 Auto Increment의 줄임말이고(별도로 값을 써주지 않아도 자동으로 값이 증가한다), 따로 명시하지 않았지만 event_id는 PK(프라이머리 키)이기때문에 unique & !null이 추가로 붙는다. !null이 낫 널임. 저기 열쇠는 뭐냐고? 프라이머리 키라는 얘기다.
맥북 반납해서 EDA를 못합니다... 리눅스 노트북은 데이터 분석 돌리면 뻗어서 평일에 글쓸때나 자바스크립트 할 때 말고 켜본적 없음...
그새 글꼴이 많이 바뀌긴 했는데... 그래서 이 달력에 뭔 기능을 추가할거임? 바로 오늘 날짜로 되돌아가는 기능과 몇년 몇월로 이동하는 기능이다. 전자는 뭔지 알겠는데 후자는 뭐임? 여러분 이 달력에서 1991년 3월로 어떻게 가는지 아십니까? 1991년 3월 될때까지 이전달을 급나 눌러야됩니다. 일단 햇수로만 36년인데 1년이 12개월이니까 대충 클릭질 몇 번 해야 하는지 견적이 나오시죠?
오늘 날짜로 돌아가는 버튼
let goTodayButton = document.querySelector('#gotoday');
이게 HTML 구조는 간단한데 CSS가… ㅋㅋㅋㅋㅋ 내가 오래전에 한거라 뭘 어떻게 한건지 헷갈림… 여러분은 이런 사태를 미연에 방지하실거면 주석 다십쇼.
아무튼 갖고옵시다.
// 오늘 날짜로 고고싱하는 버튼
goTodayButton.addEventListener('click', (e) => {
// 1. 전역 변수인 'date'를 현재 시점의 날짜로 갱신
date = new Date();
// 2. 갱신된 date 객체를 바탕으로 달력을 다시 그림
renderCalendar();
});
사실 원리는 간단합니다. 오늘 날짜로 객체 생성하고 달력 다시 그리면 돼요. 그러면 일단 오늘 날짜로 복귀는 됐고… 이제 특정 년도 특정 달로 이동하는 기능 만들자.
특정 날짜로 이동하기
// 특정 날짜로 고고싱하는 버튼
goDateButton.addEventListener('click', (e) => {
let goYear = document.querySelector('#goyear').value;
let goMonth = document.querySelector('#gomonth').value;
// 1. 입력값이 있는지 확인 (비어있으면 동작 안 함)
if (goYear && goMonth) {
// 2. 전역 변수인 date 객체의 연도와 월을 업데이트
// 숫자로 형변환(Number)해주고, 월은 반드시 -1을 해줍니다.
date.setFullYear(Number(goYear));
date.setMonth(Number(goMonth) - 1);
// 날짜가 해당 월의 마지막 날을 넘어가는 버그를 방지하기 위해 1일로 세팅하는 것이 안전합니다.
date.setDate(1);
// 3. 갱신된 정보를 바탕으로 달력 다시 그리기
renderCalendar();
} else {
alert("이동할 연도와 월을 모두 입력해주세요!");
}
});
이것도 걍 그 날짜로 달력 다시 그리면 되던데요? 근데 날짜는 왜 1일로 바꿔서 그렸나요? 어차피 우리는 그 특정 해 특정 월로 이동만 할 거잖아요. 그리고 이 달력을 쓰는 당일 날짜가 31일이면 말이이 30일인 달이나 2월로 이동할때 날짜 안 바꾸면 2월 31일은 뭐야!!! 이러고 컴퓨터가 밥상 뒤집어요.
이렇게 하면 일단은 되는'데'… 아직 끝나지 않은 문제가 있다.
콘솔에만 뜨는 메시지 처리하기
여러분들에게는 보이지 않지만, 사실 이 달력을 구동할 때 콘솔창에 보이는 메시지가 하나 있다.
바로 이건데… 저 달력을 만들때 아마 채찍피티가 없었거나 초창기라 디버깅을 못했을거다. 근데 지금은 채찍피티 클로드 에미나이가 다 있잖아요? 그래서 디버깅 해달라고 할거임.
그렇다고 합니다. 그러니까 기존 코드에서는 함수에서 자체적으로 범위를 체크하고 있었는데, 이것때문에 혼선이 와서 저런 메시지가 뜨는 것이다. 그러니까 걍 자체 체크하는걸 없애고 Date 객체가 알아서 보정하게 두면 된다 이거죠.
function dayCalcDisplay(startYear, startMonth, startDay) {
// 1. 넘겨받은 연, 월, 일을 바탕으로 실제 Date 객체 생성
// (월은 0~11이므로 -1, 일은 그대로)
// 예: 2025년 2월의 이전달 날짜로 '31'이 들어와도
// new Date(2025, 1, 31)은 자동으로 2025년 3월 3일로 인식됩니다.
var tempDate = new Date(startYear, startMonth - 1, startDay);
// 2. 보정된 진짜 연/월/일을 다시 추출
var realYear = tempDate.getFullYear();
var realMonth = tempDate.getMonth() + 1;
var realDay = tempDate.getDate();
// 3. (옵션) 윤년 체크 로직은 사실 lunarCalc 내부에서 수행하므로
// 여기서는 범위 체크를 삭제하거나 보정된 값을 사용합니다.
if (realYear < 1900 || realYear > 2040) {
return ""; // 지원 범위 밖이면 빈칸 처리
}
/* 양력/음력 변환 (보정된 진짜 날짜를 넣음) */
var lunar = lunarCalc(realYear, realMonth, realDay, 1);
return (lunar.leapMonth ? "윤" : "") + lunar.month + "." + lunar.day;
}
그래서 함수가 더 짧아졌음. 근데 저거 생각해보니까 2040년 넘어가면 ""준다고? 아니… 그때 나 50살인데? 뭐 그래요… 그때쯤 되면 뭔가 새로운 방법이 나오겠지… 그때 코드를 바꾸던가 합시다…
내가 이걸 또 하게 되다니… 싶었는데 쓰다가 또 불편한 게 나와서 에미나이 도움 받아서 기능을 추가했다.
이거 보여요? 기존에는 없었던 저 HEX 코드들. 이게 중간색을 쓰고 싶은데 #rrggbb 코드가 없으니까 내가 개발자도구 열고 들어가서 변환을 해야되는데 이게 증말 번거롭습니다… 그리고 나야 개발자도구의 존재를 안다지만 모든 사람들이 그렇진 않잖아요?
for (let i = 0; i < n; i++) {
const step = i / (n - 1);
const r = Math.round(startRgb[0] + (endRgb[0] - startRgb[0]) * step);
const g = Math.round(startRgb[1] + (endRgb[1] - startRgb[1]) * step);
const b = Math.round(startRgb[2] + (endRgb[2] - startRgb[2]) * step);
const toHex = (c) => c.toString(16).padStart(2, '0');
const hexCode = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
const chip = document.createElement('div');
chip.classList.add('palette_chip');
chip.style.backgroundColor = `rgb(${r}, ${g}, ${b})`;
chip.style.flex = "1";
// 스타일 및 텍스트 설정
chip.style.display = "flex";
chip.style.alignItems = "center";
chip.style.justifyContent = "center";
chip.style.cursor = "pointer"; // 클릭 가능하다는 표시
chip.style.color = (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000' : '#fff';
chip.innerText = hexCode;
chip.onclick = () => {
navigator.clipboard.writeText(hexCode).then(() => {
// alert 대신 토스트 호출!
showToast(`${hexCode} copied!`);
}).catch(err => {
console.error('복사 실패:', err);
});
};
palette_div.appendChild(chip);
}
저정도 되면 어디까지가 한 블록인지 헷갈려요... const toHex = (c) => c.toString(16).padStart(2, '0');랑 const hexCode = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); 보여요? 저걸로 10개 색상을 HEX코드로 변환한 다음에 이너텍스트 박을거고, 그 네모를 누르면 HEX 코드가 복사되면서 복사됐음! 도 할 거다. 처음에는 alert였는데 alert는 확인버튼을 눌러야되거든요? 그래서 토스트 메시지로 바꿨음.
function showToast(message) {
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
document.body.appendChild(toast);
}
toast.innerText = message;
toast.classList.add('show');
// 2초 뒤에 사라지게 설정
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
그래서 토스트 창(그 있어요 나왔다가 들어가는 창) 생성 함수가 추가됐고
/* 토스트 메시지 기본 스타일 */
#toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s, bottom 0.3s;
z-index: 9999;
pointer-events: none; /* 클릭 방해 금지 */
}
/* 토스트가 보일 때의 상태 */
#toast.show {
opacity: 1;
bottom: 50px;
}
그 전에 clinVar 하면서 태블로 대시보드 만들었죠? 거기서 염색체별로 Top 25 만들고 그랬는데… 그거를 이제 CLNVC(변이)별로 뭐가 제일 많은지 보자는 얘기다.
SNV는 저번 글에 나와있었기 때문에 생략.
들어가기 전에-이게 뭔 변이임?
이 블로그에 들어오시는 분들중에는 생물정보학을 하고 있거나, 나처럼 업으로 삼지는 않았지만 거기에 관심이 있거나, 생물학 전공인 경우도 있겠지만 어때요? 여기 들어와서 clinVar라는 걸 처음 보신 분도 계시지 않습니까? 그래요, 그겁니다. EDA 따라오면서도 이게 뭐여 해서 뭔가 찾아보니 보이는 것은 꼬부랑글씨였으며…
물론 유전자에 문제가 생긴다고 다 질병이 되는 건 아니고, 피부 색이나 눈 색, 머리카락 색같이 사람의 형질이 달라지는 경우도 있긴 있습니다. 근데 여기 부록으로 나온건 다 패소제닉(Pathogenic)이라 뻑나면 병나는거예요.
1. Deletion: 일단 뭔가 나간 변이다. 나간 게 염기일수도 있고, 염색체 일부(염기 뭉텅이)일수도 있다. 그러니까 집 설계도라고 치면 벽이나 문, 창문같은게 하나 빠지거나 방 하나가 나가거나 뭐 그런거.
2. Duplication: 뭔가 1+1이 됐다. 집 설계도에 방이 두개였는데 세개가 된다거나, 창문이 하나였는데 두개가 된다거나… 이게 무조건 위험한 건 아니고 카피 넘버 배리언스(CNV)라고 해서 그 자체가 형질이 되는 경우도 있습니다. 헌팅턴 무도병도 유전자 내에 CAG가 불어나서 생기는거예요.
3. Insertion: 뭔가 껴들었다. 염기가 끼어들 수도 있고 유전자 자체가 끼어들 수도 있는데, Transposon(유전자가 이사다님)으로 인해 유전자가 끼어들어서 생기는 현상중에 유명한건 얼룩덜룩한 옥수수.
4. 1+3. 뭐가 드나드는게 인델이다. 인서션 딜리션 해서 인델.
5. Inversion: 유전자가 반대로 들어갔다. 그니까 집 설계도로 치자면 문이 밖으로 열려야되는데 안으로 열리게 들어간거다. 물론 유전자가 반대로 들어가는건 방...에 비유할수가... 있어요? 방이 위아래 거꾸로 들어갈 수가 있음?
6. Microsatellite: 짤막한 염기서열이 반복되는 돌연변이. 이것도 뭐 사람들한테 위험하다… 이런 건 아니고… 맞죠? 전공수업때는 못봤던거임…
fig = px.treemap(
clnvc_grp,
path = ['CLNVC'],
values = 'Total',
color = 'Total',
color_continuous_scale = 'algae',
title = "Pathogenic Variant Distribution",
)
# text_auto 대신 update_traces를 사용하여 블록 안에 텍스트를 강제로 넣습니다.
fig.update_traces(
# label(그룹명)과 value(숫자)를 함께 표시하라는 명령입니다.
textinfo = "label+value",
# 숫자 포맷을 지정합니다 (천 단위 콤마: ,d)
texttemplate = "%{label}<br>%{value:,d}",
# 텍스트가 블록 크기에 맞게 조절되도록 설정
textfont_size = 20,
insidetextfont_family = "NanumSquare_ac" # 설정하신 폰트 적용
)
fig.update_layout(
width = 1200,
height = 800,
margin = dict(t=50, l=10, r=10, b=10)
)
# fig.write_image('pathigenic_distributuion_heatmap.png')
fig.show()
SNV 면적이 압도적이죠?
fig = go.Figure()
fig.add_trace(go.Pie(
labels = clnvc_grp['CLNVC'],
values = clnvc_grp['Total'],
textinfo = 'label+percent', # 이름과 퍼센트 동시에 표시
insidetextorientation = 'radial',
marker = dict(colors=px.colors.qualitative.Vivid) # 전역 설정 색감 유지
))
fig.update_layout(
title_text = "Pathogenic Variant Distribution", title_x=0.9,
width = 1200,
height = 800,
margin = dict(t=50, l=20, r=20, b=10)
)
# fig.write_image('pathgenic_distributuion_pie.png')
fig.show()
음… 이거 포폴에 써먹을 수 있나…
상염색체가 쪽수가 많은 건 알겠는데, 뭐가 제일 많음?
# 1. 데이터에 있는 실제 값 순서 (가출 방지용)
real_values = ["Sex_Chrom", "Autosome", "Mitochondria"]
# 2. 그래프 위에 표시하고 싶은 예쁜 제목들 (순서 일치 필수!)
display_titles = ["Sex Chromosome", "Autosome", "Mitochondria"]
# 3. 서브플롯 생성
fig = make_subplots(
rows=1, cols=3,
subplot_titles=display_titles, # 여기서 예쁜 제목 적용!
horizontal_spacing=0.08
)
# 4. 반복문은 실제 값(real_values)으로 돌립니다
for i, real_val in enumerate(real_values):
curr_df = clnvc_grp.filter(pl.col('CHROM_Type') == real_val)
# 내림차순 정렬해서 보기 좋게
curr_df = curr_df.sort("Total", descending=True)
fig.add_trace(
go.Bar(
x = curr_df['CLNVC'],
y = curr_df['Total'],
text = curr_df['Total'],
texttemplate = '%{y:,}',
textposition = 'outside',
# 색깔은 이미 설정한 대로!
marker_color = px.colors.sequential.algae[-(i*3+1)]
),
row = 1, col = i + 1
)
# 5. 레이아웃 정리
fig.update_layout(
height=600,
width=1250,
title_text="ClinVar Distribution by Chromosome Type",
showlegend=False,
margin=dict(t=100, l=20, r=20, b=50), # 제목 공간 확보 위해 t(top) 늘림
)
# fig.write_image('pathgenic_chr_distributuion_subplot.png')
fig.show()
그냥 SNV가 압도적이다.
chr_order = [str(i) for i in range(1, 23)] + ['X', 'Y', 'MT']
# 2. 비율 계산
clnsig_perc = clnvc_grp.with_columns(
(pl.col("Total") / pl.col("Total").sum().over("CHROM") * 100).alias("Percentage")
)
# 3. Plotly Express로 그리기
fig = px.bar(
clnsig_perc,
x = "CHROM",
y = "Percentage",
color = "CLNVC",
text = "Percentage",
title = "CLNVC ratio by Chromosome",
color_discrete_sequence = px.colors.qualitative.Vivid,
# --- 이 부분이 마법의 한 줄입니다 ---
category_orders = {"CHROM": chr_order}
# ----------------------------------
)
fig.update_traces(
texttemplate='%{text:.1f}%',
textposition='inside',
insidetextfont_family="NanumSquare_ac" # 설정하신 폰트 적용
)
fig.update_layout(
width=1250,
height=800,
barmode='stack',
xaxis=dict(title='Chromosome'),
yaxis=dict(title='Percentage (%)'),
margin=dict(t=100, l=20, r=20, b=50),
)
# fig.write_image('chr_ratio.png')
fig.show()
저게 문자로 잡혀서 순서 수기로 잡아줘야 합니다. 아무튼 각 염색체별로 제일 많은 건 SNV고 그 다음에 Deletion이다. 미토콘드리아의 경우 Deletion 다음으로 Insertion, 나머지 염색체는 Deletion 다음으로 Duplication이 제일 많다.
# 1. 염색체 정렬 순서 정의
chr_order = [str(i) for i in range(1, 23)] + ['X', 'Y', 'MT', 'M']
# 순서대로 숫자를 매칭한 딕셔너리 생성 (1:0, 2:1, ..., X:22, Y:23...)
chr_map = {val: i for i, val in enumerate(chr_order)}
# 2. 데이터 집계 및 필터링
top_gene_df = (
pathogenic_df
.group_by(['CHROM', 'GENE_SYMBOL'])
.agg(pl.count('GENE_SYMBOL').alias("Count"))
.filter(pl.col("Count") == pl.col("Count").max().over("CHROM"))
.unique(subset=["CHROM"], keep="first")
)
# 3. 정렬 로직 수정 (최신 Polars 버전 대응)
top_gene_df = (
top_gene_df
.with_columns(
pl.col("CHROM").cast(pl.Utf8)
)
.with_columns(
# replace 대신 replace_strict를 사용하고, 매핑 안 되는 값은 99로 처리합니다.
pl.col("CHROM").replace_strict(chr_map, default=99).alias("sort_idx")
)
.sort("sort_idx")
)
위에도 썼지만 X, Y가 껴서 문자라 수동으로 정렬해줘야 합니다... 아무튼 각 염색체별로 변이가 가장 많은 유전자를 갖고왔다. 판다스였으면 idxmax 쓰는건데 까비...
# 1. 표에 들어갈 데이터 정리 (위에서 정렬된 top_gene_df 활용)
header_values = ["<b>Chromosome</b>", "<b>Top Gene Symbol</b>", "<b>Variant Count</b>"]
cell_values = [
top_gene_df["CHROM"],
[f"<b>{gene}</b>" for gene in top_gene_df["GENE_SYMBOL"]], # 유전자 이름 강조
[f"{count:,}" for count in top_gene_df["Count"]] # 천 단위 콤마
]
# 2. Plotly Table 생성
fig = go.Figure(data=[go.Table(
# 컬럼 너비 비율 조절
columnwidth = [100, 150, 120],
# 헤더 스타일
header = dict(
values = header_values,
fill_color = '#2E4A3F', # Algae 계열의 짙은 색
align = 'center',
font = dict(color='white', size=16, family="NanumSquare_ac"),
height = 40
),
# 셀 스타일
cells = dict(
values = cell_values,
fill_color = ['#F5F5F5', 'white'], # 줄바꿈 배경색 효과
align = ['center', 'center', 'right'],
font = dict(color='#333', size=14, family="NanumSquare_ac"),
height = 30
)
)])
# 3. 레이아웃 설정
fig.update_layout(
title = "Top Mutated Gene by Chromosome (ClinVar Pathogenic)",
width = 800,
height = 900, # 데이터가 24줄(1~22, X, Y, M) 정도 되니 높이를 넉넉하게
margin = dict(t=80, l=20, r=20, b=20)
)
# fig.write_image('chr_gene.png')
fig.show()
그… 여러분… Plotly에서는 표를 그릴 수 있습니다. 뭐 마우스 올려도 뭐 없긴 한데 아무튼.
근데 이런것도 될 줄은 몰랐지… 이게 뭔 표임? 각 염색체별로 변이가 가장 많은 유전자들만 꼽아서 그 중에서 1, 2, 3등을 매긴 결과다. BRCA2, TTN, BRCA1 순으로 변이가 많다. 근데 저 SRY는 뭐죠? 성별 결정하는 유전자입니다. 찾아보니 고환 결정 인자란다.
# 1. 염색체 정렬 순서 정의
chr_order = [str(i) for i in range(1, 23)] + ['X', 'Y', 'MT', 'M']
# 순서대로 숫자를 매칭한 딕셔너리 생성 (1:0, 2:1, ..., X:22, Y:23...)
chr_map = {val: i for i, val in enumerate(chr_order)}
# 2. 데이터 집계 및 필터링
top_gene_df = (
pathogenic_df
.group_by(['CHROM', 'CLNVC','GENE_SYMBOL'])
.agg(pl.count('GENE_SYMBOL').alias("Count"))
.filter(pl.col("Count") == pl.col("Count").max().over("CHROM"))
.unique(subset=["CHROM"], keep="first")
)
# 3. 정렬 로직 수정 (최신 Polars 버전 대응)
top_gene_df = (
top_gene_df
.with_columns(
pl.col("CHROM").cast(pl.Utf8)
)
.with_columns(
# replace 대신 replace_strict를 사용하고, 매핑 안 되는 값은 99로 처리합니다.
pl.col("CHROM").replace_strict(chr_map, default=99).alias("sort_idx")
)
.sort("sort_idx")
)
# 1. 변이 수 기준으로 데이터 정렬 (표 상단에 TOP 3가 오게 하려면 여기서 정렬)
# 현재 top_gene_df가 염색체 순서라면, 강조 로직을 수치 기준으로 적용해야 합니다.
top_gene_df = top_gene_df.sort("Count", descending=True)
# 2. 행별 배경색 결정 함수
def get_rank_color(i):
if i == 0: return '#FFD700' # Gold (1위: BRCA2)
if i == 1: return '#E5E4E2' # Platinum/Silver (2위: TTN)
if i == 2: return '#CD7F32' # Bronze (3위: BRCA1)
return 'white' if i % 2 == 0 else '#F9F9F9' # 나머지 가독성용 줄무늬
row_colors = [get_rank_color(i) for i in range(len(top_gene_df))]
# 3. 데이터 구성
header_values = ["<b>Chromosome</b>", "<b>CLNVC</b>", "<b>Top Gene Symbol</b>", "<b>Variant Count</b>"]
cell_values = [
top_gene_df["CHROM"],top_gene_df["CLNVC"],
[f"<b>{g}</b>" if i < 3 else g for i, g in enumerate(top_gene_df["GENE_SYMBOL"])],
[f"{c:,}" for c in top_gene_df["Count"]]
]
# 4. Table 생성
fig = go.Figure(data=[go.Table(
columnwidth = [80, 150, 100],
header = dict(
values = header_values,
fill_color = '#2E4A3F', # 헤더는 짙은 Algae색 유지
align = 'center',
font = dict(color='white', size=16, family="NanumSquare_ac")
),
cells = dict(
values = cell_values,
fill_color = [row_colors] * 3,
align = ['center', 'center', 'right'],
# 글자색은 무조건 검은색 계열로 고정하여 가시성 확보
font = dict(color='#333', size=14, family="NanumSquare_ac"),
height = 35,
line_color = '#E5E5E5' # 셀 구분선 살짝 추가
)
)])
fig.update_layout(
title = "Top 3 Mutated Genes Highlighted",
width = 750,
height = 1010,
margin = dict(t=80, l=20, r=20, b=20)
)
# fig.write_image('chr_gene_clnvc.png')
fig.show()
보시면 미토콘드리아(MT-ATP6) 빼고 다 SNV죠? 저게 개인적으로는 나비효과라고 생각하는데, 염기 하나가 바뀐 것 때문에 단백질이 바뀌고, 그걸로 인해서 기능에 문제가 생기는 게 질병으로 이어지기 때문이다. 그러니까 아주 작은 염기 하나가 바뀌는 것 때문에, 염기보다 훨배 큰 사람이 영향을 받는다 이거지.
반대로 변이가 가장 적은 유전자인데... 이게 1? 왜 1이죠? 일단 이렇다 하고 단정지을 수는 없는데... 치사유전 아세요? 혈우병이 치사유전이잖아요. 여자의 경우 혈우병 유전자를 양쪽 염색체가 둘 다 갖고있으면 보통은 사산됩니다. 물론 생물학에는 100%가 없으니 그 와중에도 정말 드문 확률로 살아서 나올 가능성은 있지만...
웨스턴블록 해본 사람들은 표적 단백질 말고 컨트롤? 그런거 같이 매겨서 밴드 굵기 보셨을텐데 이걸 왜 볼까요? 그죠. 이거 발현 안되면 X되는 단백질이라 보는겁니다. 풀때기에 엽록체가 없다고 생각해보세요. 광합성을 못해서 멀리 가십니다.
아니 왜 갑자기 웨스턴 얘기가 나옴? 나도 100% 그렇다고는 못 하지만, 저 유전자들은 변이가 생기면 질병 생기는 수준이 아니라 그냥 죽거나(생존을 못하거나) 대를 못 이을 가능성이 있습니다.
얘는 판다스에서 붙는 메모리 관련 옵션이 아예 안 붙는다. 근데 뭔가 붙어있지 않냐고? 걍 열면 Original error: invalid primitive value found during CSV parsing 에러 뜨니까 걍 다 읽고 판별하셈 한 겁니다. 그렇게 해도 1초 좀 넘게 걸려요.
CLNSIG으로 묶기
이게 일단 돌리는 방식은 예전이랑 동일한데, 데이터가 달라져서(4월 4일자껄로 진행) 결과가 달라질 수 있다. 4월 4일자에 든 게 4,403,603개고 전에 했던건 좀 적게 들어있었음... 그래서 총계 이런거에서는 차이가 날 수밖에 없음.
놀라운 사실을 하나 알려주지면, Polars에서는 단순히 그룹바이가 되는 걸 떠나서 .agg로 집계 매긴 다음 그 집계 칼럼에 별칭을 붙일 수 있다. 판다스도 되긴 되겠지만 쟤는 한번에 저게 된다. 두번째줄 pl.col('CLNSIG').count().alias("Total")가 무슨 의미냐면 CLNSIG 칼럼으로 묶고 셀 건데 그 결과물 칼럼 이름을 Total로 해달라는 의미. SQL 해보셨으면 감이 좀 왔을 것이다.
당연한 얘기지만, alias 없어도 저 코드는 작동을 한다. 근데 어지간하면 있는 편이 좋다.
alias 안 주면 Total이 아니라 CLNSIG으로 나오는데 이게 뭔줄 앎? 그럼 이걸로 이제 플롯틀리 그래프를 그리기 전에 정렬을 좀 해야되는데…
여러분들은 여기서 피똥 쌀 것이다. 일단 나는 여기서 피똥 쌌음... 아니 전역으로 컬러테이블 박아놨는데 마커는 또 일일이 주래요 이게 말이야 당나귀야 세상에...
그 결과물이 이거임. 다행히도 폰트 전역으로 설정해둔 건 듣는다. 어 근데 저기 저장 코드는 왜 주석처리 했어요? Plotly는 그래프 복사가 안 되고 저장한 다음에 올려야 하는데, 문제는 그 저장하는걸 할 때 시간이 증말 완전 개같이 오래 걸립니다. 그래서 그래프를 보여줄때는 저 코드에 주석을 씌워놓고 그래프_최종_진짜최종_저장.png 느낌으로 저장할때만 주석을 해제하는 것이다. 참고로 저거 그냥 저장되는 거 아니고 kaleido 까셔야 합니다.
# 트리맵은 Plotly Express(px)가 훨씬 직관적입니다.
fig = px.treemap(
clnsig_grp,
path = ['CLNSIG_Group'], # 계층 구조 (여기선 그룹 하나)
values = 'Total',
color = 'Total', # 숫자에 따라 색상 농도 조절
color_continuous_scale = 'algae',
title = "ClinVar Significance Hierarchy"
)
# 전역 설정된 폰트 적용 확인
fig.update_layout(width=1200, height=800)
# fig.write_image('group_distributuion_heatmap.png')
fig.show()
이 트리맵에는 문제가 하나 있다. 저기 막대그래프를 보시면 Risk/Other가 다른 값들에 비해 비중이 좀 적죠? 예. 블록이 안보입니다. 착한 사람만 보이는 블록임.
fig = go.Figure()
fig.add_trace(go.Pie(
labels = clnsig_grp['CLNSIG_Group'],
values = clnsig_grp['Total'],
textinfo = 'label+percent', # 이름과 퍼센트 동시에 표시
insidetextorientation = 'radial',
marker = dict(colors=px.colors.sequential.algae) # 전역 설정 색감 유지
))
fig.update_layout(
title_text = "ClinVar Significance Proportion",
width = 1200,
height = 800,
margin = dict(t=50, l=10, r=10, b=10)
)
# fig.write_image('group_distributuion_pie.png')
fig.show()
일단 왜 이런 사태가 일어났는지를 먼저 봐야 한다. clinVar 데이터에 들어있는 염색체가 언노운 빼고 총 25종인데 그 25종이 각개가 아니라 1~22번까지가 상염색체(남녀 다 똑같이 있는거), X랑 Y가 성염색체(남녀 분포 달라요), 그리고 미토콘드리아가 있어요. 이게 다 길이가 같다고 쳐도 쪽수때문에 일단 상염색체 물량이 압도적인데 염색체 그림 찾아보면 어때요? 1번 봐봐요. 크기가 벌써부터 이놈은 키가 커서 1번인가 하게 길어요…
그러니까 상염색체에 비하면 다른 염색체들은 거의 뭐 종합운동장에 축구공 하나씩 굴러다니는 정도로 미미한 수준이라 저렇게 나오는거다. 저걸 파훼할 방법이 있냐고?
# 1. 데이터에 있는 실제 값 순서 (가출 방지용)
real_values = ["Sex_Chrom", "Autosome", "Mitochondria"]
# 2. 그래프 위에 표시하고 싶은 예쁜 제목들 (순서 일치 필수!)
display_titles = ["Sex Chromosome", "Autosome", "Mitochondria"]
# 3. 서브플롯 생성
fig = make_subplots(
rows=1, cols=3,
subplot_titles=display_titles, # 여기서 예쁜 제목 적용!
horizontal_spacing=0.08
)
# 4. 반복문은 실제 값(real_values)으로 돌립니다
for i, real_val in enumerate(real_values):
curr_df = clnsig_grp.filter(pl.col('CHROM_Type') == real_val)
# 내림차순 정렬해서 보기 좋게
curr_df = curr_df.sort("Total", descending=True)
fig.add_trace(
go.Bar(
x = curr_df['CLNSIG_Group'],
y = curr_df['Total'],
text = curr_df['Total'],
texttemplate = '%{y:,}',
textposition = 'outside',
# 색깔은 이미 설정한 대로!
marker_color = px.colors.sequential.algae[-(i*3+1)]
),
row = 1, col = i + 1
)
# 5. 레이아웃 정리
fig.update_layout(
height=600,
width=1250,
title_text="ClinVar Distribution by Chromosome Type",
showlegend=False,
margin=dict(t=100, l=20, r=20, b=50), # 제목 공간 확보 위해 t(top) 늘림
)
# fig.write_image('group_distributuion_subplot.png')
fig.show()