File size: 8,096 Bytes
64b5d29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# src/analysis/temporal.py (Yarı ömür fonksiyonu eklendi)

import pandas as pd
import numpy as np
from scipy.optimize import curve_fit
import logging
from pathlib import Path
from datetime import datetime

# Yerel modüllerimizi içe aktaralım
from src.data_management import storage

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def calculate_concept_frequencies(time_period: str = 'Y') -> pd.DataFrame | None:
    """

    Konseptlerin zaman içindeki kullanım sıklıklarını hesaplar. (Önceki kodla aynı)

    """
    logging.info(f"Konsept frekansları '{time_period}' periyodu için hesaplanıyor...")
    mentions_df = storage.load_dataframe('mentions', storage.MENTION_COLUMNS)
    documents_df = storage.load_dataframe('documents', storage.DOC_COLUMNS)

    if mentions_df is None or documents_df is None:
        logging.error("Mention veya Document verisi yüklenemedi. Frekans hesaplanamıyor.")
        return None
    if mentions_df.empty:
        logging.warning("Mention verisi boş. Frekans hesaplanamıyor.")
        return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])
    if documents_df.empty:
        logging.warning("Document verisi boş. Tarih bilgisi alınamıyor, frekans hesaplanamıyor.")
        return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])

    docs_subset = documents_df[['doc_id', 'publication_date']].copy()
    try:
        docs_subset['publication_date'] = pd.to_datetime(docs_subset['publication_date'], errors='coerce')
    except Exception as e:
         logging.error(f"Dokümanlardaki 'publication_date' sütunu datetime'a çevrilemedi: {e}")
         return None

    original_doc_count = len(docs_subset)
    docs_subset.dropna(subset=['publication_date'], inplace=True)
    valid_date_count = len(docs_subset)
    if original_doc_count > valid_date_count:
        logging.warning(f"{original_doc_count - valid_date_count} dokümanın geçerli yayın tarihi yok, frekans hesaplamasına dahil edilmeyecek.")

    if docs_subset.empty:
        logging.warning("Geçerli yayın tarihine sahip doküman bulunamadı. Frekans hesaplanamıyor.")
        return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])

    mentions_with_dates = pd.merge(mentions_df, docs_subset, on='doc_id', how='inner')

    if mentions_with_dates.empty:
        logging.warning("Mention'lar ile doküman tarihleri birleştirilemedi veya sonuç boş.")
        return pd.DataFrame(columns=['concept_id', 'time_period_start', 'frequency'])

    logging.info(f"{len(mentions_with_dates)} mention için tarih bilgisi bulundu.")

    try:
        frequency_df = mentions_with_dates.groupby(
            ['concept_id', pd.Grouper(key='publication_date', freq=time_period)]
        ).size().reset_index(name='frequency')
        frequency_df.rename(columns={'publication_date': 'time_period_start'}, inplace=True)
        logging.info(f"Frekans hesaplaması tamamlandı. {len(frequency_df)} satır sonuç üretildi.")
        frequency_df.sort_values(by=['concept_id', 'time_period_start'], inplace=True)
        return frequency_df
    except Exception as e:
        logging.exception(f"Frekans hesaplanırken hata oluştu: {e}")
        return None

# --- YENİ: Yarı Ömür Hesaplama ---

def exponential_decay(t, A, decay_rate):
    """Üstel bozulma fonksiyonu: A * exp(-decay_rate * t)."""
    # Decay rate negatif olmamalı (bozunma varsayımı)
    decay_rate = max(0, decay_rate) # Negatifse sıfır yap
    return A * np.exp(-decay_rate * t)

