upliftlab 0.26.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. upliftlab-0.26.1/LICENSE +21 -0
  2. upliftlab-0.26.1/PKG-INFO +147 -0
  3. upliftlab-0.26.1/README.md +114 -0
  4. upliftlab-0.26.1/engine/upliftlab/__init__.py +5 -0
  5. upliftlab-0.26.1/engine/upliftlab/__main__.py +119 -0
  6. upliftlab-0.26.1/engine/upliftlab/algorithms/__init__.py +6 -0
  7. upliftlab-0.26.1/engine/upliftlab/algorithms/dml.py +35 -0
  8. upliftlab-0.26.1/engine/upliftlab/algorithms/duality_r_learner.py +86 -0
  9. upliftlab-0.26.1/engine/upliftlab/algorithms/lite/dml.py +53 -0
  10. upliftlab-0.26.1/engine/upliftlab/algorithms/lite/duality_r_learner.py +78 -0
  11. upliftlab-0.26.1/engine/upliftlab/algorithms/lite/r_learner.py +43 -0
  12. upliftlab-0.26.1/engine/upliftlab/algorithms/lite/t_learner.py +33 -0
  13. upliftlab-0.26.1/engine/upliftlab/algorithms/lite/x_learner.py +42 -0
  14. upliftlab-0.26.1/engine/upliftlab/algorithms/r_learner.py +45 -0
  15. upliftlab-0.26.1/engine/upliftlab/algorithms/random_baseline.py +15 -0
  16. upliftlab-0.26.1/engine/upliftlab/algorithms/t_learner.py +28 -0
  17. upliftlab-0.26.1/engine/upliftlab/algorithms/x_learner.py +34 -0
  18. upliftlab-0.26.1/engine/upliftlab/arena.py +473 -0
  19. upliftlab-0.26.1/engine/upliftlab/bridge.py +208 -0
  20. upliftlab-0.26.1/engine/upliftlab/contract.py +254 -0
  21. upliftlab-0.26.1/engine/upliftlab/datasets.py +330 -0
  22. upliftlab-0.26.1/engine/upliftlab/metrics.py +65 -0
  23. upliftlab-0.26.1/engine/upliftlab/scenario.py +132 -0
  24. upliftlab-0.26.1/engine/upliftlab/server.py +134 -0
  25. upliftlab-0.26.1/engine/upliftlab/templates/start_lightgbm.py +34 -0
  26. upliftlab-0.26.1/engine/upliftlab/templates/start_pytorch.py +52 -0
  27. upliftlab-0.26.1/engine/upliftlab/templates/start_sklearn.py +43 -0
  28. upliftlab-0.26.1/pyproject.toml +46 -0
  29. upliftlab-0.26.1/setup.cfg +4 -0
  30. upliftlab-0.26.1/tests/test_engine.py +652 -0
  31. upliftlab-0.26.1/upliftlab.egg-info/PKG-INFO +147 -0
  32. upliftlab-0.26.1/upliftlab.egg-info/SOURCES.txt +153 -0
  33. upliftlab-0.26.1/upliftlab.egg-info/dependency_links.txt +1 -0
  34. upliftlab-0.26.1/upliftlab.egg-info/entry_points.txt +2 -0
  35. upliftlab-0.26.1/upliftlab.egg-info/requires.txt +16 -0
  36. upliftlab-0.26.1/upliftlab.egg-info/top_level.txt +1 -0
  37. upliftlab-0.26.1/web/app.js +57 -0
  38. upliftlab-0.26.1/web/catalog.js +3 -0
  39. upliftlab-0.26.1/web/charts.js +115 -0
  40. upliftlab-0.26.1/web/data/LICENSE-criteo.md +14 -0
  41. upliftlab-0.26.1/web/editor-entry.js +32 -0
  42. upliftlab-0.26.1/web/engine-files.js +2 -0
  43. upliftlab-0.26.1/web/fonts/LICENSE +198 -0
  44. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.0.woff2 +0 -0
  45. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.1.woff2 +0 -0
  46. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.10.woff2 +0 -0
  47. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.11.woff2 +0 -0
  48. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.12.woff2 +0 -0
  49. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.13.woff2 +0 -0
  50. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.14.woff2 +0 -0
  51. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.15.woff2 +0 -0
  52. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.16.woff2 +0 -0
  53. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.17.woff2 +0 -0
  54. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.18.woff2 +0 -0
  55. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.19.woff2 +0 -0
  56. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.2.woff2 +0 -0
  57. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.20.woff2 +0 -0
  58. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.21.woff2 +0 -0
  59. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.22.woff2 +0 -0
  60. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.23.woff2 +0 -0
  61. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.24.woff2 +0 -0
  62. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.25.woff2 +0 -0
  63. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.26.woff2 +0 -0
  64. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.27.woff2 +0 -0
  65. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.28.woff2 +0 -0
  66. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.29.woff2 +0 -0
  67. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.3.woff2 +0 -0
  68. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.30.woff2 +0 -0
  69. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.31.woff2 +0 -0
  70. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.32.woff2 +0 -0
  71. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.33.woff2 +0 -0
  72. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.34.woff2 +0 -0
  73. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.35.woff2 +0 -0
  74. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.36.woff2 +0 -0
  75. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.37.woff2 +0 -0
  76. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.38.woff2 +0 -0
  77. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.39.woff2 +0 -0
  78. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.4.woff2 +0 -0
  79. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.40.woff2 +0 -0
  80. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.41.woff2 +0 -0
  81. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.42.woff2 +0 -0
  82. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.43.woff2 +0 -0
  83. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.44.woff2 +0 -0
  84. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.45.woff2 +0 -0
  85. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.46.woff2 +0 -0
  86. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.47.woff2 +0 -0
  87. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.48.woff2 +0 -0
  88. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.49.woff2 +0 -0
  89. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.5.woff2 +0 -0
  90. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.50.woff2 +0 -0
  91. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.51.woff2 +0 -0
  92. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.52.woff2 +0 -0
  93. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.53.woff2 +0 -0
  94. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.54.woff2 +0 -0
  95. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.55.woff2 +0 -0
  96. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.56.woff2 +0 -0
  97. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.57.woff2 +0 -0
  98. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.58.woff2 +0 -0
  99. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.59.woff2 +0 -0
  100. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.6.woff2 +0 -0
  101. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.60.woff2 +0 -0
  102. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.61.woff2 +0 -0
  103. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.62.woff2 +0 -0
  104. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.63.woff2 +0 -0
  105. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.64.woff2 +0 -0
  106. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.65.woff2 +0 -0
  107. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.66.woff2 +0 -0
  108. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.67.woff2 +0 -0
  109. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.68.woff2 +0 -0
  110. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.69.woff2 +0 -0
  111. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.7.woff2 +0 -0
  112. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.70.woff2 +0 -0
  113. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.71.woff2 +0 -0
  114. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.72.woff2 +0 -0
  115. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.73.woff2 +0 -0
  116. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.74.woff2 +0 -0
  117. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.75.woff2 +0 -0
  118. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.76.woff2 +0 -0
  119. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.77.woff2 +0 -0
  120. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.78.woff2 +0 -0
  121. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.79.woff2 +0 -0
  122. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.8.woff2 +0 -0
  123. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.80.woff2 +0 -0
  124. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.81.woff2 +0 -0
  125. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.82.woff2 +0 -0
  126. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.83.woff2 +0 -0
  127. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.84.woff2 +0 -0
  128. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.85.woff2 +0 -0
  129. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.86.woff2 +0 -0
  130. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.87.woff2 +0 -0
  131. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.88.woff2 +0 -0
  132. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.89.woff2 +0 -0
  133. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.9.woff2 +0 -0
  134. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.90.woff2 +0 -0
  135. upliftlab-0.26.1/web/fonts/files/PretendardVariable.subset.91.woff2 +0 -0
  136. upliftlab-0.26.1/web/fonts/files/archivo-latin-ext-wdth-normal.woff2 +0 -0
  137. upliftlab-0.26.1/web/fonts/files/archivo-latin-wdth-normal.woff2 +0 -0
  138. upliftlab-0.26.1/web/fonts/files/geist-mono-latin-ext-wght-normal.woff2 +0 -0
  139. upliftlab-0.26.1/web/fonts/files/geist-mono-latin-wght-normal.woff2 +0 -0
  140. upliftlab-0.26.1/web/fonts/font.css +770 -0
  141. upliftlab-0.26.1/web/index.html +24 -0
  142. upliftlab-0.26.1/web/lib.js +91 -0
  143. upliftlab-0.26.1/web/report.js +188 -0
  144. upliftlab-0.26.1/web/runtime.js +117 -0
  145. upliftlab-0.26.1/web/store.js +292 -0
  146. upliftlab-0.26.1/web/styles.css +516 -0
  147. upliftlab-0.26.1/web/summary.js +126 -0
  148. upliftlab-0.26.1/web/vendor/LICENSES.txt +340 -0
  149. upliftlab-0.26.1/web/vendor/editor.js +13 -0
  150. upliftlab-0.26.1/web/views/algorithms.js +174 -0
  151. upliftlab-0.26.1/web/views/data.js +141 -0
  152. upliftlab-0.26.1/web/views/home.js +125 -0
  153. upliftlab-0.26.1/web/views/launcher.js +134 -0
  154. upliftlab-0.26.1/web/views/results.js +313 -0
  155. upliftlab-0.26.1/web/views/workspace.js +53 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Funbucket
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: upliftlab
3
+ Version: 0.26.1
4
+ Summary: Find the uplift model that gives the most incremental outcome for the same budget, on your own experiment data
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://uplift-lab.vercel.app
7
+ Project-URL: Source, https://github.com/Funbucket/upliftlab
8
+ Project-URL: Documentation, https://github.com/Funbucket/upliftlab/blob/main/docs/guide.md
9
+ Project-URL: Changelog, https://github.com/Funbucket/upliftlab/blob/main/CHANGELOG.md
10
+ Keywords: uplift modeling,causal inference,CATE,targeting,marketing,A/B testing
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Topic :: Scientific/Engineering
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ License-File: web/fonts/LICENSE
19
+ License-File: web/vendor/LICENSES.txt
20
+ License-File: web/data/LICENSE-criteo.md
21
+ Requires-Dist: numpy>=1.21
22
+ Requires-Dist: scikit-learn>=1.2
23
+ Requires-Dist: econml>=0.16; platform_machine != "aarch64" and platform_machine != "ARM64"
24
+ Provides-Extra: gpu
25
+ Requires-Dist: torch; extra == "gpu"
26
+ Provides-Extra: templates
27
+ Requires-Dist: lightgbm>=4; extra == "templates"
28
+ Requires-Dist: torch; extra == "templates"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: lightgbm>=4; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ <a href="https://uplift-lab.vercel.app"><img src="https://raw.githubusercontent.com/Funbucket/upliftlab/main/docs/images/banner.png" alt="upliftlab. 같은 예산을 주고, 모델끼리 겨뤄요. Criteo 예시의 최종 순위표: Duality R-Learner 786, R-Learner 783, X-Learner 759, T-Learner 757, DML 699, Random 485. 사진 판정: 1~5위 차이는 95% CI 안이고 Random과의 차이는 유의해요."></a>
35
+
36
+ <div align="center">
37
+
38
+ **같은 예산에서 증분 성과를 가장 많이 내는 uplift 모델을, 운영 전에 찾아요.**
39
+
40
+ [![Live demo](https://img.shields.io/badge/demo-uplift--lab.vercel.app-4d8dff?style=flat-square)](https://uplift-lab.vercel.app)
41
+ [![PyPI](https://img.shields.io/pypi/v/upliftlab?style=flat-square&color=11141a&label=pypi)](https://pypi.org/project/upliftlab/)
42
+ [![Built on EconML](https://img.shields.io/badge/built%20on-EconML-11141a?style=flat-square)](https://github.com/py-why/EconML)
43
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-11141a?style=flat-square)](https://github.com/Funbucket/upliftlab/blob/main/pyproject.toml)
44
+ [![MIT License](https://img.shields.io/badge/license-MIT-11141a?style=flat-square)](https://github.com/Funbucket/upliftlab/blob/main/LICENSE)
45
+
46
+ **[웹에서 바로 실행 →](https://uplift-lab.vercel.app)** &nbsp;·&nbsp; [로컬 설치](#빠른-시작) &nbsp;·&nbsp; [사용 안내](https://github.com/Funbucket/upliftlab/blob/main/docs/guide.md)
47
+
48
+ <sub>위 순위표는 Criteo 광고 실험 199,837명을 예산 40%, 8사이클로 돌린 실제 결과예요(EconML). 사이트에서는 사이클 1부터 8까지 다시 보여 줘요.</sub>
49
+
50
+ </div>
51
+
52
+ ## 왜 upliftlab인가요
53
+
54
+ Qini가 가장 높은 모델이 운영에서도 가장 많이 남길까요? 오프라인 지표 하나로 모델을 고르면, 대상 선정과 재학습을 반복하면서 생기는 차이를 놓쳐요.
55
+
56
+ - **운영 루프를 그대로 돌려요.** 모델마다 대상을 고르고, 처치군과 대조군으로 효과를 재고, 재학습하는 과정을 사이클마다 반복해요.
57
+ - **같은 조건에서만 비교해요.** 모든 모델이 같은 사용자, 같은 예산, 같은 무작위 Explore 그룹을 써요. 차이는 대상을 고른 방식에서만 생겨요.
58
+ - **차이가 유의한지 보여 줘요.** 1위와의 차이를 95% CI로 그려서, 순위를 확정할 수 없는 박빙이면 그렇다고 적어요.
59
+ - **데이터는 밖으로 나가지 않아요.** 웹은 브라우저 안에서, 로컬은 내 Python에서 돌아요. 공유 보고서에는 집계 값만 들어가요.
60
+
61
+ ## 빠른 시작
62
+
63
+ **웹 · 설치 없이.** [uplift-lab.vercel.app](https://uplift-lab.vercel.app)을 열고 `비교 시작`을 눌러요. Criteo 데이터, 기본 모델 6개, 예산 40%가 이미 골라져 있어요.
64
+
65
+ **로컬 · EconML로.** 같은 화면이 내 Python에서 돌아가요. 큰 데이터, 이미 설치한 라이브러리, GPU를 그대로 써요.
66
+
67
+ ```bash
68
+ pip install upliftlab
69
+ upliftlab # http://127.0.0.1:4180 이 열려요
70
+ ```
71
+
72
+ uv를 쓴다면 설치 없이 `uvx upliftlab` 한 줄이면 돼요. 처음 실행할 때 Criteo 표본(2.3MB)을 한 번 내려받아 두고, 그다음부터는 그대로 써요.
73
+
74
+ > [!NOTE]
75
+ > 브라우저에서는 EconML을 쓸 수 없어서, 웹은 같은 방법을 scikit-learn으로 쓴 경량판으로 돌려요. 결과 화면과 보고서에 어느 쪽으로 돌렸는지 표시해요.
76
+
77
+ ## 결과 읽는 법
78
+
79
+ <a href="https://uplift-lab.vercel.app"><img src="https://raw.githubusercontent.com/Funbucket/upliftlab/main/docs/images/verdict.png" alt="결과 화면. 결승선은 박빙, 기준선과는 큰 차이. Random 대비 증분 방문 +62%, 1위와의 차이와 95% CI, P1~P5와 기준선 순위 카드"></a>
80
+
81
+ - **제목**이 결론이에요. 2위와의 차이가 95% CI 안이면 `결승선은 박빙`이라고 쓰고, 기준선(Random)과의 차이가 확실한지도 함께 적어요.
82
+ - **Random 대비**는 1위가 무작위로 고를 때보다 같은 예산으로 성과를 얼마나 더 냈는지예요.
83
+ - **1위와의 차이 · 95% CI**에서 구간이 0을 지나는 모델은 회색, 확실히 낮은 모델은 파란색이에요. 박빙이면 `사진 판정` 표시가 붙어요.
84
+ - 그 아래에 순위표, 1위가 고른 사용자의 특성, 사이클별 지표, Row-level 로그, Cost Curve가 이어져요.
85
+
86
+ ## 데이터에서 보고서까지
87
+
88
+ | ① 데이터 넣기 | ② 비교 시작 | ③ 보고서 공유 |
89
+ | :-- | :-- | :-- |
90
+ | 실험 로그 CSV를 끌어다 놓으면 처치, 결과, 비용, 공변량 열을 알아서 골라요. | 모델, 선정 방식, 예산을 확인하고 `비교 시작`을 눌러요. 끝나면 결과로 바로 내려가요. | 1위 모델, Random 대비 증분 성과, 2위와의 차이를 HTML 보고서, PDF, 메신저 요약으로 보내요. |
91
+
92
+ ## 기본 모델
93
+
94
+ | 모델 | EconML 구현 | 하는 일 |
95
+ | :-- | :-- | :-- |
96
+ | T-Learner | `metalearners.TLearner` | 처치군과 대조군 모델의 예측 차이 |
97
+ | X-Learner | `metalearners.XLearner` | 대체한 효과를 propensity로 가중 |
98
+ | R-Learner | `dml.NonParamDML` | 잔차로 푸는 비모수 CATE |
99
+ | DML | `dml.LinearDML` | 잔차로 푸는 선형 CATE |
100
+ | Duality R-Learner | `dml.NonParamDML` × 2 | 성과와 비용의 CATE를 함께 보고 λ를 validation에서 골라요 |
101
+ | Random | NumPy | 비교 기준선 |
102
+
103
+ 각 파일은 `class Model` 하나라서 화면에서 바로 열어 보고 고쳐 쓸 수 있어요.
104
+
105
+ ## 내 알고리즘 추가
106
+
107
+ upliftlab을 import하지 않는 Python 파일 하나예요. scikit-learn, LightGBM, PyTorch를 평소처럼 써요.
108
+
109
+ ```python
110
+ class Model:
111
+ def fit(self, train, validation, context): # train.X, train.t, train.y, train.cost
112
+ ...
113
+ def predict(self, X): # 사용자마다 점수 하나. 높은 순서로 대상을 골라요
114
+ ...
115
+ ```
116
+
117
+ 웹에서는 `알고리즘` 화면의 `알고리즘 추가`에서 시작 틀을 골라 편집해요. 로컬에서는 화면 없이도 비교할 수 있어요.
118
+
119
+ ```bash
120
+ upliftlab run --data my_experiment.csv --algorithm my_model.py
121
+ ```
122
+
123
+ <details>
124
+ <summary><b>내 데이터 형식</b> · 처치를 무작위로 배정한 실험 로그, 1,000행 이상</summary>
125
+ <br>
126
+
127
+ 열 이름은 자유예요. 형식이 헷갈리면 화면의 `예시 파일 내려받기`로 받은 파일을 참고하세요.
128
+
129
+ | 역할 | 예시 열 | 값 |
130
+ | :-- | :-- | :-- |
131
+ | 처치 | `treatment` | 0 = 대조군, 1 = 처치. 여러 종류면 1, 2, 3… |
132
+ | 결과 | `retained` | 전환, 구매, 리텐션처럼 늘리고 싶은 값 |
133
+ | 비용 (선택) | `coupon_cost` | 처치로 쓴 비용. 없으면 처치 1건당 1로 계산해요 |
134
+ | 공변량 | `visit_freq`, … | 처치 **전에** 측정한 사용자 특성 |
135
+
136
+ 기본 데이터는 [Criteo](https://ailab.criteo.com/criteo-uplift-prediction-dataset/) 광고 실험이에요. 원본 1,398만 명에서 처치군과 대조군을 10만 명씩 뽑은 표본(199,837명, 결과는 사이트 방문)이 사이트와 패키지에 들어 있어요.
137
+
138
+ </details>
139
+
140
+ ## 더 보기
141
+
142
+ [사용 안내](https://github.com/Funbucket/upliftlab/blob/main/docs/guide.md) · 선정 방식 5가지, 명령줄 옵션, 결과 읽는 법<br>
143
+ [알고리즘 계약](https://github.com/Funbucket/upliftlab/blob/main/engine/README.md) · `fit`과 `predict`에 들어오는 값, 코드 검사 규칙<br>
144
+ [개발 참여](https://github.com/Funbucket/upliftlab/blob/main/CONTRIBUTING.md) · 빌드, 테스트, 폴더 구조<br>
145
+ [변경 기록](https://github.com/Funbucket/upliftlab/blob/main/CHANGELOG.md)
146
+
147
+ <sub>[MIT](https://github.com/Funbucket/upliftlab/blob/main/LICENSE). Criteo 표본은 [CC BY-NC-SA 4.0](https://github.com/Funbucket/upliftlab/blob/main/web/data/LICENSE-criteo.md)이라 비상업적으로만 쓸 수 있어요. pip 패키지에는 넣지 않고 처음 쓸 때 이 저장소에서 내려받아요. Pretendard, Archivo, Geist Mono 글꼴은 SIL Open Font License(`web/fonts/LICENSE`), 편집기 번들은 `web/vendor/LICENSES.txt`를 따라요.</sub>
@@ -0,0 +1,114 @@
1
+ <a href="https://uplift-lab.vercel.app"><img src="https://raw.githubusercontent.com/Funbucket/upliftlab/main/docs/images/banner.png" alt="upliftlab. 같은 예산을 주고, 모델끼리 겨뤄요. Criteo 예시의 최종 순위표: Duality R-Learner 786, R-Learner 783, X-Learner 759, T-Learner 757, DML 699, Random 485. 사진 판정: 1~5위 차이는 95% CI 안이고 Random과의 차이는 유의해요."></a>
2
+
3
+ <div align="center">
4
+
5
+ **같은 예산에서 증분 성과를 가장 많이 내는 uplift 모델을, 운영 전에 찾아요.**
6
+
7
+ [![Live demo](https://img.shields.io/badge/demo-uplift--lab.vercel.app-4d8dff?style=flat-square)](https://uplift-lab.vercel.app)
8
+ [![PyPI](https://img.shields.io/pypi/v/upliftlab?style=flat-square&color=11141a&label=pypi)](https://pypi.org/project/upliftlab/)
9
+ [![Built on EconML](https://img.shields.io/badge/built%20on-EconML-11141a?style=flat-square)](https://github.com/py-why/EconML)
10
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-11141a?style=flat-square)](https://github.com/Funbucket/upliftlab/blob/main/pyproject.toml)
11
+ [![MIT License](https://img.shields.io/badge/license-MIT-11141a?style=flat-square)](https://github.com/Funbucket/upliftlab/blob/main/LICENSE)
12
+
13
+ **[웹에서 바로 실행 →](https://uplift-lab.vercel.app)** &nbsp;·&nbsp; [로컬 설치](#빠른-시작) &nbsp;·&nbsp; [사용 안내](https://github.com/Funbucket/upliftlab/blob/main/docs/guide.md)
14
+
15
+ <sub>위 순위표는 Criteo 광고 실험 199,837명을 예산 40%, 8사이클로 돌린 실제 결과예요(EconML). 사이트에서는 사이클 1부터 8까지 다시 보여 줘요.</sub>
16
+
17
+ </div>
18
+
19
+ ## 왜 upliftlab인가요
20
+
21
+ Qini가 가장 높은 모델이 운영에서도 가장 많이 남길까요? 오프라인 지표 하나로 모델을 고르면, 대상 선정과 재학습을 반복하면서 생기는 차이를 놓쳐요.
22
+
23
+ - **운영 루프를 그대로 돌려요.** 모델마다 대상을 고르고, 처치군과 대조군으로 효과를 재고, 재학습하는 과정을 사이클마다 반복해요.
24
+ - **같은 조건에서만 비교해요.** 모든 모델이 같은 사용자, 같은 예산, 같은 무작위 Explore 그룹을 써요. 차이는 대상을 고른 방식에서만 생겨요.
25
+ - **차이가 유의한지 보여 줘요.** 1위와의 차이를 95% CI로 그려서, 순위를 확정할 수 없는 박빙이면 그렇다고 적어요.
26
+ - **데이터는 밖으로 나가지 않아요.** 웹은 브라우저 안에서, 로컬은 내 Python에서 돌아요. 공유 보고서에는 집계 값만 들어가요.
27
+
28
+ ## 빠른 시작
29
+
30
+ **웹 · 설치 없이.** [uplift-lab.vercel.app](https://uplift-lab.vercel.app)을 열고 `비교 시작`을 눌러요. Criteo 데이터, 기본 모델 6개, 예산 40%가 이미 골라져 있어요.
31
+
32
+ **로컬 · EconML로.** 같은 화면이 내 Python에서 돌아가요. 큰 데이터, 이미 설치한 라이브러리, GPU를 그대로 써요.
33
+
34
+ ```bash
35
+ pip install upliftlab
36
+ upliftlab # http://127.0.0.1:4180 이 열려요
37
+ ```
38
+
39
+ uv를 쓴다면 설치 없이 `uvx upliftlab` 한 줄이면 돼요. 처음 실행할 때 Criteo 표본(2.3MB)을 한 번 내려받아 두고, 그다음부터는 그대로 써요.
40
+
41
+ > [!NOTE]
42
+ > 브라우저에서는 EconML을 쓸 수 없어서, 웹은 같은 방법을 scikit-learn으로 쓴 경량판으로 돌려요. 결과 화면과 보고서에 어느 쪽으로 돌렸는지 표시해요.
43
+
44
+ ## 결과 읽는 법
45
+
46
+ <a href="https://uplift-lab.vercel.app"><img src="https://raw.githubusercontent.com/Funbucket/upliftlab/main/docs/images/verdict.png" alt="결과 화면. 결승선은 박빙, 기준선과는 큰 차이. Random 대비 증분 방문 +62%, 1위와의 차이와 95% CI, P1~P5와 기준선 순위 카드"></a>
47
+
48
+ - **제목**이 결론이에요. 2위와의 차이가 95% CI 안이면 `결승선은 박빙`이라고 쓰고, 기준선(Random)과의 차이가 확실한지도 함께 적어요.
49
+ - **Random 대비**는 1위가 무작위로 고를 때보다 같은 예산으로 성과를 얼마나 더 냈는지예요.
50
+ - **1위와의 차이 · 95% CI**에서 구간이 0을 지나는 모델은 회색, 확실히 낮은 모델은 파란색이에요. 박빙이면 `사진 판정` 표시가 붙어요.
51
+ - 그 아래에 순위표, 1위가 고른 사용자의 특성, 사이클별 지표, Row-level 로그, Cost Curve가 이어져요.
52
+
53
+ ## 데이터에서 보고서까지
54
+
55
+ | ① 데이터 넣기 | ② 비교 시작 | ③ 보고서 공유 |
56
+ | :-- | :-- | :-- |
57
+ | 실험 로그 CSV를 끌어다 놓으면 처치, 결과, 비용, 공변량 열을 알아서 골라요. | 모델, 선정 방식, 예산을 확인하고 `비교 시작`을 눌러요. 끝나면 결과로 바로 내려가요. | 1위 모델, Random 대비 증분 성과, 2위와의 차이를 HTML 보고서, PDF, 메신저 요약으로 보내요. |
58
+
59
+ ## 기본 모델
60
+
61
+ | 모델 | EconML 구현 | 하는 일 |
62
+ | :-- | :-- | :-- |
63
+ | T-Learner | `metalearners.TLearner` | 처치군과 대조군 모델의 예측 차이 |
64
+ | X-Learner | `metalearners.XLearner` | 대체한 효과를 propensity로 가중 |
65
+ | R-Learner | `dml.NonParamDML` | 잔차로 푸는 비모수 CATE |
66
+ | DML | `dml.LinearDML` | 잔차로 푸는 선형 CATE |
67
+ | Duality R-Learner | `dml.NonParamDML` × 2 | 성과와 비용의 CATE를 함께 보고 λ를 validation에서 골라요 |
68
+ | Random | NumPy | 비교 기준선 |
69
+
70
+ 각 파일은 `class Model` 하나라서 화면에서 바로 열어 보고 고쳐 쓸 수 있어요.
71
+
72
+ ## 내 알고리즘 추가
73
+
74
+ upliftlab을 import하지 않는 Python 파일 하나예요. scikit-learn, LightGBM, PyTorch를 평소처럼 써요.
75
+
76
+ ```python
77
+ class Model:
78
+ def fit(self, train, validation, context): # train.X, train.t, train.y, train.cost
79
+ ...
80
+ def predict(self, X): # 사용자마다 점수 하나. 높은 순서로 대상을 골라요
81
+ ...
82
+ ```
83
+
84
+ 웹에서는 `알고리즘` 화면의 `알고리즘 추가`에서 시작 틀을 골라 편집해요. 로컬에서는 화면 없이도 비교할 수 있어요.
85
+
86
+ ```bash
87
+ upliftlab run --data my_experiment.csv --algorithm my_model.py
88
+ ```
89
+
90
+ <details>
91
+ <summary><b>내 데이터 형식</b> · 처치를 무작위로 배정한 실험 로그, 1,000행 이상</summary>
92
+ <br>
93
+
94
+ 열 이름은 자유예요. 형식이 헷갈리면 화면의 `예시 파일 내려받기`로 받은 파일을 참고하세요.
95
+
96
+ | 역할 | 예시 열 | 값 |
97
+ | :-- | :-- | :-- |
98
+ | 처치 | `treatment` | 0 = 대조군, 1 = 처치. 여러 종류면 1, 2, 3… |
99
+ | 결과 | `retained` | 전환, 구매, 리텐션처럼 늘리고 싶은 값 |
100
+ | 비용 (선택) | `coupon_cost` | 처치로 쓴 비용. 없으면 처치 1건당 1로 계산해요 |
101
+ | 공변량 | `visit_freq`, … | 처치 **전에** 측정한 사용자 특성 |
102
+
103
+ 기본 데이터는 [Criteo](https://ailab.criteo.com/criteo-uplift-prediction-dataset/) 광고 실험이에요. 원본 1,398만 명에서 처치군과 대조군을 10만 명씩 뽑은 표본(199,837명, 결과는 사이트 방문)이 사이트와 패키지에 들어 있어요.
104
+
105
+ </details>
106
+
107
+ ## 더 보기
108
+
109
+ [사용 안내](https://github.com/Funbucket/upliftlab/blob/main/docs/guide.md) · 선정 방식 5가지, 명령줄 옵션, 결과 읽는 법<br>
110
+ [알고리즘 계약](https://github.com/Funbucket/upliftlab/blob/main/engine/README.md) · `fit`과 `predict`에 들어오는 값, 코드 검사 규칙<br>
111
+ [개발 참여](https://github.com/Funbucket/upliftlab/blob/main/CONTRIBUTING.md) · 빌드, 테스트, 폴더 구조<br>
112
+ [변경 기록](https://github.com/Funbucket/upliftlab/blob/main/CHANGELOG.md)
113
+
114
+ <sub>[MIT](https://github.com/Funbucket/upliftlab/blob/main/LICENSE). Criteo 표본은 [CC BY-NC-SA 4.0](https://github.com/Funbucket/upliftlab/blob/main/web/data/LICENSE-criteo.md)이라 비상업적으로만 쓸 수 있어요. pip 패키지에는 넣지 않고 처음 쓸 때 이 저장소에서 내려받아요. Pretendard, Archivo, Geist Mono 글꼴은 SIL Open Font License(`web/fonts/LICENSE`), 편집기 번들은 `web/vendor/LICENSES.txt`를 따라요.</sub>
@@ -0,0 +1,5 @@
1
+ """UpliftLab engine: an explore/exploit targeting loop with pluggable algorithms.
2
+
3
+ The same package runs in the browser (Pyodide) and natively (`python -m upliftlab serve`).
4
+ """
5
+ __version__ = '0.26.1'
@@ -0,0 +1,119 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ parser = argparse.ArgumentParser(prog='upliftlab', description='upliftlab local edition: the same UI and engine as uplift-lab.vercel.app, on your own Python. Run it with no command to open the UI.')
7
+ sub = parser.add_subparsers(dest='command', metavar='{serve,run,check}')
8
+ web = sub.add_parser('serve', help='open the web UI on localhost and run the engine natively (the default)')
9
+ web.add_argument('--host', default='127.0.0.1')
10
+ web.add_argument('--port', type=int, default=4180)
11
+ web.add_argument('--web', help='folder with the UI (defaults to web/ in a source checkout, else the UI inside the package)')
12
+ web.add_argument('--no-browser', action='store_true', help='do not open the browser')
13
+ check = sub.add_parser('check', help='list the libraries an algorithm file uses, then fit it once on synthetic data')
14
+ check.add_argument('file', metavar='FILE.py')
15
+ run = sub.add_parser('run', help='run a head-to-head comparison without the UI and write the result as JSON')
16
+ run.add_argument('--data', default='public/criteo', help='public/criteo (the bundled 200k-row sample) | synthetic/base | path to a CSV')
17
+ run.add_argument('--algorithms', default='all', help='comma-separated built-in ids, or "all"')
18
+ run.add_argument('--algorithm', action='append', default=[], metavar='FILE.py', help='add your own algorithm file (repeatable)')
19
+ run.add_argument('--cycles', type=int, default=8)
20
+ run.add_argument('--explore', type=float, default=.2)
21
+ run.add_argument('--target', type=float, default=.4)
22
+ run.add_argument('--seed', type=int, default=42)
23
+ run.add_argument('--mode', choices=['quantile', 'budget', 'knapsack', 'roi', 'validation'], default='budget', help='budget (default): by score until the budget runs out. quantile: the top q%%. knapsack: value - price * cost > 0, priced to the budget. roi: by value / cost while the expected efficiency meets --efficiency. validation: up to the share where uplift peaked on validation rows')
24
+ run.add_argument('--budget', type=float, default=.4, help='budget as a share of the expected cost of treating every candidate')
25
+ run.add_argument('--efficiency', type=float, default=.2, help='with --mode roi: required efficiency gain over random selection')
26
+ run.add_argument('--base-learner', choices=['ridge', 'tree'], default='ridge', help='base model of the built-in algorithms')
27
+ run.add_argument('--out', help='write the result JSON here (open it in the web UI under 실행 기록 → 결과 JSON 열기)')
28
+
29
+
30
+ def main(argv=None):
31
+ args = parser.parse_args(argv)
32
+ if args.command is None: # plain `upliftlab` opens the UI
33
+ args = parser.parse_args(['serve'])
34
+ if args.command == 'serve':
35
+ from .server import serve
36
+ serve(args.host, args.port, args.web, open_browser=not args.no_browser)
37
+ elif args.command == 'check':
38
+ from . import bridge
39
+ from .server import detect_devices
40
+ devices = detect_devices()
41
+ bridge.state.update(runtime='local', device=devices[0], devices=devices)
42
+ path = Path(args.file)
43
+ code = path.read_text(encoding='utf-8')
44
+ out = bridge.check(code, path.stem)
45
+ print(f"{path.name} · {out.get('name') or path.stem}")
46
+ shown = {'ok': lambda l: f"{l['package']} {l['version'] or ''}".strip(), 'missing': lambda l: f"{l['package']} (missing · pip install {l['package']})"}
47
+ print('libraries: ' + (' · '.join(shown.get(l['status'], shown['missing'])(l) for l in out['libraries']) or 'standard library only'))
48
+ if out['ok']:
49
+ out = bridge.try_algorithm(code, path.stem, out.get('name'))
50
+ if not out['ok']:
51
+ sys.exit(f"{path}:{out.get('line') or '?'}: {out['error']}")
52
+ print(f"quick check: ok · test AUCC truth {out['auccTruth']:.3f} · observed {out['aucc']:.3f} · fit {out['ms']:.0f} ms on {out['split']['train']:,} rows · device {devices[0]}")
53
+ else:
54
+ from . import bridge, datasets
55
+ from .server import detect_devices
56
+ devices = detect_devices()
57
+ bridge.state.update(runtime='local', device=devices[0], devices=devices)
58
+ builtins = {a['id']: a for a in bridge.builtins()}
59
+ ids = list(builtins) if args.algorithms == 'all' else [a for a in args.algorithms.split(',') if a]
60
+ unknown = [a for a in ids if a not in builtins]
61
+ if unknown:
62
+ sys.exit(f'unknown algorithm: {", ".join(unknown)} (choose from {", ".join(builtins)})')
63
+ algorithms = [{'id': a, 'name': builtins[a]['name'], 'code': builtins[a]['code']} for a in ids]
64
+ for file in args.algorithm:
65
+ code = Path(file).read_text(encoding='utf-8')
66
+ check = bridge.check(code, Path(file).stem)
67
+ if not check['ok']:
68
+ sys.exit(f'{file}:{check.get("line") or "?"}: {check["error"]}')
69
+ algorithms.append({'id': 'file-' + Path(file).stem, 'name': check.get('name') or Path(file).stem, 'code': code})
70
+ dataset = args.data
71
+ if dataset.startswith('public/'):
72
+ bridge.fetch_dataset(dataset)
73
+ elif not dataset.startswith('synthetic/'):
74
+ path = Path(dataset)
75
+ dataset = 'upload/' + path.name
76
+ bridge.add_dataset(dataset, path.read_text(encoding='utf-8'), path.name)
77
+ print(f'{dataset} · {len(algorithms)} algorithms · {args.cycles} cycles · device {devices[0]}', file=sys.stderr)
78
+ bridge.set_reporter(lambda m: (lambda d: d.get('phase') == 'done' and print(f" c{d['cycle']} {d['arm']} done", file=sys.stderr))(json.loads(m)))
79
+ result = bridge.run_all(dataset, algorithms, args.cycles, args.explore, args.target, args.seed, args.mode, args.budget, args.base_learner, args.efficiency)
80
+ by = result['summary']['by']
81
+ measure = ('incremental' if by.lower().endswith('incr') else 'gain') + (' (truth)' if by.startswith('truth') else ' (observed)')
82
+ volume = by.lower().endswith('incr') # ranked by incremental outcome: the band is the gap to first place, as on the screen
83
+ print(f"\n{'rank':<5}{'algorithm':<26}{measure:>22}{'vs 1st, 95% CI' if volume else 'observed 95% CI':>30}{'AUCC':>8}{'Qini':>8}{'fit ms':>9}")
84
+ for s in sorted(result['summary']['arms'], key=lambda s: s['rank']):
85
+ g, ci, auc, ms = s[by], s['ci'], s['aucc'], s['trainMs']
86
+ gain = '—' if g is None else f'{g:,.0f}' if volume else f'{g:+.1%}'
87
+ q = '—' if s['qini'] is None else f"{s['qini']:.3f}"
88
+ gap = s.get('vsLeader') or {}
89
+ if volume:
90
+ band = '1st' if s['rank'] == 1 else '—' if gap.get('diff') is None or gap.get('ci', [None])[0] is None else f"{gap['diff']:+,.0f} [{gap['ci'][0]:+,.0f}, {gap['ci'][1]:+,.0f}]"
91
+ else:
92
+ band = '—' if ci[0] is None else f'[{ci[0]:+.1%}, {ci[1]:+.1%}]'
93
+ auc = '—' if auc is None else f'{auc:.3f}'
94
+ ms = '—' if ms is None else f'{ms:.0f}'
95
+ print(f"{s['rank']:<5}{s['name'][:25]:<26}{gain:>22}{band:>30}{auc:>8}{q:>8}{ms:>9}")
96
+ print('\n' + conclusion(result, args.mode))
97
+ if args.out:
98
+ Path(args.out).write_text(json.dumps({'format': 'upliftlab.run/1', **result}, ensure_ascii=False))
99
+ print(f'\nwrote {args.out}', file=sys.stderr)
100
+
101
+
102
+ def conclusion(result, mode):
103
+ """One line under the table: who won, by how much over Random, on which basis."""
104
+ arms, by = result['summary']['arms'], result['summary']['by']
105
+ lead = min(arms, key=lambda s: s['rank'])
106
+ random = next((s for s in arms if s['arm'] == 'random_baseline' and s is not lead), None)
107
+ basis = 'true effect, known for synthetic data' if by.startswith('truth') else 'observed: treated minus control'
108
+ if lead.get(by) is None:
109
+ return f"{lead['name']} ranks first ({basis})."
110
+ if by.lower().endswith('incr'):
111
+ line = f"{lead['name']} ranks first with {lead[by]:,.0f} incremental outcome"
112
+ if random and random.get(by):
113
+ line += f", {lead[by] - random[by]:+,.0f} ({lead[by] / random[by] - 1:+.0%}) over Random"
114
+ return line + (' for the same budget' if mode in ('budget', 'knapsack') else '') + f' ({basis}).'
115
+ return f"{lead['name']} ranks first with {lead[by]:+.1%} cost efficiency over random selection ({basis})."
116
+
117
+
118
+ if __name__ == '__main__':
119
+ main()
@@ -0,0 +1,6 @@
1
+ """Built-in algorithms. Each file is a self-contained `class Model` that imports common libraries, never upliftlab.
2
+
3
+ The files here use EconML (PyWhy) and run in the local edition. The browser has no EconML, so the web runs the files in
4
+ lite/: the same methods written with scikit-learn only. Random has no lite version because it needs neither.
5
+ The screen shows and runs these exact files."""
6
+ ORDER = ['random_baseline', 'x_learner', 't_learner', 'r_learner', 'dml', 'duality_r_learner']
@@ -0,0 +1,35 @@
1
+ """Y와 T의 잔차를 cross-fitting으로 구하고, 그 관계로 CATE를 추정해요(Double Machine Learning). 비용은 보지 않아요.
2
+
3
+ EconML의 LinearDML이에요. ỹ = y − m̂(x), t̃ = t − ê(x)를 3-fold cross-fitting으로 구하고 τ̂(x) = θ₀ + θ'x를 ỹ ≈ t̃·τ(x)로 맞춰요.
4
+ R-Learner와 달리 마지막 단계가 선형이라 해석하기 쉽고, 처치가 공변량에 따라 달라지는 로그에서도 ê(x)로 보정해요.
5
+ """
6
+ import numpy as np
7
+ from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
8
+ from sklearn.linear_model import LogisticRegression, Ridge
9
+ from sklearn.pipeline import make_pipeline
10
+ from sklearn.preprocessing import StandardScaler
11
+ from econml.dml import LinearDML
12
+
13
+ NAME = 'DML'
14
+
15
+
16
+ def regressor(kind):
17
+ if kind == 'tree':
18
+ return HistGradientBoostingRegressor(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
19
+ return make_pipeline(StandardScaler(), Ridge(alpha=1.0))
20
+
21
+
22
+ def classifier(kind):
23
+ if kind == 'tree':
24
+ return HistGradientBoostingClassifier(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
25
+ return make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
26
+
27
+
28
+ class Model:
29
+ def fit(self, train, validation, context):
30
+ kind = getattr(context, 'base_learner', 'ridge')
31
+ self.est = LinearDML(model_y=regressor(kind), model_t=classifier(kind), discrete_treatment=True, cv=3, random_state=context.model_seed)
32
+ self.est.fit(train.y, train.t, X=train.X)
33
+
34
+ def predict(self, X):
35
+ return np.asarray(self.est.const_marginal_effect(X)).reshape(len(X), -1)
@@ -0,0 +1,86 @@
1
+ """성과와 비용의 CATE를 R-Learner로 학습하고 τ̂ʳ − λ·τ̂ᶜ 순으로 정렬해요. λ는 validation AUCC가 가장 높은 값으로 정해요.
2
+
3
+ τ̂ʳ, τ̂ᶜ는 y / σ_y, cost / σ_c의 CATE이고, EconML의 NonParamDML(R-Learner)로 학습해요.
4
+ σ_y, σ_c는 train의 표준편차예요. 정규화하므로 성과와 비용의 단위가 바뀌어도 순위가 같아요.
5
+ """
6
+ import numpy as np
7
+ from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
8
+ from sklearn.linear_model import LogisticRegression, Ridge
9
+ from sklearn.pipeline import make_pipeline
10
+ from sklearn.preprocessing import StandardScaler
11
+ from econml.dml import NonParamDML
12
+
13
+ NAME = 'Duality R-Learner'
14
+ LAMBDAS = [0, 0.05, 0.1, 0.2, 0.4, 0.8, 1.6] # 정규화한 성과와 비용 사이의 가중치
15
+
16
+
17
+ def regressor(kind):
18
+ if kind == 'tree':
19
+ return HistGradientBoostingRegressor(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
20
+ return make_pipeline(StandardScaler(), Ridge(alpha=1.0))
21
+
22
+
23
+ def classifier(kind):
24
+ if kind == 'tree':
25
+ return HistGradientBoostingClassifier(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
26
+ return make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
27
+
28
+
29
+ def final(kind):
30
+ """마지막 단계 회귀. EconML이 sample_weight를 넘기므로 파이프라인이 아닌 모델을 써요."""
31
+ if kind == 'tree':
32
+ return HistGradientBoostingRegressor(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
33
+ return Ridge(alpha=1.0)
34
+
35
+
36
+ def r_learner(X, t, y, seed, kind):
37
+ est = NonParamDML(model_y=regressor(kind), model_t=classifier(kind), model_final=final(kind), discrete_treatment=True, cv=2, random_state=seed)
38
+ return est.fit(y, t, X=X)
39
+
40
+
41
+ def aucc(score, t, y, cost, points=20):
42
+ """점수 순으로 상위 S를 골랐을 때 (증분 비용, 증분 성과) 곡선 아래 넓이를 0–1로 맞춘 값. 무작위 순서 ≈ 0.5."""
43
+ order = np.argsort(-score, kind='stable')
44
+ cut = np.round(np.arange(1, points + 1) * len(order) / points).astype(int) - 1
45
+ T = (t[order] > 0).astype(float)
46
+ nT, nC = np.cumsum(T)[cut], np.cumsum(1 - T)[cut]
47
+ ok = (nT > 0) & (nC > 0)
48
+
49
+ def lift(v):
50
+ with np.errstate(divide='ignore', invalid='ignore'):
51
+ return np.where(ok, nT * (np.cumsum(v[order] * T)[cut] / nT - np.cumsum(v[order] * (1 - T))[cut] / nC), 0.)
52
+ gain, spend = np.r_[0., lift(y)], np.r_[0., lift(cost)]
53
+ if gain.max() <= 0 or spend.max() <= 0:
54
+ return None
55
+ x, h = spend / spend.max(), gain / gain.max()
56
+ return float(np.sum(np.diff(x) * (h[1:] + h[:-1]) / 2))
57
+
58
+
59
+ class Model:
60
+ score_is_effect = False # 점수에 비용이 이미 들어 있어서 효과로 읽지 않아요. Knapsack과 효율 제약 선정에서 이 순서를 그대로 써요.
61
+
62
+ def fit(self, train, validation, context):
63
+ kind = getattr(context, 'base_learner', 'ridge')
64
+ y, cost = train.y / (train.y.std() or 1), train.cost / (train.cost.std() or 1)
65
+ self.arms = []
66
+ for arm in range(1, train.treatments + 1):
67
+ rows = (train.t == arm) | (train.t == 0)
68
+ t = (train.t[rows] == arm).astype(int)
69
+ self.arms.append((r_learner(train.X[rows], t, y[rows], context.model_seed, kind), r_learner(train.X[rows], t, cost[rows], context.model_seed, kind)))
70
+ best = None
71
+ for i, lam in enumerate(LAMBDAS):
72
+ values = []
73
+ for arm, (gain, spend) in enumerate(self.arms, 1):
74
+ rows = (validation.t == arm) | (validation.t == 0)
75
+ X = validation.X[rows]
76
+ score = np.ravel(gain.const_marginal_effect(X)) - lam * np.ravel(spend.const_marginal_effect(X))
77
+ values.append(aucc(score, validation.t[rows], validation.y[rows], validation.cost[rows]))
78
+ values = [v for v in values if v is not None]
79
+ val = float(np.mean(values)) if values else None
80
+ context.report(step=i + 1, total=len(LAMBDAS), lam=lam, val=val if val is not None else 0)
81
+ if best is None or (val is not None and (best[0] is None or val > best[0])):
82
+ best = (val, lam)
83
+ self.lam = best[1]
84
+
85
+ def predict(self, X):
86
+ return np.column_stack([np.ravel(gain.const_marginal_effect(X)) - self.lam * np.ravel(spend.const_marginal_effect(X)) for gain, spend in self.arms])
@@ -0,0 +1,53 @@
1
+ """Y와 T의 잔차를 cross-fitting으로 구하고, 그 관계로 CATE를 추정해요(Double Machine Learning). 비용은 보지 않아요.
2
+
3
+ 경량판: 브라우저에는 EconML이 없어서 웹에서는 같은 방법을 scikit-learn으로 직접 구현한 이 파일을 실행해요.
4
+ 로컬(upliftlab)에서는 EconML로 쓴 upliftlab/algorithms/dml.py를 실행해요.
5
+
6
+ ỹ = y − m̂(x) t̃ = t − ê(x) τ̂(x) = θ₀ + θ'x θ = argmin Σ (ỹ − t̃·τ(x))²
7
+ m̂와 ê는 3-fold cross-fitting으로 구해요. 모델은 context.base_learner에 따라 Ridge·로지스틱 회귀 또는 Gradient Boosting이에요(scikit-learn).
8
+ R-Learner와 달리 propensity ê(x)도 추정해서, 처치가 공변량에 따라 달라지는 로그에서도 쓸 수 있어요.
9
+ 무작위 배정 데이터에서는 ê(x)가 상수에 가까워 R-Learner와 비슷한 결과가 나와요.
10
+ """
11
+ import numpy as np
12
+ from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
13
+ from sklearn.linear_model import LogisticRegression, Ridge
14
+ from sklearn.model_selection import KFold, cross_val_predict
15
+ from sklearn.pipeline import make_pipeline
16
+ from sklearn.preprocessing import StandardScaler
17
+
18
+ NAME = 'DML'
19
+ FOLDS = 3
20
+
21
+
22
+ def outcome_model(kind):
23
+ if kind == 'tree':
24
+ return HistGradientBoostingRegressor(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
25
+ return make_pipeline(StandardScaler(), Ridge(alpha=1.0))
26
+
27
+
28
+ def treatment_model(kind):
29
+ if kind == 'tree':
30
+ return HistGradientBoostingClassifier(max_depth=3, max_iter=100, early_stopping=False, random_state=0)
31
+ return make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
32
+
33
+
34
+ def design(scaler, X):
35
+ return np.column_stack([np.ones(len(X)), scaler.transform(X)])
36
+
37
+
38
+ class Model:
39
+ def fit(self, train, validation, context):
40
+ kind = getattr(context, 'base_learner', 'ridge')
41
+ self.scaler = StandardScaler().fit(train.X)
42
+ self.finals = []
43
+ for arm in range(1, train.treatments + 1):
44
+ rows = (train.t == arm) | (train.t == 0)
45
+ X, y, t = train.X[rows], train.y[rows], (train.t[rows] == arm).astype(int)
46
+ folds = KFold(FOLDS, shuffle=True, random_state=context.model_seed)
47
+ y_res = y - cross_val_predict(outcome_model(kind), X, y, cv=folds)
48
+ t_res = t - np.clip(cross_val_predict(treatment_model(kind), X, t, cv=folds, method='predict_proba')[:, 1], .01, .99)
49
+ self.finals.append(Ridge(alpha=1.0, fit_intercept=False).fit(design(self.scaler, X) * t_res[:, None], y_res))
50
+
51
+ def predict(self, X):
52
+ Z = design(self.scaler, X)
53
+ return np.column_stack([final.predict(Z) for final in self.finals])