در پردازش جریانی، نگه نداشتن همهٔ نمونه‌ها فقط بخشی از مسئله است؛ فرمول خلاصه‌سازی هم باید از نظر عددی بررسی شود. درس دسته‌بندی داده با مولد پایتون از میانگین و مجموع انحراف مرکزی استفاده می‌کرد. اینجا همان ایده را از زاویهٔ دیگری باز می‌کنیم: چرا تفریق دو گشتاور بزرگ می‌تواند پراکندگی را محو کند، چگونه مرجع مستقل بسازیم و کدام محدودیت‌ها حتی با روش بهتر باقی می‌مانند. داده‌ها کاملاً تألیفی‌اند و هیچ ولتاژ واقعی، نویز حسگر یا دقت دستگاه اندازه‌گیری نمی‌شود.

۱. دادهٔ ساختگی و قرارداد واریانس

عبور یک‌باره از مولد نمونه‌های ساختگی ولتاژ
تصویر مفهومی: عبور یک‌باره از مولد نمونه‌های ساختگی ولتاژ؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پنج مقدار صحیح را برحسب میلی‌ولت انتخاب می‌کنیم: یک میلیارد، به اضافهٔ انحراف‌های منفی دو، منفی یک، صفر، یک و دو. این آفست بسیار بزرگ عمداً برای فشار عددی انتخاب شده است؛ محدودهٔ کاری یک حسگر یا نمونهٔ معمول آزمایشگاه نیست. همهٔ پنج عدد صحیح این مجموعه در نمایش شناور اجرای حاضر قابل نمایش دقیق‌اند، ولی حاصل بعضی ضرب‌ها الزاماً دقیق باقی نمی‌ماند.

میانگین مرجع یک میلیارد میلی‌ولت و مجموع مربع انحراف از آن، ۱۰ میلی‌ولت مربع است. نام این مجموع را M2 می‌گذاریم. با تقسیم بر تعداد پنج، واریانس توصیفی همین مجموعه ۲ میلی‌ولت مربع می‌شود. تقسیم بر چهار، واریانس نمونهٔ ۲٫۵ را می‌دهد. مستندات variance و pvariance پایتون تفاوت مخرج‌ها و حداقل تعداد را مشخص می‌کند.

«نمونه» در نام خروجی، به‌تنهایی روش نمونه‌برداری تصادفی ایجاد نمی‌کند. تفسیر آماریِ برآورد بدون اریبی به فرض‌هایی دربارهٔ داده نیاز دارد؛ این پنج عدد ساختگی شاهدی برای واریانس جمعیت یک دستگاه نیستند. واحد واریانس نیز مربع واحد ورودی است، نه خود میلی‌ولت. انحراف معیار کمیت دیگری است که با ریشهٔ دوم به واحد اولیه برمی‌گردد و در این برنامه گزارش نمی‌شود.

۲. به‌روزرسانی مرکزی در یک گذر

به‌روزرسانی میانگین و مجموع انحراف‌های مرکزی
تصویر مفهومی: به‌روزرسانی میانگین و مجموع انحراف‌های مرکزی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مصرف‌کننده سه مقدار نگه می‌دارد: شمار، میانگین و M2. با ورود x، ابتدا اختلاف آن از میانگین قبلی را delta می‌گیریم، شمار را یک واحد افزایش می‌دهیم و میانگین تازه را از رابطهٔ mean_new = mean_old + delta/count می‌سازیم. سپس M2_new = M2_old + delta*(x-mean_new) را حساب می‌کنیم. ترتیب میانگین قدیم و جدید در این دو اختلاف مهم است؛ جایگزینی هر دو با یک مقدار، فرمول دیگری می‌سازد.

این روابط مرکزی در گزارش اصلی Pébay، صفحهٔ ۷، معادلات ۱٫۱ و ۱٫۲ آمده‌اند و در بحث واریانس به روش Welford ارجاع داده شده است. مقالهٔ حاضر فقط حالت تک‌متغیرهٔ افزایشی را پیاده می‌کند؛ فرمول ادغام موازی، وزن‌دهی و گشتاورهای مرتبهٔ بالاتر گزارش را اجرا نمی‌کند.

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

۳. چرا فرمول ظاهراً ساده پاسخ صفر می‌دهد؟