def calculate_half_life(concept_id: str,

                        frequency_df: pd.DataFrame,

                        concept_name: str | None = None,

                        min_data_points: int = 4,

                        min_decay_rate: float = 1e-6) -> float | None:
    """

    Verilen konsept için frekans verisine üstel bozulma modeli uygulayarak

    yarı ömrü (yıl olarak) hesaplar.



    Args:

        concept_id (str): Hesaplanacak konseptin ID'si.

        frequency_df (pd.DataFrame): calculate_concept_frequencies'ten dönen DataFrame.

                                     ('concept_id', 'time_period_start', 'frequency' sütunları olmalı).

        concept_name (str | None): Loglama için konseptin adı (opsiyonel).

        min_data_points (int): Yarı ömür hesaplamak için gereken minimum zaman noktası sayısı.

        min_decay_rate (float): Kabul edilebilir minimum bozunma oranı (çok küçükse yarı ömür sonsuz kabul edilir).



    Returns:

        float | None: Hesaplanan yarı ömür (yıl olarak) veya hesaplanamazsa None.

                      np.inf dönebilir eğer bozunma oranı çok küçükse.

    """
    log_prefix = f"Yarı Ömür ({concept_name or concept_id}):"

    if frequency_df is None or frequency_df.empty:
        logging.warning(f"{log_prefix} Frekans verisi boş.")
        return None

    # Konsepte ait veriyi filtrele ve zamana göre sırala
    concept_data = frequency_df[frequency_df['concept_id'] == concept_id].sort_values(by='time_period_start').copy()

    # Yeterli veri noktası var mı?
    if len(concept_data) < min_data_points:
        logging.info(f"{log_prefix} Yeterli veri noktası yok ({len(concept_data)} < {min_data_points}). Hesaplama yapılamıyor.")
        return None

    # Zamanı sayısal değere çevir (ilk yıldan itibaren geçen yıl sayısı)
    try:
        # İlk zaman noktasını t=0 kabul et
        start_date = concept_data['time_period_start'].min()
        # Zaman farkını gün olarak hesapla ve yıla çevir
        concept_data['time_elapsed_years'] = (concept_data['time_period_start'] - start_date).dt.days / 365.25
    except Exception as e:
        logging.error(f"{log_prefix} Zaman farkı hesaplanırken hata: {e}")
        return None

    time_values = concept_data['time_elapsed_years'].values
    frequency_values = concept_data['frequency'].values

    # Frekanslar artıyor mu veya sabit mi kontrol et (basit kontrol)
    # Eğer son değer ilk değerden büyükse veya tüm değerler aynıysa, bozunma yok kabul et
    if frequency_values[-1] > frequency_values[0] or np.all(frequency_values == frequency_values[0]):
         logging.info(f"{log_prefix} Veride belirgin bir azalma gözlenmedi. Yarı ömür hesaplanamıyor.")
         return None # Veya np.inf? Şimdilik None.

    # Modeli uydurmak için başlangıç tahminleri
    initial_A_guess = frequency_values[0] # İlk frekans değeri
    initial_lambda_guess = 0.1 # Küçük pozitif bir bozunma oranı tahmini

    try:
        # curve_fit ile modeli verilere uydur
        params, covariance = curve_fit(
            exponential_decay,
            time_values,
            frequency_values,
            p0=[initial_A_guess, initial_lambda_guess],
            bounds=([0, 0], [np.inf, np.inf]) # Parametrelerin pozitif olmasını sağla
            # maxfev artırılabilir eğer "Optimal parameters not found" hatası alınırsa
        )

        A_fit, decay_rate_fit = params

        # Bozunma oranı anlamlı mı?
        if decay_rate_fit < min_decay_rate:
            logging.info(f"{log_prefix} Hesaplanan bozunma oranı ({decay_rate_fit:.4f}) çok düşük. Yarı ömür sonsuz kabul ediliyor.")
            return np.inf # Sonsuz yarı ömür

        # Yarı ömrü hesapla: ln(2) / decay_rate
        half_life_years = np.log(2) / decay_rate_fit
        logging.info(f"{log_prefix} Başarıyla hesaplandı. A={A_fit:.2f}, Bozunma Oranı={decay_rate_fit:.4f}, Yarı Ömür={half_life_years:.2f} yıl.")
        return half_life_years

    except RuntimeError as e:
        logging.warning(f"{log_prefix} Üstel bozulma modeli uydurulamadı: {e}. Yarı ömür hesaplanamıyor.")
        return None
    except Exception as e:
        logging.exception(f"{log_prefix} Yarı ömür hesaplanırken beklenmeyen hata: {e}")
        return None