diff --git a/Python/Scatter3dPlot.qmd b/Python/Scatter3dPlot.qmd new file mode 100644 index 000000000..a4fefcc7b --- /dev/null +++ b/Python/Scatter3dPlot.qmd @@ -0,0 +1,126 @@ +--- +title: "3D Scatter Plot (Python)" +author: + - "**[Editor]** [Shixiang Wang](https://github.com/ShixiangWang);" + - "**[Contributors]** Codex (GPT-5) — AI-generated, independently AI-reviewed;" +--- + +The Python counterpart of [3D Scatter](../Hiplot/159-scatter-3d.html) — a 3D scatter places each subject at $(x, y, z)$ coordinates of three quantitative variables, adding one biomarker dimension to the classic 2D plot. In translational research it helps when group separation genuinely lives in three variables (age x marker x marker, cell size x granularity x intensity) and a static 2D projection visibly flattens it \[1\]. + +matplotlib's `mplot3d` renders the projection \[2\]. Two properties make 3D scatter plots easy to misread: the *view angle* decides which clusters hide behind which, and *depth* is encoded only by size/alpha shading — a printed 3D plot is a single fixed projection of a 3D object. Both should be treated as parameters to report, like a model. + +This tutorial covers two workflows: + +1. A default-view 3D scatter of three patient groups; +2. A rotation panel showing how the same data changes its story. + +## Example + +![](../images/Python/Scatter3dPlot_demo.png){fig-align="center" width="65%"} + +## Setup + +- System Requirements: Cross-platform (Linux/MacOS/Windows) +- Programming Language: **Python** +- Dependencies: `matplotlib`, `numpy` + +```{python} +#| label: packages-setup +#| output: false +import matplotlib.pyplot as plt +import numpy as np +from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers 3d projection) +``` + +## Data Preparation + +We simulate 240 subjects in three groups with distinct (age, marker A, marker B) profiles: responders are older with mid-range markers, non-responders have the highest markers, healthy donors the lowest. The separation direction is chosen so some viewpoints separate the groups and others collapse them. + +```{python} +#| label: data-prep +rng = np.random.default_rng(21) +groups = { + "Responder": (np.array([60, 25, 8]), np.array([8, 6, 2.2]), + "#1B7837"), + "Non-responder": (np.array([48, 40, 15]), np.array([9, 7, 3.0]), + "#B2182B"), + "Healthy": (np.array([35, 12, 4]), np.array([10, 4, 1.4]), + "#4C72B0"), +} +n_per = 80 +data = [] +for g, (mu, sd, color) in groups.items(): + xyz = np.column_stack([rng.normal(m, s, n_per) + for m, s in zip(mu, sd)]) + data.append((g, xyz, color)) + +def draw(ax, elev, azim, title): + for g, xyz, color in data: + ax.scatter(xyz[:, 0], xyz[:, 1], xyz[:, 2], s=14, alpha=0.6, + color=color, label=g, depthshade=True) + ax.set_xlabel("Age (years)") + ax.set_ylabel("Marker A (U/mL)") + ax.set_zlabel("Marker B (ng/mL)") + ax.view_init(elev=elev, azim=azim) + ax.set_title(title, fontsize=10, pad=0) + ax.set_box_aspect((1, 1, 0.8)) +``` + +## Visualization + +### 1. Default View + +```{python} +#| label: fig-py-3d-basic +#| fig-cap: "Three patient groups in biomarker space at elev=20, azim=-60: the groups occupy distinct regions of the 3D volume." +#| fig-align: center + +fig = plt.figure(figsize=(7.5, 5.6)) +ax = fig.add_subplot(111, projection="3d") +draw(ax, elev=20, azim=-60, + title="Three patient groups in biomarker space") +ax.legend(loc="upper left", fontsize=9) +fig.tight_layout() +plt.show() +``` + +### 2. Rotation Panel + +```{python} +#| label: fig-py-3d-views +#| fig-cap: "The same dataset from two viewpoints: the separability reading at left is not available edge-on at right, where depth order and the reversed axis direction become hard to judge." +#| fig-align: center + +fig = plt.figure(figsize=(11.5, 4.8)) +for i, (elev, azim, title) in enumerate([ + (20, -60, "View A (elev=20, azim=-60): groups look separable"), + (2, 55, "View B (elev=2, azim=55): edge-on view obscures depth")]): + ax = fig.add_subplot(1, 2, i + 1, projection="3d") + draw(ax, elev=elev, azim=azim, title=title) +fig.suptitle("One dataset, two stories: rotation changes the reading", + y=1.0) +fig.tight_layout() +plt.show() +``` + +::: callout-tip +## Practice tips + +- Report `elev`/`azim` in the caption when a claim depends on the viewpoint; a reviewer cannot otherwise reproduce your separation. +- `alpha` + `depthshade` only *suggest* depth — points still overplot. Lower `alpha` and smaller markers reduce false occlusion judgments. +- Prefer 2D panels of the variable pairs (Application panel B) for any quantitative comparison; use the 3D view to motivate them. +- Set `ax.set_box_aspect()` to a physically meaningful ratio, or a 3x range axis will visually dominate a 1x range axis. +- Interactive rotation (notebook `%matplotlib widget`, plotly `Scatter3d`) is for exploration; export the chosen angle for publication. +::: + +## Application + +The publication pairing places the chosen 3D view (A) next to its strongest 2D projection (B): the 3D panel motivates the three-group structure, and the 2D panel carries the readable, quantitative comparison that the paper's statistics refer to. + +![](../images/Python/Scatter3dPlot_app1.png){fig-align="center" width="85%"} + +## Reference + +\[1\] Li J, Martens JB. Judging correlation from scatterplots: an evaluation of 3D visualizations. IEEE Trans Vis Comput Graph. 2010;16(6):1027-1036. + +\[2\] Hunter JD. Matplotlib: a 2D graphics environment. Comput Sci Eng. 2007;9(3):90-95. doi:10.1109/MCSE.2007.55 diff --git a/Python/Scatter3dPlot.zh.qmd b/Python/Scatter3dPlot.zh.qmd new file mode 100644 index 000000000..66e27972d --- /dev/null +++ b/Python/Scatter3dPlot.zh.qmd @@ -0,0 +1,126 @@ +--- +title: "3D 散点图 (Python)" +author: + - "**[编辑]** [王诗翔](https://github.com/ShixiangWang)" + - "**[贡献]** Codex (GPT-5) — AI 生成,独立 AI 复核" +--- + +[3D 散点图](../zh/Hiplot/159-scatter-3d.html)的 Python 对应版本——3D 散点图把每个受试者放到三个定量变量的 $(x, y, z)$ 坐标上,在经典二维散点之上再加一维生物标志物。当组间分离确实存在于三个变量(年龄 x 标志物 x 标志物、细胞大小 x 颗粒度 x 荧光强度)、而二维投影明显压平了结构时,转化医学研究可以用它辅助探索 \[1\]。 + +matplotlib 的 `mplot3d` 负责渲染 \[2\]。3D 散点易被误读的两个原因:*视角*决定哪些簇藏在哪些簇后面;*深度*只靠大小/透明度着色暗示——印刷出来的 3D 图是三维对象的单一固定投影。两者都应作为需要报告的参数对待,就像模型一样。 + +本教程涵盖两种工作流: + +1. 三个患者组的默认视角 3D 散点; +2. 展示同一数据"换角度换结论"的旋转面板。 + +## Example + +![](../images/Python/Scatter3dPlot_demo.png){fig-align="center" width="65%"} + +## Setup + +- 系统要求:跨平台(Linux/MacOS/Windows) +- 编程语言:**Python** +- 依赖包:`matplotlib`、`numpy` + +```{python} +#| label: packages-setup +#| output: false +import matplotlib.pyplot as plt +import numpy as np +from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (注册 3d 投影) +``` + +## Data Preparation + +我们模拟 240 名受试者的三组(年龄, 标志物 A, 标志物 B)轮廓:应答者年龄较大、标志物居中,非应答者标志物最高,健康供者最低。分离方向经过挑选——有些视角能分开三组,有些视角会把它们压扁。 + +```{python} +#| label: data-prep +rng = np.random.default_rng(21) +groups = { + "Responder": (np.array([60, 25, 8]), np.array([8, 6, 2.2]), + "#1B7837"), + "Non-responder": (np.array([48, 40, 15]), np.array([9, 7, 3.0]), + "#B2182B"), + "Healthy": (np.array([35, 12, 4]), np.array([10, 4, 1.4]), + "#4C72B0"), +} +n_per = 80 +data = [] +for g, (mu, sd, color) in groups.items(): + xyz = np.column_stack([rng.normal(m, s, n_per) + for m, s in zip(mu, sd)]) + data.append((g, xyz, color)) + +def draw(ax, elev, azim, title): + for g, xyz, color in data: + ax.scatter(xyz[:, 0], xyz[:, 1], xyz[:, 2], s=14, alpha=0.6, + color=color, label=g, depthshade=True) + ax.set_xlabel("Age (years)") + ax.set_ylabel("Marker A (U/mL)") + ax.set_zlabel("Marker B (ng/mL)") + ax.view_init(elev=elev, azim=azim) + ax.set_title(title, fontsize=10, pad=0) + ax.set_box_aspect((1, 1, 0.8)) +``` + +## Visualization + +### 1. 默认视角 + +```{python} +#| label: fig-py-3d-basic +#| fig-cap: "elev=20、azim=-60 下的三维生物标志物空间:三组占据体积中的不同区域。" +#| fig-align: center + +fig = plt.figure(figsize=(7.5, 5.6)) +ax = fig.add_subplot(111, projection="3d") +draw(ax, elev=20, azim=-60, + title="Three patient groups in biomarker space") +ax.legend(loc="upper left", fontsize=9) +fig.tight_layout() +plt.show() +``` + +### 2. 旋转面板 + +```{python} +#| label: fig-py-3d-views +#| fig-cap: "同一数据的两个视角:左图的组间分离在右图的侧视中不可得——深度顺序与反转的轴方向都难以判断。" +#| fig-align: center + +fig = plt.figure(figsize=(11.5, 4.8)) +for i, (elev, azim, title) in enumerate([ + (20, -60, "View A (elev=20, azim=-60): groups look separable"), + (2, 55, "View B (elev=2, azim=55): edge-on view obscures depth")]): + ax = fig.add_subplot(1, 2, i + 1, projection="3d") + draw(ax, elev=elev, azim=azim, title=title) +fig.suptitle("One dataset, two stories: rotation changes the reading", + y=1.0) +fig.tight_layout() +plt.show() +``` + +::: callout-tip +## 实践要点 + +- 当结论依赖视角时,在图注中报告 `elev`/`azim`;否则审稿人无法复现你的分离结果。 +- `alpha` + `depthshade` 只是*暗示*深度——点仍会互相覆盖。降低 `alpha`、缩小点径可减少错误的遮挡判断。 +- 任何定量比较都优先用变量两两的 2D 面板(Application 面板 B);3D 视图只用来引出它们。 +- 用 `ax.set_box_aspect()` 设定有物理含义的比例,否则量程 3x 的轴会在视觉上碾压量程 1x 的轴。 +- 交互旋转(notebook 的 `%matplotlib widget`、plotly `Scatter3d`)用于探索;出版导出时固定所选角度。 +::: + +## Application + +出版搭配把选定的 3D 视角(A)与最强的 2D 投影(B)并排:3D 面板引出三组结构,2D 面板承载论文统计所对应的可读、可量化的比较。 + +![](../images/Python/Scatter3dPlot_app1.png){fig-align="center" width="85%"} + +## Reference + +\[1\] Li J, Martens JB. Judging correlation from scatterplots: an evaluation of 3D visualizations. IEEE Trans Vis Comput Graph. 2010;16(6):1027-1036. + +\[2\] Hunter JD. Matplotlib: a 2D graphics environment. Comput Sci Eng. 2007;9(3):90-95. doi:10.1109/MCSE.2007.55 diff --git a/Python/WafflePlot.qmd b/Python/WafflePlot.qmd new file mode 100644 index 000000000..c1185204c --- /dev/null +++ b/Python/WafflePlot.qmd @@ -0,0 +1,116 @@ +--- +title: "Waffle Plot (Python)" +author: + - "**[Editor]** [Shixiang Wang](https://github.com/ShixiangWang);" + - "**[Contributors]** Codex (GPT-5) — AI-generated, independently AI-reviewed;" +--- + +The Python counterpart of [Waffle Chart](../Composition/Waffle.html) — a waffle chart replaces area-encoding pie/donut wedges with a grid of unit blocks, one block per patient (or per 1% of the cohort), so proportions are *counted* rather than angle-estimated. In clinical reporting it is the honest alternative to pies for cohort composition, RECIST response categories, and consent/enrollment breakdowns, because human readers judge counts and lengths far more accurately than angles \[1\]. + +`pywaffle` provides the `Waffle` figure class on top of matplotlib \[2\]; the same grid is also easy to hand-roll. The two decisions that matter: the block scale (1 block = 1 patient, or 1%) and the rounding rule when counts do not divide into the grid — state both, or the blocks silently misstate the proportions. + +This tutorial covers two workflows: + +1. A 10 x 10 waffle of RECIST response for one cohort; +2. Two treatment arms side by side with a shared category palette. + +## Example + +![](../images/Python/WafflePlot_demo.png){fig-align="center" width="60%"} + +## Setup + +- System Requirements: Cross-platform (Linux/MacOS/Windows) +- Programming Language: **Python** +- Dependencies: `matplotlib`, `pywaffle` + +```{python} +#| label: packages-setup +#| output: false +import matplotlib.pyplot as plt +from pywaffle import Waffle +``` + +## Data Preparation + +We use a small Phase-II-style cohort: 60 patients assessed by RECIST at week 12 — 25 partial responses, 22 stable disease, 13 progressive disease. A control arm of the same size (12/26/22) motivates the two-panel comparison. + +```{python} +#| label: data-prep +treated = {"Partial response": 25, "Stable disease": 22, + "Progressive disease": 13} +control = {"Partial response": 12, "Stable disease": 26, + "Progressive disease": 22} +colors = {"Partial response": "#1B7837", "Stable disease": "#2166AC", + "Progressive disease": "#B2182B"} +assert sum(treated.values()) == sum(control.values()) == 60 +``` + +## Visualization + +### 1. Single-Cohort Waffle + +```{python} +#| label: fig-py-waffle-basic +#| fig-cap: "RECIST response as a 10 x 10 grid: one block = one patient, so the 13 progressions are countable instead of encoded as a wedge angle." +#| fig-align: center + +fig = plt.figure(figsize=(7, 5.6), FigureClass=Waffle, + rows=10, columns=10, + values=treated, colors=list(colors.values()), + block_aspect_ratio=1, + title={"label": "RECIST response, n = 60 patients", + "fontsize": 12}, + legend={"loc": "lower center", "bbox_to_anchor": (0.5, -0.14), + "ncol": 3, "fontsize": 9, "frameon": False}) +fig.tight_layout() +plt.show() +``` + +### 2. Two Arms Side by Side + +```{python} +#| label: fig-py-waffle-arms +#| fig-cap: "Control and treatment waffles share one palette and one block scale, so the extra green blocks in the treatment arm read as a count, not an angle." +#| fig-align: center + +legend_spec = {"loc": "lower center", "bbox_to_anchor": (0.5, -0.16), + "ncol": 2, "fontsize": 8, "frameon": False} +fig = plt.figure(figsize=(11, 6.2), FigureClass=Waffle, + plots={ + 121: {"rows": 10, "columns": 6, "values": control, + "colors": list(colors.values()), + "title": {"label": "Control (n = 60)", + "fontsize": 11}, + "legend": legend_spec}, + 122: {"rows": 10, "columns": 6, "values": treated, + "colors": list(colors.values()), + "title": {"label": "Treatment (n = 60)", + "fontsize": 11}, + "legend": legend_spec}, + }) +fig.suptitle("One block = 1 patient", fontsize=12, y=0.99) +plt.show() +``` + +::: callout-tip +## Practice tips + +- Fix the block scale first: 1 block = 1 patient below ~200 subjects, 1 block = 1% (100 blocks) above — never let the library auto-scale silently between panels. +- When converting counts to percentage blocks, round explicitly and state the rule; `round()` can make blocks sum to 99 or 101. +- Keep one palette and one legend across arms so blocks, not colors, carry the comparison. +- Waffles show composition only — pair them with counts in the labels when the denominator matters. +- Ordered categories (RECIST) should keep a consistent color direction across figures; unordered ones should use colorblind-safe hues. +::: + +## Application + +For a large cohort, switch the block scale to 1 block = 1% and the waffle becomes a direct objective-response-rate statement: the 37 green blocks below are the ORR itself, which is why waffles work well in patient-facing summaries and trial posters. + +![](../images/Python/WafflePlot_app1.png){fig-align="center" width="60%"} + +## Reference + +\[1\] Cleveland WS, McGill R. Graphical perception: theory, experimentation, and application to the development of graphical methods. J Am Stat Assoc. 1984;79(387):531-554. doi:10.1080/01621459.1984.10478080 + +\[2\] Liu G. PyWaffle: waffle charts in matplotlib. 2024. https://github.com/gyli/PyWaffle diff --git a/Python/WafflePlot.zh.qmd b/Python/WafflePlot.zh.qmd new file mode 100644 index 000000000..94ea3703c --- /dev/null +++ b/Python/WafflePlot.zh.qmd @@ -0,0 +1,116 @@ +--- +title: "华夫饼图 (Python)" +author: + - "**[编辑]** [王诗翔](https://github.com/ShixiangWang)" + - "**[贡献]** Codex (GPT-5) — AI 生成,独立 AI 复核" +--- + +[华夫饼图](../zh/Composition/Waffle.html)的 Python 对应版本——华夫饼图用单位方块网格取代以面积编码的饼图/环形图,一个方块代表一名患者(或 1% 的队列),比例靠*数数*而非估计角度获得。在临床报告中,它是队列构成、RECIST 反应类别、入组/知情同意分布等数据的饼图诚实替代:人类读者对计数和长度的判断远比对角度准确 \[1\]。 + +`pywaffle` 在 matplotlib 之上提供了 `Waffle` 图形类 \[2\];同样的网格也完全可以手工绘制。两个关键决定:方块比例(1 方块 = 1 名患者,或 1%)与计数不能整除网格时的取整规则——两者都要注明,否则方块会悄悄歪曲比例。 + +本教程涵盖两种工作流: + +1. 单队列 RECIST 反应的 10 x 10 华夫图; +2. 共享类别配色的两治疗臂并排对比。 + +## Example + +![](../images/Python/WafflePlot_demo.png){fig-align="center" width="60%"} + +## Setup + +- 系统要求:跨平台(Linux/MacOS/Windows) +- 编程语言:**Python** +- 依赖包:`matplotlib`、`pywaffle` + +```{python} +#| label: packages-setup +#| output: false +import matplotlib.pyplot as plt +from pywaffle import Waffle +``` + +## Data Preparation + +我们使用一个小型 II 期风格队列:60 名患者在第 12 周接受 RECIST 评估——25 例部分缓解、22 例疾病稳定、13 例疾病进展。另一个同规模对照臂(12/26/22)用于双面板对比。 + +```{python} +#| label: data-prep +treated = {"Partial response": 25, "Stable disease": 22, + "Progressive disease": 13} +control = {"Partial response": 12, "Stable disease": 26, + "Progressive disease": 22} +colors = {"Partial response": "#1B7837", "Stable disease": "#2166AC", + "Progressive disease": "#B2182B"} +assert sum(treated.values()) == sum(control.values()) == 60 +``` + +## Visualization + +### 1. 单队列华夫图 + +```{python} +#| label: fig-py-waffle-basic +#| fig-cap: "RECIST 反应的 10 x 10 网格:一个方块 = 一名患者,13 例进展可以直接数出来,而不是编码成一个扇形角度。" +#| fig-align: center + +fig = plt.figure(figsize=(7, 5.6), FigureClass=Waffle, + rows=10, columns=10, + values=treated, colors=list(colors.values()), + block_aspect_ratio=1, + title={"label": "RECIST response, n = 60 patients", + "fontsize": 12}, + legend={"loc": "lower center", "bbox_to_anchor": (0.5, -0.14), + "ncol": 3, "fontsize": 9, "frameon": False}) +fig.tight_layout() +plt.show() +``` + +### 2. 两臂并排 + +```{python} +#| label: fig-py-waffle-arms +#| fig-cap: "对照臂与治疗臂共用同一配色和同一方块比例,治疗臂多出的绿色方块读作计数差异,而非角度差异。" +#| fig-align: center + +legend_spec = {"loc": "lower center", "bbox_to_anchor": (0.5, -0.16), + "ncol": 2, "fontsize": 8, "frameon": False} +fig = plt.figure(figsize=(11, 6.2), FigureClass=Waffle, + plots={ + 121: {"rows": 10, "columns": 6, "values": control, + "colors": list(colors.values()), + "title": {"label": "Control (n = 60)", + "fontsize": 11}, + "legend": legend_spec}, + 122: {"rows": 10, "columns": 6, "values": treated, + "colors": list(colors.values()), + "title": {"label": "Treatment (n = 60)", + "fontsize": 11}, + "legend": legend_spec}, + }) +fig.suptitle("One block = 1 patient", fontsize=12, y=0.99) +plt.show() +``` + +::: callout-tip +## 实践要点 + +- 先定方块比例:约 200 例以下用 1 方块 = 1 名患者,以上用 1 方块 = 1%(100 方块)——绝不让库在面板之间悄悄自动缩放。 +- 计数转百分比方块时显式取整并注明规则;`round()` 可能让方块总数变成 99 或 101。 +- 多臂共用一套配色和图例,让方块而不是颜色承载比较。 +- 华夫图只表达构成——分母重要时把计数写进标签。 +- 有序类别(RECIST)在多图间保持一致的颜色方向;无序类别使用色盲安全色。 +::: + +## Application + +大队列时把方块比例切换为 1 方块 = 1%,华夫图就变成直接的客观缓解率(ORR)陈述:下图 37 个绿色方块就是 ORR 本身——这也是华夫图在患者沟通材料和研究海报中好用的原因。 + +![](../images/Python/WafflePlot_app1.png){fig-align="center" width="60%"} + +## Reference + +\[1\] Cleveland WS, McGill R. Graphical perception: theory, experimentation, and application to the development of graphical methods. J Am Stat Assoc. 1984;79(387):531-554. doi:10.1080/01621459.1984.10478080 + +\[2\] Liu G. PyWaffle: waffle charts in matplotlib. 2024. https://github.com/gyli/PyWaffle diff --git a/Python/WordcloudPlot.qmd b/Python/WordcloudPlot.qmd new file mode 100644 index 000000000..16b00cce9 --- /dev/null +++ b/Python/WordcloudPlot.qmd @@ -0,0 +1,138 @@ +--- +title: "Wordcloud Plot (Python)" +author: + - "**[Editor]** [Shixiang Wang](https://github.com/ShixiangWang);" + - "**[Contributors]** Codex (GPT-5) — AI-generated, independently AI-reviewed;" +--- + +The Python counterpart of [Wordcloud](../Ranking/Wordcloud.html) — a wordcloud sizes terms by frequency, giving an instant overview of the vocabulary of a corpus: research topics in a conference program, patient-reported symptoms in a survey, or indexing terms across a PubMed query. It is an exploratory display, not a measurement — precise comparison belongs in a frequency table or bar chart. + +The `wordcloud` package builds the layout \[1\]. Two settings dominate the result: `collocations` (whether adjacent word pairs like "immune checkpoint" are kept as one term) and the input mode (free text vs a precomputed frequency dictionary). Both change the *unit of analysis*, so they must be stated in the caption. + +This tutorial covers two workflows: + +1. A text-driven cloud with collocations (bigrams) kept; +2. A frequency-driven cloud with a print-friendly sequential palette. + +## Example + +![](../images/Python/WordcloudPlot_demo.png){fig-align="center" width="70%"} + +## Setup + +- System Requirements: Cross-platform (Linux/MacOS/Windows) +- Programming Language: **Python** +- Dependencies: `matplotlib`, `numpy`, `wordcloud` + +```{python} +#| label: packages-setup +#| output: false +import matplotlib.pyplot as plt +import numpy as np +from collections import Counter +from wordcloud import WordCloud, STOPWORDS +``` + +## Data Preparation + +We assemble a pseudo-corpus of oncology abstract sentences, sampled 600 times with fixed weights so the topic distribution is reproducible. Token frequencies are also counted directly (minus stopwords) for the frequency-driven variant. For Chinese text, tokenize with `jieba` first and pass the token list or frequency dictionary — `wordcloud` splits on whitespace otherwise. + +```{python} +#| label: data-prep +rng = np.random.default_rng(13) +templates = [ + "immune checkpoint inhibitors improve overall survival in {d} patients", + "high tumor mutational burden predicts response to immunotherapy", + "single-cell profiling resolves tumor microenvironment niches", + "liquid biopsy tracks clonal evolution during combination therapy", + "a biomarker panel stratifies adverse events after radiotherapy", + "neoantigen load and t cell exhaustion shape immunotherapy outcomes", + "spatial transcriptomics maps angiogenesis in metastasis", + "machine learning on multi-omics data supports precision oncology", + "drug resistance emerges through genomic instability", + "a real world cohort study of progression-free survival", + "the microbiome modulates immune checkpoint therapy", + "car-t therapy in relapsed disease: a clinical trial", + "chronic inflammation accelerates tumor growth and metastasis", + "quality of life and adverse events in elderly cohorts", + "pathology review confirmed tumor suppressor loss", +] +weights = np.array([8, 7, 7, 6, 6, 5, 4, 5, 4, 4, 3, 3, 3, 2, 2], float) +picks = rng.choice(len(templates), size=600, p=weights / weights.sum()) +text = " ".join( + templates[i].format(d=rng.choice(["melanoma", "lung", "breast"])) + for i in picks) + +terms = Counter() +for i in picks: + for tok in templates[i].replace("{d}", "patients").split(): + if tok.lower() not in STOPWORDS and tok.isalpha(): + terms[tok.lower()] += 1 +freq = dict(terms) +``` + +## Visualization + +### 1. Text-Driven Cloud with Collocations + +```{python} +#| label: fig-py-wc-text +#| fig-cap: "Text-driven cloud with collocations kept: adjacent terms such as 'immune checkpoint' and 'overall survival' stay joined as phrases, preserving the domain vocabulary." +#| fig-align: center + +wc = WordCloud(width=900, height=520, background_color="white", + colormap="viridis", stopwords=STOPWORDS, + collocations=True, random_state=7, max_words=60) +wc = wc.generate_from_text(text) +fig, ax = plt.subplots(figsize=(8.5, 5)) +ax.imshow(wc, interpolation="bilinear") +ax.axis("off") +ax.set_title("Oncology abstract terms (collocations on: bigrams kept)") +fig.tight_layout() +plt.show() +``` + +### 2. Frequency-Driven Cloud for Print + +```{python} +#| label: fig-py-wc-freq +#| fig-cap: "Cloud drawn from a precomputed frequency dictionary (stopwords removed): the top-25 terms use a sequential Blues palette that survives greyscale printing." +#| fig-align: center + +from matplotlib.colors import to_hex +blues = plt.get_cmap("Blues")(np.linspace(0.45, 0.95, 256)) + +def print_blues(*args, **kwargs): + return to_hex(blues[rng.integers(0, len(blues))]) + +wc2 = WordCloud(width=900, height=520, background_color="white", + random_state=7, max_words=25) +wc2.generate_from_frequencies(freq) +wc2.recolor(color_func=print_blues) +fig, ax = plt.subplots(figsize=(8.5, 5)) +ax.imshow(wc2, interpolation="bilinear") +ax.axis("off") +ax.set_title("Top-25 terms from frequencies (sequential palette for print)") +fig.tight_layout() +plt.show() +``` + +::: callout-tip +## Practice tips + +- Always pass `random_state=` — word placement is randomized, and an unseeded cloud changes shape on every rerun. +- Decide collocations deliberately: `True` keeps multiword concepts, `False` counts single words; never mix the two modes between figures of the same comparison. +- Remove domain stopwords (patient, study, result) before drawing, or they dominate by mere grammatical frequency. +- Word area encodes frequency only approximately (layout packing perturbs it); quote exact counts in a companion table for analysis. +- For Chinese corpora, tokenize with `jieba` and feed frequencies; the default whitespace tokenizer cannot segment Chinese text. +::: + +## Application + +The collocations switch is the most misused setting: with `True` the corpus keeps "immune checkpoint" as one concept; with `False` the same corpus splits into "immune" and "checkpoint", doubling the apparent frequency of each. Show the two clouds side by side when the unit of analysis matters to your claim. + +![](../images/Python/WordcloudPlot_app1.png){fig-align="center" width="85%"} + +## Reference + +\[1\] Müller A. wordcloud: generate word clouds in Python. 2024. https://github.com/amueller/word_cloud diff --git a/Python/WordcloudPlot.zh.qmd b/Python/WordcloudPlot.zh.qmd new file mode 100644 index 000000000..8892ac74b --- /dev/null +++ b/Python/WordcloudPlot.zh.qmd @@ -0,0 +1,138 @@ +--- +title: "词云图 (Python)" +author: + - "**[编辑]** [王诗翔](https://github.com/ShixiangWang)" + - "**[贡献]** Codex (GPT-5) — AI 生成,独立 AI 复核" +--- + +[词云图](../zh/Ranking/Wordcloud.html)的 Python 对应版本——词云图按词频缩放词语大小,一眼呈现语料的词汇概况:会议议题、患者报告症状、PubMed 检索式的索引词。它是探索性展示而非测量工具——精确比较请使用频率表或条形图。 + +`wordcloud` 包负责布局 \[1\]。两个设置决定结果:`collocations`(相邻词对如 "immune checkpoint" 是否作为一个词保留)与输入模式(自由文本 vs 预计算频率字典)。两者都会改变*分析单元*,因此必须在图注中说明。 + +本教程涵盖两种工作流: + +1. 保留搭配(二元组)的文本驱动词云; +2. 使用印刷友好的顺序调色板的频率驱动词云。 + +## Example + +![](../images/Python/WordcloudPlot_demo.png){fig-align="center" width="70%"} + +## Setup + +- 系统要求:跨平台(Linux/MacOS/Windows) +- 编程语言:**Python** +- 依赖包:`matplotlib`、`numpy`、`wordcloud` + +```{python} +#| label: packages-setup +#| output: false +import matplotlib.pyplot as plt +import numpy as np +from collections import Counter +from wordcloud import WordCloud, STOPWORDS +``` + +## Data Preparation + +我们用肿瘤学摘要句子组装伪语料,按固定权重抽样 600 次,使主题分布可复现。词频同时被直接统计(去除停用词)供频率驱动变体使用。中文文本需先用 `jieba` 分词,再传入词表或频率字典——否则 `wordcloud` 只按空格切分。 + +```{python} +#| label: data-prep +rng = np.random.default_rng(13) +templates = [ + "immune checkpoint inhibitors improve overall survival in {d} patients", + "high tumor mutational burden predicts response to immunotherapy", + "single-cell profiling resolves tumor microenvironment niches", + "liquid biopsy tracks clonal evolution during combination therapy", + "a biomarker panel stratifies adverse events after radiotherapy", + "neoantigen load and t cell exhaustion shape immunotherapy outcomes", + "spatial transcriptomics maps angiogenesis in metastasis", + "machine learning on multi-omics data supports precision oncology", + "drug resistance emerges through genomic instability", + "a real world cohort study of progression-free survival", + "the microbiome modulates immune checkpoint therapy", + "car-t therapy in relapsed disease: a clinical trial", + "chronic inflammation accelerates tumor growth and metastasis", + "quality of life and adverse events in elderly cohorts", + "pathology review confirmed tumor suppressor loss", +] +weights = np.array([8, 7, 7, 6, 6, 5, 4, 5, 4, 4, 3, 3, 3, 2, 2], float) +picks = rng.choice(len(templates), size=600, p=weights / weights.sum()) +text = " ".join( + templates[i].format(d=rng.choice(["melanoma", "lung", "breast"])) + for i in picks) + +terms = Counter() +for i in picks: + for tok in templates[i].replace("{d}", "patients").split(): + if tok.lower() not in STOPWORDS and tok.isalpha(): + terms[tok.lower()] += 1 +freq = dict(terms) +``` + +## Visualization + +### 1. 保留搭配的文本驱动词云 + +```{python} +#| label: fig-py-wc-text +#| fig-cap: "开启搭配的文本驱动词云:'immune checkpoint'、'overall survival' 等相邻词保持为短语,领域词汇得以保留。" +#| fig-align: center + +wc = WordCloud(width=900, height=520, background_color="white", + colormap="viridis", stopwords=STOPWORDS, + collocations=True, random_state=7, max_words=60) +wc = wc.generate_from_text(text) +fig, ax = plt.subplots(figsize=(8.5, 5)) +ax.imshow(wc, interpolation="bilinear") +ax.axis("off") +ax.set_title("Oncology abstract terms (collocations on: bigrams kept)") +fig.tight_layout() +plt.show() +``` + +### 2. 面向印刷的频率驱动词云 + +```{python} +#| label: fig-py-wc-freq +#| fig-cap: "从预计算频率字典(已去除停用词)绘制的词云:前 25 个词使用顺序 Blues 调色板,灰度打印仍可辨识。" +#| fig-align: center + +from matplotlib.colors import to_hex +blues = plt.get_cmap("Blues")(np.linspace(0.45, 0.95, 256)) + +def print_blues(*args, **kwargs): + return to_hex(blues[rng.integers(0, len(blues))]) + +wc2 = WordCloud(width=900, height=520, background_color="white", + random_state=7, max_words=25) +wc2.generate_from_frequencies(freq) +wc2.recolor(color_func=print_blues) +fig, ax = plt.subplots(figsize=(8.5, 5)) +ax.imshow(wc2, interpolation="bilinear") +ax.axis("off") +ax.set_title("Top-25 terms from frequencies (sequential palette for print)") +fig.tight_layout() +plt.show() +``` + +::: callout-tip +## 实践要点 + +- 始终传入 `random_state=`——词的摆放是随机的,不给种子则每次重跑形状都不同。 +- 搭配开关要有意识地选择:`True` 保留多词概念,`False` 按单词计数;同一对比的多张图绝不能混用两种模式。 +- 绘制前移除领域停用词(patient、study、result),否则它们会凭语法频率霸占画面。 +- 词的面积只是近似编码词频(布局装箱会扰动它);分析请配合精确计数的表格。 +- 中文语料先用 `jieba` 分词再传频率;默认的空白分词无法切分中文。 +::: + +## Application + +搭配开关是最常被误用的设置:`True` 时语料保留 "immune checkpoint" 这一概念;`False` 时同一语料被拆成 "immune" 与 "checkpoint",两者的表观频率都翻倍。当分析单元影响结论时,把两种词云并排展示。 + +![](../images/Python/WordcloudPlot_app1.png){fig-align="center" width="85%"} + +## Reference + +\[1\] Müller A. wordcloud: generate word clouds in Python. 2024. https://github.com/amueller/word_cloud diff --git a/_quarto.yml b/_quarto.yml index e566e89c3..377197424 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -259,6 +259,8 @@ website: contents: - text: "Scatter Plot (Python)" href: Python/ScatterPlot.qmd + - text: "3D Scatter (Python)" + href: Python/Scatter3dPlot.qmd - text: "Violin Plot (Python)" href: Python/ViolinPlot.qmd - text: "Heatmap (Python)" @@ -283,12 +285,16 @@ website: href: Python/LollipopPlot.qmd - text: "Radar Plot (Python)" href: Python/RadarPlot.qmd + - text: "Wordcloud (Python)" + href: Python/WordcloudPlot.qmd - text: "Joint & Pair Plot (Python)" href: Python/JointPairPlot.qmd - text: "Pie & Donut (Python)" href: Python/PieDonutPlot.qmd - text: "Treemap & Sunburst (Python)" href: Python/TreemapSunburst.qmd + - text: "Waffle (Python)" + href: Python/WafflePlot.qmd - text: "Area & Streamgraph (Python)" href: Python/AreaPlot.qmd - text: "Calendar Heatmap (Python)" diff --git a/files/gallery_data.csv b/files/gallery_data.csv index ba85996d3..aa58e9b3e 100644 --- a/files/gallery_data.csv +++ b/files/gallery_data.csv @@ -1064,3 +1064,9 @@ Id,Name,Image_url,Tutorial_url,Description,Type,Level1,Level2 1064,"PCA (linear, preserves global variance) versus t-SNE (perplexity=30, seed=42): both recover the three cell types, but t-SNE's tight, well-separated blobs are an algorithmic artifact, not evidence of stronger separation.",https://openbiox.github.io/Bizard/Python/TsnePlot_files/figure-html/fig-py-tsne-pca-compare-output-1.png,https://openbiox.github.io/Bizard/Python/TsnePlot.html#fig-py-tsne-pca-compare,"PCA (linear, preserves global variance) versus t-SNE (perplexity=30, seed=42): both recover the three cell types, but t-SNE's tight, well-separated blobs are an algorithmic artifact, not evidence of stronger separation.",t-SNE Plot (Python),PYTHON,Python 1065,Perplexity 5 fragments each type into artificial subclusters; 30 gives a balanced picture; 100 over-smooths the geometry into dense blobs whose spacing no longer reflects the data.,https://openbiox.github.io/Bizard/Python/TsnePlot_files/figure-html/fig-py-tsne-perplexity-output-1.png,https://openbiox.github.io/Bizard/Python/TsnePlot.html#fig-py-tsne-perplexity,Perplexity 5 fragments each type into artificial subclusters; 30 gives a balanced picture; 100 over-smooths the geometry into dense blobs whose spacing no longer reflects the data.,t-SNE Plot (Python),PYTHON,Python 1066,"t-SNE on all 200 scaled features versus t-SNE on the top-50 PCs: pre-reduction denoises the input, cleaning up within-type structure without changing the three-cluster conclusion.",https://openbiox.github.io/Bizard/Python/TsnePlot_files/figure-html/fig-py-tsne-prereduce-output-1.png,https://openbiox.github.io/Bizard/Python/TsnePlot.html#fig-py-tsne-prereduce,"t-SNE on all 200 scaled features versus t-SNE on the top-50 PCs: pre-reduction denoises the input, cleaning up within-type structure without changing the three-cluster conclusion.",t-SNE Plot (Python),PYTHON,Python +1067,"Three patient groups in biomarker space at elev=20, azim=-60: the groups occupy distinct regions of the 3D volume.",https://openbiox.github.io/Bizard/Python/Scatter3dPlot_files/figure-html/fig-py-3d-basic-output-1.png,https://openbiox.github.io/Bizard/Python/Scatter3dPlot.html#fig-py-3d-basic,"Three patient groups in biomarker space at elev=20, azim=-60: the groups occupy distinct regions of the 3D volume.",3D Scatter Plot (Python),PYTHON,Python +1068,"The same dataset from two viewpoints: the separability reading at left is not available edge-on at right, where depth order and the reversed axis direction become hard to judge.",https://openbiox.github.io/Bizard/Python/Scatter3dPlot_files/figure-html/fig-py-3d-views-output-1.png,https://openbiox.github.io/Bizard/Python/Scatter3dPlot.html#fig-py-3d-views,"The same dataset from two viewpoints: the separability reading at left is not available edge-on at right, where depth order and the reversed axis direction become hard to judge.",3D Scatter Plot (Python),PYTHON,Python +1069,"RECIST response as a 10 x 10 grid: one block = one patient, so the 13 progressions are countable instead of encoded as a wedge angle.",https://openbiox.github.io/Bizard/Python/WafflePlot_files/figure-html/fig-py-waffle-basic-output-1.png,https://openbiox.github.io/Bizard/Python/WafflePlot.html#fig-py-waffle-basic,"RECIST response as a 10 x 10 grid: one block = one patient, so the 13 progressions are countable instead of encoded as a wedge angle.",Waffle Plot (Python),PYTHON,Python +1070,"Control and treatment waffles share one palette and one block scale, so the extra green blocks in the treatment arm read as a count, not an angle.",https://openbiox.github.io/Bizard/Python/WafflePlot_files/figure-html/fig-py-waffle-arms-output-1.png,https://openbiox.github.io/Bizard/Python/WafflePlot.html#fig-py-waffle-arms,"Control and treatment waffles share one palette and one block scale, so the extra green blocks in the treatment arm read as a count, not an angle.",Waffle Plot (Python),PYTHON,Python +1071,"Text-driven cloud with collocations kept: adjacent terms such as 'immune checkpoint' and 'overall survival' stay joined as phrases, preserving the domain vocabulary.",https://openbiox.github.io/Bizard/Python/WordcloudPlot_files/figure-html/fig-py-wc-text-output-1.png,https://openbiox.github.io/Bizard/Python/WordcloudPlot.html#fig-py-wc-text,"Text-driven cloud with collocations kept: adjacent terms such as 'immune checkpoint' and 'overall survival' stay joined as phrases, preserving the domain vocabulary.",Wordcloud Plot (Python),PYTHON,Python +1072,Cloud drawn from a precomputed frequency dictionary (stopwords removed): the top-25 terms use a sequential Blues palette that survives greyscale printing.,https://openbiox.github.io/Bizard/Python/WordcloudPlot_files/figure-html/fig-py-wc-freq-output-1.png,https://openbiox.github.io/Bizard/Python/WordcloudPlot.html#fig-py-wc-freq,Cloud drawn from a precomputed frequency dictionary (stopwords removed): the top-25 terms use a sequential Blues palette that survives greyscale printing.,Wordcloud Plot (Python),PYTHON,Python diff --git a/files/gallery_data_zh.csv b/files/gallery_data_zh.csv index d4e7da23f..16cd03ccb 100644 --- a/files/gallery_data_zh.csv +++ b/files/gallery_data_zh.csv @@ -1064,3 +1064,9 @@ Id,Name,Image_url,Tutorial_url,Description,Type,Level1,Level2 1064,"PCA(线性、保留全局方差)与 t-SNE(perplexity=30, seed=42):两者都找回了三种细胞类型,但 t-SNE 紧凑、分离良好的团块是算法产物,并不能证明分离更强。",https://openbiox.github.io/Bizard/Python/TsnePlot_files/figure-html/fig-py-tsne-pca-compare-output-1.png,https://openbiox.github.io/Bizard/zh/Python/TsnePlot.html#fig-py-tsne-pca-compare,"PCA(线性、保留全局方差)与 t-SNE(perplexity=30, seed=42):两者都找回了三种细胞类型,但 t-SNE 紧凑、分离良好的团块是算法产物,并不能证明分离更强。",t-SNE 图 (Python),PYTHON,Python 1065,困惑度 5 把每种类型碎裂成人为的小簇;30 给出平衡的画面;100 把几何过度平滑成致密团块,其间距不再反映数据。,https://openbiox.github.io/Bizard/Python/TsnePlot_files/figure-html/fig-py-tsne-perplexity-output-1.png,https://openbiox.github.io/Bizard/zh/Python/TsnePlot.html#fig-py-tsne-perplexity,困惑度 5 把每种类型碎裂成人为的小簇;30 给出平衡的画面;100 把几何过度平滑成致密团块,其间距不再反映数据。,t-SNE 图 (Python),PYTHON,Python 1066,在全部 200 个标准化特征上做 t-SNE,与在 top-50 主成分上做 t-SNE 对比:预降维对输入去噪,清理了类型内部结构,但不改变三簇结论。,https://openbiox.github.io/Bizard/Python/TsnePlot_files/figure-html/fig-py-tsne-prereduce-output-1.png,https://openbiox.github.io/Bizard/zh/Python/TsnePlot.html#fig-py-tsne-prereduce,在全部 200 个标准化特征上做 t-SNE,与在 top-50 主成分上做 t-SNE 对比:预降维对输入去噪,清理了类型内部结构,但不改变三簇结论。,t-SNE 图 (Python),PYTHON,Python +1067,elev=20、azim=-60 下的三维生物标志物空间:三组占据体积中的不同区域。,https://openbiox.github.io/Bizard/Python/Scatter3dPlot_files/figure-html/fig-py-3d-basic-output-1.png,https://openbiox.github.io/Bizard/zh/Python/Scatter3dPlot.html#fig-py-3d-basic,elev=20、azim=-60 下的三维生物标志物空间:三组占据体积中的不同区域。,3D 散点图 (Python),PYTHON,Python +1068,同一数据的两个视角:左图的组间分离在右图的侧视中不可得——深度顺序与反转的轴方向都难以判断。,https://openbiox.github.io/Bizard/Python/Scatter3dPlot_files/figure-html/fig-py-3d-views-output-1.png,https://openbiox.github.io/Bizard/zh/Python/Scatter3dPlot.html#fig-py-3d-views,同一数据的两个视角:左图的组间分离在右图的侧视中不可得——深度顺序与反转的轴方向都难以判断。,3D 散点图 (Python),PYTHON,Python +1069,RECIST 反应的 10 x 10 网格:一个方块 = 一名患者,13 例进展可以直接数出来,而不是编码成一个扇形角度。,https://openbiox.github.io/Bizard/Python/WafflePlot_files/figure-html/fig-py-waffle-basic-output-1.png,https://openbiox.github.io/Bizard/zh/Python/WafflePlot.html#fig-py-waffle-basic,RECIST 反应的 10 x 10 网格:一个方块 = 一名患者,13 例进展可以直接数出来,而不是编码成一个扇形角度。,华夫饼图 (Python),PYTHON,Python +1070,对照臂与治疗臂共用同一配色和同一方块比例,治疗臂多出的绿色方块读作计数差异,而非角度差异。,https://openbiox.github.io/Bizard/Python/WafflePlot_files/figure-html/fig-py-waffle-arms-output-1.png,https://openbiox.github.io/Bizard/zh/Python/WafflePlot.html#fig-py-waffle-arms,对照臂与治疗臂共用同一配色和同一方块比例,治疗臂多出的绿色方块读作计数差异,而非角度差异。,华夫饼图 (Python),PYTHON,Python +1071,开启搭配的文本驱动词云:'immune checkpoint'、'overall survival' 等相邻词保持为短语,领域词汇得以保留。,https://openbiox.github.io/Bizard/Python/WordcloudPlot_files/figure-html/fig-py-wc-text-output-1.png,https://openbiox.github.io/Bizard/zh/Python/WordcloudPlot.html#fig-py-wc-text,开启搭配的文本驱动词云:'immune checkpoint'、'overall survival' 等相邻词保持为短语,领域词汇得以保留。,词云图 (Python),PYTHON,Python +1072,从预计算频率字典(已去除停用词)绘制的词云:前 25 个词使用顺序 Blues 调色板,灰度打印仍可辨识。,https://openbiox.github.io/Bizard/Python/WordcloudPlot_files/figure-html/fig-py-wc-freq-output-1.png,https://openbiox.github.io/Bizard/zh/Python/WordcloudPlot.html#fig-py-wc-freq,从预计算频率字典(已去除停用词)绘制的词云:前 25 个词使用顺序 Blues 调色板,灰度打印仍可辨识。,词云图 (Python),PYTHON,Python diff --git a/images/Python/Scatter3dPlot_app1.png b/images/Python/Scatter3dPlot_app1.png new file mode 100644 index 000000000..2f9cca61c Binary files /dev/null and b/images/Python/Scatter3dPlot_app1.png differ diff --git a/images/Python/Scatter3dPlot_demo.png b/images/Python/Scatter3dPlot_demo.png new file mode 100644 index 000000000..dbdd3e8c5 Binary files /dev/null and b/images/Python/Scatter3dPlot_demo.png differ diff --git a/images/Python/WafflePlot_app1.png b/images/Python/WafflePlot_app1.png new file mode 100644 index 000000000..2fc82271f Binary files /dev/null and b/images/Python/WafflePlot_app1.png differ diff --git a/images/Python/WafflePlot_demo.png b/images/Python/WafflePlot_demo.png new file mode 100644 index 000000000..718b59e5a Binary files /dev/null and b/images/Python/WafflePlot_demo.png differ diff --git a/images/Python/WordcloudPlot_app1.png b/images/Python/WordcloudPlot_app1.png new file mode 100644 index 000000000..e406af020 Binary files /dev/null and b/images/Python/WordcloudPlot_app1.png differ diff --git a/images/Python/WordcloudPlot_demo.png b/images/Python/WordcloudPlot_demo.png new file mode 100644 index 000000000..a287519f8 Binary files /dev/null and b/images/Python/WordcloudPlot_demo.png differ diff --git a/requirements.txt b/requirements.txt index 5c2d92871..6c3da0c15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ plotly ptitprince pycirclize PyComplexHeatmap +pywaffle scanpy scienceplots scikit-learn @@ -18,3 +19,4 @@ scipy>=1.10 seaborn>=0.12 squarify statsmodels +wordcloud