حذف فاجعه‌آمیز در تفریق دو مقدار بزرگ نزدیک
تصویر مفهومی: حذف فاجعه‌آمیز در تفریق دو مقدار بزرگ نزدیک؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در حساب دقیق، «میانگین مربع مقدارها منهای مربع میانگین» همان واریانس با مخرج تعداد است. اما دو طرف تفریق در این مثال حدود ده به توان هجده‌اند و پاسخ مورد نیاز فقط دو است. در اجرای شناور، حاصل تفریق صفر می‌شود. حتی استفاده از math.fsum برای جمع محصول‌ها این مورد را اصلاح نمی‌کند، چون محصول‌ها پیش از جمع گرد شده‌اند و تفریق نهایی نیز اطلاعات محدودی دارد.

PEP 450 در بخش انگیزهٔ ماژول statistics خطر فرمول محاسباتی واریانس را با مثال‌های حذف عددی توضیح می‌دهد. این یک متن تاریخی است، نه توصیف همهٔ جزئیات پیاده‌سازی امروز statistics. داده و آزمایش این مقاله مستقل‌اند؛ از خطای نسخه‌های قدیمی یک کتابخانه، نتیجه‌ای دربارهٔ نسخهٔ فعلی نمی‌سازیم.

برای مرجع مستقل، از اعداد صحیح اولیه Fraction می‌سازیم، میانگین دقیق را یک بار محاسبه و در گذر دوم مربع فاصله‌ها را جمع می‌کنیم. مرجع Fraction فرق کسر دقیق با ساخت کسر از یک float گرد شده را روشن می‌کند. مرجع ما بازنویسی همان رابطهٔ افزایشی نیست. البته برای این نمونهٔ کوچک دو گذر و نگهداری داده ممکن است؛ آن را به عنوان راهکار خودکار هر جریان نامحدود پیشنهاد نمی‌کنیم.

۴. کد کامل، مرجع دستی و خروجی اجرا

مرجع گویای دقیق در کنار نتیجهٔ شناور
تصویر مفهومی: مرجع گویای دقیق در کنار نتیجهٔ شناور؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل را با نام streaming_variance.py ذخیره کنید. اجرا با Python 3.14.5 و فقط کتابخانهٔ استاندارد انجام شده است. ورودی هر نمونه باید دقیقاً int یا float و پس از تبدیل متناهی باشد؛ بولی، رشته، مختلط، NaN و بی‌نهایت پذیرفته نمی‌شوند. اگر میانگین یا M2 تازه نامتناهی یا M2 منفی شود، برنامه خطا می‌دهد و آن را با صفر جایگزین نمی‌کند.

۲۰ بررسی نام‌دار شامل پاسخ دستی، مرجع گویای مستقل، statistics، هر پنج پیشوند، دادهٔ ثابت، تک‌عضوی، تغییر مقیاس و ترتیب معکوس همین مجموعه‌اند. تلورانس مطلق ۱۰ به توان منفی دوازده در واحد هر خروجی برای همین آزمون انتخاب شده است؛ تضمین دقت تمام ورودی‌ها نیست. تکرار آزمایش نیز تعداد بررسی‌های مستقل را افزایش نمی‌دهد.

"""Single-pass central moments for authored millivolt fixtures, not measured sensors."""
from fractions import Fraction
import json
import math
import platform
import statistics


def finite_float(value):
    if type(value) not in (int, float):
        raise TypeError('Exact int/float required; bool, strings and complex excluded')
    try:
        value = float(value)
    except OverflowError as error:
        raise ValueError('Finite float representation required') from error
    if not math.isfinite(value):
        raise ValueError('Finite input required')
    return value


def summarize_millivolts(readings):
    count, mean_mv, m2_mv2 = 0, 0.0, 0.0
    for reading in readings:
        value_mv = finite_float(reading)
        count += 1
        delta_mv = value_mv - mean_mv
        next_mean_mv = mean_mv + delta_mv / count
        next_m2_mv2 = m2_mv2 + delta_mv * (value_mv - next_mean_mv)
        if not (math.isfinite(next_mean_mv) and math.isfinite(next_m2_mv2)) or next_m2_mv2 < 0:
            raise ArithmeticError('Finite nonnegative central moment required')
        mean_mv, m2_mv2 = next_mean_mv, next_m2_mv2
    if count == 0:
        raise ValueError('Nonempty finite iterable required')
    return {'count': count, 'mean_mv': mean_mv, 'm2_mv2': m2_mv2,
            'population_variance_mv2': m2_mv2 / count,
            'sample_variance_mv2': m2_mv2 / (count - 1) if count > 1 else None}


