Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- **상관일까, 인과일까?:** 데이터 상황 10개를 보고 상관인지 인과인지 고르는 2분짜리 게임입니다. 답마다 관련 웹북 장으로 이어집니다. [게임 하기](https://causalinferencelab.github.io/game.html)
- **완주 카드:** 한 단계의 자료를 모두 학습 완료로 표시하면, 공유할 수 있는 완주 카드 이미지를 만들 수 있습니다.
- **Builder 페이지:** 프로젝트를 연 Builder마다 이력, 만든 레포, 배지를 모은 페이지가 있습니다. [Builder 전체 보기](https://causalinferencelab.github.io/builders/)
- **Builder 메달:** Builder로 연 기수가 쌓이면 메달이 바뀝니다. 첫 기수는 Builder, 2개 기수는 다시 온 Builder, 3개 기수 이상은 베테랑 Builder, 5개 기수 이상은 명예 Builder입니다. 러너 20명 이상과 함께한 멘토, 러너로 시작해 Builder가 된 사람에게는 특별 메달이 붙습니다. 메달 카드 이미지는 허브에서 바로 만들 수 있습니다.

## 커리큘럼: L0–L4

Expand All @@ -65,10 +66,16 @@ L0–L2는 **배우는 단계**로, 무료 자료만으로 끝까지 갈 수 있

팀이 번역한 『[실무로 통하는 인과추론 with 파이썬](https://www.hanbit.co.kr/store/books/look.php?p_code=B6208936856)』(한빛미디어, 2024)과 그 내용을 실무로 잇는 온라인 강의 「[데이터 실무자의 사고력 강화: AI 인과추론 실무](https://www.hanbit.co.kr/product/C0781073345)」가 커리큘럼 L2에 들어 있습니다. 책의 장별 예제 코드와 발표 자료는 무료 레포로 공개되어 있습니다.

## 행사: Data Forum
## 행사: Data Forum과 학회 협력

현업과 학계가 만나는 Data Forum을 2024년부터 열고 있습니다. 회차별 내용과 다음 행사 일정은 허브의 행사 섹션에 있습니다.

한국경영정보학회가 주최하는 학회 행사에도 협력하고 있습니다. 오래 함께한 Builder에게는 이런 학회와 Data Forum에서 발표할 기회가 열립니다.

- [한국 비즈니스 실험 심포지엄 2026](https://sites.google.com/view/business-experimentation2026/home) (2026.07.11, 한국과학기술회관)
- [한국 비즈니스 실험 심포지엄 2025](https://sites.google.com/view/business-experimentation2025/home) (2025.07.12, 고려대학교 LG-POSCO 경영관)
- [인과추론 여름 워크숍 2024](https://sites.google.com/view/causal-inference2024/home) (2024.06, 온라인)

## 참여하는 방법

1. **러너로 참여:** 가짜연구소 기수 모집 때 인과추론팀 프로젝트에 러너로 들어와 14주 동안 함께 공부하고 만듭니다.
Expand Down
165 changes: 137 additions & 28 deletions assets/css/hub.css

Large diffs are not rendered by default.

215 changes: 189 additions & 26 deletions assets/js/hub.js

Large diffs are not rendered by default.

Binary file modified assets/og/builders/bang-taemo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/cho-haechang.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/choi-seyoung.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/kim-sangwon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/kim-seongsu.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/kwon-myeongseok.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/kwon-namtaek.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/lee-dongwook.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/park-isak.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/park-sangho.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/og/builders/shin-jinsoo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions data/stages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"host": "한국경영정보학회",
"note": "한국경영정보학회가 주최하는 학회 행사에 가짜연구소 인과추론팀이 협력하고 있습니다. 오래 함께한 Builder에게는 이런 학회와 Data Forum에서 발표할 기회가 열립니다.",
"items": [
{
"id": "kbes2026",
"year": 2026,
"name": "한국 비즈니스 실험 심포지엄 2026",
"en": "Korea Business Experimentation Symposium 2026",
"when": "2026.07.11",
"place": "한국과학기술회관",
"about": "생성형 AI 시대의 데이터 사이언스와 실험",
"url": "https://sites.google.com/view/business-experimentation2026/home"
},
{
"id": "kbes2025",
"year": 2025,
"name": "한국 비즈니스 실험 심포지엄 2025",
"en": "Korea Business Experimentation Symposium 2025",
"when": "2025.07.12",
"place": "고려대학교 LG-POSCO 경영관",
"about": "A/B 테스트와 현장 실험을 다루는 국내 첫 산학 심포지엄",
"url": "https://sites.google.com/view/business-experimentation2025/home"
},
{
"id": "kswci2024",
"year": 2024,
"name": "인과추론 여름 워크숍 2024",
"en": "Korea Summer Workshop on Causal Inference 2024",
"when": "2024.06.13 – 06.28",
"place": "온라인",
"about": "실무를 위한 인과추론과 의사결정을 위한 데이터과학",
"url": "https://sites.google.com/view/causal-inference2024/home"
}
]
}
10 changes: 7 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ <h1 id="hero-title">Causal Inference Hub</h1>
<p class="partner-k" id="partner-title"><b>With 한빛미디어</b><span>팀이 옮긴 책과 그 내용을 실무로 잇는 강의입니다. 둘 다 커리큘럼 L2에 들어 있습니다.</span></p>
<ul class="partner-list" id="partner-list"></ul>
</div>
<p class="academy" id="academy"></p>
</div>
<div class="banner-side">
<img class="banner-mark" src="assets/img/team-mark.png" alt="" width="132" height="132">
Expand Down Expand Up @@ -100,8 +101,9 @@ <h2 id="curriculum-title">커리큘럼과 오픈소스</h2>

<div id="panel-map" role="tabpanel" aria-labelledby="tab-map">
<div class="map-card">
<div class="map-scroll">
<svg id="metro" role="img" aria-labelledby="metro-title"><title id="metro-title">인과추론 트랙 지도: L0부터 L4까지 6개 트랙</title></svg>
<p class="map-hint" id="map-hint" hidden>지도가 화면보다 넓습니다. 옆으로 밀면 L3·L4까지 보입니다.</p>
<div class="map-scroll" id="map-scroll">
<svg id="metro" role="group" aria-labelledby="metro-title"><title id="metro-title">인과추론 트랙 지도: L0부터 L4까지 6개 트랙</title></svg>
</div>
<div class="vlines" id="vlines"></div>
<ul class="line-legend" id="line-legend"></ul>
Expand Down Expand Up @@ -129,6 +131,7 @@ <h3 class="contrib-head">가장 많이 기여한 Builder</h3>
<p class="contrib-note">Builder로 연 프로젝트 수, 그다음 함께한 러너 수 순입니다. 카드를 누르면 이력이 열립니다.</p>
</div>
<ol class="top3" id="top3"></ol>
<section class="medal-ladder" id="medal-ladder" aria-label="Builder 메달"></section>
<h3 class="contrib-head rail-head">함께한 Builder</h3>
<div class="people-rail" id="people-rail" aria-label="함께한 Builder"></div>
<h3 class="contrib-head">전체 Builder</h3>
Expand All @@ -144,9 +147,10 @@ <h3 class="contrib-head">전체 Builder</h3>
<div class="wrap">
<div class="section-head">
<h2 id="events-title">행사</h2>
<p>현업과 학계가 만나는 Data Forum을 열고 있습니다.</p>
<p>현업과 학계가 만나는 Data Forum을 열고, 학회 행사에 협력합니다.</p>
</div>
<ol class="events" id="event-list"></ol>
<section class="stage" id="stage" aria-labelledby="stage-title"></section>
</div>
</section>

Expand Down
62 changes: 59 additions & 3 deletions scripts/build_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,52 @@ def tenure(p):
return f"{p['first']}기 Builder" if len(c) == 1 else f"{p['first']}기부터 · {len(c)}개 기수 ({'·'.join(str(x) for x in c)}기)"


# Medal rules, kept in step with TIERS and SPECIALS in assets/js/hub.js.
TIERS = [
{"id": "honor", "min": 5, "name": "명예 Builder", "rule": "5개 기수 이상", "c": ("#9C95E8", "#33378C"), "ink": "#FFFFFF"},
{"id": "gold", "min": 3, "name": "베테랑 Builder", "rule": "3개 기수 이상", "c": ("#F8D978", "#B98318"), "ink": "#3A2700"},
{"id": "silver", "min": 2, "name": "다시 온 Builder", "rule": "2개 기수", "c": ("#E6EBF1", "#8995A6"), "ink": "#1B2230"},
{"id": "bronze", "min": 1, "name": "Builder", "rule": "첫 기수", "c": ("#E3A36A", "#8E5226"), "ink": "#FFFFFF"},
]
SPECIALS = [
{"id": "mentor", "name": "멘토", "rule": "러너 20명 이상과 함께", "c": ("#63D2B1", "#1B7A61"), "ink": "#FFFFFF",
"has": lambda p: p["runners"] >= 20},
{"id": "rise", "name": "러너에서 Builder로", "rule": "러너로 참여한 뒤 Builder가 됨", "c": ("#FFAE8C", "#C04F33"), "ink": "#FFFFFF",
"has": lambda p: any(x.get("cohort", 99) < p["first"] for x in p["runnerOf"])},
]


def tier_of(p):
return next(t for t in TIERS if len(p["cohorts"]) >= t["min"])


def specials_of(p):
return [m for m in SPECIALS if m["has"](p)]


def medal_svg(m, size, n=0, uid="m", standalone=False):
"""Same drawing as medalSVG() in hub.js. standalone adds the namespace for a file of its own."""
ns = ' xmlns="http://www.w3.org/2000/svg"' if standalone else ""
grad = (f'<defs><linearGradient id="{uid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="{m["c"][0]}"/>'
f'<stop offset="1" stop-color="{m["c"][1]}"/></linearGradient></defs>')
font = ' font-family="Pretendard Variable,Pretendard,Apple SD Gothic Neo,Noto Sans KR,sans-serif"'
if m.get("min"):
h = round(size * 84 / 64)
return (f'<svg{ns} class="medal" viewBox="0 0 64 84" width="{size}" height="{h}" role="img" aria-label="{esc(m["name"])} 메달">{grad}'
f'<path d="M15 0h15l8 32H23z" fill="#2A2E7A"/><path d="M49 0H34l-8 32h15z" fill="#A9A4F5"/>'
f'<circle cx="32" cy="56" r="26" fill="url(#{uid})"/><circle cx="32" cy="56" r="20.5" fill="none" stroke="#fff" stroke-opacity=".55" stroke-width="1.5"/>'
f'<ellipse cx="23" cy="44" rx="9" ry="5" fill="#fff" fill-opacity=".28" transform="rotate(-30 23 44)"/>'
f'<text x="32" y="61" text-anchor="middle" font-size="18" font-weight="800" fill="{m["ink"]}"{font}>{n}</text>'
f'<text x="32" y="71" text-anchor="middle" font-size="7.5" font-weight="700" fill="{m["ink"]}" fill-opacity=".85"{font}>기수</text></svg>')
glyph = (f'<text x="26" y="31" text-anchor="middle" font-size="13" font-weight="800" fill="{m["ink"]}"{font}>20+</text>' if m["id"] == "mentor"
else f'<path d="M26 14l9 10h-6v13h-6V24h-6z" fill="{m["ink"]}"/>')
return (f'<svg{ns} class="medal" viewBox="0 0 52 52" width="{size}" height="{size}" role="img" aria-label="{esc(m["name"])} 메달">{grad}'
f'<circle cx="26" cy="26" r="24" fill="url(#{uid})"/><circle cx="26" cy="26" r="19" fill="none" stroke="#fff" stroke-opacity=".55" stroke-width="1.5"/>{glyph}</svg>')


def badge_message(p):
n = len(p["cohorts"])
return f"Builder · {p['first']}기" if n == 1 else f"Builder · {p['first']}기부터 · {n}개 기수"
return f"Builder · {p['first']}기" if n == 1 else f"{tier_of(p)['name']} · {n}개 기수"


def page(p, repos, projects, terms):
Expand All @@ -240,25 +283,36 @@ def page(p, repos, projects, terms):
f'<span class="num" aria-hidden="true"></span><span><b>{esc(r["title"])}</b><span>{esc(r.get("summary", ""))}</span></span></a></li>'
for r in own)
top = rank_tag(p)
t, sp = tier_of(p), specials_of(p)
medal_file = f"/badges/{slug}-medal.svg"
ribbons = "".join(f'<li>{medal_svg(m, 30, uid="sp-" + m["id"])}<span><b>{esc(m["name"])}</b>{esc(m["rule"])}</span></li>' for m in sp)
body = f"""<main id="main" class="bp">
<div class="wrap">
<a class="back" href="/builders/">Builder 전체 보기</a>
<section class="bp-hero" data-line="{esc(token(p['line']))}">
{avatar(p)}
<div class="bp-id">
<p class="bp-kicker">{top}<span>Causal Inference Hub Builder</span></p>
<p class="bp-kicker">{top}<span class="bp-tier">{esc(t["name"])}</span><span>Causal Inference Hub Builder</span></p>
<h1>{esc(p['name'])}</h1>
<p class="bp-since">{esc(tenure(p))}</p>
{f'<p class="bp-intro">{esc(intro)}</p>' if intro else ''}
<p class="bp-links">{f'<a href="{esc(safe_url(linkedin))}" target="_blank" rel="noopener">LinkedIn</a>' if linkedin else ''}<button type="button" class="btn ghost sm" data-copy="{esc(share)}" data-share="builder">이 페이지 링크 복사</button></p>
{'' if (intro and p["info"].get("github")) else f'<p class="bp-self">본인이신가요? <a href="https://github.com/{ORG}/CausalInferenceLab.github.io/edit/main/data/builders.json" target="_blank" rel="noopener">소개와 사진 추가하기</a></p>'}
</div>
<span class="bp-medal">{medal_svg(t, 112, len(p["cohorts"]), uid="tier")}</span>
</section>
<ul class="p-stats bp-stats"><li><b>{esc(p['projects'])}</b>프로젝트</li><li><b>{len(p['cohorts'])}</b>기수</li><li><b>{esc(p['runners'])}</b>함께한 러너</li><li><b>{len(own)}</b>레포</li></ul>
<div class="bp-grid">
<section aria-labelledby="h-history"><h2 id="h-history">이력</h2><ol class="p-timeline">{timeline(p, repos, projects, terms)}</ol></section>
<aside>
{f'<section aria-labelledby="h-repos"><h2 id="h-repos">만든 레포</h2><ul class="bp-repos">{repo_cards}</ul></section>' if own else ''}
<section class="bp-badge bp-medals" aria-labelledby="h-medal">
<h2 id="h-medal">Builder 메달</h2>
<p class="bp-tier-line">{medal_svg(t, 40, len(p["cohorts"]), uid="tier-s")}<span><b>{esc(t["name"])}</b>Builder로 연 기수 {len(p["cohorts"])}개 · 기준: {esc(t["rule"])}</span></p>
{f'<ul class="p-ribbons">{ribbons}</ul>' if ribbons else ''}
<p>오래 함께한 Builder에게는 학회와 Data Forum에서 발표할 기회가 열립니다. <a href="/#stage">학회와 함께 여는 무대</a></p>
<a class="btn ghost sm" href="{esc(medal_file)}" download="causal-inference-hub-{esc(slug)}-medal.svg">메달 이미지 받기</a>
</section>
<section class="bp-badge" aria-labelledby="h-badge">
<h2 id="h-badge">Builder 배지</h2>
<p>GitHub 프로필이나 레포 README에 붙이면 이 페이지로 연결됩니다.</p>
Expand All @@ -285,7 +339,7 @@ def rank_tag(p):
def index_page(ranked):
cards = "".join(
f'<li data-line="{esc(token(p["line"]))}"><a href="/builders/{esc(p["info"]["slug"])}.html">{avatar(p)}'
f'<span class="bi-body"><b>{esc(p["name"])}</b><span>{esc(tenure(p))} · 프로젝트 {esc(p["projects"])}개 · 러너 {esc(p["runners"])}명</span>'
f'<span class="bi-body"><b>{esc(p["name"])} <span class="bi-medal" title="{esc(tier_of(p)["name"])}">{medal_svg(tier_of(p), 16, len(p["cohorts"]), uid="i" + str(p["rank"]))}</span></b><span>{esc(tier_of(p)["name"])} · {esc(tenure(p))} · 프로젝트 {esc(p["projects"])}개 · 러너 {esc(p["runners"])}명</span>'
f'<span class="bi-last">최근 · {esc(p["items"][-1]["name"])}</span></span>{rank_tag(p)}</a></li>'
for p in ranked)
total_projects = len(load("projects.json"))
Expand Down Expand Up @@ -313,6 +367,8 @@ def main():
f.write(page(p, repos, projects, terms))
with open(os.path.join(ROOT, "badges", f"{slug}.svg"), "w", encoding="utf-8") as f:
f.write(badge_svg("CausalInferenceLab", badge_message(p)))
with open(os.path.join(ROOT, "badges", f"{slug}-medal.svg"), "w", encoding="utf-8") as f:
f.write(medal_svg(tier_of(p), 256, len(p["cohorts"]), uid="g", standalone=True) + "\n")
with open(os.path.join(ROOT, "builders", "index.html"), "w", encoding="utf-8") as f:
f.write(index_page(ranked))
print(f"wrote {len(ranked)} Builder pages and badges")
Expand Down
18 changes: 10 additions & 8 deletions scripts/builder_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from build_builders import ROOT, builder_stats, span # noqa: E402
from build_builders import ROOT, builder_stats, medal_svg, tenure, tier_of # noqa: E402

TEMPLATE = """<!doctype html><html lang="ko"><head><meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css">
Expand All @@ -26,16 +26,18 @@
body:before{{content:"";position:absolute;inset:0;background:radial-gradient(60% 90% at 88% 20%,rgba(255,255,255,.16),transparent 60%)}}
.in{{position:relative;padding:72px 80px;height:100%;display:flex;flex-direction:column}}
.k{{font-size:28px;font-weight:600;color:rgba(255,255,255,.82)}}
h1{{font-size:104px;font-weight:800;letter-spacing:-.03em;line-height:1.05;margin-top:22px}}
h1{{font-size:96px;font-weight:800;letter-spacing:-.03em;line-height:1.05;margin-top:6px}}
.s{{font-size:36px;font-weight:600;margin-top:14px;color:rgba(255,255,255,.9)}}
.st{{display:flex;gap:56px;margin-top:44px}}
.st{{display:flex;gap:56px;margin-top:34px}}
.st div{{display:flex;flex-direction:column;font-size:24px;color:rgba(255,255,255,.75)}}
.st b{{font-size:60px;font-weight:800;color:#fff;line-height:1.1}}
.f{{margin-top:auto;font-size:24px;color:rgba(255,255,255,.7)}}
img{{position:absolute;right:80px;top:72px;width:180px;height:180px}}
.md{{position:absolute;right:96px;top:110px;filter:drop-shadow(0 18px 30px rgba(0,0,0,.35))}}.md svg{{height:400px;width:auto;display:block}}
.tier{{display:inline-block;font-size:30px;font-weight:800;color:#FFD54A;margin-top:26px}}
.top{{display:inline-block;margin-left:14px;font-size:22px;font-weight:800;color:#14162B;background:#FFD54A;border-radius:999px;padding:3px 14px;vertical-align:middle}}
</style></head><body><img src="file://{mark}"><div class="in">
</style></head><body><span class="md">{medal}</span><div class="in">
<p class="k">Causal Inference Hub Builder · 가짜연구소 인과추론팀</p>
<p class="tier">{tier}</p>
<h1>{name}{top}</h1>
<p class="s">{since}</p>
<div class="st"><div><b>{projects}</b>프로젝트</div><div><b>{runners}</b>함께한 러너</div><div><b>{cohorts}</b>기수</div></div>
Expand All @@ -52,7 +54,6 @@ async def main():
ranked = builder_stats()[0]
out = os.path.join(ROOT, "assets", "og", "builders")
os.makedirs(out, exist_ok=True)
mark = os.path.join(ROOT, "assets", "img", "team-mark.png")
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={"width": 1200, "height": 630})
Expand All @@ -61,9 +62,10 @@ async def main():
if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", slug or ""):
raise SystemExit(f"bad slug {slug!r}")
doc = TEMPLATE.format(
mark=mark, name=html.escape(b["name"]), slug=slug,
medal=medal_svg(tier_of(b), 300, len(b["cohorts"]), uid="og"), tier=html.escape(tier_of(b)["name"]),
name=html.escape(b["name"]), slug=slug,
top=f'<span class="top">Top&nbsp;{b["rank"]}</span>' if b["rank"] <= 3 else "",
since=f'{span(b["cohorts"])} Builder · 최근 {html.escape(short(b["items"][-1]["name"]))}',
since=html.escape(tenure(b)),
projects=b["projects"], runners=b["runners"], cohorts=len(b["cohorts"]))
path = os.path.join(out, f"{slug}.html")
with open(path, "w", encoding="utf-8") as f:
Expand Down
8 changes: 8 additions & 0 deletions scripts/check_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ def main():
if s not in repos and s != "external":
err(w, f"stop {s!r} is not in repos.json")

for i, x in enumerate((data.get("stages.json") or {}).get("items", [])):
w = f"stages.json items[{i}]"
token(x.get("id"), w, "id")
if not is_int(x.get("year")):
err(w, "year must be a whole number")
if not str(x.get("url", "")).startswith("https://"):
err(w, "url must be an https link")

for name in ("game.json", "daily.json"):
for q in (data.get(name) or {}).get("questions", []):
w = f"{name} {q.get('id')!r}"
Expand Down