netviz_tools
원시 엣지 테이블을 세 단계 만에 인터랙티브 네트워크 차트로 만드는 Python 패키지입니다. 데이터를 정제하면서 바뀐 내용은 모두 보고서로 남기고 NetworkX 그래프를 만든 뒤 Plotly로 그립니다. 세 단계 모두 같은 인자 이름을 씁니다. 1.0은 2025년에 공개한 0.x 버전을 처음부터 다시 만든 것으로, 지저분한 입력 데이터의 예로 FAOSTAT 농산물 무역 데이터를 사용합니다.
한눈에 보기
- 분야: 오픈소스 툴링 / 네트워크 데이터 정제·시각화
- 역할: 개인 프로젝트
- 기술 스택: Python 3.11+, NetworkX, pandas, Plotly, DuckDB, Parquet(pyarrow), pooch, SciPy
- 검증: 테스트 260개, 커버리지 99.11%, Linux(Python 3.11·3.12·3.13)와 Windows에서 통과, mypy strict 모드 적용
- 상태: v1.0.0 완성, MIT 라이선스, PyPI 배포 예정(현재 PyPI 최신 버전은 0.2.0)
문제 / 배경
네트워크 데이터가 NetworkX에 바로 넣을 수 있는 형태로 들어오는 경우는 드뭅니다. 출처마다 컬럼 이름이 다르고 같은 나라도 여러 가지 표기로 적혀 있습니다. 무역 통계는 수출국과 수입국이 같은 흐름을 각각 보고하기 때문에 하나의 흐름이 서로 다른 숫자로 두 번 나옵니다. 단위가 섞여 있는 경우도 많습니다. 정제를 마쳐도 끝이 아닙니다. 입력 형식도 특성도 제각각인 시각화 패키지 가운데 하나를 골라야 합니다.
2025년에 공개한 0.x 버전도 이 문제를 제대로 풀지 못했습니다. 약 736MB의 FAOSTAT CSV 파일을 wheel 안에 그대로 넣었고 Python 3.13 이상에서만 설치할 수 있었습니다. 상태를 가진 매니저 클래스 중심의 구조여서 출력, 로깅, 그래프 표시가 부수 효과로 일어났습니다. 1.0은 처음부터 다시 만들었고 0.x API와는 호환되지 않습니다.
접근 방식
1.0은 하나의 파이프라인입니다. 어느 단계에서든 시작할 수 있어서 이미 NetworkX 그래프가 있다면 바로 시각화로 넘어가면 됩니다.
raw table ──clean()──▶ flow table ──build_graph()──▶ NetworkX graph ──plot.<kind>()──▶ Plotly figure
- 정제.
clean은 원본 테이블에서 각 역할을 맡은 컬럼을 찾아내고 추정으로 정한 컬럼은 따로 알려 줍니다. 보고국/상대국 형식의 행을 출발지에서 도착지로 가는 흐름으로 바꾸고 양쪽이 모두 보고한 흐름은 하나로 정리합니다. 텍스트로 적힌 숫자와 연도를 읽고 단위를 변환하며 표기만 다른 이름을 합치고 집계 행·0값·자기 루프는 제외합니다. 결과와 함께 돌려주는CleaningReport에는 단계별 건수와 제외한 모든 행의 사유가 담깁니다. - 그래프 생성.
build_graph는 한 구간(기간 하나, 범주 하나)을 가중 NetworkX 그래프로 만듭니다. 기간별로 그래프가 필요하면graphs_by를 씁니다. - 시각화. 차트 함수 9개(네트워크, 흐름 지도, 에고, Sankey, 인접 행렬, 순위, 비교, 시계열, 차수 분포)는 NetworkX 그래프, 흐름 테이블, 기간별 그래프 매핑을 모두 입력으로 받습니다.
color_by,size_by,edge_width_by,top_n,focus,time,labels같은 키워드 인자도 모든 차트에서 이름과 뜻이 같습니다. - 차트 지정 또는 자동 선택.
plot.auto(g, kind="network")는plot.network(g)와 같습니다. kind를 주지 않으면 문서에 적힌 규칙을 순서대로 따릅니다. focus 노드가 있으면 에고·비교·시계열 차트를 고르고 모든 노드에 좌표가 있으면 흐름 지도를 그립니다. 보내기와 받기를 함께 하는 노드가 없는 방향 그래프는 Sankey로, 나머지는 네트워크로 그립니다. 어떤 차트를 왜 골랐는지는 제목 아래와fig.layout.meta에 남습니다. - 시간 축. 여러 기간을 넘기면 네트워크·에고·흐름 지도 차트가 재생 버튼과 타임라인 슬라이더가 달린 애니메이션이 됩니다. 프레임이 바뀌어도 노드 위치와 크기 척도는 그대로여서 움직임이 보이면 실제로 값이 바뀐 것입니다.
- 전체 데이터는 필요할 때만. wheel에는 0.6MB짜리 FAOSTAT 샘플만 들어 있어 모든 예제가 오프라인에서 돌아갑니다.
build_store()는 pooch로 Detailed Trade Matrix 전체(1986년 이후 558개 품목, 약 420MB)를 내려받아 릴리스에 고정한 SHA-256과 대조합니다. 이어서 DuckDB로 품목별로 파티션된 Parquet 데이터셋을 만듭니다.manifest.json에는 원본 URL, 실제로 쓴 해시, 수집 시각, 행 수, 라이브러리 버전이 기록되고 이후load()는 요청한 품목만 읽습니다.
결과
- 정제 결과. 내장된 밀 샘플에서
clean은 FAOSTAT 원본 44,738행을 흐름 29,627건으로 정리합니다. 보고서에 따르면 양국이 모두 보고한 흐름이 15,095건, 수출국만 보고한 흐름이 6,994건, 수입국만 보고한 흐름이 7,538건입니다. 두 보고 사이의 차이는 중앙값 기준 23.9%입니다. - 테스트. Hypothesis 속성 기반 테스트와 doctest를 포함한 테스트 260개가 브랜치 커버리지 99.11%로 통과합니다(기준 90%). CI는 Linux의 Python 3.11·3.12·3.13, Windows, 최저 지원 버전 의존성 조합에서 테스트를 돌리고 ruff와 mypy strict 검사도 함께 실행합니다.
- 용량과 지원 범위. wheel 크기는 약 0.75MB로, 0.x의 약 736MB에서 크게 줄었습니다. 최소 Python 버전도 3.13에서 3.11로 낮췄습니다.
- 문서. Concepts 페이지, API 레퍼런스, 실행 결과까지 커밋한 갤러리 노트북 5개로 문서 사이트를 꾸렸습니다.
차트로 무엇을 볼 수 있는지 내장 샘플(FAOSTAT 물량, 톤 단위)로 두 가지 예를 들어 보겠습니다. 차트는 숫자를 보여줄 뿐, 숫자가 바뀐 이유까지 알려 주지는 않습니다.
- 2022년에서 2023년 사이 밀 수출이 가장 많이 줄어든 나라는?
plot.ranking(flows, category="Wheat", time=[2022, 2023], size_by="out_strength", change=True)한 줄이면 수출국을 변화량 순으로 볼 수 있습니다. 감소 폭이 가장 큰 나라는 아르헨티나로, 1,450만 톤에서 310만 톤으로 1,140만 톤 줄었습니다. 인도(710만 톤 감소)와 프랑스(640만 톤 감소)가 뒤를 잇고 가장 많이 늘어난 나라는 러시아(1,150만 톤 증가)입니다. - 한 나라가 무엇을 보내고 받는가?
plot.compare(flows, "Brazil", time=2024)를 보면 브라질은 2024년에 대두 9,980만 톤과 옥수수 4,310만 톤을 수출했습니다. 밀은 290만 톤을 수출하면서 660만 톤을 수입했습니다. 애니메이션 흐름 지도에서는 브라질의 중국 본토 대두 수출이 2010년 1,860만 톤에서 2024년 7,460만 톤으로 늘어나는 과정이 보입니다.
주요 결정 사항 / 학습 내용
컬럼 이름은 무역 용어 대신 범용 어휘로 정했습니다.
source, target, time, category, weight, unit입니다.
무역 데이터라면 source가 수출국이고 이주 데이터라면 출발국입니다.
clean, build_graph, 모든 차트가 같은 단어를 인자 이름으로 씁니다.
화면에 표시할 이름은 flows.attrs["labels"]와 G.graph["labels"]에 담겨 데이터와 함께 움직입니다.
그래서 시각화 코드는 무역을 전혀 몰라도 FAOSTAT 차트에는 Exporter, Importer라는 이름이 제대로 붙습니다.
양쪽이 모두 보고한 흐름(mirror flow)은 사용자에게 맡기지 않고 clean에서 처리합니다.
두 값을 더하면 같은 흐름을 두 번 세게 되고 말없이 하나만 고르면 두 보고가 얼마나 다른지 가려집니다.
그래서 흐름마다 보고를 하나만 남기고(prefer=로 선택), 두 보고의 차이는 보고서에 기록합니다.
그래프 하나에는 구간 하나만 담습니다.
여러 기간이나 범주를 섞어 그래프를 만들면 aggregate="sum" 또는 "mean"을 지정하지 않는 한 MixedSliceError가 납니다.
단위가 섞이면 MixedUnitError가 납니다.
FAOSTAT의 살아 있는 가축 품목은 물량이 마리 수와 톤으로 함께 들어 있는데 이 둘을 더한 값에는 아무 의미가 없기 때문입니다.
렌더링 백엔드는 Plotly 하나로 통일했습니다.
모든 차트는 색상과 호버 스타일이 같은 Plotly figure를 돌려줍니다.
0.x와는 정반대로 show()를 호출하거나 파일을 쓰는 함수는 하나도 없습니다.
차트가 쓰지 않는 인자는 아예 받지 않게 해서 넘기면 조용히 무시하는 대신 TypeError를 냅니다.
size_by가 무시된 차트는 겉보기엔 멀쩡하지만 사용자가 요청한 차트가 아닙니다.
plot.auto를 거쳤다면 어떤 차트를 왜 골랐는지도 오류 메시지에 함께 나옵니다.
전체 데이터 로더는 기본으로 수입국 보고 기준을 씁니다.
러시아는 2021년 이후 밀 수출을 보고하지 않아서 수출국 보고 기준으로는 2022년 밀 네트워크에서 러시아가 아예 빠집니다.
수입국 보고 기준으로는 러시아산 밀이 2022년 2,240만 톤, 2023년 3,400만 톤으로 잡힙니다.
수입을 보고하는 나라도 더 많습니다. 2021년 밀의 경우 수입국은 155개국, 수출국은 99개국이 보고했습니다.
필요하면 reporter="exporter"나 "combined"를 고를 수 있습니다.
한계점
- PyPI 배포 전. v1.0.0은 완성되어 CI를 통과했지만 업로드 전까지는
pip install netviz-tools로 0.2.0이 설치됩니다. - 밀집 그래프. 큰 네트워크의 중심부처럼 노드가 몰린 곳에서는 텍스트 라벨이 겹칠 수 있습니다. 노드가 30개를 넘으면 기본값으로 가장 큰 12개에만 라벨을 달고 2,000개가 넘는 그래프는 경고와 함께 가장 큰 2,000개만 남깁니다. 밀집 그래프는 노드-링크 다이어그램보다 커뮤니티 순으로 정렬한 인접 행렬(
plot.adjacency)이 읽기 쉽습니다. - 레이아웃 선택. 스펙트럴 레이아웃은 라플라시안 고유벡터로 노드 위치를 정하기 때문에, 척도 없는(scale-free) 그래프에서 한 허브에 매달린 잎 노드처럼 구조상 위치가 같은 노드는 한 점에 겹칠 수 있습니다. Kamada-Kawai는 밀집된 중심부를 더 고르게 펼치지만 노드가 500개 정도를 넘으면 느려집니다.
- 멱법칙 적합의 한계. KS 거리는 보고하지만 Clauset 등이 제안한 부트스트랩 적합도 검정은 하지 않습니다. 갤러리 분석에서도 모든 경우에 로그정규 꼬리가 멱법칙만큼 잘 맞았으므로, 이 무역 네트워크를 척도 없는 네트워크라고 부를 근거는 없습니다.
- 보고 주체에 따라 달라지는 중심성. 수입국 보고 데이터에서 러시아의 밀 역방향 PageRank는 2021년 0.105에서 2022년 0.024로 떨어지지만 수출량은 2,240만 톤으로 그대로입니다. 러시아가 자국 수입을 더 이상 보고하지 않아서 생긴 현상이며 갤러리에서는 이를 보고 방식이 만든 착시로 설명합니다. 물량을 물을 때는 out-strength나 점유율을 봐야 합니다.
- 고정된 원본 파일. FAO는 벌크 파일을 같은 URL에서 통째로 교체합니다. 새 파일로 빌드하면 사용자가
known_hash=None으로 직접 허용하거나 새 릴리스에서 해시를 다시 고정하기 전까지SourceHashMismatchError가 납니다. 최초 1회 전체 빌드에는 임시 디스크 공간이 약 9GB 필요합니다.
기술 스택
- 그래프·분석: NetworkX (Louvain 커뮤니티, PageRank, 매개 중심성), SciPy (멱법칙 적합, 우도비 검정)
- 데이터: pandas, NumPy, DuckDB, Parquet(pyarrow), pooch (SHA-256 검증 다운로드)
- 시각화: Plotly
- 품질 관리: pytest, Hypothesis, pytest-cov (커버리지 기준 90%), mypy (strict), ruff, pre-commit
- 문서·배포: MkDocs Material, mkdocstrings, mkdocs-jupyter; hatchling; GitHub Actions CI, PyPI Trusted Publishing, Dependabot; MIT 라이선스
- 데이터 출처: FAOSTAT Detailed Trade Matrix (CC BY 4.0), UNSD M49 지역 분류, Natural Earth 라벨 좌표
링크
- GitHub: github.com/tysonjohnsondev/netviz_tools
- 문서: tysonjohnsondev.github.io/netviz_tools
- 갤러리(실행된 노트북): tysonjohnsondev.github.io/netviz_tools/gallery
- PyPI 패키지: pypi.org/project/netviz-tools
- 데이터 출처: FAOSTAT Detailed Trade Matrix