def rational_reference(integer_readings):
    # Independent two-pass exact arithmetic on the small integer fixture only.
    numbers = [Fraction(value) for value in integer_readings]
    mean = sum(numbers, Fraction(0)) / len(numbers)
    m2 = sum(((value - mean)**2 for value in numbers), Fraction(0))
    return {'count': len(numbers), 'mean': mean, 'm2': m2,
            'population': m2 / len(numbers),
            'sample': m2 / (len(numbers) - 1) if len(numbers) > 1 else None}


def near(actual, expected, atol=1e-12):
    return math.isclose(actual, float(expected), rel_tol=0.0, abs_tol=atol)


def rejects(call, exception):
    try:
        call()
    except exception:
        return True
    return False


def run_example():
    base_mv = 10**9  # Numerical stress offset, not a claimed sensor voltage range.
    authored_integers = [base_mv + offset for offset in (-2, -1, 0, 1, 2)]
    input_before = list(authored_integers)
    pull_count = [0]

    def source():
        for value in authored_integers:
            pull_count[0] += 1
            yield value

    iterator = source()
    summary = summarize_millivolts(iterator)
    exact = rational_reference(authored_integers)
    prefixes = [summarize_millivolts(iter(authored_integers[:end]))
                for end in range(1, 6)]  # Test fixture replay, not the streaming consumer.
    centered = summarize_millivolts(iter([-2, -1, 0, 1, 2]))
    scaled = summarize_millivolts((-3 * value for value in authored_integers))
    reversed_summary = summarize_millivolts(reversed(authored_integers))
    constant = summarize_millivolts(iter([12, 12, 12]))
    single = summarize_millivolts(iter([12]))
    as_floats = [float(value) for value in authored_integers]
    float_mean = math.fsum(as_floats) / len(as_floats)
    naive_population = math.fsum(value * value for value in as_floats) / len(as_floats) - float_mean**2
    lost_integers = [2**54, 2**54 + 1]
    lost_summary = summarize_millivolts(iter(lost_integers))
    lost_reference = rational_reference(lost_integers)
    checks = {
        'five_generator_values_are_consumed_exactly_once': summary['count'] == pull_count[0] == 5,
        'exhausted_generator_remains_empty': iter(iterator) is iterator and list(iterator) == [],
        'mean_matches_hand_one_billion_millivolts': near(summary['mean_mv'], base_mv),
        'm2_matches_hand_ten_square_millivolts': near(summary['m2_mv2'], 10),
        'population_denominator_five_gives_two_mv2': near(summary['population_variance_mv2'], 2),
        'sample_denominator_four_gives_two_point_five_mv2': near(summary['sample_variance_mv2'], Fraction(5, 2)),
        'independent_exact_reference_and_stdlib_agree': exact['mean'] == base_mv and exact['m2'] == 10
            and exact['population'] == 2 and exact['sample'] == Fraction(5, 2)
            and statistics.pvariance(authored_integers) == 2 and statistics.variance(authored_integers) == 2.5,
        'all_five_prefix_means_and_m2_match_hand_reference': all(
            near(row['mean_mv'], expected_mean) and near(row['m2_mv2'], expected_m2)
            for row, expected_mean, expected_m2 in zip(prefixes,
                [base_mv - 2, base_mv - 1.5, base_mv - 1, base_mv - 0.5, base_mv],
                [0, 0.5, 2, 5, 10])),
        'removing_this_exact_offset_preserves_variance': near(centered['population_variance_mv2'], summary['population_variance_mv2'])
            and near(centered['sample_variance_mv2'], summary['sample_variance_mv2']),
        'negative_triple_scaling_multiplies_variance_by_nine': near(scaled['mean_mv'], -3 * base_mv)
            and near(scaled['population_variance_mv2'], 18) and near(scaled['sample_variance_mv2'], 22.5),
        'reversed_fixture_preserves_this_mean_and_variance': near(reversed_summary['mean_mv'], summary['mean_mv'])
            and near(reversed_summary['population_variance_mv2'], summary['population_variance_mv2']),
        'constant_three_value_fixture_has_zero_variance': constant['count'] == 3
            and constant['mean_mv'] == 12.0 and constant['population_variance_mv2'] == constant['sample_variance_mv2'] == 0.0,
        'singleton_population_zero_but_sample_undefined': single['count'] == 1
            and single['population_variance_mv2'] == 0.0 and single['sample_variance_mv2'] is None,
        'empty_and_reused_exhausted_sources_are_rejected': rejects(lambda: summarize_millivolts(iter([])), ValueError)
            and rejects(lambda: summarize_millivolts(iterator), ValueError),
        'bool_string_and_complex_values_are_rejected': all(
            rejects(lambda value=value: summarize_millivolts(iter([value])), TypeError)
            for value in (True, '12', 12+0j)),
        'nan_infinities_and_unrepresentable_integer_are_rejected': all(
            rejects(lambda value=value: summarize_millivolts(iter([value])), ValueError)
            for value in (math.nan, math.inf, -math.inf, 10**400)),
        'finite_inputs_can_overflow_intermediate_arithmetic': rejects(lambda: summarize_millivolts(iter([1e308, -1e308])), ArithmeticError),
        'naive_difference_of_large_moments_loses_this_variance': naive_population == 0.0
            and near(summary['population_variance_mv2'], 2),
        'float_conversion_can_destroy_distinctions_before_algorithm': lost_reference['population'] == Fraction(1, 4)
            and float(lost_integers[0]) == float(lost_integers[1])
            and lost_summary['population_variance_mv2'] == 0.0,
        'authored_input_list_is_unchanged': authored_integers == input_before,
    }
    assert len(checks) == 20 and all(type(value) is bool for value in checks.values())
    assert all(checks.values()), checks
    return {'python': platform.python_version(), 'dependencies': 'standard library only',
            'scope': 'authored_finite_millivolt_numeric_stress_fixtures_no_measurements',
            'fixture': {'integer_readings_mv': authored_integers, 'large_offset_mv': base_mv,
                        'offset_is_numeric_stress_not_device_range': True},
            'summary': summary,
            'prefix_summaries': prefixes,
            'exact_reference': {key: str(value) for key, value in exact.items()},
            'generator_values_pulled': pull_count[0],
            'naive_population_variance_mv2': naive_population,
            'representation_loss_probe': {'exact_integer_readings_mv': lost_integers,
                                          'converted_float_values_mv': [float(v) for v in lost_integers],
                                          'exact_population_variance_mv2': str(lost_reference['population']),
                                          'computed_population_variance_mv2': lost_summary['population_variance_mv2']},
            'absolute_tolerance_for_this_fixture': {'mean_mv': 1e-12, 'm2_and_variance_mv2': 1e-12},
            'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run_example(), indent=2))

نتیجهٔ جریانی، شمار پنج، میانگین یک میلیارد، M2 برابر ۱۰ و دو واریانس ۲ و ۲٫۵ است. مولد دقیقاً پنج مقدار تحویل داده و گذر بعدی خالی می‌ماند. M2 پیشوندها به ترتیب صفر، ۰٫۵، ۲، ۵ و ۱۰ می‌شود. فرمول تفریق گشتاورها در همین محیط صفر داد. این تفاوت شاهد محدود همین اجراست؛ اندازه‌گیری زمان پردازنده یا رتبه‌بندی سرعت الگوریتم‌ها انجام نشده است.

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "scope": "authored_finite_millivolt_numeric_stress_fixtures_no_measurements",
  "fixture": {
    "integer_readings_mv": [
      999999998,
      999999999,
      1000000000,
      1000000001,
      1000000002
    ],
    "large_offset_mv": 1000000000,
    "offset_is_numeric_stress_not_device_range": true
  },
  "summary": {
    "count": 5,
    "mean_mv": 1000000000.0,
    "m2_mv2": 10.0,
    "population_variance_mv2": 2.0,
    "sample_variance_mv2": 2.5
  },
  "prefix_summaries": [
    {
      "count": 1,
      "mean_mv": 999999998.0,
      "m2_mv2": 0.0,
      "population_variance_mv2": 0.0,
      "sample_variance_mv2": null
    },
    {
      "count": 2,
      "mean_mv": 999999998.5,
      "m2_mv2": 0.5,
      "population_variance_mv2": 0.25,
      "sample_variance_mv2": 0.5
    },
    {
      "count": 3,
      "mean_mv": 999999999.0,
      "m2_mv2": 2.0,
      "population_variance_mv2": 0.6666666666666666,
      "sample_variance_mv2": 1.0
    },
    {
      "count": 4,
      "mean_mv": 999999999.5,
      "m2_mv2": 5.0,
      "population_variance_mv2": 1.25,
      "sample_variance_mv2": 1.6666666666666667
    },
    {
      "count": 5,
      "mean_mv": 1000000000.0,
      "m2_mv2": 10.0,
      "population_variance_mv2": 2.0,
      "sample_variance_mv2": 2.5
    }
  ],
  "exact_reference": {
    "count": "5",
    "mean": "1000000000",
    "m2": "10",
    "population": "2",
    "sample": "5/2"
  },
  "generator_values_pulled": 5,
  "naive_population_variance_mv2": 0.0,
  "representation_loss_probe": {
    "exact_integer_readings_mv": [
      18014398509481984,
      18014398509481985
    ],
    "converted_float_values_mv": [
      1.8014398509481984e+16,
      1.8014398509481984e+16
    ],
    "exact_population_variance_mv2": "1/4",
    "computed_population_variance_mv2": 0.0
  },
  "absolute_tolerance_for_this_fixture": {
    "mean_mv": 1e-12,
    "m2_and_variance_mv2": 1e-12
  },
  "checks": {
    "five_generator_values_are_consumed_exactly_once": true,
    "exhausted_generator_remains_empty": true,
    "mean_matches_hand_one_billion_millivolts": true,
    "m2_matches_hand_ten_square_millivolts": true,
    "population_denominator_five_gives_two_mv2": true,
    "sample_denominator_four_gives_two_point_five_mv2": true,
    "independent_exact_reference_and_stdlib_agree": true,
    "all_five_prefix_means_and_m2_match_hand_reference": true,
    "removing_this_exact_offset_preserves_variance": true,
    "negative_triple_scaling_multiplies_variance_by_nine": true,
    "reversed_fixture_preserves_this_mean_and_variance": true,
    "constant_three_value_fixture_has_zero_variance": true,
    "singleton_population_zero_but_sample_undefined": true,
    "empty_and_reused_exhausted_sources_are_rejected": true,
    "bool_string_and_complex_values_are_rejected": true,
    "nan_infinities_and_unrepresentable_integer_are_rejected": true,
    "finite_inputs_can_overflow_intermediate_arithmetic": true,
    "naive_difference_of_large_moments_loses_this_variance": true,
    "float_conversion_can_destroy_distinctions_before_algorithm": true,
    "authored_input_list_is_unchanged": true
  }
}

