فهرست مقاله
امتیازِ بزرگترِ یک نتیجهٔ جستوجو، لزوماً احتمالِ معتبرِ مرتبطبودنِ آن نیست. فرض کنیم برای چند نتیجه، هم امتیاز و هم قضاوتِ دودوییِ ارتباط داده شده است و میخواهیم یک نگاشتِ نزولینباشنده برازش کنیم. این درس با هشت مشاهدهٔ کاملاً تألیفی، روشِ ادغامِ همسایههای ناسازگار را میسازد. مسئله، سنجیدنِ حسابِ برازش است؛ هیچ موتور جستوجو، مدلِ واقعی یا قضاوتِ انسانی در برنامه اجرا نمیشود.
۱. امتیازِ ارتباط را از احتمالِ تأییدشده جدا کنیم

هر مشاهده یک زوجِ امتیاز و برچسب است. امتیازِ بزرگتر، طبق قراردادِ ازپیشاعلامشده بیشتر به نفعِ ارتباط است؛ برچسبِ یک یعنی مرتبط و صفر یعنی نامرتبط در همین دادهٔ ساختگی. امتیاز، واحد فیزیکی ندارد و عددِ بیست احتمالِ بیست یا بیست درصد نیست. معناداربودنِ قضاوت برای پرسش، مسئلهای بیرون از تابع است و با مرتبکردنِ عددها تأیید نمیشود.
در ارزیابیِ رتبهبندیِ جستوجو، ترتیبِ سندها و قضاوتِ ارتباط، ورودیِ معیار بودند. اینجا یک لایهٔ متفاوت داریم: برای امتیازهای مشاهدهشده، مقدارِ برازششده میسازیم و میخواهیم این مقدار با افزایشِ امتیاز کاهش نیابد. این تبدیل ممکن است چند امتیاز را همسطح کند؛ از آن، افزایشِ کیفیتِ بازیابی یا حفظِ تمامِ تمایزهای رتبهبندی را نتیجه نمیگیریم.
تعریفِ رسمیِ برازشِ یکنوا در SciPy، کمینهکردنِ مجموعِ مربعِ اختلاف با وزنهای دادهشده و قیدِ ترتیب را توضیح میدهد. ما فقط جهتِ افزایشی را انتخاب کردهایم؛ جهت، پس از دیدنِ جواب عوض نمیشود. سطحهای مساوی مجازند و «افزایشی» در این متن، معنایِ اکید ندارد. چون برچسبها صفر و یکاند، میانگینِ هر بلوک هم داخلِ همین بازه میماند.
۲. امتیازهای مساوی را با وزنِ تعداد نمونه یک گروه کنیم

یک تابع نمیتواند برای امتیازِ یکسان، دو جوابِ متفاوت داشته باشد. اگر دو مشاهده با امتیازِ بیست، برچسبهای صفر و یک بگیرند، مرتبسازیِ ردیفها و اعمالِ صرفِ قیدِ صعودی کافی نیست؛ ترتیبِ صفر سپس یک میتواند دو مقدارِ متفاوت بسازد. بنابراین پیش از ادغامِ همسایهها، همهٔ مشاهدههای همامتیاز را در یک گروه قرار میدهیم و سطحِ مشترک برایشان میخواهیم.
در مثالِ اصلی، امتیازهای متمایز ده، بیست، سی، چهل و پنجاهاند. تعدادِ مشاهدههای آنها بهترتیب یک، سه، یک، دو و یک است؛ سهمِ برچسبِ یک در هر گروه، صفر، دوسوم، صفر، نیم و یک میشود. این سهمها از هشت زوجِ خودِ کد حساب میشوند. تعدادِ سه به معنیِ سه برابر اطمینانِ معتبر نیست؛ فقط تعدادِ سهمهای واردشده در تابعِ هدف است.
برای هر سطحِ مشترک، جمعِ مربعِ خطاهای ردیفهای یک گروه برابر است با خطای ثابتِ پیرامونِ میانگینِ همان گروه، بهعلاوهٔ تعدادِ ردیفها ضربدر مربعِ فاصلهٔ میانگین از سطحِ انتخابی. پس وزنِ هر گروه باید تعدادش باشد. اگر به هر امتیازِ متمایز وزنِ یک بدهیم، تابعِ هدف عوض میشود؛ گروهِ سهمشاهدهای، بیتوضیح هموزنِ گروهِ تکمشاهدهای شده است.
۳. تخلفِ همسایهها را با PAVA ادغام کنیم

