waliullah123 commited on
Commit
04d39e4
·
verified ·
1 Parent(s): 5907e4a

Upload feature_extraction.py

Browse files
Files changed (1) hide show
  1. feature_extraction.py +333 -0
feature_extraction.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import numpy as np
3
+ import nltk
4
+ from sklearn.feature_extraction.text import TfidfVectorizer
5
+ from nltk.sentiment.vader import SentimentIntensityAnalyzer
6
+
7
+ # Deception-detection lexicons (LIWC-aligned)
8
+ HEDGE_WORDS = {'maybe', 'perhaps', 'possibly', 'might', 'could', 'seems',
9
+ 'apparently', 'supposedly', 'allegedly', 'reportedly', 'somewhat',
10
+ 'rather', 'quite', 'fairly', 'presumably'}
11
+
12
+ CERTAINTY_WORDS = {'definitely', 'certainly', 'absolutely', 'always', 'never',
13
+ 'guaranteed', 'proven', 'undeniable', 'obvious', 'clearly',
14
+ 'without doubt', 'of course', 'undoubtedly', 'surely'}
15
+
16
+ MODAL_VERBS = {'can', 'could', 'may', 'might', 'must', 'shall', 'should',
17
+ 'will', 'would'}
18
+
19
+ NEGATION_WORDS = {'not', "n't", 'no', 'never', 'neither', 'nor', 'none',
20
+ 'nothing', 'nowhere', 'nobody', 'cannot', "don't", "doesn't",
21
+ "didn't", "won't", "isn't", "aren't", "wasn't", "weren't"}
22
+
23
+ EMOTIONAL_INTENSITY = {'shocking', 'outrageous', 'incredible', 'unbelievable',
24
+ 'devastating', 'horrible', 'amazing', 'terrible',
25
+ 'catastrophic', 'disgusting', 'explosive', 'disastrous'}
26
+
27
+ # Sensationalist / clickbait phrases common in fake news
28
+ SENSATIONAL_PHRASES = [
29
+ 'miracle cure', 'cures all', 'cure all', 'cures cancer', 'cure cancer',
30
+ 'secret cure', 'miracle drug', 'instant cure', 'instantly cures',
31
+ 'breakthrough cure', 'wonder drug', 'magic pill', 'one weird trick',
32
+ 'doctors hate', 'pharma doesn', 'big pharma', 'they don\'t want you',
33
+ 'what they don\'t tell you', 'the truth about', 'exposed',
34
+ 'banned by', 'cover-up', 'coverup', 'conspiracy',
35
+ ]
36
+
37
+ # Conspiracy / misinformation language patterns
38
+ CONSPIRACY_PHRASES = [
39
+ 'secret documents', 'secret hospital', 'secret government',
40
+ 'secret plan', 'secretly adding', 'secret program',
41
+ 'control minds', 'mind control', 'control people',
42
+ 'chemtrails', 'flat earth', 'illuminati', 'new world order',
43
+ 'they are hiding', 'what they hide', 'hidden truth',
44
+ 'wake up', 'open your eyes', 'the real truth',
45
+ 'confirmed by secret', 'leaked documents', 'internal documents',
46
+ 'secretly control', 'control the population', 'government coverup',
47
+ 'adding chemicals', 'putting chemicals', 'chemical weapons',
48
+ 'control minds and', 'control peoples', 'control emotions',
49
+ ]
50
+
51
+ # Extreme health / pseudoscience claims
52
+ HEALTH_MISINFO_PATTERNS = [
53
+ 'drinking bleach', 'eat bleach', 'bleach cure',
54
+ 'cures all types', 'cures every', 'cures 100',
55
+ 'within 48 hours', 'within 24 hours', 'overnight cure',
56
+ 'all types of cancer', 'all diseases', 'every disease',
57
+ 'no side effects', 'completely safe', '100 percent effective',
58
+ 'natural cure', 'home remedy cure', 'detox cleanse',
59
+ ]
60
+
61
+ ATTRIBUTION_VERBS = {'said', 'claimed', 'stated', 'announced', 'reported',
62
+ 'according', 'revealed', 'disclosed', 'alleged', 'insisted'}
63
+
64
+
65
+ class FeatureExtractor:
66
+ def __init__(self):
67
+ # Syntactic features (n-grams 1-3) per Shu et al. §3.2.1
68
+ self.vectorizer = TfidfVectorizer(
69
+ ngram_range=(1, 3), max_features=8000,
70
+ analyzer='word', sublinear_tf=True
71
+ )
72
+ self.char_vectorizer = TfidfVectorizer(
73
+ ngram_range=(2, 5), max_features=3000,
74
+ analyzer='char_wb', sublinear_tf=True
75
+ )
76
+ for res in ['punkt', 'punkt_tab', 'averaged_perceptron_tagger', 'averaged_perceptron_tagger_eng', 'universal_tagset', 'vader_lexicon']:
77
+ try:
78
+ nltk.download(res, quiet=True)
79
+ except Exception:
80
+ pass
81
+ try:
82
+ self.sid = SentimentIntensityAnalyzer()
83
+ except Exception:
84
+ self.sid = None
85
+
86
+ def count_syllables(self, word):
87
+ word = word.lower()
88
+ count = 0
89
+ vowels = "aeiouy"
90
+ if word[0] in vowels:
91
+ count += 1
92
+ for index in range(1, len(word)):
93
+ if word[index] in vowels and word[index - 1] not in vowels:
94
+ count += 1
95
+ if word.endswith("e"):
96
+ count -= 1
97
+ if count == 0:
98
+ count += 1
99
+ return count
100
+
101
+ def extract_content_features(self, text):
102
+ """
103
+ Extracts News Content Features (§3.2.1)
104
+ Focuses on Lexical, Syntactic, and Style (Deception cues)
105
+ """
106
+ if not text:
107
+ return {}
108
+
109
+ tokens = nltk.word_tokenize(text)
110
+ words = [w.lower() for w in tokens if w.isalnum()]
111
+ total_words = len(words)
112
+ unique_words = len(set(words))
113
+ text_lower = text.lower()
114
+ text_len = max(1, len(text))
115
+
116
+ # 1. Lexical Features
117
+ lexical = {
118
+ 'total_words': total_words,
119
+ 'lexical_density': unique_words / total_words if total_words > 0 else 0,
120
+ 'avg_word_length': np.mean([len(w) for w in words]) if words else 0,
121
+ 'capital_ratio': sum(1 for c in text if c.isupper()) / text_len
122
+ }
123
+
124
+ # 2. Syntactic & Style (POS Tagging)
125
+ pos_tags_raw = nltk.pos_tag(tokens)
126
+ pos_tags_univ = nltk.pos_tag(tokens, tagset='universal')
127
+ tag_counts = {}
128
+ for _, tag in pos_tags_univ:
129
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
130
+
131
+ total_tags = max(1, len(tokens))
132
+ syntax = {
133
+ 'noun_ratio': tag_counts.get('NOUN', 0) / total_tags,
134
+ 'verb_ratio': tag_counts.get('VERB', 0) / total_tags,
135
+ 'adj_ratio': tag_counts.get('ADJ', 0) / total_tags,
136
+ 'adv_ratio': tag_counts.get('ADV', 0) / total_tags,
137
+ 'punctuation_aggression': sum(1 for char in text if char in '!') / text_len
138
+ }
139
+
140
+ # 3. NER & POS Trigrams (Linguistic Cadence)
141
+ try:
142
+ chunks = nltk.ne_chunk(pos_tags_raw)
143
+ entities = [chunk for chunk in chunks if hasattr(chunk, 'label')]
144
+ entity_density = len(entities) / max(1, total_words)
145
+ except:
146
+ entity_density = 0
147
+
148
+ tags_only = [t for _, t in pos_tags_univ]
149
+ trigrams = list(nltk.trigrams(tags_only))
150
+ formal_markers = {('NOUN','VERB','NOUN'), ('ADJ','NOUN','VERB'), ('NOUN','ADP','NOUN')}
151
+ formal_cadence = sum(1 for tr in trigrams if tr in formal_markers) / max(1, len(trigrams))
152
+
153
+ # 4. Sentiment & Subjectivity
154
+ sentiment = self.sid.polarity_scores(text)
155
+ sentences = nltk.sent_tokenize(text)
156
+ num_sentences = max(1, len(sentences))
157
+ num_syllables = sum(self.count_syllables(w) for w in words)
158
+ flesch = 206.835 - 1.015 * (total_words / num_sentences) - 84.6 * (num_syllables / max(1, total_words))
159
+ subjectivity = (tag_counts.get('ADJ', 0) + tag_counts.get('ADV', 0)) / total_tags
160
+
161
+ advanced = {
162
+ 'sentiment_score': sentiment['compound'],
163
+ 'complexity_score': flesch,
164
+ 'subjectivity_score': subjectivity,
165
+ 'entity_density': entity_density,
166
+ 'formal_cadence': formal_cadence,
167
+ 'official_marker': 1.0 if any(s in text.upper() for s in [
168
+ 'BUREAU OF', 'FEDERAL RESERVE', 'NOAA', 'STATISTICS REPORTED',
169
+ 'CENSUS BUREAU', 'WORLD HEALTH ORGANIZATION', 'PEER-REVIEWED',
170
+ 'PUBLISHED IN', 'ACCORDING TO DATA'
171
+ ]) else 0.0
172
+ }
173
+
174
+ # 5. Style-based Deception Cues
175
+ deception = {
176
+ 'exclamation_marks': text.count('!'),
177
+ 'question_marks': text.count('?'),
178
+ 'quotes_count': text.count('"') + text.count("'")
179
+ }
180
+
181
+ # ===== NEW: Deception Linguistic Features =====
182
+
183
+ # 6. Hedging vs Certainty (deception often uses more certainty words)
184
+ word_set = set(words)
185
+ hedge_count = sum(1 for w in words if w in HEDGE_WORDS)
186
+ certainty_count = sum(1 for w in words if w in CERTAINTY_WORDS)
187
+ hedge_certainty = {
188
+ 'hedge_ratio': hedge_count / max(1, total_words),
189
+ 'certainty_ratio': certainty_count / max(1, total_words),
190
+ 'hedge_certainty_diff': (hedge_count - certainty_count) / max(1, total_words),
191
+ }
192
+
193
+ # 7. Emotional Intensity (fake news uses more emotional language)
194
+ emotional_count = sum(1 for w in words if w in EMOTIONAL_INTENSITY)
195
+ sentiment_abs = abs(sentiment['compound'])
196
+ emotional = {
197
+ 'emotional_intensity': emotional_count / max(1, total_words),
198
+ 'sentiment_extremity': sentiment_abs,
199
+ 'negativity_score': abs(sentiment['neg']),
200
+ 'positivity_score': abs(sentiment['pos']),
201
+ }
202
+
203
+ # 8. Numerical Features (real news tends to have more specific numbers)
204
+ numbers = re.findall(r'\b\d+\.?\d*\b', text)
205
+ percentages = re.findall(r'\d+\s*%', text)
206
+ dollar_amounts = re.findall(r'\$[\d,]+\.?\d*', text)
207
+ numerical = {
208
+ 'number_density': len(numbers) / max(1, total_words),
209
+ 'has_percentage': 1.0 if percentages else 0.0,
210
+ 'has_dollar_amount': 1.0 if dollar_amounts else 0.0,
211
+ 'number_count': len(numbers),
212
+ }
213
+
214
+ # 9. Attribution & Source Cues (real news cites sources)
215
+ attribution_count = sum(1 for w in words if w in ATTRIBUTION_VERBS)
216
+ has_source = 1.0 if any(p in text_lower for p in [
217
+ 'according to', 'studies show', 'research suggests',
218
+ 'data from', 'report by', 'analysis by'
219
+ ]) else 0.0
220
+ source = {
221
+ 'attribution_ratio': attribution_count / max(1, total_words),
222
+ 'has_source_citation': has_source,
223
+ }
224
+
225
+ # 10. Readability (Automated Readability Index)
226
+ char_count = sum(1 for c in text if c.isalnum())
227
+ ari = (4.71 * char_count / max(1, total_words)) + (0.5 * total_words / num_sentences) - 21.43
228
+ gunning_fog = 0.4 * ((total_words / num_sentences) + 100 * (
229
+ sum(1 for w in words if self.count_syllables(w) >= 3) / max(1, total_words)))
230
+ readability = {
231
+ 'ari_score': ari,
232
+ 'gunning_fog': gunning_fog,
233
+ 'avg_sentence_length': total_words / num_sentences,
234
+ }
235
+
236
+ # 11. Pronoun & Modality Features
237
+ first_person = sum(1 for w in words if w in {'i', 'me', 'my', 'mine', 'we', 'our', 'ours', 'myself'})
238
+ modal_count = sum(1 for w in words if w in MODAL_VERBS)
239
+ negation_count = sum(1 for w in words if w in NEGATION_WORDS or w.endswith("n't"))
240
+ pronoun_modality = {
241
+ 'first_person_ratio': first_person / max(1, total_words),
242
+ 'modal_ratio': modal_count / max(1, total_words),
243
+ 'negation_ratio': negation_count / max(1, total_words),
244
+ }
245
+
246
+ # 12. Comparative & Superlative markers
247
+ comparative = sum(1 for _, t in pos_tags_raw if t in ('JJR', 'RBR'))
248
+ superlative = sum(1 for _, t in pos_tags_raw if t in ('JJS', 'RBS'))
249
+ comparison = {
250
+ 'comparative_ratio': comparative / max(1, total_tags),
251
+ 'superlative_ratio': superlative / max(1, total_tags),
252
+ }
253
+
254
+ # 13. Passive voice detection (be + VBN patterns)
255
+ passive_patterns = sum(1 for i in range(len(pos_tags_raw) - 1)
256
+ if pos_tags_raw[i][1] in ('VBZ', 'VBP', 'VBD', 'VBN')
257
+ and pos_tags_raw[i+1][1] == 'VBN')
258
+ voice = {
259
+ 'passive_ratio': passive_patterns / max(1, total_tags),
260
+ }
261
+
262
+ # 14. ALL-CAPS word ratio (shouting / urgency signal)
263
+ caps_words = sum(1 for w in words if w.isupper() and len(w) > 2)
264
+ urgency = {
265
+ 'caps_word_ratio': caps_words / max(1, total_words),
266
+ 'ellipsis_count': text.count('...'),
267
+ }
268
+
269
+ # 15. Sensationalism & Conspiracy Score (fake news signature)
270
+ text_lower_joined = ' ' + text_lower + ' '
271
+ sensational_hits = sum(1 for p in SENSATIONAL_PHRASES if p in text_lower_joined)
272
+ conspiracy_hits = sum(1 for p in CONSPIRACY_PHRASES if p in text_lower_joined)
273
+ health_misinfo_hits = sum(1 for p in HEALTH_MISINFO_PATTERNS if p in text_lower_joined)
274
+ sensationalism = {
275
+ 'sensationalism_score': (sensational_hits + conspiracy_hits + health_misinfo_hits) / max(1, total_words),
276
+ 'conspiracy_score': conspiracy_hits / max(1, total_words),
277
+ 'health_misinfo_score': health_misinfo_hits / max(1, total_words),
278
+ 'sensational_hit_count': sensational_hits + conspiracy_hits + health_misinfo_hits,
279
+ }
280
+
281
+ # 16. Absolutist language (ALL, EVERY, NEVER — common in fake news)
282
+ absolutist_words = sum(1 for w in words if w in {
283
+ 'all', 'every', 'always', 'never', 'everyone', 'nobody',
284
+ 'nothing', 'completely', 'totally', 'absolutely', 'entirely',
285
+ '100', 'instantly', 'instant', 'guaranteed',
286
+ })
287
+ extremity = {
288
+ 'absolutist_ratio': absolutist_words / max(1, total_words),
289
+ }
290
+
291
+ return {
292
+ **lexical, **syntax, **advanced, **deception,
293
+ **hedge_certainty, **emotional, **numerical,
294
+ **source, **readability, **pronoun_modality,
295
+ **comparison, **voice, **urgency,
296
+ **sensationalism, **extremity
297
+ }
298
+
299
+ def get_combined_features(self, text, metadata=None):
300
+ """
301
+ Combines News Content features with Social Context proxies (§3.2.2)
302
+ """
303
+ features = self.extract_content_features(text)
304
+
305
+ if metadata:
306
+ # AUXILIARY INFORMATION (Social Context Proxy §3.2.2)
307
+ history_cols = ['barely_true_counts', 'false_counts', 'half_true_counts',
308
+ 'mostly_true_counts', 'pants_on_fire_counts']
309
+
310
+ total_history = sum(float(metadata.get(c, 0)) for c in history_cols)
311
+ reliable_history = float(metadata.get('half_true_counts', 0)) + \
312
+ float(metadata.get('mostly_true_counts', 0))
313
+
314
+ features['speaker_reliability'] = reliable_history / total_history if total_history > 0 else 0.5
315
+
316
+ # Publisher Context (§3.1 distortion bias)
317
+ features['is_republican'] = 1 if metadata.get('party') == 'republican' else 0
318
+ features['is_democrat'] = 1 if metadata.get('party') == 'democrat' else 0
319
+
320
+ # NEW: Historical track record features
321
+ false_total = float(metadata.get('false_counts', 0)) + float(metadata.get('pants_on_fire_counts', 0))
322
+ true_total = float(metadata.get('mostly_true_counts', 0)) + float(metadata.get('half_true_counts', 0))
323
+ features['false_history_ratio'] = false_total / max(1, total_history)
324
+ features['true_history_ratio'] = true_total / max(1, total_history)
325
+ features['history_volume'] = np.log1p(total_history) # log-scaled volume
326
+
327
+ return features
328
+
329
+ def transform_text_tfidf(self, corpus):
330
+ return self.vectorizer.fit_transform(corpus)
331
+
332
+ def transform_char_tfidf(self, corpus):
333
+ return self.char_vectorizer.fit_transform(corpus)