-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmake_figures.py
More file actions
241 lines (202 loc) · 9.64 KB
/
Copy pathmake_figures.py
File metadata and controls
241 lines (202 loc) · 9.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
"""
Figures for the README of Export Declaration Quality Checker.
Every number below is copied from a saved output cell in
Export_Declaration_Quality_Checker.ipynb. Nothing is recomputed or estimated.
Palette: indigo / ochre / madder, the three classic natural textile dyes.
Validated with the dataviz palette validator (all six checks pass, all-pairs,
light mode, surface #faf8f4).
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
# --- design tokens -----------------------------------------------------------
SURFACE = "#faf8f4" # undyed cotton
INDIGO = "#3a5da8" # agreement / passed
OCHRE = "#b07d16" # partial disagreement
MADDER = "#b03f2c" # disagreement / flagged
INK = "#14130f"
INK_2 = "#4e4c45"
MUTED = "#84806f"
GRID = "#e6e2d8"
AXIS = "#c9c4b6"
plt.rcParams.update({
"font.family": "DejaVu Sans",
"figure.facecolor": SURFACE,
"axes.facecolor": SURFACE,
"savefig.facecolor": SURFACE,
"text.color": INK,
"axes.labelcolor": INK_2,
"xtick.color": MUTED,
"ytick.color": MUTED,
"axes.edgecolor": AXIS,
"axes.linewidth": 0.8,
"xtick.major.size": 0,
"ytick.major.size": 0,
"font.size": 10.5,
})
def strip(ax, grid_axis="y"):
for side in ("top", "right"):
ax.spines[side].set_visible(False)
ax.grid(axis=grid_axis, color=GRID, linewidth=0.8, zorder=0)
ax.set_axisbelow(True)
def caption(fig, text):
fig.text(0.013, 0.02, text, fontsize=8.5, color=MUTED, va="bottom")
def header(fig, title, subtitle):
"""Figure-left title block, so every figure aligns the same way."""
fig.text(0.013, 0.965, title, fontsize=16, fontweight="bold",
color=INK, va="top")
fig.text(0.013, 0.878, subtitle, fontsize=10, color=INK_2,
va="top", linespacing=1.5)
# =============================================================================
# Figure 1 (hero) — the decision margin separates agreement
# Source: notebook Step 11, threshold_table
# =============================================================================
coverage = [1.0000, 0.9004, 0.8010, 0.7001, 0.6065, 0.5001, 0.4001]
kept_agree = [0.7457, 0.7971, 0.8377, 0.8718, 0.9296, 0.9635, 0.9688]
rest_cov = coverage[1:]
rest_agree = [0.2810, 0.3750, 0.4512, 0.4623, 0.5277, 0.5969]
fig, ax = plt.subplots(figsize=(9.6, 5.4), dpi=200)
strip(ax)
ax.axhline(0.7457, color=AXIS, linewidth=0.9, zorder=1)
ax.text(103.2, 0.7457 - 0.035, "0.746 across all lines", fontsize=8.5,
color=MUTED, va="top", ha="right")
ax.plot([c * 100 for c in coverage], kept_agree, color=INDIGO, linewidth=2,
marker="o", markersize=5.5, markerfacecolor=INDIGO,
markeredgecolor=SURFACE, markeredgewidth=1.6, zorder=3,
label="Lines the model kept (highest margin)")
ax.plot([c * 100 for c in rest_cov], rest_agree, color=MADDER, linewidth=2,
marker="o", markersize=5.5, markerfacecolor=MADDER,
markeredgecolor=SURFACE, markeredgewidth=1.6, zorder=3,
label="The remainder (routed to a reviewer)")
# selective direct labels: the 50% operating point only
ax.annotate("0.964", xy=(50.01, 0.9635), xytext=(50.01, 0.9635 + 0.045),
fontsize=11, fontweight="bold", color=INDIGO, ha="center")
ax.annotate("0.528", xy=(50.01, 0.5277), xytext=(50.01, 0.5277 - 0.075),
fontsize=11, fontweight="bold", color=MADDER, ha="center")
ax.plot([50.01, 50.01], [0.5277, 0.9635], color=AXIS, linewidth=0.9,
linestyle="-", zorder=1)
ax.set_xlim(36, 104)
ax.set_ylim(0.18, 1.06)
ax.set_xticks([40, 50, 60, 70, 80, 90, 100])
ax.set_xticklabels([f"{v}%" for v in [40, 50, 60, 70, 80, 90, 100]])
ax.set_yticks([0.2, 0.4, 0.6, 0.8, 1.0])
ax.set_xlabel("Share of test lines kept, ranked by decision margin", labelpad=9)
ax.set_ylabel("Agreement with the filed HS6 code", labelpad=9)
leg = ax.legend(frameon=False, loc="lower left", fontsize=9.5,
handlelength=1.6, borderaxespad=0.6)
for t in leg.get_texts():
t.set_color(INK_2)
header(fig, "The model knows when it is unsure",
"Ranking lines by the gap between the best and second-best class score "
"concentrates the disagreements.\nAt the 50% operating point, agreement on "
"the kept half is 0.964 against 0.528 on the half sent to review.")
fig.subplots_adjust(top=0.755, bottom=0.16, left=0.082, right=0.975)
caption(fig, "Agreement with the code the exporter filed, not verified correctness. "
"7,679 held-out lines, split grouped by description.")
fig.savefig("figures/margin_ranking.png")
plt.close(fig)
# =============================================================================
# Figure 2 - how the checker routes a month of declarations
# Source: notebook Step 14, queue value_counts
# =============================================================================
TOTAL = 7679
fig = plt.figure(figsize=(9.6, 5.2), dpi=200)
gs = fig.add_gridspec(2, 1, height_ratios=[1.15, 1.5], hspace=0.75)
# -- top: the routing split, one stacked bar, both segments wide enough to label
ax = fig.add_subplot(gs[0])
GAP = TOTAL * 0.0022
x = 0
for name, value, color in [("Left alone", 5420, INDIGO),
("Routed to a reviewer", 2259, MADDER)]:
ax.add_patch(Rectangle((x, 0), value - GAP, 1, facecolor=color,
edgecolor="none", zorder=2))
mid = x + (value - GAP) / 2
ax.text(mid, 0.46, f"{value:,}", color=SURFACE, fontsize=17,
fontweight="bold", ha="center", va="center")
# descriptive label sits below the bar, where it cannot overflow the segment
ax.text(x, -0.30, f"{name} · {value / TOTAL * 100:.1f}%", color=INK_2,
fontsize=9.5, ha="left", va="center")
x += value
ax.set_xlim(0, TOTAL)
ax.set_ylim(-0.55, 1)
ax.axis("off")
ax.text(0, 1.20, "Routing decision on 7,679 held-out lines", fontsize=9.5,
color=MUTED, ha="left", va="bottom")
# -- bottom: why they disagree, as separate bars so no label can collide
ax = fig.add_subplot(gs[1])
strip(ax, grid_axis="x")
ax.spines["left"].set_visible(False)
verdicts = [
("Same code", 5726, INDIGO),
("Same heading, different sub-heading", 579, OCHRE),
("Different heading", 1374, MADDER),
]
for y, (name, value, color) in enumerate(verdicts):
ax.barh(y, value, height=0.55, color=color, edgecolor="none", zorder=2)
ax.text(value + TOTAL * 0.012, y, f"{value:,} · {value / TOTAL * 100:.1f}%",
va="center", ha="left", fontsize=10, color=INK_2)
ax.set_yticks(range(len(verdicts)))
ax.set_yticklabels([v[0] for v in verdicts], fontsize=10, color=INK_2)
ax.invert_yaxis()
ax.set_xlim(0, TOTAL)
ax.set_xticks([0, 2000, 4000, 6000])
ax.set_xticklabels(["0", "2,000", "4,000", "6,000"])
ax.text(0, -0.95, "Predicted HS6 code against the code the exporter filed",
fontsize=9.5, color=MUTED, ha="left", va="bottom")
header(fig, "A reviewer reads 2,259 lines instead of 7,679",
"A line is routed when the predicted code differs from the filed code, or "
"the unit price falls outside\nthe robust band for its HS code, or one of "
"seven deterministic rules fires.")
fig.subplots_adjust(top=0.66, bottom=0.115, left=0.30, right=0.975)
caption(fig, "The two panels do not sum to each other: routing combines three "
"independent signals, not the code check alone.")
fig.savefig("figures/triage_split.png")
plt.close(fig)
# =============================================================================
# Figure 3 — what the rule checks found
# Source: notebook Step 13, rule_summary (full dataset, 38,897 lines)
# =============================================================================
rules = [
("Description under 10 characters", 1873, False),
("Implied PKR/USD rate out of range", 903, False),
("Value does not equal qty \u00d7 unit price", 723, False),
("Woven goods in a knitted-only chapter", 602, True),
("Description says KG, unit filed as pieces", 275, False),
("Unit price zero or negative", 9, False),
("Quantity zero or negative", 0, False),
]
fig, ax = plt.subplots(figsize=(9.6, 4.6), dpi=200)
strip(ax, grid_axis="x")
ax.spines["left"].set_visible(False)
ys = range(len(rules))
for y, (name, value, highlight) in zip(ys, rules):
ax.barh(y, value, height=0.58,
color=MADDER if highlight else INDIGO,
alpha=1.0 if highlight else 0.42,
edgecolor="none", zorder=2)
ax.text(value + 32, y, f"{value:,}", va="center", ha="left",
fontsize=10.5, fontweight="bold" if highlight else "normal",
color=MADDER if highlight else INK_2)
ax.set_yticks(list(ys))
ax.set_yticklabels([r[0] for r in rules], fontsize=10, color=INK_2)
ax.invert_yaxis()
ax.set_xlim(0, 2150)
ax.set_xticks([0, 500, 1000, 1500, 2000])
ax.set_xticklabels(["0", "500", "1,000", "1,500", "2,000"])
ax.set_xlabel("Declaration lines flagged", labelpad=9)
ax.annotate("Chapter 61 covers knitted and crocheted\ngarments only. "
"Woven garments belong in Chapter 62.",
xy=(700, 3.0), xytext=(940, 3.62),
fontsize=9, color=MADDER, va="center", linespacing=1.45,
arrowprops=dict(arrowstyle="-", color=MADDER, linewidth=0.9,
shrinkA=2, shrinkB=4))
header(fig, "Seven rules, no model, 3,838 lines flagged",
"Conditions that are true by definition of the tariff schedule or by "
"arithmetic, checked directly on\nthe line. 9.87% of the file breaks at "
"least one of them.")
fig.subplots_adjust(top=0.755, bottom=0.165, left=0.315, right=0.955)
caption(fig, "All 38,897 cleaned lines, HS Chapter 61, February 2024.")
fig.savefig("figures/rule_checks.png")
plt.close(fig)
print("wrote figures/margin_ranking.png, figures/triage_split.png, figures/rule_checks.png")