""" Moduł do obliczania statystycznej wielkości próbki. Implementuje wzory statystyczne dla próbkowania populacji. """ import math from dataclasses import dataclass from app.core.config import settings # Wartości Z dla poziomów ufności Z_SCORES = { 0.90: 1.645, 0.95: 1.96, 0.99: 2.576, } @dataclass class SamplePlan: """ Plan próbkowania dla gry. Attributes: top_helpful: Liczba najprzydatniejszych recenzji. statistical_sample: Wielkość próbki statystycznej. positive_count: Ile pobrać pozytywnych (stratified). negative_count: Ile pobrać negatywnych (stratified). total: Łączna liczba recenzji do pobrania. """ top_helpful: int statistical_sample: int positive_count: int negative_count: int total: int def calculate_sample_size( population: int, confidence_level: float | None = None, margin_of_error: float | None = None, ) -> int: """ Oblicza minimalną wielkość próbki dla danej populacji. Wykorzystuje wzór Cochrana z korektą dla populacji skończonej. """ if confidence_level is None: confidence_level = settings.sample_confidence_level if margin_of_error is None: margin_of_error = settings.sample_margin_of_error # 1. Pobieramy Z-score (np. 1.96 dla 95% ufności). # Mówi on, jak bardzo wynik może odbiegać od średniej w jednostkach odchylenia standardowego. z = Z_SCORES.get(confidence_level, 1.96) # 2. Zakładamy p=0.5 (maksymalna zmienność). # To daje nam najbezpieczniejszą (największą) wielkość próbki. p = 0.5 # 3. Wzór Cochrana dla nieskończonej populacji: # n0 = (Z^2 * p * (1-p)) / e^2 # Wyjaśnienie: Z kwadrat razy zmienność, podzielone przez kwadrat błędu. n_0 = (z ** 2 * p * (1 - p)) / (margin_of_error ** 2) # 4. Korekta dla populacji skończonej (Steam ma policzalną liczbę recenzji): # n = n0 / (1 + (n0 - 1) / N) # Wyjaśnienie: Zmniejszamy próbkę, bo wiemy dokładnie, ile osób (recenzji) jest w "całym świecie" tej gry. n = n_0 / (1 + (n_0 - 1) / population) # Zaokrąglamy w górę do pełnej recenzji return math.ceil(n) def create_sample_plan( total_reviews: int, positive_reviews: int, negative_reviews: int, ) -> SamplePlan: """ Tworzy plan próbkowania, łącząc dwa podejścia. """ top_helpful = settings.sample_top_helpful max_reviews = settings.sample_max_reviews # Obliczamy, ile recenzji musimy pobrać, żeby wynik był wiarygodny statistical_sample = calculate_sample_size(total_reviews) # Pilnujemy, żeby nie przekroczyć ustawionego limitu (np. 3000) statistical_sample = min(statistical_sample, max_reviews - top_helpful) # Obliczamy jaki procent stanowią pozytywy i negatywy w całości if total_reviews > 0: pos_ratio = positive_reviews / total_reviews neg_ratio = negative_reviews / total_reviews else: pos_ratio = 0.5 neg_ratio = 0.5 # Rozdzielamy naszą próbkę proporcjonalnie do tych wyników (Stratified Sampling) pos_target = math.ceil(statistical_sample * pos_ratio) neg_target = math.ceil(statistical_sample * neg_ratio) # Minority protection: boost the smaller group to minority_min if possible minority_min = settings.sample_minority_min if pos_target < minority_min and positive_reviews > pos_target: pos_target = min(minority_min, positive_reviews) if neg_target < minority_min and negative_reviews > neg_target: neg_target = min(minority_min, negative_reviews) # Final adjustment to stay within statistical_sample limit if pos_target + neg_target > statistical_sample: if pos_target > neg_target: pos_target = max(pos_target - (pos_target + neg_target - statistical_sample), minority_min) else: neg_target = max(neg_target - (pos_target + neg_target - statistical_sample), minority_min) # Final cap by actual availability positive_count = min(pos_target, positive_reviews) negative_count = min(neg_target, negative_reviews) # Sumujemy wszystko (Top Helpful + Próbka Statystyczna) total = top_helpful + positive_count + negative_count return SamplePlan( top_helpful=top_helpful, statistical_sample=statistical_sample, positive_count=positive_count, negative_count=negative_count, total=total, )