Skip to content

Commit 84a7687

Browse files
committed
fix(tools): the ruler was normalised by the quantity that falls with what it measures
measure-prose-shape.py watched CV — sd over mean — and said so proudly: scale-free, so a text can lose 30% of its words and hold its CV. That freedom is the blindness. Chopping a sentence in half divides sd and mean in the same proportion, and their ratio does not move. Chopping is what the check does. Dose-response, run on a graded fixture built for it: five real texts, four rungs, long sentences cut at the nearest comma. Absolute spread fell on 5 texts of 5 — the dose is real, measured outside the instrument. CV was monotone on ZERO of the five and rose on two. The arbiter reproduced it through the instrument's own splitter and reached the same nulls. director sd/mean 8.9/10.1=0.877 → 4.4/7.3 =0.594 audit sd/mean 4.8/12.0=0.402 → 4.7/10.1=0.463 ← rose landing sd/mean 8.3/16.8=0.495 → 4.5/8.7 =0.520 ← rose This is not a column heading. Two «not reproduced» verdicts were reported to the owner from median CV while three blind judges voted 24 to 4 for the unchecked text. The metric could not have shown otherwise. The median was a second, separate muffler — the arbiter's correction, and it is recorded rather than folded into this one. The headline is now p90 of sentence length: where a text's long breath ends. Strictly monotone on 5 of 5 against the dose. Two secondaries join it — the mean adjacent step, which is the rhythm of alternation that chopping halves by construction, and absolute sd; each strictly monotone on 4 of 5. CV stays in the output, marked in the table and explained in the docstring, and is barred from verdicts. Acceptance was fixed before the numbers were seen: headline strictly monotone on ≥4 of 5, each secondary on ≥3 of 5. Result 5, 4, 4. Red-test: n/a (instrument gains two statistics and demotes a third; every previous number remains printed, selftest 108/108, gates exit 0 before and after)
1 parent 2bcbe74 commit 84a7687

1 file changed

Lines changed: 40 additions & 15 deletions

File tree