ابتدا هر گروه یک بلوک دارد. اگر میانگینِ بلوکِ چپ از راست بزرگتر باشد، آن دو را با جمعِ تعدادها و جمعِ برچسبهای مثبت ادغام میکنیم؛ میانگینِ تازه از همین دو جمع ساخته میشود. میانگینِ سادهٔ دو سطح، بدونِ وزنِ تعداد، درست نیست. بلوکِ تازه ممکن است با همسایهٔ پیشین هم ناسازگار شود؛ تا برطرفشدنِ تخلف، به عقب برمیگردیم.
در دادهٔ اصلی، گروهِ بیست با دوسوم و سه مشاهده، کنارِ گروهِ سی با صفر و یک مشاهده قرار دارد. ادغام، دو برچسبِ مثبت در چهار مشاهده، یعنی نیم میدهد. سطحهای نهایی روی پنج امتیاز، صفر، نیم، نیم، نیم و یکاند. بلوکِ چهل از قبل نیم بود؛ کد لازم نمیداند بلوکهای مساوی را هم ادغام کند، پس گزارشِ داخلی، افرازِ بیشینهٔ نواحیِ ثابت معرفی نمیشود.
خطای ثابتِ درونِ گروههای همامتیاز، هفتششم و بخشِ وزندارِ ناشی از قیدِ ترتیب، یکسوم است؛ جمعِ آنها سهدوم میشود. میانگینِ مربعِ خطای هشت مشاهده، سهشانزدهم است و بیبُعد باقی میماند. اگر وزنِ همهٔ گروهها را یک بگیریم، سطحِ بیست و سی یکسوم میشود؛ خطای همان هشت ردیف، بیستونههجدهم است و از سهدوم بیشتر است.
۴. کدِ کامل و مرجعِ مستقلِ افرازها را بسنجیم