۵. چهار پاسخ و دو محدودیت باقی‌مانده

مرز نمایش ممیز شناور پیش از هر الگوریتم آماری
تصویر مفهومی: مرز نمایش ممیز شناور پیش از هر الگوریتم آماری؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

تمرین اول: اگر نمونه‌ها در منفی سه ضرب شوند چه می‌شود؟ پاسخ: میانگین در منفی سه و واریانس در نه ضرب می‌شود؛ دو واریانس این مثال ۱۸ و ۲۲٫۵ میلی‌ولت مربع خواهند شد. برنامه این تغییر را واقعاً بررسی می‌کند.

تمرین دوم: برای یک عضو چرا واریانس نمونه صفر نیست؟ پاسخ: مخرج تعداد منهای یک صفر می‌شود، پس تعریف نشده است. قرارداد خروجی ما برای آن None و برای واریانس توصیفی تک‌عضوی صفر است. ورودی خالی اصلاً پذیرفته نمی‌شود.

تمرین سوم: آیا روش مرکزی، اطلاعات از دست رفته در تبدیل ورودی را برمی‌گرداند؟ پاسخ: خیر. دو عدد صحیح 2**54 و یک واحد بیشتر، در تبدیل اجرای حاضر یک float یکسان می‌شوند. واریانس دقیق آن دو یک‌چهارم است، اما نتیجهٔ جریانی صفر می‌شود. راهنمای نمایش ممیز شناور پایتون زمینهٔ این محدودیت را توضیح می‌دهد؛ انتخاب الگوریتم، جای انتخاب نمایش داده را نمی‌گیرد.

تمرین چهارم: آیا متناهی بودن ورودی برای محاسبهٔ متناهی کافی است؟ پاسخ: خیر؛ زوج مثبت و منفی ده به توان ۳۰۸ در این کد باعث خطای محاسبهٔ میانی می‌شود. هنگام خطا، بخشی از تکرارگر مصرف شده است و بازگشت تراکنشی انجام نمی‌شود. پایداری عددی نیز مقاومت آماری در برابر دادهٔ پرت نیست. برای سنجش جداگانهٔ حافظه، آزمایش tracemalloc را ببینید. منابع در ۲۴ مهر ۱۴۰۵ خوانده شده‌اند؛ تصاویر مفهومی‌اند و نتیجهٔ اندازه‌گیری حسگر نیستند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.