tools/measure-prose-shape.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,20 @@
3838
clause and no aside. Falls when a text is chopped, and cannot be gamed by
3939
a rule that only shortens words.
4040
41-
Reported per file: n, mean, standard deviation, coefficient of variation, and the quartiles.
42-
CV — sd/mean — is what to watch across a before/after pair, because it is scale-free: a text
43-
can lose 30% of its words and hold its CV, and that is the outcome that falsifies the
44-
flattening hypothesis.
41+
Reported per file: n, p90, mean adjacent step, standard deviation, CV and the quartiles.
42+
43+
THE HEADLINE IS p90, and CV is NOT a verdict metric. That reversal is measured, not stylistic.
44+
CV — sd/mean — was chosen here because it is scale-free, and scale-freedom is exactly the
45+
blindness: chopping a sentence in half divides sd and mean in the same proportion, so their
46+
ratio does not move. Dose-response run of 09.08.2026 over tools' own splitter on a graded
47+
fixture (5 real texts, 4 rungs, long sentences chopped at the nearest comma): absolute spread
48+
fell on 5 texts of 5, and CV was monotone on ZERO of them — rising on two. p90 was strictly
49+
monotone on 5 of 5. The fixture and the numbers live in
50+
~/Projects/_scratch/ru-text-flattening/dose/.
51+
52+
This matters beyond a column heading: two «not reproduced» verdicts were reported from median
53+
CV while three blind judges voted 24 to 4 for the unchecked text. The metric could not have
54+
shown otherwise. Read the delta inside a pair, never the absolute between texts.
4555
"""
4656

4757
import io
@@ -131,6 +141,8 @@ class Stats:
131141
q1: int
132142
med: int
133143
q3: int
144+
p90: int = 0
145+
step: float = 0.0
134146

135147

136148
@dataclass
@@ -147,7 +159,11 @@ class Shape:
147159

148160
def stats(values: List[int]) -> Stats:
149161
if not values:
150-
return Stats(0, 0.0, 0.0, 0.0, 0, 0, 0)
162+
return Stats(0, 0.0, 0.0, 0.0, 0, 0, 0, 0, 0.0)
163+
# The step is computed on the ORIGINAL order: it is the rhythm of alternation, and
164+
# sorting would destroy exactly what it measures.
165+
step = (sum(abs(values[i] - values[i + 1]) for i in range(len(values) - 1))
166+
/ (len(values) - 1)) if len(values) > 1 else 0.0
151167
v = sorted(values)
152168
n = len(v)
153169
mean = sum(v) / n
@@ -157,7 +173,8 @@ def stats(values: List[int]) -> Stats:
157173
def q(f: float) -> int:
158174
return v[min(n - 1, int(f * n))]
159175

160-
return Stats(n, mean, sd, (sd / mean if mean else 0.0), q(0.25), q(0.5), q(0.75))
176+
return Stats(n, mean, sd, (sd / mean if mean else 0.0), q(0.25), q(0.5), q(0.75),
177+
q(0.90), step)
161178

162179

163180
class Unreadable(Exception):
@@ -193,10 +210,12 @@ def report(m: Shape) -> None:
193210
print(' слов %d, предложений %d' % (m.words, m.length.n))
194211
rows = [('длина предложения', m.length), ('подчинение на предложение', m.subord),
195212
('запятых на предложение', m.commas), ('предложений в абзаце', m.para)]
196-
print(' %-28s %7s %7s %7s %s' % ('', 'среднее', 'ст.откл', 'CV', 'кварт. 25/50/75'))
213+
print(' %-28s %6s %7s %7s %7s %s'
214+
% ('', 'p90', 'перепад', 'ст.откл', 'CV*', 'кварт. 25/50/75'))
197215
for name, s in rows:
198-
print(' %-28s %7.2f %7.2f %7.3f %d / %d / %d'
199-
% (name, s.mean, s.sd, s.cv, s.q1, s.med, s.q3))
216+
print(' %-28s %6d %7.2f %7.2f %7.3f %d / %d / %d'
217+
% (name, s.p90, s.step, s.sd, s.cv, s.q1, s.med, s.q3))
218+
print(' * CV слеп к рубке — не использовать для вердиктов, см. докстринг.')
200219

201220

202221
def compare(before: str, after: str) -> None:
@@ -208,18 +227,24 @@ def pct(x: float, y: float) -> float:
208227
return ((y - x) / x * 100) if x else float('nan')
209228

210229
print('\nДЕЛЬТА (после − до), в процентах от «до»:')
211-
print(' %-28s %10s %10s %10s' % ('', 'среднее', 'ст.откл', 'CV'))
230+
print(' %-28s %10s %10s %10s %10s' % ('', 'p90', 'перепад', 'ст.откл', 'CV*'))
212231
for name, key in [('длина предложения', 'length'), ('подчинение', 'subord'),
213232
('запятые', 'commas'), ('абзац', 'para')]:
214233
sa: Stats = getattr(a, key)
215234
sb: Stats = getattr(b, key)
216-
print(' %-28s %+9.1f%% %+9.1f%% %+9.1f%%'
217-
% (name, pct(sa.mean, sb.mean), pct(sa.sd, sb.sd), pct(sa.cv, sb.cv)))
235+
print(' %-28s %+9.1f%% %+9.1f%% %+9.1f%% %+9.1f%%'
236+
% (name, pct(sa.p90, sb.p90), pct(sa.step, sb.step),
237+
pct(sa.sd, sb.sd), pct(sa.cv, sb.cv)))
218238
print(' %-28s %+9.1f%%' % ('слов всего', pct(a.words, b.words)))
219239
print("""
220-
Как читать. Гипотеза уплощения (docs/roadmap-v2.1-conservation.md) предсказывает, что
221-
CV длины предложения СУЖАЕТСЯ, а подчинение падает. Если упало только среднее, а CV устоял,
222-
текст стал короче, но не площе — и гипотеза не подтверждается.""")
240+
Как читать. Головная метрика — p90 длины предложения: где кончается длинное дыхание текста.
241+
Вторичные — средний перепад соседних длин (ритм чередования) и абсолютное ст.откл. Гипотеза
242+
уплощения предсказывает, что все три падают. Смотреть дельту внутри пары, никогда абсолют
243+
между текстами.
244+
245+
CV в вердиктах НЕ участвует. Замер на dose/ 09.08.2026: при механической рубке предложений
246+
разброс упал на 5 текстах из 5, а CV не сдвинулся ни на одном и на двух вырос. Причина
247+
арифметическая: рубка делит и ст.откл, и среднее примерно поровну, а CV — их частное.""")
223248

224249

225250
if __name__ == '__main__':

0 commit comments

Comments
 (0)