ورودی باید tuple واقعیِ یک تا هشت مشاهده باشد و هر مشاهده، tuple واقعیِ دوعضوی. امتیاز فقط int دقیق در بازهٔ منفی صد تا صد و برچسب فقط int دقیقِ صفر یا یک است. بولی، رشته، float و عددِ نامتناهی تبدیل نمیشوند. این کرانها دامنهٔ تمریناند؛ نگاشتِ عددِ اعشاریِ امتیازِ یک مدل به این دامنه، بخشی از برنامه نیست.
"""Exact bounded monotone fit to synthetic score/relevance observations."""
from fractions import Fraction
from itertools import combinations_with_replacement, permutations, product
import json
import platform
def checked_records(records):
if type(records) is not tuple or not 1 <= len(records) <= 8:
raise ValueError('an actual tuple of one to eight observations is required')
for row in records:
if type(row) is not tuple or len(row) != 2:
raise ValueError('each observation must be an actual (score, label) tuple')
score, label = row
if type(score) is not int or not -100 <= score <= 100:
raise ValueError('an exact integer score in [-100,100] is required')
if type(label) is not int or label not in (0, 1):
raise ValueError('an exact integer relevance label zero or one is required')
def fit_isotonic(records):
checked_records(records)
groups = {}
for score, label in records:
count, positives = groups.get(score, (0, 0))
groups[score] = count + 1, positives + label
scores = tuple(sorted(groups))
counts = tuple(groups[s][0] for s in scores)
positives = tuple(groups[s][1] for s in scores)
empirical = tuple(Fraction(p, n) for p, n in zip(positives, counts, strict=True))
stack = []
merge_steps = 0
for i, (count, positive) in enumerate(zip(counts, positives, strict=True)):
stack.append((i, i + 1, count, positive))
while len(stack) >= 2:
left, right = stack[-2:]
if Fraction(left[3], left[2]) <= Fraction(right[3], right[2]):
break
stack[-2:] = [(left[0], right[1], left[2] + right[2], left[3] + right[3])]
merge_steps += 1
fitted = [Fraction(0)] * len(scores)
blocks = []
for lo, hi, count, positive in stack:
level = Fraction(positive, count)
fitted[lo:hi] = [level] * (hi - lo)
blocks.append({'score_index_start': lo, 'score_index_stop': hi,
'observation_count': count, 'positive_count': positive, 'level': level})
fitted = tuple(fitted)
table = dict(zip(scores, fitted, strict=True))
row_fitted = tuple(table[score] for score, _ in records)
loss = sum(((label - estimate)**2 for (_, label), estimate in zip(records, row_fitted, strict=True)), Fraction(0))
within = sum(((label - empirical[scores.index(score)])**2 for score, label in records), Fraction(0))
group_loss = sum((n * (mean - level)**2 for n, mean, level in zip(counts, empirical, fitted, strict=True)), Fraction(0))
return {'scores': scores, 'counts': counts, 'empirical_fractions': empirical,
'fitted_at_scores': fitted, 'row_fitted_in_input_order': row_fitted,
'blocks': tuple(blocks), 'merge_steps': merge_steps,
'squared_loss': loss, 'mean_squared_loss': loss / len(records),
'within_equal_score_loss': within, 'weighted_group_loss': group_loss}
def lookup_known(fit, query_scores):
# Internal fit argument must be the unmodified result of fit_isotonic.
# This is not an arbitrary model-deserialization or public model validator.
if type(query_scores) is not tuple or not 1 <= len(query_scores) <= 8:
raise ValueError('one to eight explicit query scores are required')
if any(type(s) is not int or not -100 <= s <= 100 for s in query_scores):
raise ValueError('query scores must be exact bounded integers')
table = dict(zip(fit['scores'], fit['fitted_at_scores'], strict=True))
if any(s not in table for s in query_scores):
raise ValueError('every queried score must have occurred in the fitting records')
return tuple(table[s] for s in query_scores)
def partition_reference(records):
"""Independent active-partition enumeration over raw observations, no PAVA."""
scores = tuple(sorted({s for s, _ in records}))
best_loss = None
best_values = set()
feasible = 0
for cuts in product((False, True), repeat=len(scores) - 1):
starts = (0,) + tuple(i + 1 for i, cut in enumerate(cuts) if cut)
stops = starts[1:] + (len(scores),)
levels = []
values = []
for start, stop in zip(starts, stops, strict=True):
observations = tuple(y for s, y in records if scores[start] <= s <= scores[stop - 1])
level = Fraction(sum(observations), len(observations))
levels.append(level)
values.extend([level] * (stop - start))
if any(a > b for a, b in zip(levels, levels[1:])):
continue
feasible += 1
values = tuple(values)
table = dict(zip(scores, values, strict=True))
loss = sum(((y - table[s])**2 for s, y in records), Fraction(0))
if best_loss is None or loss < best_loss:
best_loss, best_values = loss, {values}
elif loss == best_loss:
best_values.add(values)
assert feasible > 0 and len(best_values) == 1
return scores, next(iter(best_values)), best_loss
def rejects(function, *args):
try:
function(*args)
except ValueError:
return True
return False
def serializable(value):
if isinstance(value, Fraction):
return str(value)
if isinstance(value, dict):
return {key: serializable(item) for key, item in value.items()}
if isinstance(value, (tuple, list)):
return [serializable(item) for item in value]
return value
def run_example():
records = ((10, 0), (20, 1), (20, 1), (20, 0), (30, 0), (40, 1), (40, 0), (50, 1))
fit = fit_isotonic(records)
checks = {}
def check(label, condition):
checks[label] = bool(condition)
assert checks[label], label
check('hand_weighted_five_score_fit_and_raw_loss',
fit['scores'] == (10, 20, 30, 40, 50)
and fit['counts'] == (1, 3, 1, 2, 1)
and fit['fitted_at_scores'] == (0, Fraction(1, 2), Fraction(1, 2), Fraction(1, 2), 1)
and fit['squared_loss'] == Fraction(3, 2)
and fit['mean_squared_loss'] == Fraction(3, 16))
oracle_cases = 0
for size in range(1, 7):
for scores in combinations_with_replacement((0, 1, 2), size):
for labels in product((0, 1), repeat=size):
rows = tuple(zip(scores, labels, strict=True))
actual = fit_isotonic(rows)
expected = partition_reference(rows)
assert (actual['scores'], actual['fitted_at_scores'], actual['squared_loss']) == expected
assert actual['squared_loss'] == actual['within_equal_score_loss'] + actual['weighted_group_loss']
oracle_cases += 1
check('independent_exhaustive_contiguous_partition_oracle_ONE_family', oracle_cases > 0)
tied = fit_isotonic(((20, 0), (20, 1)))
check('equal_scores_force_equal_fitted_values_not_input_order',
tied['row_fitted_in_input_order'] == (Fraction(1, 2), Fraction(1, 2))
and tied['squared_loss'] == Fraction(1, 2))
wrong_levels = (Fraction(0), Fraction(1, 3), Fraction(1, 3), Fraction(1, 2), Fraction(1))
wrong_table = dict(zip(fit['scores'], wrong_levels, strict=True))
wrong_loss = sum(((y - wrong_table[s])**2 for s, y in records), Fraction(0))
check('unit_weight_per_distinct_score_solves_a_different_problem',
wrong_loss == Fraction(29, 18) and wrong_loss > fit['squared_loss'])
cascade = fit_isotonic(((1, 1), (2, 1), (3, 0)))
check('new_pool_can_violate_previous_pool_and_require_cascade',
cascade['merge_steps'] == 2 and cascade['fitted_at_scores'] == (Fraction(2, 3),)*3)
monotone = fit_isotonic(((1, 0), (2, 0), (3, 1), (4, 1)))
check('already_monotone_labels_need_no_merges_and_zero_loss',
monotone['merge_steps'] == 0 and monotone['squared_loss'] == 0)
check('within_score_constant_plus_weighted_group_loss_is_raw_objective',
fit['within_equal_score_loss'] == Fraction(7, 6)
and fit['weighted_group_loss'] == Fraction(1, 3)
and fit['squared_loss'] == Fraction(7, 6) + Fraction(1, 3))
small = ((2, 1), (1, 0), (2, 0), (3, 1), (1, 1))
baseline = fit_isotonic(small)
permutations_checked = 0
for rows in permutations(small):
got = fit_isotonic(rows)
assert (got['scores'], got['fitted_at_scores'], got['squared_loss']) == (
baseline['scores'], baseline['fitted_at_scores'], baseline['squared_loss'])
assert got['row_fitted_in_input_order'] == lookup_known(got, tuple(s for s, _ in rows))
permutations_checked += 1
check('whole_observation_permutations_preserve_fit_not_returned_row_order', permutations_checked == 120)
reverse = fit_isotonic(tuple((-s, 1-y) for s, y in records))
check('joint_score_reversal_and_label_complement_preserve_squared_loss',
reverse['fitted_at_scores'] == tuple(1-v for v in reversed(fit['fitted_at_scores']))
and reverse['squared_loss'] == fit['squared_loss'])
shifted = fit_isotonic(tuple((s+5, y) for s, y in records))
check('chosen_increasing_score_relabeling_preserves_fitted_levels',
shifted['scores'] == tuple(s+5 for s in fit['scores'])
and shifted['fitted_at_scores'] == fit['fitted_at_scores'])
short = ((1, 1), (2, 0), (3, 0), (4, 1))
once, twice = fit_isotonic(short), fit_isotonic(short*2)
check('replicating_whole_fixture_preserves_fit_and_doubles_raw_loss',
once['fitted_at_scores'] == twice['fitted_at_scores']
and twice['squared_loss'] == 2*once['squared_loss'])
trained = fit_isotonic(((0, 0), (1, 1)))
holdout_prediction = lookup_known(trained, (0, 1))
holdout_loss = sum(((y-p)**2 for y, p in zip((1, 0), holdout_prediction, strict=True)), Fraction(0))/2
check('training_zero_loss_does_not_imply_good_separate_fixture_loss',
trained['squared_loss'] == 0 and holdout_loss == 1)
check('known_score_lookup_refuses_inside_and_outside_unseen_values',
lookup_known(fit, (50, 10, 20)) == (1, 0, Fraction(1, 2))
and all(rejects(lookup_known, fit, query) for query in ((25,), (0,), (60,), (), [20], (True,), (20.0,))))
check('single_class_singleton_and_inclusive_score_resource_endpoints',
fit_isotonic(((-100, 0),))['fitted_at_scores'] == (0,)
and fit_isotonic(((100, 1),)*8)['fitted_at_scores'] == (1,)
and fit_isotonic(((1, 0),)*8)['squared_loss'] == 0)
invalid = [(), [], iter(records), records+(records[0],), ((1,),), ([1, 0],), ((1, 0, 1),)]
for bad in (True, 1.0, '1', float('nan'), float('inf'), -101, 101, 10**100):
invalid.append(((bad, 0),))
for bad in (True, 0.0, '0', None, -1, 2):
invalid.append(((0, bad),))
check('full_exact_container_label_score_bounds_reject_before_fitting',
all(rejects(fit_isotonic, value) for value in invalid))
check('caller_records_unchanged_and_no_hidden_direction_selection',
records == ((10, 0), (20, 1), (20, 1), (20, 0), (30, 0), (40, 1), (40, 0), (50, 1))
and fit['merge_steps'] == 1)
assert all(type(v) is bool and v for v in checks.values())
return serializable({'python': platform.python_version(), 'dependencies': 'standard library only',
'scipy_executed': False, 'scikit_learn_executed': False,
'synthetic_records_score_label': records, 'units': 'ordinal scores, binary labels, fitted fractions and squared loss are dimensionless',
'example': fit, 'wrong_unit_group_weight_levels': wrong_levels, 'wrong_raw_squared_loss': wrong_loss,
'separate_reversed_label_fixture_mean_squared_loss': holdout_loss,
'oracle_cases_inside_one_family': oracle_cases, 'whole_pair_permutations_inside_one_family': permutations_checked,
'checks': checks, 'unique_check_groups': len(checks), 'all_passed': True,
'limits': ['Supplied binary relevance judgments are synthetic, not independently verified retrieval labels',
'Exact training-fixture constrained fit is not population calibration, retrieval improvement or actual model training',
'Only known-score lookup: no interpolation, extrapolation or full third-party API parity',
'Adjacent equal fitted blocks may stay separate internally; fitted values, not a maximal partition, are the target',
'lookup_known accepts only an unmodified fit_isotonic report as its internal model argument',
'Replicating observations in algebra does not create independent experimental evidence']})
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
کد با کتابخانهٔ استاندارد و حسابِ Fraction نوشته شده است؛ کسرها در خروجی، رشتههای دقیقاند. SciPy و scikit-learn فقط مرجعاند و در این مثال اجرا نمیشوند. تابعِ جستوجوی مقدار، فقط گزارشِ دستنخوردهٔ تابعِ برازشِ خودمان را بهعنوان مدلِ داخلی میپذیرد؛ اعتبارسنجِ مدلِ دلخواه یا فایلِ ذخیرهشده نیست. امتیازِ پرسوجو نیز باید از امتیازهای واقعاً دیدهشده باشد.
مرجعِ مستقل، PAVA یا پشتهٔ آن را صدا نمیزند. تمامِ افرازهای پیاپیِ امتیازهای متمایز را میشمارد، برای هر بخش از خودِ ردیفهای خام میانگین میگیرد و فقط سطحهای سازگار با ترتیب را نگه میدارد. در جوابِ بهینه، هر ناحیهٔ ثابتِ بیشینه میانگینِ مشاهدههای همان ناحیه است؛ بنابراین یکی از این افرازها جواب را دربر دارد. وزنِ مثبت، بردارِ مقدارهای برازششده را یکتا میکند، حتی اگر مرزبندیِ بلوکهای مساوی یکتا نباشد.
شانزده گروهِ نامدار، جوابِ دستی، خطای وزنِ یک، ادغامِ زنجیرهای، تساویِ امتیاز، جابهجاییِ زوجها و مرزهای ورودی را بررسی میکنند. تمامِ پروفایلهای مرتب از سه امتیاز با طولِ یک تا شش و همهٔ برچسبهای دودوییشان، یک گروهِ مرجعاند؛ تعدادِ حالتها در خروجی آمده است. برابریِ ترتیبِ دادهٔ خام با ترتیبِ جوابِ ردیفی اشتباه گرفته نمیشود و ورودیها تغییر نمیکنند.
{
"python": "3.14.5",
"dependencies": "standard library only",
"scipy_executed": false,
"scikit_learn_executed": false,
"synthetic_records_score_label": [
[
10,
0
],
[
20,
1
],
[
20,
1
],
[
20,
0
],
[
30,
0
],
[
40,
1
],
[
40,
0
],
[
50,
1
]
],
"units": "ordinal scores, binary labels, fitted fractions and squared loss are dimensionless",
"example": {
"scores": [
10,
20,
30,
40,
50
],
"counts": [
1,
3,
1,
2,
1
],
"empirical_fractions": [
"0",
"2/3",
"0",
"1/2",
"1"
],
"fitted_at_scores": [
"0",
"1/2",
"1/2",
"1/2",
"1"
],
"row_fitted_in_input_order": [
"0",
"1/2",
"1/2",
"1/2",
"1/2",
"1/2",
"1/2",
"1"
],
"blocks": [
{
"score_index_start": 0,
"score_index_stop": 1,
"observation_count": 1,
"positive_count": 0,
"level": "0"
},
{
"score_index_start": 1,
"score_index_stop": 3,
"observation_count": 4,
"positive_count": 2,
"level": "1/2"
},
{
"score_index_start": 3,
"score_index_stop": 4,
"observation_count": 2,
"positive_count": 1,
"level": "1/2"
},
{
"score_index_start": 4,
"score_index_stop": 5,
"observation_count": 1,
"positive_count": 1,
"level": "1"
}
],
"merge_steps": 1,
"squared_loss": "3/2",
"mean_squared_loss": "3/16",
"within_equal_score_loss": "7/6",
"weighted_group_loss": "1/3"
},
"wrong_unit_group_weight_levels": [
"0",
"1/3",
"1/3",
"1/2",
"1"
],
"wrong_raw_squared_loss": "29/18",
"separate_reversed_label_fixture_mean_squared_loss": "1",
"oracle_cases_inside_one_family": 2814,
"whole_pair_permutations_inside_one_family": 120,
"checks": {
"hand_weighted_five_score_fit_and_raw_loss": true,
"independent_exhaustive_contiguous_partition_oracle_ONE_family": true,
"equal_scores_force_equal_fitted_values_not_input_order": true,
"unit_weight_per_distinct_score_solves_a_different_problem": true,
"new_pool_can_violate_previous_pool_and_require_cascade": true,
"already_monotone_labels_need_no_merges_and_zero_loss": true,
"within_score_constant_plus_weighted_group_loss_is_raw_objective": true,
"whole_observation_permutations_preserve_fit_not_returned_row_order": true,
"joint_score_reversal_and_label_complement_preserve_squared_loss": true,
"chosen_increasing_score_relabeling_preserves_fitted_levels": true,
"replicating_whole_fixture_preserves_fit_and_doubles_raw_loss": true,
"training_zero_loss_does_not_imply_good_separate_fixture_loss": true,
"known_score_lookup_refuses_inside_and_outside_unseen_values": true,
"single_class_singleton_and_inclusive_score_resource_endpoints": true,
"full_exact_container_label_score_bounds_reject_before_fitting": true,
"caller_records_unchanged_and_no_hidden_direction_selection": true
},
"unique_check_groups": 16,
"all_passed": true,
"limits": [
"Supplied binary relevance judgments are synthetic, not independently verified retrieval labels",
"Exact training-fixture constrained fit is not population calibration, retrieval improvement or actual model training",
"Only known-score lookup: no interpolation, extrapolation or full third-party API parity",
"Adjacent equal fitted blocks may stay separate internally; fitted values, not a maximal partition, are the target",
"lookup_known accepts only an unmodified fit_isotonic report as its internal model argument",
"Replicating observations in algebra does not create independent experimental evidence"
]
}
۵. دادهٔ دیدهنشده و چهار پرسشِ ارزیابی

API رسمیِ IsotonicRegression برای پیشبینی، درونیابی و سیاستِ خارج از دامنه دارد. تابعِ آموزشیِ ما محدودتر است: حتی امتیازِ بیستوپنج، با وجودِ قرارداشتن بینِ دو امتیازِ دیدهشده، رد میشود. چنین محدودیتی سیاستِ این مثال است، نه رفتارِ همگانیِ برازشِ یکنوا؛ اجرای کتابخانه یا برابریِ کامل با API ادعا نشده است.
در درسِ زیان Brier و فراوانیِ بازهها، احتمالها از پیش داده شده بودند؛ اینجا خودِ یک نگاشت روی برچسبهای برازش ساخته میشود. کمشدنِ خطای همین ردیفها، ارزیابیِ مستقل نیست. شاهدِ جداگانهٔ کد روی دو امتیاز، خطای برازشِ صفر و سپس میانگینِ خطای یک با برچسبهای وارونه دارد؛ هیچ تضمینِ جمعیتی از کمینهٔ آموزشی به دست نمیآید.
پرسش اول: چرا دو سطحِ دوسوم و صفر به یکسوم تبدیل نشدند؟ پاسخ: سه مشاهده در گروهِ نخست و یک مشاهده در دومی داریم. جمعِ مثبتها دو و تعداد چهار است، پس سطحِ مشترک نیم میشود. یکسوم، میانگینِ بدونِ وزنِ دو گروه است و مسئلهٔ متفاوتی را حل میکند.
پرسش دوم: چرا دو ردیفِ همامتیاز را با ترتیبِ دلخواه جدا نمیگذاریم؟ پاسخ: نگاشت باید برای یک امتیاز یک جواب داشته باشد. قیدِ ترتیبِ ردیفها بهتنهایی این برابری را تحمیل نمیکند؛ زوجِ صفر و یک میتواند بدونِ ادغام باقی بماند. گروهبندیِ تساوی، شرطِ تابعبودنِ نگاشت را پیش از PAVA اعمال میکند.
پرسش سوم: دوبرابرکردنِ تمامِ ردیفها، برازش را معتبرتر میکند؟ پاسخ: از نظرِ حسابی، وزنِ تمامِ بخشها دوبرابر میشود، سطحها ثابت و مجموعِ خطا دوبرابر میماند. تکرارِ همان داده، مشاهدهٔ مستقلِ تازه تولید نمیکند؛ در نتیجه شاهدِ جدیدی برای اعتبارِ قضاوت یا تعمیمِ نگاشت نیست.
پرسش چهارم: آیا سطحِ نیم، احتمالِ کالیبرهٔ ارتباط است؟ پاسخ: تنها مقدارِ بهینهٔ محدود برای این برچسبهاست. دادهٔ مستقل، منشأ قضاوت، تغییرِ توزیع و کاربردِ تصمیم بررسی نشدهاند. منابع در ۲۸ مهر ۱۴۰۵ با مستندات SciPy1.18.0 و scikit-learn1.9.1 خوانده شدند؛ تاریخِ انتشارِ اولیهٔ صفحات مشخص نیست و این درس خبر یا گزارشِ محصول نیست.
منابع و مطالعهٔ بیشتر
- SciPy1.18.0؛ تابعِ هدفِ وزندار، بلوکها و روشِ PAVA
- scikit-learn1.9.1؛ برازشِ یکنوا، وزن نمونه و مرزِ پیشبینی
بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

