Spaces:
Runtime error
Runtime error
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +46 -10
- .github/workflows/scrape.yml +34 -34
- .gitignore +46 -35
- DEPLOYMENT.md +71 -71
- Dockerfile +21 -8
- add_cebuano_data.py +329 -0
- api/main.py +30 -0
- backend/analyze_languages.py +275 -275
- backend/benchmark.py +538 -0
- backend/bias_words_report.txt +148 -148
- backend/evaluate_model.py +820 -820
- backend/mine_bias_words.py +194 -194
- backend/tune_rf.py +333 -0
- check_app/.flutter-plugins-dependencies +1 -0
- check_app/.gitignore +45 -45
- check_app/.metadata +45 -45
- check_app/README.md +17 -17
- check_app/analysis_options.yaml +28 -28
- check_app/android/.gitignore +14 -14
- check_app/android/.gradle/8.14/checksums/checksums.lock +0 -0
- check_app/android/.gradle/8.14/checksums/md5-checksums.bin +0 -0
- check_app/android/.gradle/8.14/checksums/sha1-checksums.bin +3 -0
- check_app/android/.gradle/8.14/executionHistory/executionHistory.bin +3 -0
- check_app/android/.gradle/8.14/executionHistory/executionHistory.lock +0 -0
- check_app/android/.gradle/8.14/fileChanges/last-build.bin +0 -0
- check_app/android/.gradle/8.14/fileHashes/fileHashes.bin +3 -0
- check_app/android/.gradle/8.14/fileHashes/fileHashes.lock +0 -0
- check_app/android/.gradle/8.14/fileHashes/resourceHashesCache.bin +0 -0
- check_app/android/.gradle/8.14/gc.properties +0 -0
- check_app/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- check_app/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- check_app/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- check_app/android/.gradle/file-system.probe +0 -0
- check_app/android/.gradle/noVersion/buildLogic.lock +0 -0
- check_app/android/.gradle/vcs-1/gc.properties +0 -0
- check_app/android/app/build.gradle.kts +44 -44
- check_app/android/app/src/debug/AndroidManifest.xml +7 -7
- check_app/android/app/src/main/AndroidManifest.xml +46 -45
- check_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +39 -0
- check_app/android/app/src/main/kotlin/com/bantaypahayag/app/MainActivity.kt +5 -0
- check_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -12
- check_app/android/app/src/main/res/drawable/launch_background.xml +12 -12
- check_app/android/app/src/main/res/values-night/styles.xml +18 -18
- check_app/android/app/src/main/res/values/styles.xml +18 -18
- check_app/android/app/src/profile/AndroidManifest.xml +7 -7
- check_app/android/build.gradle.kts +24 -24
- check_app/android/build/reports/problems/problems-report.html +0 -0
- check_app/android/gradle.properties +2 -2
- check_app/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- check_app/android/gradle/wrapper/gradle-wrapper.properties +5 -5
.gitattributes
CHANGED
|
@@ -1,10 +1,46 @@
|
|
| 1 |
-
*.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
check_app/android/.gradle/8.14/checksums/sha1-checksums.bin filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
check_app/android/.gradle/8.14/executionHistory/executionHistory.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
check_app/android/.gradle/8.14/fileHashes/fileHashes.bin filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
check_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
check_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
check_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
check_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
check_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
check_app/assets/images/app_logo_dark.jpg filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
check_app/assets/images/app_logo_light.jpg filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
check_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
check_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
check_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
check_app/web/favicon.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
check_app/web/icons/Icon-192.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
check_app/web/icons/Icon-512.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
check_app/web/icons/Icon-maskable-192.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
check_app/web/icons/Icon-maskable-512.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
check_app/windows/runner/resources/app_icon.ico filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
Icons/648286034_929277936354011_247561233351299912_n.jpg filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
Icons/648528435_1489135242834045_123474419086154405_n.jpg filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
Icons/IMG_7342.PNG filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
Icons/IMG_7343.PNG filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/scrape.yml
CHANGED
|
@@ -1,34 +1,34 @@
|
|
| 1 |
-
name: Daily News Scraper
|
| 2 |
-
|
| 3 |
-
on:
|
| 4 |
-
schedule:
|
| 5 |
-
# Runs every day at 00:00 UTC (8:00 AM PHT)
|
| 6 |
-
- cron: '0 0 * * *'
|
| 7 |
-
workflow_dispatch: # Allows manual trigger from GitHub UI
|
| 8 |
-
|
| 9 |
-
jobs:
|
| 10 |
-
scrape:
|
| 11 |
-
runs-on: ubuntu-latest
|
| 12 |
-
|
| 13 |
-
steps:
|
| 14 |
-
- name: Checkout repository
|
| 15 |
-
uses: actions/checkout@v4
|
| 16 |
-
|
| 17 |
-
- name: Set up Python
|
| 18 |
-
uses: actions/setup-python@v5
|
| 19 |
-
with:
|
| 20 |
-
python-version: '3.12'
|
| 21 |
-
|
| 22 |
-
- name: Install dependencies
|
| 23 |
-
run: pip install feedparser
|
| 24 |
-
|
| 25 |
-
- name: Run scraper
|
| 26 |
-
run: python scraper/news_scraper.py
|
| 27 |
-
|
| 28 |
-
- name: Commit updated database
|
| 29 |
-
run: |
|
| 30 |
-
git config user.name "GitHub Actions Bot"
|
| 31 |
-
git config user.email "actions@github.com"
|
| 32 |
-
git add data/news.db
|
| 33 |
-
git diff --staged --quiet || git commit -m "Auto-update: scraped news articles $(date -u +'%Y-%m-%d')"
|
| 34 |
-
git push
|
|
|
|
| 1 |
+
name: Daily News Scraper
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
schedule:
|
| 5 |
+
# Runs every day at 00:00 UTC (8:00 AM PHT)
|
| 6 |
+
- cron: '0 0 * * *'
|
| 7 |
+
workflow_dispatch: # Allows manual trigger from GitHub UI
|
| 8 |
+
|
| 9 |
+
jobs:
|
| 10 |
+
scrape:
|
| 11 |
+
runs-on: ubuntu-latest
|
| 12 |
+
|
| 13 |
+
steps:
|
| 14 |
+
- name: Checkout repository
|
| 15 |
+
uses: actions/checkout@v4
|
| 16 |
+
|
| 17 |
+
- name: Set up Python
|
| 18 |
+
uses: actions/setup-python@v5
|
| 19 |
+
with:
|
| 20 |
+
python-version: '3.12'
|
| 21 |
+
|
| 22 |
+
- name: Install dependencies
|
| 23 |
+
run: pip install feedparser
|
| 24 |
+
|
| 25 |
+
- name: Run scraper
|
| 26 |
+
run: python scraper/news_scraper.py
|
| 27 |
+
|
| 28 |
+
- name: Commit updated database
|
| 29 |
+
run: |
|
| 30 |
+
git config user.name "GitHub Actions Bot"
|
| 31 |
+
git config user.email "actions@github.com"
|
| 32 |
+
git add data/news.db
|
| 33 |
+
git diff --staged --quiet || git commit -m "Auto-update: scraped news articles $(date -u +'%Y-%m-%d')"
|
| 34 |
+
git push
|
.gitignore
CHANGED
|
@@ -1,35 +1,46 @@
|
|
| 1 |
-
# Virtual environment
|
| 2 |
-
venv/
|
| 3 |
-
.venv/
|
| 4 |
-
|
| 5 |
-
# Python
|
| 6 |
-
__pycache__/
|
| 7 |
-
*.pyc
|
| 8 |
-
*.pyo
|
| 9 |
-
*.egg-info/
|
| 10 |
-
dist/
|
| 11 |
-
build/
|
| 12 |
-
|
| 13 |
-
# IDE
|
| 14 |
-
.vscode/
|
| 15 |
-
.idea/
|
| 16 |
-
|
| 17 |
-
# OS
|
| 18 |
-
.DS_Store
|
| 19 |
-
Thumbs.db
|
| 20 |
-
|
| 21 |
-
# Environment
|
| 22 |
-
.env
|
| 23 |
-
|
| 24 |
-
# Large data files — too big for GitHub LFS free tier
|
| 25 |
-
# Download datasets separately / run train.py to regenerate
|
| 26 |
-
data/raw/
|
| 27 |
-
data/embeddings.npz
|
| 28 |
-
.pyre_configuration
|
| 29 |
-
|
| 30 |
-
# Trained model binaries —
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
data/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Virtual environment
|
| 2 |
+
venv/
|
| 3 |
+
.venv/
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__/
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.egg-info/
|
| 10 |
+
dist/
|
| 11 |
+
build/
|
| 12 |
+
|
| 13 |
+
# IDE
|
| 14 |
+
.vscode/
|
| 15 |
+
.idea/
|
| 16 |
+
|
| 17 |
+
# OS
|
| 18 |
+
.DS_Store
|
| 19 |
+
Thumbs.db
|
| 20 |
+
|
| 21 |
+
# Environment
|
| 22 |
+
.env
|
| 23 |
+
|
| 24 |
+
# Large data files — too big for GitHub LFS free tier
|
| 25 |
+
# Download datasets separately / run train.py to regenerate
|
| 26 |
+
data/raw/
|
| 27 |
+
data/embeddings.npz
|
| 28 |
+
.pyre_configuration
|
| 29 |
+
|
| 30 |
+
# Trained model binaries — tracked via Git LFS (see .gitattributes)
|
| 31 |
+
# Do NOT gitignore these: the HF Space needs them to start without retraining.
|
| 32 |
+
# data_models/*.pkl
|
| 33 |
+
|
| 34 |
+
# Augmented translation caches — regenerate with train.py
|
| 35 |
+
data/augmented_tl_fakes.csv
|
| 36 |
+
data/augmented_ceb_fakes.csv
|
| 37 |
+
|
| 38 |
+
# Temp deployment/sync scripts & artifacts (not needed in repo)
|
| 39 |
+
dummy.bin
|
| 40 |
+
fix_hf_models.py
|
| 41 |
+
hf_start.sh
|
| 42 |
+
hf_tfidf.pkl
|
| 43 |
+
restore_remaining_models.py
|
| 44 |
+
sync_all_hf_models.py
|
| 45 |
+
sync_source_to_hf.py
|
| 46 |
+
commitchecker.txt
|
DEPLOYMENT.md
CHANGED
|
@@ -1,71 +1,71 @@
|
|
| 1 |
-
# Deployment Guide
|
| 2 |
-
|
| 3 |
-
## ⚠️ Required Step After Every Fresh Deploy / Clone
|
| 4 |
-
|
| 5 |
-
The ML model files (`.pkl`) are **not stored in the repository** (binary files, too large for GitHub).
|
| 6 |
-
You must generate them **once on the server** after the first clone.
|
| 7 |
-
|
| 8 |
-
### Step 1 — Install dependencies
|
| 9 |
-
```bash
|
| 10 |
-
pip install -r requirements.txt
|
| 11 |
-
```
|
| 12 |
-
|
| 13 |
-
### Step 2 — Train the main model
|
| 14 |
-
```bash
|
| 15 |
-
python backend/train.py
|
| 16 |
-
```
|
| 17 |
-
This will generate the following files in `data_models/`:
|
| 18 |
-
- `rf_fakenews_model.pkl`
|
| 19 |
-
- `tfidf_fakenews.pkl`
|
| 20 |
-
- `stylo_scaler.pkl`
|
| 21 |
-
|
| 22 |
-
⏱ Takes approximately **10–20 minutes** depending on server specs.
|
| 23 |
-
📦 Requires ~**2 GB RAM** during training.
|
| 24 |
-
|
| 25 |
-
### Step 3 — (Optional) Train the Tagalog sub-model
|
| 26 |
-
```bash
|
| 27 |
-
python backend/train.py --tagalog-only
|
| 28 |
-
```
|
| 29 |
-
|
| 30 |
-
### Step 4 — Start the app normally
|
| 31 |
-
```bash
|
| 32 |
-
# Whatever your usual start command is, e.g.:
|
| 33 |
-
python app.py
|
| 34 |
-
# or
|
| 35 |
-
uvicorn app:app --host 0.0.0.0 --port 8000
|
| 36 |
-
```
|
| 37 |
-
|
| 38 |
-
---
|
| 39 |
-
|
| 40 |
-
## After the First Deploy
|
| 41 |
-
|
| 42 |
-
**You do NOT need to retrain on every redeploy.**
|
| 43 |
-
The `.pkl` files persist on the server. Future `git pull` + restart is enough.
|
| 44 |
-
|
| 45 |
-
Only retrain if:
|
| 46 |
-
- The server is wiped / re-imaged
|
| 47 |
-
- `backend/train.py` was updated with new features
|
| 48 |
-
- Model files were accidentally deleted
|
| 49 |
-
|
| 50 |
-
---
|
| 51 |
-
|
| 52 |
-
## Checking if Models Exist
|
| 53 |
-
|
| 54 |
-
```bash
|
| 55 |
-
ls data_models/*.pkl
|
| 56 |
-
```
|
| 57 |
-
|
| 58 |
-
Expected output (6 files):
|
| 59 |
-
```
|
| 60 |
-
data_models/rf_fakenews_model.pkl
|
| 61 |
-
data_models/rf_fakenews_tagalog.pkl
|
| 62 |
-
data_models/rf_fakenews_cebuano.pkl
|
| 63 |
-
data_models/tfidf_fakenews.pkl
|
| 64 |
-
data_models/tfidf_fakenews_tagalog.pkl
|
| 65 |
-
data_models/tfidf_fakenews_cebuano.pkl
|
| 66 |
-
data_models/stylo_scaler.pkl
|
| 67 |
-
data_models/stylo_scaler_tagalog.pkl
|
| 68 |
-
data_models/stylo_scaler_cebuano.pkl
|
| 69 |
-
```
|
| 70 |
-
|
| 71 |
-
If any are missing → run `python backend/train.py`.
|
|
|
|
| 1 |
+
# Deployment Guide
|
| 2 |
+
|
| 3 |
+
## ⚠️ Required Step After Every Fresh Deploy / Clone
|
| 4 |
+
|
| 5 |
+
The ML model files (`.pkl`) are **not stored in the repository** (binary files, too large for GitHub).
|
| 6 |
+
You must generate them **once on the server** after the first clone.
|
| 7 |
+
|
| 8 |
+
### Step 1 — Install dependencies
|
| 9 |
+
```bash
|
| 10 |
+
pip install -r requirements.txt
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### Step 2 — Train the main model
|
| 14 |
+
```bash
|
| 15 |
+
python backend/train.py
|
| 16 |
+
```
|
| 17 |
+
This will generate the following files in `data_models/`:
|
| 18 |
+
- `rf_fakenews_model.pkl`
|
| 19 |
+
- `tfidf_fakenews.pkl`
|
| 20 |
+
- `stylo_scaler.pkl`
|
| 21 |
+
|
| 22 |
+
⏱ Takes approximately **10–20 minutes** depending on server specs.
|
| 23 |
+
📦 Requires ~**2 GB RAM** during training.
|
| 24 |
+
|
| 25 |
+
### Step 3 — (Optional) Train the Tagalog sub-model
|
| 26 |
+
```bash
|
| 27 |
+
python backend/train.py --tagalog-only
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Step 4 — Start the app normally
|
| 31 |
+
```bash
|
| 32 |
+
# Whatever your usual start command is, e.g.:
|
| 33 |
+
python app.py
|
| 34 |
+
# or
|
| 35 |
+
uvicorn app:app --host 0.0.0.0 --port 8000
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## After the First Deploy
|
| 41 |
+
|
| 42 |
+
**You do NOT need to retrain on every redeploy.**
|
| 43 |
+
The `.pkl` files persist on the server. Future `git pull` + restart is enough.
|
| 44 |
+
|
| 45 |
+
Only retrain if:
|
| 46 |
+
- The server is wiped / re-imaged
|
| 47 |
+
- `backend/train.py` was updated with new features
|
| 48 |
+
- Model files were accidentally deleted
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
## Checking if Models Exist
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
ls data_models/*.pkl
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Expected output (6 files):
|
| 59 |
+
```
|
| 60 |
+
data_models/rf_fakenews_model.pkl
|
| 61 |
+
data_models/rf_fakenews_tagalog.pkl
|
| 62 |
+
data_models/rf_fakenews_cebuano.pkl
|
| 63 |
+
data_models/tfidf_fakenews.pkl
|
| 64 |
+
data_models/tfidf_fakenews_tagalog.pkl
|
| 65 |
+
data_models/tfidf_fakenews_cebuano.pkl
|
| 66 |
+
data_models/stylo_scaler.pkl
|
| 67 |
+
data_models/stylo_scaler_tagalog.pkl
|
| 68 |
+
data_models/stylo_scaler_cebuano.pkl
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
If any are missing → run `python backend/train.py`.
|
Dockerfile
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# Use the official Python base image
|
| 2 |
-
FROM python:3.10
|
| 3 |
|
| 4 |
# Set the working directory for root installations
|
| 5 |
WORKDIR /setup
|
|
@@ -7,20 +7,31 @@ WORKDIR /setup
|
|
| 7 |
# Copy requirements first to leverage Docker cache
|
| 8 |
COPY ./requirements.txt /setup/requirements.txt
|
| 9 |
|
| 10 |
-
# Install python packages
|
| 11 |
-
RUN pip install --no-cache-dir --upgrade pip
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
# Hugging Face Spaces require running as a non-root user for security.
|
| 15 |
# Set up a new user named "user" with user ID 1000
|
| 16 |
RUN useradd -m -u 1000 user
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# Switch to the "user" user
|
| 19 |
USER user
|
| 20 |
|
| 21 |
# Set home to the user's home directory
|
| 22 |
ENV HOME=/home/user \
|
| 23 |
-
PATH=/home/user/.local/bin:$PATH
|
|
|
|
| 24 |
|
| 25 |
# Set the working directory to the user's home directory
|
| 26 |
WORKDIR $HOME/app
|
|
@@ -28,10 +39,12 @@ WORKDIR $HOME/app
|
|
| 28 |
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
| 29 |
COPY --chown=user . $HOME/app
|
| 30 |
|
|
|
|
|
|
|
|
|
|
| 31 |
# HuggingFace Spaces requires port 7860
|
| 32 |
ENV PORT=7860
|
| 33 |
EXPOSE 7860
|
| 34 |
|
| 35 |
-
#
|
| 36 |
-
CMD
|
| 37 |
-
|
|
|
|
| 1 |
# Use the official Python base image
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
|
| 4 |
# Set the working directory for root installations
|
| 5 |
WORKDIR /setup
|
|
|
|
| 7 |
# Copy requirements first to leverage Docker cache
|
| 8 |
COPY ./requirements.txt /setup/requirements.txt
|
| 9 |
|
| 10 |
+
# Install python packages (single RUN to reduce layers)
|
| 11 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 12 |
+
pip install --no-cache-dir -r /setup/requirements.txt
|
| 13 |
+
|
| 14 |
+
# Pre-download the MiniLM model during BUILD so it's baked into the image.
|
| 15 |
+
# This saves ~2-3 minutes on every cold start.
|
| 16 |
+
ENV HF_HOME=/setup/hf_cache
|
| 17 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')"
|
| 18 |
|
| 19 |
# Hugging Face Spaces require running as a non-root user for security.
|
| 20 |
# Set up a new user named "user" with user ID 1000
|
| 21 |
RUN useradd -m -u 1000 user
|
| 22 |
|
| 23 |
+
# Move the cached model to the user's home so it's accessible after user switch
|
| 24 |
+
RUN mkdir -p /home/user/.cache && \
|
| 25 |
+
cp -r /setup/hf_cache /home/user/.cache/huggingface && \
|
| 26 |
+
chown -R user:user /home/user/.cache
|
| 27 |
+
|
| 28 |
# Switch to the "user" user
|
| 29 |
USER user
|
| 30 |
|
| 31 |
# Set home to the user's home directory
|
| 32 |
ENV HOME=/home/user \
|
| 33 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 34 |
+
HF_HOME=/home/user/.cache/huggingface
|
| 35 |
|
| 36 |
# Set the working directory to the user's home directory
|
| 37 |
WORKDIR $HOME/app
|
|
|
|
| 39 |
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
| 40 |
COPY --chown=user . $HOME/app
|
| 41 |
|
| 42 |
+
# Make the startup script executable
|
| 43 |
+
RUN chmod +x start.sh
|
| 44 |
+
|
| 45 |
# HuggingFace Spaces requires port 7860
|
| 46 |
ENV PORT=7860
|
| 47 |
EXPOSE 7860
|
| 48 |
|
| 49 |
+
# Startup: auto-train models if missing, then start Uvicorn
|
| 50 |
+
CMD ["bash", "start.sh"]
|
|
|
add_cebuano_data.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Cebuano Fake News Data Entry Tool
|
| 3 |
+
===================================
|
| 4 |
+
Manually input Cebuano fake news articles into
|
| 5 |
+
data/raw/augmented_ceb_fakes.csv for model training.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
python add_cebuano_data.py
|
| 9 |
+
|
| 10 |
+
Features:
|
| 11 |
+
- Interactive CLI with menu options
|
| 12 |
+
- Paste single-line or multi-line articles
|
| 13 |
+
- Preview entries before saving
|
| 14 |
+
- View existing dataset stats
|
| 15 |
+
- Duplicate detection (warns if near-identical text exists)
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import csv
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
import textwrap
|
| 22 |
+
from datetime import datetime
|
| 23 |
+
|
| 24 |
+
# ── Paths ──
|
| 25 |
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 26 |
+
CSV_PATH = os.path.join(SCRIPT_DIR, "data", "raw", "augmented_ceb_fakes.csv")
|
| 27 |
+
BACKUP_DIR = os.path.join(SCRIPT_DIR, "data", "raw", ".backups")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _count_existing_rows() -> int:
|
| 31 |
+
"""Count how many articles are already in the CSV."""
|
| 32 |
+
if not os.path.exists(CSV_PATH):
|
| 33 |
+
return 0
|
| 34 |
+
with open(CSV_PATH, "r", encoding="utf-8") as f:
|
| 35 |
+
reader = csv.reader(f)
|
| 36 |
+
header = next(reader, None)
|
| 37 |
+
return sum(1 for _ in reader)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _load_existing_articles() -> list[str]:
|
| 41 |
+
"""Load all existing article texts (for duplicate checking)."""
|
| 42 |
+
articles = []
|
| 43 |
+
if not os.path.exists(CSV_PATH):
|
| 44 |
+
return articles
|
| 45 |
+
with open(CSV_PATH, "r", encoding="utf-8") as f:
|
| 46 |
+
reader = csv.reader(f)
|
| 47 |
+
next(reader, None) # skip header
|
| 48 |
+
for row in reader:
|
| 49 |
+
if row:
|
| 50 |
+
articles.append(row[0].strip().lower())
|
| 51 |
+
return articles
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _is_duplicate(new_text: str, existing: list[str], threshold: int = 50) -> bool:
|
| 55 |
+
"""Check if the first `threshold` characters match any existing article."""
|
| 56 |
+
snippet = new_text.strip().lower()[:threshold]
|
| 57 |
+
if len(snippet) < 20:
|
| 58 |
+
return False
|
| 59 |
+
for art in existing:
|
| 60 |
+
if art[:threshold] == snippet:
|
| 61 |
+
return True
|
| 62 |
+
return False
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _backup_csv():
|
| 66 |
+
"""Create a timestamped backup of the current CSV."""
|
| 67 |
+
if not os.path.exists(CSV_PATH):
|
| 68 |
+
return
|
| 69 |
+
os.makedirs(BACKUP_DIR, exist_ok=True)
|
| 70 |
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 71 |
+
backup_path = os.path.join(BACKUP_DIR, f"augmented_ceb_fakes_{ts}.csv")
|
| 72 |
+
with open(CSV_PATH, "r", encoding="utf-8") as src:
|
| 73 |
+
with open(backup_path, "w", encoding="utf-8", newline="") as dst:
|
| 74 |
+
dst.write(src.read())
|
| 75 |
+
print(f" Backup saved: {os.path.relpath(backup_path, SCRIPT_DIR)}")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _ensure_csv_exists():
|
| 79 |
+
"""Create the CSV with header if it doesn't exist yet."""
|
| 80 |
+
if not os.path.exists(CSV_PATH):
|
| 81 |
+
os.makedirs(os.path.dirname(CSV_PATH), exist_ok=True)
|
| 82 |
+
with open(CSV_PATH, "w", encoding="utf-8", newline="") as f:
|
| 83 |
+
writer = csv.writer(f)
|
| 84 |
+
writer.writerow(["article"])
|
| 85 |
+
print(f" Created new CSV: {CSV_PATH}")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _append_articles(articles: list[str]):
|
| 89 |
+
"""Append a list of article texts to the CSV."""
|
| 90 |
+
with open(CSV_PATH, "a", encoding="utf-8", newline="") as f:
|
| 91 |
+
writer = csv.writer(f)
|
| 92 |
+
for text in articles:
|
| 93 |
+
writer.writerow([text])
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _preview_text(text: str, width: int = 80) -> str:
|
| 97 |
+
"""Return a truncated preview of article text."""
|
| 98 |
+
preview = text.replace("\n", " ").strip()
|
| 99 |
+
if len(preview) > width:
|
| 100 |
+
return preview[:width] + "..."
|
| 101 |
+
return preview
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# ── CLI Display ──
|
| 105 |
+
|
| 106 |
+
def print_header():
|
| 107 |
+
print()
|
| 108 |
+
print("=" * 60)
|
| 109 |
+
print(" CEBUANO FAKE NEWS — DATA ENTRY TOOL")
|
| 110 |
+
print("=" * 60)
|
| 111 |
+
print(f" CSV: {os.path.relpath(CSV_PATH, SCRIPT_DIR)}")
|
| 112 |
+
print(f" Existing articles: {_count_existing_rows():,}")
|
| 113 |
+
print("=" * 60)
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def print_menu():
|
| 117 |
+
print()
|
| 118 |
+
print(" [1] Add a single article")
|
| 119 |
+
print(" [2] Add multiple articles (batch mode)")
|
| 120 |
+
print(" [3] View dataset stats")
|
| 121 |
+
print(" [4] View last 5 entries")
|
| 122 |
+
print(" [5] Exit")
|
| 123 |
+
print()
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def input_single_article() -> str | None:
|
| 127 |
+
"""Prompt the user to type/paste a single article.
|
| 128 |
+
|
| 129 |
+
Supports multi-line input: type on multiple lines, then enter
|
| 130 |
+
a blank line to finish.
|
| 131 |
+
"""
|
| 132 |
+
print()
|
| 133 |
+
print(" Paste or type the Cebuano fake news article below.")
|
| 134 |
+
print(" (Enter a blank line when done, or type 'cancel' to abort)")
|
| 135 |
+
print(" " + "-" * 50)
|
| 136 |
+
|
| 137 |
+
lines = []
|
| 138 |
+
while True:
|
| 139 |
+
try:
|
| 140 |
+
line = input(" > ")
|
| 141 |
+
except EOFError:
|
| 142 |
+
break
|
| 143 |
+
if line.strip().lower() == "cancel":
|
| 144 |
+
return None
|
| 145 |
+
if line.strip() == "" and lines:
|
| 146 |
+
break
|
| 147 |
+
lines.append(line)
|
| 148 |
+
|
| 149 |
+
text = " ".join(lines).strip()
|
| 150 |
+
if not text:
|
| 151 |
+
print(" (empty input — skipped)")
|
| 152 |
+
return None
|
| 153 |
+
return text
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def add_single_article():
|
| 157 |
+
"""Menu option 1: add one article."""
|
| 158 |
+
_ensure_csv_exists()
|
| 159 |
+
existing = _load_existing_articles()
|
| 160 |
+
|
| 161 |
+
text = input_single_article()
|
| 162 |
+
if text is None:
|
| 163 |
+
print(" Cancelled.")
|
| 164 |
+
return
|
| 165 |
+
|
| 166 |
+
# Duplicate check
|
| 167 |
+
if _is_duplicate(text, existing):
|
| 168 |
+
print()
|
| 169 |
+
print(" ⚠ WARNING: This article appears to be a duplicate!")
|
| 170 |
+
confirm = input(" Add anyway? (y/n): ").strip().lower()
|
| 171 |
+
if confirm != "y":
|
| 172 |
+
print(" Skipped.")
|
| 173 |
+
return
|
| 174 |
+
|
| 175 |
+
# Preview and confirm
|
| 176 |
+
print()
|
| 177 |
+
print(" Preview:")
|
| 178 |
+
print(f" {_preview_text(text, 100)}")
|
| 179 |
+
print(f" Length: {len(text):,} chars, {len(text.split()):,} words")
|
| 180 |
+
print()
|
| 181 |
+
confirm = input(" Save this article? (y/n): ").strip().lower()
|
| 182 |
+
if confirm != "y":
|
| 183 |
+
print(" Discarded.")
|
| 184 |
+
return
|
| 185 |
+
|
| 186 |
+
_append_articles([text])
|
| 187 |
+
print(f" ✓ Article saved! Total articles now: {_count_existing_rows():,}")
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def add_batch_articles():
|
| 191 |
+
"""Menu option 2: add multiple articles in batch."""
|
| 192 |
+
_ensure_csv_exists()
|
| 193 |
+
existing = _load_existing_articles()
|
| 194 |
+
|
| 195 |
+
print()
|
| 196 |
+
print(" BATCH MODE — Enter one article per prompt.")
|
| 197 |
+
print(" Type 'done' when finished, 'cancel' to discard all.")
|
| 198 |
+
print()
|
| 199 |
+
|
| 200 |
+
pending: list[str] = []
|
| 201 |
+
article_num = 1
|
| 202 |
+
|
| 203 |
+
while True:
|
| 204 |
+
print(f" --- Article #{article_num} ---")
|
| 205 |
+
text = input_single_article()
|
| 206 |
+
|
| 207 |
+
if text is None:
|
| 208 |
+
# Check if user typed cancel
|
| 209 |
+
break
|
| 210 |
+
|
| 211 |
+
if _is_duplicate(text, existing):
|
| 212 |
+
print(" ⚠ Possible duplicate — skipping this one.")
|
| 213 |
+
continue
|
| 214 |
+
|
| 215 |
+
pending.append(text)
|
| 216 |
+
print(f" ✓ Queued ({len(pending)} pending)")
|
| 217 |
+
article_num += 1
|
| 218 |
+
|
| 219 |
+
cont = input(" Add another? (y/n/done): ").strip().lower()
|
| 220 |
+
if cont in ("n", "done"):
|
| 221 |
+
break
|
| 222 |
+
|
| 223 |
+
if not pending:
|
| 224 |
+
print(" No articles to save.")
|
| 225 |
+
return
|
| 226 |
+
|
| 227 |
+
# Preview all
|
| 228 |
+
print()
|
| 229 |
+
print(f" === {len(pending)} articles ready to save ===")
|
| 230 |
+
for i, art in enumerate(pending, 1):
|
| 231 |
+
print(f" {i}. {_preview_text(art, 70)}")
|
| 232 |
+
print()
|
| 233 |
+
|
| 234 |
+
confirm = input(f" Save all {len(pending)} articles? (y/n): ").strip().lower()
|
| 235 |
+
if confirm != "y":
|
| 236 |
+
print(" Discarded all.")
|
| 237 |
+
return
|
| 238 |
+
|
| 239 |
+
_backup_csv()
|
| 240 |
+
_append_articles(pending)
|
| 241 |
+
print(f" ✓ {len(pending)} articles saved! Total now: {_count_existing_rows():,}")
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def show_stats():
|
| 245 |
+
"""Menu option 3: show dataset statistics."""
|
| 246 |
+
if not os.path.exists(CSV_PATH):
|
| 247 |
+
print(" CSV not found — no data yet.")
|
| 248 |
+
return
|
| 249 |
+
|
| 250 |
+
articles = _load_existing_articles()
|
| 251 |
+
total = len(articles)
|
| 252 |
+
if total == 0:
|
| 253 |
+
print(" Dataset is empty.")
|
| 254 |
+
return
|
| 255 |
+
|
| 256 |
+
word_counts = [len(a.split()) for a in articles]
|
| 257 |
+
char_counts = [len(a) for a in articles]
|
| 258 |
+
|
| 259 |
+
print()
|
| 260 |
+
print(" ╔══════════════════════════════════════╗")
|
| 261 |
+
print(" ║ DATASET STATISTICS ║")
|
| 262 |
+
print(" ╠══════════════════════════════════════╣")
|
| 263 |
+
print(f" ║ Total articles: {total:>10,} ║")
|
| 264 |
+
print(f" ║ Avg words/article: {sum(word_counts)//total:>10,} ║")
|
| 265 |
+
print(f" ║ Min words: {min(word_counts):>10,} ║")
|
| 266 |
+
print(f" ║ Max words: {max(word_counts):>10,} ║")
|
| 267 |
+
print(f" ║ Avg chars/article: {sum(char_counts)//total:>10,} ║")
|
| 268 |
+
print(f" ║ CSV size: {os.path.getsize(CSV_PATH):>10,} B ║")
|
| 269 |
+
print(" ╚══════════════════════════════════════╝")
|
| 270 |
+
print()
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def show_last_entries():
|
| 274 |
+
"""Menu option 4: show last 5 entries."""
|
| 275 |
+
if not os.path.exists(CSV_PATH):
|
| 276 |
+
print(" CSV not found — no data yet.")
|
| 277 |
+
return
|
| 278 |
+
|
| 279 |
+
articles = []
|
| 280 |
+
with open(CSV_PATH, "r", encoding="utf-8") as f:
|
| 281 |
+
reader = csv.reader(f)
|
| 282 |
+
next(reader, None)
|
| 283 |
+
for row in reader:
|
| 284 |
+
if row:
|
| 285 |
+
articles.append(row[0])
|
| 286 |
+
|
| 287 |
+
if not articles:
|
| 288 |
+
print(" No articles in dataset.")
|
| 289 |
+
return
|
| 290 |
+
|
| 291 |
+
last5 = articles[-5:]
|
| 292 |
+
print()
|
| 293 |
+
print(f" Last {len(last5)} entries (of {len(articles)} total):")
|
| 294 |
+
print(" " + "-" * 50)
|
| 295 |
+
for i, art in enumerate(last5, len(articles) - len(last5) + 1):
|
| 296 |
+
print(f" #{i}: {_preview_text(art, 70)}")
|
| 297 |
+
print()
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# ── Main Loop ──
|
| 301 |
+
|
| 302 |
+
def main():
|
| 303 |
+
print_header()
|
| 304 |
+
|
| 305 |
+
while True:
|
| 306 |
+
print_menu()
|
| 307 |
+
try:
|
| 308 |
+
choice = input(" Choose an option [1-5]: ").strip()
|
| 309 |
+
except (EOFError, KeyboardInterrupt):
|
| 310 |
+
print("\n Goodbye!")
|
| 311 |
+
break
|
| 312 |
+
|
| 313 |
+
if choice == "1":
|
| 314 |
+
add_single_article()
|
| 315 |
+
elif choice == "2":
|
| 316 |
+
add_batch_articles()
|
| 317 |
+
elif choice == "3":
|
| 318 |
+
show_stats()
|
| 319 |
+
elif choice == "4":
|
| 320 |
+
show_last_entries()
|
| 321 |
+
elif choice == "5":
|
| 322 |
+
print(" Goodbye!")
|
| 323 |
+
break
|
| 324 |
+
else:
|
| 325 |
+
print(" Invalid choice, try again.")
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
if __name__ == "__main__":
|
| 329 |
+
main()
|
api/main.py
CHANGED
|
@@ -433,6 +433,36 @@ async def delete_account(
|
|
| 433 |
|
| 434 |
|
| 435 |
# ── Fact-check endpoint ──────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
@app.post("/api/check")
|
| 437 |
async def check_article(
|
| 438 |
payload: CheckRequest,
|
|
|
|
| 433 |
|
| 434 |
|
| 435 |
# ── Fact-check endpoint ──────────────────────────────────────────────
|
| 436 |
+
class AnalyzeStructureRequest(BaseModel):
|
| 437 |
+
text: str
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
@app.post("/api/analyze-structure")
|
| 441 |
+
async def api_analyze_structure(payload: AnalyzeStructureRequest) -> Dict[str, Any]:
|
| 442 |
+
"""
|
| 443 |
+
Exposes the structural (inverted pyramid) analysis algorithm
|
| 444 |
+
to the frontend Live Writing Guide without running full ML/URL checks.
|
| 445 |
+
"""
|
| 446 |
+
if not payload.text or len(payload.text.strip()) < 30:
|
| 447 |
+
return {
|
| 448 |
+
"language": "unknown",
|
| 449 |
+
"formalism_score": 0,
|
| 450 |
+
"assessment": "Text too short for structure analysis.",
|
| 451 |
+
"recommendations": [],
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
from checker.internal.structure_analyzer import analyze_structure
|
| 455 |
+
try:
|
| 456 |
+
res = analyze_structure(payload.text)
|
| 457 |
+
return res
|
| 458 |
+
except Exception as exc:
|
| 459 |
+
logger.error("Error in analyze_structure: %s", exc)
|
| 460 |
+
raise HTTPException(
|
| 461 |
+
status_code=500,
|
| 462 |
+
detail=f"Structure analysis failed: {exc}"
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
@app.post("/api/check")
|
| 467 |
async def check_article(
|
| 468 |
payload: CheckRequest,
|
backend/analyze_languages.py
CHANGED
|
@@ -1,275 +1,275 @@
|
|
| 1 |
-
"""
|
| 2 |
-
backend/analyze_languages.py
|
| 3 |
-
============================
|
| 4 |
-
Analyzes the language distribution of every training dataset and prints a
|
| 5 |
-
summary table broken down by dataset and language.
|
| 6 |
-
|
| 7 |
-
Uses langdetect (already pulled in transitively; install with `pip install langdetect`
|
| 8 |
-
if missing).
|
| 9 |
-
|
| 10 |
-
Usage:
|
| 11 |
-
python backend/analyze_languages.py
|
| 12 |
-
"""
|
| 13 |
-
|
| 14 |
-
import sys
|
| 15 |
-
import os
|
| 16 |
-
import random
|
| 17 |
-
from collections import Counter
|
| 18 |
-
|
| 19 |
-
import pandas as pd
|
| 20 |
-
|
| 21 |
-
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
-
sys.path.insert(0, PROJECT_ROOT)
|
| 23 |
-
|
| 24 |
-
# ── Try importing langdetect ──────────────────────────────────────────────────
|
| 25 |
-
try:
|
| 26 |
-
from langdetect import detect, LangDetectException
|
| 27 |
-
from langdetect import DetectorFactory
|
| 28 |
-
|
| 29 |
-
DetectorFactory.seed = 42 # make detection deterministic
|
| 30 |
-
HAS_LANGDETECT = True
|
| 31 |
-
except ImportError:
|
| 32 |
-
HAS_LANGDETECT = False
|
| 33 |
-
print("WARNING: langdetect not installed. Run: pip install langdetect")
|
| 34 |
-
print(" Falling back to heuristic detection only.\n")
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# ── Simple Tagalog heuristic (fast, used as a sanity check) ──────────────────
|
| 38 |
-
_TAGALOG_MARKERS = {
|
| 39 |
-
"ang",
|
| 40 |
-
"ng",
|
| 41 |
-
"mga",
|
| 42 |
-
"sa",
|
| 43 |
-
"na",
|
| 44 |
-
"ay",
|
| 45 |
-
"at",
|
| 46 |
-
"hindi",
|
| 47 |
-
"ako",
|
| 48 |
-
"siya",
|
| 49 |
-
"nila",
|
| 50 |
-
"niya",
|
| 51 |
-
"ito",
|
| 52 |
-
"iyon",
|
| 53 |
-
"kung",
|
| 54 |
-
"para",
|
| 55 |
-
"nang",
|
| 56 |
-
"din",
|
| 57 |
-
"rin",
|
| 58 |
-
"kaya",
|
| 59 |
-
"pero",
|
| 60 |
-
"dahil",
|
| 61 |
-
"ayon",
|
| 62 |
-
"noon",
|
| 63 |
-
"ngayon",
|
| 64 |
-
"dito",
|
| 65 |
-
"doon",
|
| 66 |
-
"sinabi",
|
| 67 |
-
"sinasabi",
|
| 68 |
-
"nagpapatunay",
|
| 69 |
-
"araw",
|
| 70 |
-
"taon",
|
| 71 |
-
"buwan",
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
_CEBUANO_MARKERS = {
|
| 75 |
-
"ug",
|
| 76 |
-
"nga",
|
| 77 |
-
"ang",
|
| 78 |
-
"sa",
|
| 79 |
-
"si",
|
| 80 |
-
"nag",
|
| 81 |
-
"mao",
|
| 82 |
-
"kang",
|
| 83 |
-
"usab",
|
| 84 |
-
"man",
|
| 85 |
-
"dayon",
|
| 86 |
-
"gyud",
|
| 87 |
-
"kaayo",
|
| 88 |
-
"lang",
|
| 89 |
-
"pud",
|
| 90 |
-
"adto",
|
| 91 |
-
"kini",
|
| 92 |
-
"sila",
|
| 93 |
-
"niadtong",
|
| 94 |
-
"gitawag",
|
| 95 |
-
"giingon",
|
| 96 |
-
"matud",
|
| 97 |
-
"nasayran",
|
| 98 |
-
"gidakop",
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def _heuristic_lang(text: str) -> str:
|
| 103 |
-
"""Very rough heuristic: count Tagalog vs Cebuano marker hits."""
|
| 104 |
-
words = set(text.lower().split())
|
| 105 |
-
tl_hits = len(words & _TAGALOG_MARKERS)
|
| 106 |
-
ceb_hits = len(words & _CEBUANO_MARKERS)
|
| 107 |
-
if tl_hits == 0 and ceb_hits == 0:
|
| 108 |
-
return "unknown"
|
| 109 |
-
return "tl" if tl_hits >= ceb_hits else "ceb"
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def detect_lang(text: str) -> str:
|
| 113 |
-
"""Detect language; falls back to heuristic if langdetect fails."""
|
| 114 |
-
if not text or not isinstance(text, str) or len(text.split()) < 5:
|
| 115 |
-
return "unknown"
|
| 116 |
-
if HAS_LANGDETECT:
|
| 117 |
-
try:
|
| 118 |
-
return detect(text[:500]) # only need a snippet
|
| 119 |
-
except LangDetectException:
|
| 120 |
-
pass
|
| 121 |
-
return _heuristic_lang(text)
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
# ── Dataset loaders (mirrors train.py logic) ─────────────────────────────────
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def load_datasets_raw() -> list[tuple[str, pd.DataFrame]]:
|
| 128 |
-
"""Return list of (name, df) pairs, df has columns: article, label."""
|
| 129 |
-
result = []
|
| 130 |
-
|
| 131 |
-
# 1. jcblaise/fake_news_filipino
|
| 132 |
-
csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
|
| 133 |
-
if os.path.exists(csv1):
|
| 134 |
-
# The CSV has a `<<<<<<< HEAD` git conflict marker on line 1;
|
| 135 |
-
# skiprows=1 makes pandas treat the real header (line 2) as the header.
|
| 136 |
-
df1 = pd.read_csv(csv1, skiprows=1)
|
| 137 |
-
# Keep only rows where both columns are valid
|
| 138 |
-
if "article" in df1.columns and "label" in df1.columns:
|
| 139 |
-
df1 = df1[["article", "label"]].dropna()
|
| 140 |
-
# Drop any remaining git conflict marker rows
|
| 141 |
-
df1 = df1[
|
| 142 |
-
~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))
|
| 143 |
-
]
|
| 144 |
-
result.append(("jcblaise/fake_news_filipino", df1))
|
| 145 |
-
print(f" [1] Loaded jcblaise: {len(df1)} articles")
|
| 146 |
-
else:
|
| 147 |
-
print(f" [1] jcblaise not found at {csv1}, skipping.")
|
| 148 |
-
|
| 149 |
-
# 2. Philippine Fake News Corpus
|
| 150 |
-
csv2 = os.path.join(
|
| 151 |
-
PROJECT_ROOT,
|
| 152 |
-
"data",
|
| 153 |
-
"raw",
|
| 154 |
-
"philippine_corpus",
|
| 155 |
-
"Philippine Fake News Corpus.csv",
|
| 156 |
-
)
|
| 157 |
-
if os.path.exists(csv2):
|
| 158 |
-
# Same git conflict marker fix — skip line 1
|
| 159 |
-
df2 = pd.read_csv(csv2, skiprows=1)
|
| 160 |
-
df2 = df2.rename(columns={"Content": "article"})
|
| 161 |
-
df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
|
| 162 |
-
df2 = df2[["article", "label"]].dropna()
|
| 163 |
-
df2 = df2[~df2["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
|
| 164 |
-
result.append(("Philippine Fake News Corpus", df2))
|
| 165 |
-
print(f" [2] Loaded Philippine Corpus: {len(df2)} articles")
|
| 166 |
-
else:
|
| 167 |
-
print(f" [2] Philippine Corpus not found at {csv2}, skipping.")
|
| 168 |
-
|
| 169 |
-
# 3. CebuaNER — definitively Cebuano, no need to run detection on every row
|
| 170 |
-
try:
|
| 171 |
-
from datasets import load_dataset
|
| 172 |
-
|
| 173 |
-
print(" [3] Downloading CebuaNER...")
|
| 174 |
-
ds = load_dataset("josephimperial/CebuaNER")
|
| 175 |
-
sentences = []
|
| 176 |
-
for split_data in ds.values():
|
| 177 |
-
for row in split_data:
|
| 178 |
-
# CebuaNER schema: {'text': str} — one sentence per row
|
| 179 |
-
text = row.get("text") or " ".join(
|
| 180 |
-
row.get("tokens") or row.get("words") or []
|
| 181 |
-
)
|
| 182 |
-
if text and text.strip():
|
| 183 |
-
sentences.append(text.strip())
|
| 184 |
-
|
| 185 |
-
MIN_CHUNK = 30
|
| 186 |
-
articles, buf, buf_tok = [], [], 0
|
| 187 |
-
for s in sentences:
|
| 188 |
-
buf.append(s)
|
| 189 |
-
buf_tok += len(s.split())
|
| 190 |
-
if buf_tok >= MIN_CHUNK:
|
| 191 |
-
articles.append(" ".join(buf))
|
| 192 |
-
buf, buf_tok = [], 0
|
| 193 |
-
if buf:
|
| 194 |
-
articles.append(" ".join(buf))
|
| 195 |
-
|
| 196 |
-
df3 = pd.DataFrame({"article": articles, "label": 0})
|
| 197 |
-
result.append(("josephimperial/CebuaNER", df3))
|
| 198 |
-
print(f" [3] Loaded CebuaNER: {len(df3)} chunks")
|
| 199 |
-
except ImportError:
|
| 200 |
-
print(" [3] 'datasets' not installed, skipping CebuaNER.")
|
| 201 |
-
except Exception as exc:
|
| 202 |
-
print(f" [3] CebuaNER error: {exc}")
|
| 203 |
-
|
| 204 |
-
return result
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
# ── Main analysis ─────────────────────────────────────────────────────────────
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def analyze(sample_size: int = 500):
|
| 211 |
-
print("=" * 60)
|
| 212 |
-
print(" LANGUAGE DISTRIBUTION ANALYSIS")
|
| 213 |
-
print("=" * 60)
|
| 214 |
-
print("\nLoading datasets...\n")
|
| 215 |
-
|
| 216 |
-
datasets = load_datasets_raw()
|
| 217 |
-
if not datasets:
|
| 218 |
-
print("No datasets found.")
|
| 219 |
-
return
|
| 220 |
-
|
| 221 |
-
grand_total = 0
|
| 222 |
-
grand_tl = 0
|
| 223 |
-
|
| 224 |
-
for name, df in datasets:
|
| 225 |
-
total = len(df)
|
| 226 |
-
grand_total += total
|
| 227 |
-
|
| 228 |
-
# CebuaNER is definitively Cebuano — skip expensive detection
|
| 229 |
-
if "CebuaNER" in name:
|
| 230 |
-
lang_counts = Counter({"ceb": total})
|
| 231 |
-
print(f"\n [{name}]")
|
| 232 |
-
print(f" Total : {total:,}")
|
| 233 |
-
print(
|
| 234 |
-
f" ceb : {total:,} (100.0%) [source is Cebuano news by definition]"
|
| 235 |
-
)
|
| 236 |
-
continue
|
| 237 |
-
|
| 238 |
-
# Sample for speed on large datasets
|
| 239 |
-
if total > sample_size:
|
| 240 |
-
df_sample = df.sample(n=sample_size, random_state=42)
|
| 241 |
-
sampled = True
|
| 242 |
-
else:
|
| 243 |
-
df_sample = df
|
| 244 |
-
sampled = False
|
| 245 |
-
|
| 246 |
-
lang_counts: Counter = Counter()
|
| 247 |
-
for text in df_sample["article"]:
|
| 248 |
-
lang_counts[detect_lang(str(text))] += 1
|
| 249 |
-
|
| 250 |
-
# Scale up sample counts to full dataset size
|
| 251 |
-
if sampled:
|
| 252 |
-
scale = total / sample_size
|
| 253 |
-
lang_counts = Counter({k: int(v * scale) for k, v in lang_counts.items()})
|
| 254 |
-
|
| 255 |
-
tl_count = lang_counts.get("tl", 0) + lang_counts.get("fil", 0)
|
| 256 |
-
grand_tl += tl_count
|
| 257 |
-
|
| 258 |
-
print(f"\n [{name}]")
|
| 259 |
-
print(
|
| 260 |
-
f" Total : {total:,}" + (" (estimate from sample)" if sampled else "")
|
| 261 |
-
)
|
| 262 |
-
for lang, cnt in lang_counts.most_common():
|
| 263 |
-
pct = cnt / total * 100
|
| 264 |
-
print(f" {lang:<8}: {cnt:>6,} ({pct:.1f}%)")
|
| 265 |
-
|
| 266 |
-
print("\n" + "=" * 60)
|
| 267 |
-
print(f" GRAND TOTAL articles : {grand_total:,}")
|
| 268 |
-
print(f" Estimated Tagalog : {grand_tl:,} ({grand_tl/grand_total*100:.1f}%)")
|
| 269 |
-
print("=" * 60)
|
| 270 |
-
print("\nNote: 'tl'=Tagalog/Filipino, 'ceb'=Cebuano, 'en'=English")
|
| 271 |
-
print(" langdetect may mis-classify short or code-switched texts.")
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
if __name__ == "__main__":
|
| 275 |
-
analyze()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
backend/analyze_languages.py
|
| 3 |
+
============================
|
| 4 |
+
Analyzes the language distribution of every training dataset and prints a
|
| 5 |
+
summary table broken down by dataset and language.
|
| 6 |
+
|
| 7 |
+
Uses langdetect (already pulled in transitively; install with `pip install langdetect`
|
| 8 |
+
if missing).
|
| 9 |
+
|
| 10 |
+
Usage:
|
| 11 |
+
python backend/analyze_languages.py
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import sys
|
| 15 |
+
import os
|
| 16 |
+
import random
|
| 17 |
+
from collections import Counter
|
| 18 |
+
|
| 19 |
+
import pandas as pd
|
| 20 |
+
|
| 21 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 22 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 23 |
+
|
| 24 |
+
# ── Try importing langdetect ──────────────────────────────────────────────────
|
| 25 |
+
try:
|
| 26 |
+
from langdetect import detect, LangDetectException
|
| 27 |
+
from langdetect import DetectorFactory
|
| 28 |
+
|
| 29 |
+
DetectorFactory.seed = 42 # make detection deterministic
|
| 30 |
+
HAS_LANGDETECT = True
|
| 31 |
+
except ImportError:
|
| 32 |
+
HAS_LANGDETECT = False
|
| 33 |
+
print("WARNING: langdetect not installed. Run: pip install langdetect")
|
| 34 |
+
print(" Falling back to heuristic detection only.\n")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# ── Simple Tagalog heuristic (fast, used as a sanity check) ──────────────────
|
| 38 |
+
_TAGALOG_MARKERS = {
|
| 39 |
+
"ang",
|
| 40 |
+
"ng",
|
| 41 |
+
"mga",
|
| 42 |
+
"sa",
|
| 43 |
+
"na",
|
| 44 |
+
"ay",
|
| 45 |
+
"at",
|
| 46 |
+
"hindi",
|
| 47 |
+
"ako",
|
| 48 |
+
"siya",
|
| 49 |
+
"nila",
|
| 50 |
+
"niya",
|
| 51 |
+
"ito",
|
| 52 |
+
"iyon",
|
| 53 |
+
"kung",
|
| 54 |
+
"para",
|
| 55 |
+
"nang",
|
| 56 |
+
"din",
|
| 57 |
+
"rin",
|
| 58 |
+
"kaya",
|
| 59 |
+
"pero",
|
| 60 |
+
"dahil",
|
| 61 |
+
"ayon",
|
| 62 |
+
"noon",
|
| 63 |
+
"ngayon",
|
| 64 |
+
"dito",
|
| 65 |
+
"doon",
|
| 66 |
+
"sinabi",
|
| 67 |
+
"sinasabi",
|
| 68 |
+
"nagpapatunay",
|
| 69 |
+
"araw",
|
| 70 |
+
"taon",
|
| 71 |
+
"buwan",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
_CEBUANO_MARKERS = {
|
| 75 |
+
"ug",
|
| 76 |
+
"nga",
|
| 77 |
+
"ang",
|
| 78 |
+
"sa",
|
| 79 |
+
"si",
|
| 80 |
+
"nag",
|
| 81 |
+
"mao",
|
| 82 |
+
"kang",
|
| 83 |
+
"usab",
|
| 84 |
+
"man",
|
| 85 |
+
"dayon",
|
| 86 |
+
"gyud",
|
| 87 |
+
"kaayo",
|
| 88 |
+
"lang",
|
| 89 |
+
"pud",
|
| 90 |
+
"adto",
|
| 91 |
+
"kini",
|
| 92 |
+
"sila",
|
| 93 |
+
"niadtong",
|
| 94 |
+
"gitawag",
|
| 95 |
+
"giingon",
|
| 96 |
+
"matud",
|
| 97 |
+
"nasayran",
|
| 98 |
+
"gidakop",
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _heuristic_lang(text: str) -> str:
|
| 103 |
+
"""Very rough heuristic: count Tagalog vs Cebuano marker hits."""
|
| 104 |
+
words = set(text.lower().split())
|
| 105 |
+
tl_hits = len(words & _TAGALOG_MARKERS)
|
| 106 |
+
ceb_hits = len(words & _CEBUANO_MARKERS)
|
| 107 |
+
if tl_hits == 0 and ceb_hits == 0:
|
| 108 |
+
return "unknown"
|
| 109 |
+
return "tl" if tl_hits >= ceb_hits else "ceb"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def detect_lang(text: str) -> str:
|
| 113 |
+
"""Detect language; falls back to heuristic if langdetect fails."""
|
| 114 |
+
if not text or not isinstance(text, str) or len(text.split()) < 5:
|
| 115 |
+
return "unknown"
|
| 116 |
+
if HAS_LANGDETECT:
|
| 117 |
+
try:
|
| 118 |
+
return detect(text[:500]) # only need a snippet
|
| 119 |
+
except LangDetectException:
|
| 120 |
+
pass
|
| 121 |
+
return _heuristic_lang(text)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# ── Dataset loaders (mirrors train.py logic) ─────────────────────────────────
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def load_datasets_raw() -> list[tuple[str, pd.DataFrame]]:
|
| 128 |
+
"""Return list of (name, df) pairs, df has columns: article, label."""
|
| 129 |
+
result = []
|
| 130 |
+
|
| 131 |
+
# 1. jcblaise/fake_news_filipino
|
| 132 |
+
csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
|
| 133 |
+
if os.path.exists(csv1):
|
| 134 |
+
# The CSV has a `<<<<<<< HEAD` git conflict marker on line 1;
|
| 135 |
+
# skiprows=1 makes pandas treat the real header (line 2) as the header.
|
| 136 |
+
df1 = pd.read_csv(csv1, skiprows=1)
|
| 137 |
+
# Keep only rows where both columns are valid
|
| 138 |
+
if "article" in df1.columns and "label" in df1.columns:
|
| 139 |
+
df1 = df1[["article", "label"]].dropna()
|
| 140 |
+
# Drop any remaining git conflict marker rows
|
| 141 |
+
df1 = df1[
|
| 142 |
+
~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))
|
| 143 |
+
]
|
| 144 |
+
result.append(("jcblaise/fake_news_filipino", df1))
|
| 145 |
+
print(f" [1] Loaded jcblaise: {len(df1)} articles")
|
| 146 |
+
else:
|
| 147 |
+
print(f" [1] jcblaise not found at {csv1}, skipping.")
|
| 148 |
+
|
| 149 |
+
# 2. Philippine Fake News Corpus
|
| 150 |
+
csv2 = os.path.join(
|
| 151 |
+
PROJECT_ROOT,
|
| 152 |
+
"data",
|
| 153 |
+
"raw",
|
| 154 |
+
"philippine_corpus",
|
| 155 |
+
"Philippine Fake News Corpus.csv",
|
| 156 |
+
)
|
| 157 |
+
if os.path.exists(csv2):
|
| 158 |
+
# Same git conflict marker fix — skip line 1
|
| 159 |
+
df2 = pd.read_csv(csv2, skiprows=1)
|
| 160 |
+
df2 = df2.rename(columns={"Content": "article"})
|
| 161 |
+
df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
|
| 162 |
+
df2 = df2[["article", "label"]].dropna()
|
| 163 |
+
df2 = df2[~df2["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
|
| 164 |
+
result.append(("Philippine Fake News Corpus", df2))
|
| 165 |
+
print(f" [2] Loaded Philippine Corpus: {len(df2)} articles")
|
| 166 |
+
else:
|
| 167 |
+
print(f" [2] Philippine Corpus not found at {csv2}, skipping.")
|
| 168 |
+
|
| 169 |
+
# 3. CebuaNER — definitively Cebuano, no need to run detection on every row
|
| 170 |
+
try:
|
| 171 |
+
from datasets import load_dataset
|
| 172 |
+
|
| 173 |
+
print(" [3] Downloading CebuaNER...")
|
| 174 |
+
ds = load_dataset("josephimperial/CebuaNER")
|
| 175 |
+
sentences = []
|
| 176 |
+
for split_data in ds.values():
|
| 177 |
+
for row in split_data:
|
| 178 |
+
# CebuaNER schema: {'text': str} — one sentence per row
|
| 179 |
+
text = row.get("text") or " ".join(
|
| 180 |
+
row.get("tokens") or row.get("words") or []
|
| 181 |
+
)
|
| 182 |
+
if text and text.strip():
|
| 183 |
+
sentences.append(text.strip())
|
| 184 |
+
|
| 185 |
+
MIN_CHUNK = 30
|
| 186 |
+
articles, buf, buf_tok = [], [], 0
|
| 187 |
+
for s in sentences:
|
| 188 |
+
buf.append(s)
|
| 189 |
+
buf_tok += len(s.split())
|
| 190 |
+
if buf_tok >= MIN_CHUNK:
|
| 191 |
+
articles.append(" ".join(buf))
|
| 192 |
+
buf, buf_tok = [], 0
|
| 193 |
+
if buf:
|
| 194 |
+
articles.append(" ".join(buf))
|
| 195 |
+
|
| 196 |
+
df3 = pd.DataFrame({"article": articles, "label": 0})
|
| 197 |
+
result.append(("josephimperial/CebuaNER", df3))
|
| 198 |
+
print(f" [3] Loaded CebuaNER: {len(df3)} chunks")
|
| 199 |
+
except ImportError:
|
| 200 |
+
print(" [3] 'datasets' not installed, skipping CebuaNER.")
|
| 201 |
+
except Exception as exc:
|
| 202 |
+
print(f" [3] CebuaNER error: {exc}")
|
| 203 |
+
|
| 204 |
+
return result
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
# ── Main analysis ─────────────────────────────────────────────────────────────
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def analyze(sample_size: int = 500):
|
| 211 |
+
print("=" * 60)
|
| 212 |
+
print(" LANGUAGE DISTRIBUTION ANALYSIS")
|
| 213 |
+
print("=" * 60)
|
| 214 |
+
print("\nLoading datasets...\n")
|
| 215 |
+
|
| 216 |
+
datasets = load_datasets_raw()
|
| 217 |
+
if not datasets:
|
| 218 |
+
print("No datasets found.")
|
| 219 |
+
return
|
| 220 |
+
|
| 221 |
+
grand_total = 0
|
| 222 |
+
grand_tl = 0
|
| 223 |
+
|
| 224 |
+
for name, df in datasets:
|
| 225 |
+
total = len(df)
|
| 226 |
+
grand_total += total
|
| 227 |
+
|
| 228 |
+
# CebuaNER is definitively Cebuano — skip expensive detection
|
| 229 |
+
if "CebuaNER" in name:
|
| 230 |
+
lang_counts = Counter({"ceb": total})
|
| 231 |
+
print(f"\n [{name}]")
|
| 232 |
+
print(f" Total : {total:,}")
|
| 233 |
+
print(
|
| 234 |
+
f" ceb : {total:,} (100.0%) [source is Cebuano news by definition]"
|
| 235 |
+
)
|
| 236 |
+
continue
|
| 237 |
+
|
| 238 |
+
# Sample for speed on large datasets
|
| 239 |
+
if total > sample_size:
|
| 240 |
+
df_sample = df.sample(n=sample_size, random_state=42)
|
| 241 |
+
sampled = True
|
| 242 |
+
else:
|
| 243 |
+
df_sample = df
|
| 244 |
+
sampled = False
|
| 245 |
+
|
| 246 |
+
lang_counts: Counter = Counter()
|
| 247 |
+
for text in df_sample["article"]:
|
| 248 |
+
lang_counts[detect_lang(str(text))] += 1
|
| 249 |
+
|
| 250 |
+
# Scale up sample counts to full dataset size
|
| 251 |
+
if sampled:
|
| 252 |
+
scale = total / sample_size
|
| 253 |
+
lang_counts = Counter({k: int(v * scale) for k, v in lang_counts.items()})
|
| 254 |
+
|
| 255 |
+
tl_count = lang_counts.get("tl", 0) + lang_counts.get("fil", 0)
|
| 256 |
+
grand_tl += tl_count
|
| 257 |
+
|
| 258 |
+
print(f"\n [{name}]")
|
| 259 |
+
print(
|
| 260 |
+
f" Total : {total:,}" + (" (estimate from sample)" if sampled else "")
|
| 261 |
+
)
|
| 262 |
+
for lang, cnt in lang_counts.most_common():
|
| 263 |
+
pct = cnt / total * 100
|
| 264 |
+
print(f" {lang:<8}: {cnt:>6,} ({pct:.1f}%)")
|
| 265 |
+
|
| 266 |
+
print("\n" + "=" * 60)
|
| 267 |
+
print(f" GRAND TOTAL articles : {grand_total:,}")
|
| 268 |
+
print(f" Estimated Tagalog : {grand_tl:,} ({grand_tl/grand_total*100:.1f}%)")
|
| 269 |
+
print("=" * 60)
|
| 270 |
+
print("\nNote: 'tl'=Tagalog/Filipino, 'ceb'=Cebuano, 'en'=English")
|
| 271 |
+
print(" langdetect may mis-classify short or code-switched texts.")
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
if __name__ == "__main__":
|
| 275 |
+
analyze()
|
backend/benchmark.py
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Benchmark — Comparing Your RF Model Against Classic ML Baselines
|
| 3 |
+
================================================================
|
| 4 |
+
Trains and evaluates multiple fake-news classifiers on the SAME
|
| 5 |
+
locked test split so the comparison is completely fair.
|
| 6 |
+
|
| 7 |
+
Models compared
|
| 8 |
+
---------------
|
| 9 |
+
1. Naive Bayes (TF-IDF only) — simple baseline
|
| 10 |
+
2. Logistic Regression (TF-IDF only) — strong linear baseline
|
| 11 |
+
3. Linear SVM (TF-IDF only) — often best for text
|
| 12 |
+
4. Random Forest (TF-IDF only) — RF without embeddings
|
| 13 |
+
5. ★ YOUR MODEL ★ (TF-IDF + MiniLM + Stylo) — YOUR hybrid RF
|
| 14 |
+
|
| 15 |
+
Metrics reported (per model, per class + weighted avg)
|
| 16 |
+
-------------------------------------------------------
|
| 17 |
+
• Accuracy
|
| 18 |
+
• Precision (Fake class)
|
| 19 |
+
• Recall (Fake class)
|
| 20 |
+
• F1 Score (weighted)
|
| 21 |
+
• AUC-ROC
|
| 22 |
+
|
| 23 |
+
Output
|
| 24 |
+
------
|
| 25 |
+
evaluation_results/benchmark_table.txt — plaintext comparison table
|
| 26 |
+
evaluation_results/benchmark_chart.png — bar chart (Accuracy + F1)
|
| 27 |
+
evaluation_results/benchmark_roc.png — ROC curves for all models
|
| 28 |
+
|
| 29 |
+
Usage
|
| 30 |
+
-----
|
| 31 |
+
python backend/benchmark.py
|
| 32 |
+
python backend/benchmark.py --mode tagalog # Tagalog dataset only
|
| 33 |
+
python backend/benchmark.py --mode cebuano # Cebuano dataset only
|
| 34 |
+
python backend/benchmark.py --mode mixed # All languages (default)
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
import sys
|
| 38 |
+
import os
|
| 39 |
+
import re
|
| 40 |
+
import time
|
| 41 |
+
import argparse
|
| 42 |
+
import warnings
|
| 43 |
+
|
| 44 |
+
warnings.filterwarnings("ignore")
|
| 45 |
+
|
| 46 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 47 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 48 |
+
|
| 49 |
+
import numpy as np
|
| 50 |
+
import pandas as pd
|
| 51 |
+
|
| 52 |
+
import matplotlib
|
| 53 |
+
matplotlib.use("Agg")
|
| 54 |
+
import matplotlib.pyplot as plt
|
| 55 |
+
import matplotlib.patches as mpatches
|
| 56 |
+
|
| 57 |
+
from scipy.sparse import hstack, csr_matrix
|
| 58 |
+
|
| 59 |
+
from sklearn.model_selection import train_test_split
|
| 60 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 61 |
+
from sklearn.preprocessing import StandardScaler
|
| 62 |
+
from sklearn.pipeline import Pipeline
|
| 63 |
+
|
| 64 |
+
# Classifiers
|
| 65 |
+
from sklearn.naive_bayes import MultinomialNB
|
| 66 |
+
from sklearn.linear_model import LogisticRegression
|
| 67 |
+
from sklearn.svm import LinearSVC
|
| 68 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 69 |
+
from sklearn.calibration import CalibratedClassifierCV
|
| 70 |
+
|
| 71 |
+
# Metrics
|
| 72 |
+
from sklearn.metrics import (
|
| 73 |
+
accuracy_score,
|
| 74 |
+
classification_report,
|
| 75 |
+
confusion_matrix,
|
| 76 |
+
roc_auc_score,
|
| 77 |
+
roc_curve,
|
| 78 |
+
f1_score,
|
| 79 |
+
precision_score,
|
| 80 |
+
recall_score,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# Re-use helpers from train.py (keep feature extraction identical)
|
| 84 |
+
from backend.train import (
|
| 85 |
+
load_fake_news_dataset,
|
| 86 |
+
preprocess,
|
| 87 |
+
clean_text,
|
| 88 |
+
extract_stylometric_features,
|
| 89 |
+
get_minilm_model,
|
| 90 |
+
STYLOMETRIC_FEATURE_NAMES,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# ── Paths ──────────────────────────────────────────────────────────────────
|
| 94 |
+
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 98 |
+
# Feature builders
|
| 99 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def build_tfidf_features(X_train, X_test):
|
| 103 |
+
"""Plain TF-IDF (used by all baseline models)."""
|
| 104 |
+
tfidf = TfidfVectorizer(
|
| 105 |
+
max_features=15_000,
|
| 106 |
+
ngram_range=(1, 3),
|
| 107 |
+
min_df=2,
|
| 108 |
+
max_df=0.95,
|
| 109 |
+
sublinear_tf=True,
|
| 110 |
+
)
|
| 111 |
+
X_tr = tfidf.fit_transform(X_train)
|
| 112 |
+
X_te = tfidf.transform(X_test)
|
| 113 |
+
return X_tr, X_te, tfidf
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def build_hybrid_features(X_train, X_test):
|
| 117 |
+
"""TF-IDF + MiniLM embeddings + stylometric (your full pipeline)."""
|
| 118 |
+
print(" [hybrid] Fitting TF-IDF …")
|
| 119 |
+
tfidf = TfidfVectorizer(
|
| 120 |
+
max_features=15_000,
|
| 121 |
+
ngram_range=(1, 3),
|
| 122 |
+
min_df=2,
|
| 123 |
+
max_df=0.95,
|
| 124 |
+
sublinear_tf=True,
|
| 125 |
+
)
|
| 126 |
+
X_tr_tfidf = tfidf.fit_transform(X_train)
|
| 127 |
+
X_te_tfidf = tfidf.transform(X_test)
|
| 128 |
+
|
| 129 |
+
print(" [hybrid] Encoding with MiniLM …")
|
| 130 |
+
minilm = get_minilm_model()
|
| 131 |
+
emb_train = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
|
| 132 |
+
emb_test = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
|
| 133 |
+
|
| 134 |
+
print(f" [hybrid] Extracting {len(STYLOMETRIC_FEATURE_NAMES)} stylometric features …")
|
| 135 |
+
stylo_train = np.array([extract_stylometric_features(t) for t in X_train])
|
| 136 |
+
stylo_test = np.array([extract_stylometric_features(t) for t in X_test])
|
| 137 |
+
|
| 138 |
+
scaler = StandardScaler()
|
| 139 |
+
stylo_train_sc = scaler.fit_transform(stylo_train)
|
| 140 |
+
stylo_test_sc = scaler.transform(stylo_test)
|
| 141 |
+
|
| 142 |
+
X_tr = hstack([X_tr_tfidf, csr_matrix(emb_train), csr_matrix(stylo_train_sc)])
|
| 143 |
+
X_te = hstack([X_te_tfidf, csr_matrix(emb_test), csr_matrix(stylo_test_sc)])
|
| 144 |
+
|
| 145 |
+
n_total = X_tr.shape[1]
|
| 146 |
+
print(
|
| 147 |
+
f" [hybrid] Feature dimensions: {n_total} "
|
| 148 |
+
f"(TF-IDF: {X_tr_tfidf.shape[1]} + MiniLM: 384 + Stylo: {len(STYLOMETRIC_FEATURE_NAMES)})"
|
| 149 |
+
)
|
| 150 |
+
return X_tr, X_te
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 154 |
+
# Compute metrics for one model
|
| 155 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def evaluate(name, model, X_test, y_test, proba=None):
|
| 159 |
+
"""Return a metrics dict for one fitted model."""
|
| 160 |
+
y_pred = model.predict(X_test)
|
| 161 |
+
|
| 162 |
+
acc = accuracy_score(y_test, y_pred)
|
| 163 |
+
prec_fake = precision_score(y_test, y_pred, pos_label=1, zero_division=0)
|
| 164 |
+
rec_fake = recall_score(y_test, y_pred, pos_label=1, zero_division=0)
|
| 165 |
+
f1_weighted = f1_score(y_test, y_pred, average="weighted", zero_division=0)
|
| 166 |
+
f1_fake = f1_score(y_test, y_pred, pos_label=1, zero_division=0)
|
| 167 |
+
|
| 168 |
+
# AUC-ROC (needs probability scores)
|
| 169 |
+
if proba is not None:
|
| 170 |
+
try:
|
| 171 |
+
auc = roc_auc_score(y_test, proba[:, 1])
|
| 172 |
+
except Exception:
|
| 173 |
+
auc = float("nan")
|
| 174 |
+
else:
|
| 175 |
+
auc = float("nan")
|
| 176 |
+
|
| 177 |
+
report = classification_report(
|
| 178 |
+
y_test, y_pred,
|
| 179 |
+
target_names=["Real", "Fake"],
|
| 180 |
+
zero_division=0,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
return {
|
| 184 |
+
"name": name,
|
| 185 |
+
"accuracy": acc,
|
| 186 |
+
"precision": prec_fake,
|
| 187 |
+
"recall": rec_fake,
|
| 188 |
+
"f1_weighted": f1_weighted,
|
| 189 |
+
"f1_fake": f1_fake,
|
| 190 |
+
"auc": auc,
|
| 191 |
+
"report": report,
|
| 192 |
+
"y_pred": y_pred,
|
| 193 |
+
"proba": proba,
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 198 |
+
# Bar chart
|
| 199 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def plot_bar_chart(results, output_path):
|
| 203 |
+
"""Side-by-side bar chart: Accuracy vs F1 (weighted) vs AUC-ROC."""
|
| 204 |
+
names = [r["name"] for r in results]
|
| 205 |
+
accs = [r["accuracy"] for r in results]
|
| 206 |
+
f1s = [r["f1_weighted"] for r in results]
|
| 207 |
+
aucs = [r["auc"] for r in results]
|
| 208 |
+
|
| 209 |
+
x = np.arange(len(names))
|
| 210 |
+
width = 0.26
|
| 211 |
+
|
| 212 |
+
fig, ax = plt.subplots(figsize=(max(10, len(names) * 2.2), 6))
|
| 213 |
+
|
| 214 |
+
# Color highlight for YOUR model (last entry)
|
| 215 |
+
bar_colors_acc = ["#2196F3"] * (len(names) - 1) + ["#E91E63"]
|
| 216 |
+
bar_colors_f1 = ["#4CAF50"] * (len(names) - 1) + ["#FF5722"]
|
| 217 |
+
bar_colors_auc = ["#9C27B0"] * (len(names) - 1) + ["#FF9800"]
|
| 218 |
+
|
| 219 |
+
b1 = ax.bar(x - width, accs, width, color=bar_colors_acc, edgecolor="black", lw=0.5, label="Accuracy")
|
| 220 |
+
b2 = ax.bar(x, f1s, width, color=bar_colors_f1, edgecolor="black", lw=0.5, label="F1 Weighted")
|
| 221 |
+
b3 = ax.bar(x + width, aucs, width, color=bar_colors_auc, edgecolor="black", lw=0.5, label="AUC-ROC")
|
| 222 |
+
|
| 223 |
+
# Value labels
|
| 224 |
+
for bars in (b1, b2, b3):
|
| 225 |
+
for bar in bars:
|
| 226 |
+
h = bar.get_height()
|
| 227 |
+
if not np.isnan(h):
|
| 228 |
+
ax.text(
|
| 229 |
+
bar.get_x() + bar.get_width() / 2,
|
| 230 |
+
h + 0.005,
|
| 231 |
+
f"{h:.3f}",
|
| 232 |
+
ha="center", va="bottom", fontsize=7.5, fontweight="bold",
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
ax.set_xticks(x)
|
| 236 |
+
ax.set_xticklabels(names, rotation=12, ha="right", fontsize=10)
|
| 237 |
+
ax.set_ylim(0, 1.12)
|
| 238 |
+
ax.set_ylabel("Score", fontsize=12)
|
| 239 |
+
ax.set_title(
|
| 240 |
+
"Benchmark: Your RF Model vs. Classic ML Baselines\n"
|
| 241 |
+
"(Highlighted in pink/orange = Your Model)",
|
| 242 |
+
fontsize=13, fontweight="bold",
|
| 243 |
+
)
|
| 244 |
+
ax.axhline(y=0.80, color="gray", linestyle="--", alpha=0.4, linewidth=1)
|
| 245 |
+
ax.text(len(names) - 0.5, 0.805, "80% threshold", color="gray", fontsize=8)
|
| 246 |
+
|
| 247 |
+
patch_yours = mpatches.Patch(color="#E91E63", label="★ Your Hybrid RF (Accuracy)")
|
| 248 |
+
ax.legend(handles=[*ax.get_legend_handles_labels()[0][:3], patch_yours], fontsize=9)
|
| 249 |
+
|
| 250 |
+
plt.tight_layout()
|
| 251 |
+
fig.savefig(output_path, dpi=150, bbox_inches="tight")
|
| 252 |
+
plt.close(fig)
|
| 253 |
+
print(f" Saved: {output_path}")
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 257 |
+
# ROC curve chart
|
| 258 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def plot_roc_curves(results, y_test, output_path):
|
| 262 |
+
"""Overlay ROC curves for all models that have probability scores."""
|
| 263 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 264 |
+
|
| 265 |
+
COLORS = [
|
| 266 |
+
"#2196F3", "#4CAF50", "#FF9800", "#9C27B0",
|
| 267 |
+
"#E91E63", "#00BCD4", "#F44336", "#8BC34A",
|
| 268 |
+
]
|
| 269 |
+
|
| 270 |
+
for i, r in enumerate(results):
|
| 271 |
+
if r["proba"] is None or np.isnan(r["auc"]):
|
| 272 |
+
continue
|
| 273 |
+
fpr, tpr, _ = roc_curve(y_test, r["proba"][:, 1], pos_label=1)
|
| 274 |
+
lw = 2.5 if "★" in r["name"] else 1.5
|
| 275 |
+
dash = "-" if "★" in r["name"] else "--"
|
| 276 |
+
ax.plot(
|
| 277 |
+
fpr, tpr,
|
| 278 |
+
color=COLORS[i % len(COLORS)],
|
| 279 |
+
lw=lw, linestyle=dash,
|
| 280 |
+
label=f"{r['name']} (AUC={r['auc']:.3f})",
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
ax.plot([0, 1], [0, 1], "k:", lw=1, label="Random (AUC=0.500)")
|
| 284 |
+
ax.set_xlim([0.0, 1.0])
|
| 285 |
+
ax.set_ylim([0.0, 1.05])
|
| 286 |
+
ax.set_xlabel("False Positive Rate", fontsize=12)
|
| 287 |
+
ax.set_ylabel("True Positive Rate", fontsize=12)
|
| 288 |
+
ax.set_title("ROC Curves — Fake-News Detection Benchmark", fontsize=13, fontweight="bold")
|
| 289 |
+
ax.legend(loc="lower right", fontsize=9)
|
| 290 |
+
ax.grid(alpha=0.3)
|
| 291 |
+
|
| 292 |
+
plt.tight_layout()
|
| 293 |
+
fig.savefig(output_path, dpi=150, bbox_inches="tight")
|
| 294 |
+
plt.close(fig)
|
| 295 |
+
print(f" Saved: {output_path}")
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 299 |
+
# Plaintext summary table
|
| 300 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def save_table(results, output_path, mode, n_train, n_test):
|
| 304 |
+
"""Write a neatly formatted comparison table to disk and stdout."""
|
| 305 |
+
lines = []
|
| 306 |
+
sep = "=" * 90
|
| 307 |
+
|
| 308 |
+
lines.append(sep)
|
| 309 |
+
lines.append(" BENCHMARK RESULTS — Fake-News Detection (Filipino / Cebuano)")
|
| 310 |
+
lines.append(f" Mode: {mode.upper()} | Train: {n_train:,} samples | Test: {n_test:,} samples")
|
| 311 |
+
lines.append(sep)
|
| 312 |
+
lines.append("")
|
| 313 |
+
|
| 314 |
+
header = (
|
| 315 |
+
f" {'Model':<35} {'Accuracy':>9} {'Prec(Fk)':>10} {'Rec(Fk)':>9} "
|
| 316 |
+
f"{'F1(Wtd)':>9} {'F1(Fk)':>8} {'AUC-ROC':>9}"
|
| 317 |
+
)
|
| 318 |
+
lines.append(header)
|
| 319 |
+
lines.append(" " + "-" * 88)
|
| 320 |
+
|
| 321 |
+
for r in results:
|
| 322 |
+
auc_str = f"{r['auc']:.4f}" if not np.isnan(r["auc"]) else " N/A "
|
| 323 |
+
marker = " ★" if "★" in r["name"] else " "
|
| 324 |
+
row = (
|
| 325 |
+
f"{marker} {r['name']:<32} "
|
| 326 |
+
f"{r['accuracy']:>9.4f} "
|
| 327 |
+
f"{r['precision']:>10.4f} "
|
| 328 |
+
f"{r['recall']:>9.4f} "
|
| 329 |
+
f"{r['f1_weighted']:>9.4f} "
|
| 330 |
+
f"{r['f1_fake']:>8.4f} "
|
| 331 |
+
f"{auc_str:>9}"
|
| 332 |
+
)
|
| 333 |
+
lines.append(row)
|
| 334 |
+
|
| 335 |
+
lines.append("")
|
| 336 |
+
lines.append(sep)
|
| 337 |
+
lines.append(" DETAILED CLASSIFICATION REPORTS")
|
| 338 |
+
lines.append(sep)
|
| 339 |
+
|
| 340 |
+
for r in results:
|
| 341 |
+
lines.append("")
|
| 342 |
+
lines.append(f" ── {r['name']} ──────────────────────────────────────────────")
|
| 343 |
+
for ln in r["report"].splitlines():
|
| 344 |
+
lines.append(f" {ln}")
|
| 345 |
+
|
| 346 |
+
text = "\n".join(lines)
|
| 347 |
+
print(text)
|
| 348 |
+
|
| 349 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 350 |
+
f.write(text)
|
| 351 |
+
print(f"\n Saved: {output_path}")
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 355 |
+
# Main
|
| 356 |
+
# ───────────────────────────────────────────────────────────────────────────
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def main():
|
| 360 |
+
parser = argparse.ArgumentParser(description="Benchmark fake-news classifiers.")
|
| 361 |
+
parser.add_argument(
|
| 362 |
+
"--mode",
|
| 363 |
+
choices=["mixed", "tagalog", "cebuano"],
|
| 364 |
+
default="mixed",
|
| 365 |
+
help="Which language subset to benchmark on (default: mixed).",
|
| 366 |
+
)
|
| 367 |
+
parser.add_argument(
|
| 368 |
+
"--test-size",
|
| 369 |
+
type=float,
|
| 370 |
+
default=0.20,
|
| 371 |
+
help="Fraction of data to hold out as the locked test set (default: 0.20).",
|
| 372 |
+
)
|
| 373 |
+
parser.add_argument(
|
| 374 |
+
"--skip-minilm",
|
| 375 |
+
action="store_true",
|
| 376 |
+
default=False,
|
| 377 |
+
help="Skip your hybrid RF model (useful if MiniLM is too slow for a quick check).",
|
| 378 |
+
)
|
| 379 |
+
args = parser.parse_args()
|
| 380 |
+
|
| 381 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 382 |
+
|
| 383 |
+
print("=" * 60)
|
| 384 |
+
print(" FAKE-NEWS BENCHMARK")
|
| 385 |
+
print(f" Mode: {args.mode.upper()} | Test size: {args.test_size:.0%}")
|
| 386 |
+
print("=" * 60)
|
| 387 |
+
|
| 388 |
+
# ── 1. Load & preprocess dataset ─────────────────────────────────────
|
| 389 |
+
tagalog_only = args.mode == "tagalog"
|
| 390 |
+
cebuano_only = args.mode == "cebuano"
|
| 391 |
+
|
| 392 |
+
df = load_fake_news_dataset(tagalog_only=tagalog_only, cebuano_only=cebuano_only)
|
| 393 |
+
X_all, y_all = preprocess(df, undersample=False, oversample=True)
|
| 394 |
+
|
| 395 |
+
# ── 2. Locked test split (same seed → reproducible) ──────────────────
|
| 396 |
+
print(f"\nCreating locked test split ({1 - args.test_size:.0%} train / {args.test_size:.0%} test) …")
|
| 397 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 398 |
+
X_all, y_all,
|
| 399 |
+
test_size=args.test_size,
|
| 400 |
+
random_state=42,
|
| 401 |
+
stratify=y_all,
|
| 402 |
+
)
|
| 403 |
+
print(f" Train: {len(X_train):,} | Test: {len(X_test):,}")
|
| 404 |
+
print(f" Test distribution — Real: {y_test.count(0):,}, Fake: {y_test.count(1):,}")
|
| 405 |
+
|
| 406 |
+
y_train_arr = np.array(y_train)
|
| 407 |
+
y_test_arr = np.array(y_test)
|
| 408 |
+
|
| 409 |
+
# ── 3. Build TF-IDF features (shared by baseline models) ─────────────
|
| 410 |
+
print("\nBuilding TF-IDF features for baseline models …")
|
| 411 |
+
X_tr_tfidf, X_te_tfidf, tfidf = build_tfidf_features(X_train, X_test)
|
| 412 |
+
print(f" TF-IDF shape: {X_tr_tfidf.shape}")
|
| 413 |
+
|
| 414 |
+
# ── 4. Train and evaluate each model ─────────────────────────────────
|
| 415 |
+
results = []
|
| 416 |
+
|
| 417 |
+
# ── 4a. Naive Bayes (TF-IDF, no negative values — shift by min) ──────
|
| 418 |
+
print("\n[1/5] Naive Bayes (TF-IDF) …")
|
| 419 |
+
t0 = time.time()
|
| 420 |
+
nb = MultinomialNB(alpha=1.0)
|
| 421 |
+
# MultinomialNB needs non-negative input; TF-IDF with sublinear_tf is ok
|
| 422 |
+
nb.fit(X_tr_tfidf, y_train_arr)
|
| 423 |
+
nb_proba = nb.predict_proba(X_te_tfidf)
|
| 424 |
+
elapsed = time.time() - t0
|
| 425 |
+
res_nb = evaluate("Naive Bayes (TF-IDF)", nb, X_te_tfidf, y_test_arr, nb_proba)
|
| 426 |
+
res_nb["train_time"] = elapsed
|
| 427 |
+
results.append(res_nb)
|
| 428 |
+
print(f" Accuracy: {res_nb['accuracy']:.4f} | F1 Weighted: {res_nb['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
|
| 429 |
+
|
| 430 |
+
# ── 4b. Logistic Regression (TF-IDF) ─────────────────────────────────
|
| 431 |
+
print("\n[2/5] Logistic Regression (TF-IDF) …")
|
| 432 |
+
t0 = time.time()
|
| 433 |
+
lr = LogisticRegression(
|
| 434 |
+
max_iter=1000,
|
| 435 |
+
class_weight="balanced",
|
| 436 |
+
solver="saga",
|
| 437 |
+
C=1.0,
|
| 438 |
+
random_state=42,
|
| 439 |
+
n_jobs=-1,
|
| 440 |
+
)
|
| 441 |
+
lr.fit(X_tr_tfidf, y_train_arr)
|
| 442 |
+
lr_proba = lr.predict_proba(X_te_tfidf)
|
| 443 |
+
elapsed = time.time() - t0
|
| 444 |
+
res_lr = evaluate("Logistic Regression (TF-IDF)", lr, X_te_tfidf, y_test_arr, lr_proba)
|
| 445 |
+
res_lr["train_time"] = elapsed
|
| 446 |
+
results.append(res_lr)
|
| 447 |
+
print(f" Accuracy: {res_lr['accuracy']:.4f} | F1 Weighted: {res_lr['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
|
| 448 |
+
|
| 449 |
+
# ── 4c. Linear SVM (TF-IDF) ──────────────────────────────────────────
|
| 450 |
+
print("\n[3/5] Linear SVM (TF-IDF) …")
|
| 451 |
+
t0 = time.time()
|
| 452 |
+
svm = CalibratedClassifierCV(
|
| 453 |
+
LinearSVC(class_weight="balanced", max_iter=2000, random_state=42),
|
| 454 |
+
cv=3,
|
| 455 |
+
)
|
| 456 |
+
svm.fit(X_tr_tfidf, y_train_arr)
|
| 457 |
+
svm_proba = svm.predict_proba(X_te_tfidf)
|
| 458 |
+
elapsed = time.time() - t0
|
| 459 |
+
res_svm = evaluate("Linear SVM (TF-IDF)", svm, X_te_tfidf, y_test_arr, svm_proba)
|
| 460 |
+
res_svm["train_time"] = elapsed
|
| 461 |
+
results.append(res_svm)
|
| 462 |
+
print(f" Accuracy: {res_svm['accuracy']:.4f} | F1 Weighted: {res_svm['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
|
| 463 |
+
|
| 464 |
+
# ── 4d. Random Forest (TF-IDF only — no embeddings) ──────────────────
|
| 465 |
+
print("\n[4/5] Random Forest (TF-IDF only — no embeddings) …")
|
| 466 |
+
t0 = time.time()
|
| 467 |
+
rf_tf = RandomForestClassifier(
|
| 468 |
+
n_estimators=300,
|
| 469 |
+
max_depth=20,
|
| 470 |
+
min_samples_split=5,
|
| 471 |
+
min_samples_leaf=3,
|
| 472 |
+
class_weight="balanced",
|
| 473 |
+
n_jobs=-1,
|
| 474 |
+
random_state=42,
|
| 475 |
+
)
|
| 476 |
+
rf_tf.fit(X_tr_tfidf, y_train_arr)
|
| 477 |
+
rf_tf_proba = rf_tf.predict_proba(X_te_tfidf)
|
| 478 |
+
elapsed = time.time() - t0
|
| 479 |
+
res_rf_tf = evaluate("Random Forest (TF-IDF only)", rf_tf, X_te_tfidf, y_test_arr, rf_tf_proba)
|
| 480 |
+
res_rf_tf["train_time"] = elapsed
|
| 481 |
+
results.append(res_rf_tf)
|
| 482 |
+
print(f" Accuracy: {res_rf_tf['accuracy']:.4f} | F1 Weighted: {res_rf_tf['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
|
| 483 |
+
|
| 484 |
+
# ── 4e. YOUR Hybrid RF (TF-IDF + MiniLM + Stylometric) ───────────────
|
| 485 |
+
if not args.skip_minilm:
|
| 486 |
+
print("\n[5/5] ★ YOUR Hybrid RF (TF-IDF + MiniLM + Stylometric) …")
|
| 487 |
+
X_tr_hy, X_te_hy = build_hybrid_features(X_train, X_test)
|
| 488 |
+
|
| 489 |
+
t0 = time.time()
|
| 490 |
+
max_depth = 8 if cebuano_only else 20
|
| 491 |
+
rf_hy = RandomForestClassifier(
|
| 492 |
+
n_estimators=500,
|
| 493 |
+
max_depth=max_depth,
|
| 494 |
+
min_samples_split=5,
|
| 495 |
+
min_samples_leaf=3,
|
| 496 |
+
class_weight="balanced",
|
| 497 |
+
n_jobs=-1,
|
| 498 |
+
random_state=42,
|
| 499 |
+
)
|
| 500 |
+
rf_hy.fit(X_tr_hy, y_train_arr)
|
| 501 |
+
rf_hy_proba = rf_hy.predict_proba(X_te_hy)
|
| 502 |
+
elapsed = time.time() - t0
|
| 503 |
+
res_rf_hy = evaluate("★ Hybrid RF (TF-IDF + MiniLM + Stylo)", rf_hy, X_te_hy, y_test_arr, rf_hy_proba)
|
| 504 |
+
res_rf_hy["train_time"] = elapsed
|
| 505 |
+
results.append(res_rf_hy)
|
| 506 |
+
print(f" Accuracy: {res_rf_hy['accuracy']:.4f} | F1 Weighted: {res_rf_hy['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
|
| 507 |
+
else:
|
| 508 |
+
print("\n[5/5] Skipping Hybrid RF (--skip-minilm flag set).")
|
| 509 |
+
|
| 510 |
+
# ── 5. Output table ───────────────────────────────────────────────────
|
| 511 |
+
print("\n" + "=" * 60)
|
| 512 |
+
print(" BENCHMARK SUMMARY")
|
| 513 |
+
print("=" * 60)
|
| 514 |
+
table_path = os.path.join(OUTPUT_DIR, f"benchmark_table_{args.mode}.txt")
|
| 515 |
+
save_table(results, table_path, args.mode, len(X_train), len(X_test))
|
| 516 |
+
|
| 517 |
+
# ── 6. Bar chart ──────────────────────────────────────────────────────
|
| 518 |
+
chart_path = os.path.join(OUTPUT_DIR, f"benchmark_chart_{args.mode}.png")
|
| 519 |
+
plot_bar_chart(results, chart_path)
|
| 520 |
+
|
| 521 |
+
# ── 7. ROC curves ─────────────────────────────────────────────────────
|
| 522 |
+
roc_path = os.path.join(OUTPUT_DIR, f"benchmark_roc_{args.mode}.png")
|
| 523 |
+
plot_roc_curves(results, y_test_arr, roc_path)
|
| 524 |
+
|
| 525 |
+
# ── 8. Train time summary ─────────────────────────────────────────────
|
| 526 |
+
print("\n Training times:")
|
| 527 |
+
for r in results:
|
| 528 |
+
t = r.get("train_time", 0)
|
| 529 |
+
print(f" {r['name']:<45} {t:>6.1f}s")
|
| 530 |
+
|
| 531 |
+
print("\n" + "=" * 60)
|
| 532 |
+
print(" BENCHMARK COMPLETE")
|
| 533 |
+
print(f" Results saved to: {OUTPUT_DIR}")
|
| 534 |
+
print("=" * 60)
|
| 535 |
+
|
| 536 |
+
|
| 537 |
+
if __name__ == "__main__":
|
| 538 |
+
main()
|
backend/bias_words_report.txt
CHANGED
|
@@ -1,148 +1,148 @@
|
|
| 1 |
-
=================================================================
|
| 2 |
-
CHI-SQUARED BIAS WORD MINER
|
| 3 |
-
Finds words most associated with Fake vs Real articles
|
| 4 |
-
=================================================================
|
| 5 |
-
|
| 6 |
-
Loaded 25664 articles
|
| 7 |
-
Real: 16405 | Fake: 9259
|
| 8 |
-
|
| 9 |
-
Building vocabulary from 25664 articles...
|
| 10 |
-
Running chi-squared analysis...
|
| 11 |
-
|
| 12 |
-
=================================================================
|
| 13 |
-
TOP 60 WORDS ASSOCIATED WITH *** FAKE NEWS ***
|
| 14 |
-
(Consider adding to SENSATIONAL_KEYWORDS or RIGHT/LEFT lists)
|
| 15 |
-
=================================================================
|
| 16 |
-
ang chi2= 11772.6 fake_freq=2.1795 real_freq=0.6382
|
| 17 |
-
mga chi2= 7393.4 fake_freq=1.0763 real_freq=0.2492
|
| 18 |
-
miss chi2= 2917.1 fake_freq=0.2418 real_freq=0.0199
|
| 19 |
-
hindi chi2= 2627.4 fake_freq=0.3756 real_freq=0.0852
|
| 20 |
-
daw chi2= 2581.2 fake_freq=0.1800 real_freq=0.0065
|
| 21 |
-
adobo chi2= 2540.9 fake_freq=0.1594 real_freq=0.0012
|
| 22 |
-
lang chi2= 2520.2 fake_freq=0.2660 real_freq=0.0391
|
| 23 |
-
you chi2= 2501.5 fake_freq=1.1692 real_freq=0.5885
|
| 24 |
-
adobo chronicles chi2= 2490.9 fake_freq=0.1521 real_freq=0.0001
|
| 25 |
-
chronicles chi2= 2472.7 fake_freq=0.1537 real_freq=0.0008
|
| 26 |
-
the adobo chi2= 2443.3 fake_freq=0.1489 real_freq=0.0000
|
| 27 |
-
sa mga chi2= 2399.6 fake_freq=0.2914 real_freq=0.0536
|
| 28 |
-
ito chi2= 2374.0 fake_freq=0.3101 real_freq=0.0630
|
| 29 |
-
rappler chi2= 2298.3 fake_freq=0.1879 real_freq=0.0148
|
| 30 |
-
sabi chi2= 2157.7 fake_freq=0.1847 real_freq=0.0168
|
| 31 |
-
media chi2= 2148.0 fake_freq=0.4887 real_freq=0.1679
|
| 32 |
-
kung chi2= 1897.0 fake_freq=0.2617 real_freq=0.0569
|
| 33 |
-
video chi2= 1894.0 fake_freq=0.2651 real_freq=0.0587
|
| 34 |
-
sabi ni chi2= 1842.0 fake_freq=0.1377 real_freq=0.0075
|
| 35 |
-
kay chi2= 1793.6 fake_freq=0.2103 real_freq=0.0368
|
| 36 |
-
naman chi2= 1775.7 fake_freq=0.2518 real_freq=0.0566
|
| 37 |
-
facebook chi2= 1758.7 fake_freq=0.2292 real_freq=0.0464
|
| 38 |
-
netizens chi2= 1748.0 fake_freq=0.1363 real_freq=0.0090
|
| 39 |
-
ang mga chi2= 1728.9 fake_freq=0.2514 real_freq=0.0582
|
| 40 |
-
pageant chi2= 1715.6 fake_freq=0.1253 real_freq=0.0060
|
| 41 |
-
your chi2= 1701.4 fake_freq=0.3996 real_freq=0.1403
|
| 42 |
-
ng mga chi2= 1582.1 fake_freq=0.2811 real_freq=0.0794
|
| 43 |
-
yung chi2= 1508.4 fake_freq=0.1792 real_freq=0.0319
|
| 44 |
-
social media chi2= 1503.9 fake_freq=0.2093 real_freq=0.0460
|
| 45 |
-
umano chi2= 1478.0 fake_freq=0.1161 real_freq=0.0079
|
| 46 |
-
kayo chi2= 1398.4 fake_freq=0.1054 real_freq=0.0060
|
| 47 |
-
leni chi2= 1397.2 fake_freq=0.1806 real_freq=0.0362
|
| 48 |
-
ako chi2= 1364.9 fake_freq=0.1684 real_freq=0.0317
|
| 49 |
-
comment chi2= 1353.9 fake_freq=0.2057 real_freq=0.0499
|
| 50 |
-
para chi2= 1319.4 fake_freq=0.2437 real_freq=0.0712
|
| 51 |
-
pangulong chi2= 1288.2 fake_freq=0.0925 real_freq=0.0040
|
| 52 |
-
catriona chi2= 1285.2 fake_freq=0.0797 real_freq=0.0004
|
| 53 |
-
thinkingpinoy chi2= 1268.6 fake_freq=0.0773 real_freq=0.0000
|
| 54 |
-
blog chi2= 1258.3 fake_freq=0.0961 real_freq=0.0058
|
| 55 |
-
dahil chi2= 1213.0 fake_freq=0.1816 real_freq=0.0433
|
| 56 |
-
isang chi2= 1210.0 fake_freq=0.2125 real_freq=0.0594
|
| 57 |
-
ayon chi2= 1204.6 fake_freq=0.1481 real_freq=0.0277
|
| 58 |
-
ilang chi2= 1176.7 fake_freq=0.1077 real_freq=0.0117
|
| 59 |
-
noynoy chi2= 1121.0 fake_freq=0.0794 real_freq=0.0032
|
| 60 |
-
ressa chi2= 1086.1 fake_freq=0.0760 real_freq=0.0028
|
| 61 |
-
com chi2= 1082.5 fake_freq=0.1124 real_freq=0.0160
|
| 62 |
-
din chi2= 1081.8 fake_freq=0.1438 real_freq=0.0299
|
| 63 |
-
bs aquino chi2= 1062.9 fake_freq=0.0650 real_freq=0.0001
|
| 64 |
-
natin chi2= 1035.5 fake_freq=0.1229 real_freq=0.0219
|
| 65 |
-
niya chi2= 1032.8 fake_freq=0.1660 real_freq=0.0427
|
| 66 |
-
http chi2= 1029.2 fake_freq=0.0703 real_freq=0.0021
|
| 67 |
-
grp chi2= 1010.0 fake_freq=0.0731 real_freq=0.0034
|
| 68 |
-
naging chi2= 993.2 fake_freq=0.0958 real_freq=0.0118
|
| 69 |
-
featured comment chi2= 990.4 fake_freq=0.0604 real_freq=0.0000
|
| 70 |
-
is grp chi2= 988.7 fake_freq=0.0603 real_freq=0.0000
|
| 71 |
-
grp featured chi2= 983.3 fake_freq=0.0599 real_freq=0.0000
|
| 72 |
-
like chi2= 983.3 fake_freq=0.5211 real_freq=0.2752
|
| 73 |
-
news chi2= 980.1 fake_freq=0.3171 real_freq=0.1348
|
| 74 |
-
catriona gray chi2= 979.6 fake_freq=0.0599 real_freq=0.0001
|
| 75 |
-
yan chi2= 971.2 fake_freq=0.0989 real_freq=0.0136
|
| 76 |
-
|
| 77 |
-
=================================================================
|
| 78 |
-
TOP 60 WORDS ASSOCIATED WITH *** REAL NEWS ***
|
| 79 |
-
(High-credibility markers ù useful context)
|
| 80 |
-
=================================================================
|
| 81 |
-
said chi2= 30152.0 real_freq=5.3763 fake_freq=0.9817
|
| 82 |
-
said the chi2= 7892.5 real_freq=1.0162 fake_freq=0.0680
|
| 83 |
-
he said chi2= 7048.7 real_freq=1.0146 fake_freq=0.1094
|
| 84 |
-
of the chi2= 6391.5 real_freq=4.0884 fake_freq=2.1730
|
| 85 |
-
was chi2= 6127.3 real_freq=3.1958 fake_freq=1.5541
|
| 86 |
-
had chi2= 4985.5 real_freq=1.2013 fake_freq=0.3359
|
| 87 |
-
were chi2= 4869.7 real_freq=1.4602 fake_freq=0.5025
|
| 88 |
-
for chi2= 3693.8 real_freq=3.9857 fake_freq=2.5171
|
| 89 |
-
percent chi2= 3316.0 real_freq=0.4733 fake_freq=0.0496
|
| 90 |
-
said he chi2= 3054.9 real_freq=0.4525 fake_freq=0.0535
|
| 91 |
-
city chi2= 3015.1 real_freq=0.9589 fake_freq=0.3458
|
| 92 |
-
police chi2= 2951.7 real_freq=0.6889 fake_freq=0.1859
|
| 93 |
-
mr duterte chi2= 2937.8 real_freq=0.3344 fake_freq=0.0068
|
| 94 |
-
would chi2= 2682.3 real_freq=1.1915 fake_freq=0.5339
|
| 95 |
-
added chi2= 2527.3 real_freq=0.4712 fake_freq=0.0931
|
| 96 |
-
in the chi2= 2323.8 real_freq=2.2774 fake_freq=1.3999
|
| 97 |
-
government chi2= 2221.8 real_freq=0.9786 fake_freq=0.4364
|
| 98 |
-
from chi2= 2142.5 real_freq=1.9267 fake_freq=1.1543
|
| 99 |
-
court chi2= 2128.4 real_freq=0.5376 fake_freq=0.1580
|
| 100 |
-
said in chi2= 1914.3 real_freq=0.3323 fake_freq=0.0577
|
| 101 |
-
province chi2= 1836.7 real_freq=0.2501 fake_freq=0.0219
|
| 102 |
-
year chi2= 1725.1 real_freq=0.6088 fake_freq=0.2369
|
| 103 |
-
she said chi2= 1703.1 real_freq=0.2836 fake_freq=0.0451
|
| 104 |
-
sen chi2= 1646.4 real_freq=0.2895 fake_freq=0.0515
|
| 105 |
-
the department chi2= 1624.6 real_freq=0.2532 fake_freq=0.0345
|
| 106 |
-
also chi2= 1619.7 real_freq=1.1369 fake_freq=0.6263
|
| 107 |
-
for the chi2= 1617.7 real_freq=0.9776 fake_freq=0.5076
|
| 108 |
-
rep chi2= 1561.8 real_freq=0.2070 fake_freq=0.0161
|
| 109 |
-
the president chi2= 1555.6 real_freq=0.5825 fake_freq=0.2358
|
| 110 |
-
to the chi2= 1533.1 real_freq=1.4158 fake_freq=0.8552
|
| 111 |
-
department chi2= 1480.6 real_freq=0.3825 fake_freq=0.1150
|
| 112 |
-
he added chi2= 1479.9 real_freq=0.2401 fake_freq=0.0361
|
| 113 |
-
office chi2= 1426.7 real_freq=0.4416 fake_freq=0.1560
|
| 114 |
-
friday chi2= 1420.6 real_freq=0.2201 fake_freq=0.0295
|
| 115 |
-
with chi2= 1404.1 real_freq=2.2094 fake_freq=1.5269
|
| 116 |
-
officials chi2= 1398.2 real_freq=0.3345 fake_freq=0.0928
|
| 117 |
-
military chi2= 1392.7 real_freq=0.3024 fake_freq=0.0747
|
| 118 |
-
monday chi2= 1364.4 real_freq=0.2359 fake_freq=0.0406
|
| 119 |
-
tuesday chi2= 1357.9 real_freq=0.2077 fake_freq=0.0269
|
| 120 |
-
house chi2= 1340.9 real_freq=0.3684 fake_freq=0.1175
|
| 121 |
-
chief chi2= 1329.3 real_freq=0.4226 fake_freq=0.1524
|
| 122 |
-
committee chi2= 1317.5 real_freq=0.2536 fake_freq=0.0528
|
| 123 |
-
the government chi2= 1310.3 real_freq=0.3822 fake_freq=0.1285
|
| 124 |
-
the house chi2= 1290.2 real_freq=0.2091 fake_freq=0.0313
|
| 125 |
-
had been chi2= 1277.4 real_freq=0.1926 fake_freq=0.0240
|
| 126 |
-
on friday chi2= 1269.4 real_freq=0.1663 fake_freq=0.0122
|
| 127 |
-
thursday chi2= 1260.2 real_freq=0.1889 fake_freq=0.0231
|
| 128 |
-
peace chi2= 1233.9 real_freq=0.2489 fake_freq=0.0556
|
| 129 |
-
million chi2= 1231.2 real_freq=0.4199 fake_freq=0.1595
|
| 130 |
-
and the chi2= 1210.5 real_freq=0.7438 fake_freq=0.3889
|
| 131 |
-
at the chi2= 1192.8 real_freq=0.6397 fake_freq=0.3152
|
| 132 |
-
wednesday chi2= 1176.4 real_freq=0.1791 fake_freq=0.0229
|
| 133 |
-
department of chi2= 1141.5 real_freq=0.2878 fake_freq=0.0845
|
| 134 |
-
national chi2= 1125.5 real_freq=0.5410 fake_freq=0.2527
|
| 135 |
-
on monday chi2= 1123.8 real_freq=0.1721 fake_freq=0.0224
|
| 136 |
-
under chi2= 1114.4 real_freq=0.4066 fake_freq=0.1618
|
| 137 |
-
region chi2= 1113.0 real_freq=0.1859 fake_freq=0.0298
|
| 138 |
-
town chi2= 1102.8 real_freq=0.1554 fake_freq=0.0156
|
| 139 |
-
on tuesday chi2= 1101.4 real_freq=0.1529 fake_freq=0.0145
|
| 140 |
-
by the chi2= 1079.0 real_freq=0.7163 fake_freq=0.3863
|
| 141 |
-
|
| 142 |
-
=================================================================
|
| 143 |
-
HOW TO USE THESE RESULTS:
|
| 144 |
-
1. Review the FAKE words list above
|
| 145 |
-
2. Add clearly sensational/biased words to SENSATIONAL_KEYWORDS
|
| 146 |
-
in checker/internal/bias_analyzer.py
|
| 147 |
-
3. Add political framing words to RIGHT/LEFT_LEANING_KEYWORDS
|
| 148 |
-
=================================================================
|
|
|
|
| 1 |
+
=================================================================
|
| 2 |
+
CHI-SQUARED BIAS WORD MINER
|
| 3 |
+
Finds words most associated with Fake vs Real articles
|
| 4 |
+
=================================================================
|
| 5 |
+
|
| 6 |
+
Loaded 25664 articles
|
| 7 |
+
Real: 16405 | Fake: 9259
|
| 8 |
+
|
| 9 |
+
Building vocabulary from 25664 articles...
|
| 10 |
+
Running chi-squared analysis...
|
| 11 |
+
|
| 12 |
+
=================================================================
|
| 13 |
+
TOP 60 WORDS ASSOCIATED WITH *** FAKE NEWS ***
|
| 14 |
+
(Consider adding to SENSATIONAL_KEYWORDS or RIGHT/LEFT lists)
|
| 15 |
+
=================================================================
|
| 16 |
+
ang chi2= 11772.6 fake_freq=2.1795 real_freq=0.6382
|
| 17 |
+
mga chi2= 7393.4 fake_freq=1.0763 real_freq=0.2492
|
| 18 |
+
miss chi2= 2917.1 fake_freq=0.2418 real_freq=0.0199
|
| 19 |
+
hindi chi2= 2627.4 fake_freq=0.3756 real_freq=0.0852
|
| 20 |
+
daw chi2= 2581.2 fake_freq=0.1800 real_freq=0.0065
|
| 21 |
+
adobo chi2= 2540.9 fake_freq=0.1594 real_freq=0.0012
|
| 22 |
+
lang chi2= 2520.2 fake_freq=0.2660 real_freq=0.0391
|
| 23 |
+
you chi2= 2501.5 fake_freq=1.1692 real_freq=0.5885
|
| 24 |
+
adobo chronicles chi2= 2490.9 fake_freq=0.1521 real_freq=0.0001
|
| 25 |
+
chronicles chi2= 2472.7 fake_freq=0.1537 real_freq=0.0008
|
| 26 |
+
the adobo chi2= 2443.3 fake_freq=0.1489 real_freq=0.0000
|
| 27 |
+
sa mga chi2= 2399.6 fake_freq=0.2914 real_freq=0.0536
|
| 28 |
+
ito chi2= 2374.0 fake_freq=0.3101 real_freq=0.0630
|
| 29 |
+
rappler chi2= 2298.3 fake_freq=0.1879 real_freq=0.0148
|
| 30 |
+
sabi chi2= 2157.7 fake_freq=0.1847 real_freq=0.0168
|
| 31 |
+
media chi2= 2148.0 fake_freq=0.4887 real_freq=0.1679
|
| 32 |
+
kung chi2= 1897.0 fake_freq=0.2617 real_freq=0.0569
|
| 33 |
+
video chi2= 1894.0 fake_freq=0.2651 real_freq=0.0587
|
| 34 |
+
sabi ni chi2= 1842.0 fake_freq=0.1377 real_freq=0.0075
|
| 35 |
+
kay chi2= 1793.6 fake_freq=0.2103 real_freq=0.0368
|
| 36 |
+
naman chi2= 1775.7 fake_freq=0.2518 real_freq=0.0566
|
| 37 |
+
facebook chi2= 1758.7 fake_freq=0.2292 real_freq=0.0464
|
| 38 |
+
netizens chi2= 1748.0 fake_freq=0.1363 real_freq=0.0090
|
| 39 |
+
ang mga chi2= 1728.9 fake_freq=0.2514 real_freq=0.0582
|
| 40 |
+
pageant chi2= 1715.6 fake_freq=0.1253 real_freq=0.0060
|
| 41 |
+
your chi2= 1701.4 fake_freq=0.3996 real_freq=0.1403
|
| 42 |
+
ng mga chi2= 1582.1 fake_freq=0.2811 real_freq=0.0794
|
| 43 |
+
yung chi2= 1508.4 fake_freq=0.1792 real_freq=0.0319
|
| 44 |
+
social media chi2= 1503.9 fake_freq=0.2093 real_freq=0.0460
|
| 45 |
+
umano chi2= 1478.0 fake_freq=0.1161 real_freq=0.0079
|
| 46 |
+
kayo chi2= 1398.4 fake_freq=0.1054 real_freq=0.0060
|
| 47 |
+
leni chi2= 1397.2 fake_freq=0.1806 real_freq=0.0362
|
| 48 |
+
ako chi2= 1364.9 fake_freq=0.1684 real_freq=0.0317
|
| 49 |
+
comment chi2= 1353.9 fake_freq=0.2057 real_freq=0.0499
|
| 50 |
+
para chi2= 1319.4 fake_freq=0.2437 real_freq=0.0712
|
| 51 |
+
pangulong chi2= 1288.2 fake_freq=0.0925 real_freq=0.0040
|
| 52 |
+
catriona chi2= 1285.2 fake_freq=0.0797 real_freq=0.0004
|
| 53 |
+
thinkingpinoy chi2= 1268.6 fake_freq=0.0773 real_freq=0.0000
|
| 54 |
+
blog chi2= 1258.3 fake_freq=0.0961 real_freq=0.0058
|
| 55 |
+
dahil chi2= 1213.0 fake_freq=0.1816 real_freq=0.0433
|
| 56 |
+
isang chi2= 1210.0 fake_freq=0.2125 real_freq=0.0594
|
| 57 |
+
ayon chi2= 1204.6 fake_freq=0.1481 real_freq=0.0277
|
| 58 |
+
ilang chi2= 1176.7 fake_freq=0.1077 real_freq=0.0117
|
| 59 |
+
noynoy chi2= 1121.0 fake_freq=0.0794 real_freq=0.0032
|
| 60 |
+
ressa chi2= 1086.1 fake_freq=0.0760 real_freq=0.0028
|
| 61 |
+
com chi2= 1082.5 fake_freq=0.1124 real_freq=0.0160
|
| 62 |
+
din chi2= 1081.8 fake_freq=0.1438 real_freq=0.0299
|
| 63 |
+
bs aquino chi2= 1062.9 fake_freq=0.0650 real_freq=0.0001
|
| 64 |
+
natin chi2= 1035.5 fake_freq=0.1229 real_freq=0.0219
|
| 65 |
+
niya chi2= 1032.8 fake_freq=0.1660 real_freq=0.0427
|
| 66 |
+
http chi2= 1029.2 fake_freq=0.0703 real_freq=0.0021
|
| 67 |
+
grp chi2= 1010.0 fake_freq=0.0731 real_freq=0.0034
|
| 68 |
+
naging chi2= 993.2 fake_freq=0.0958 real_freq=0.0118
|
| 69 |
+
featured comment chi2= 990.4 fake_freq=0.0604 real_freq=0.0000
|
| 70 |
+
is grp chi2= 988.7 fake_freq=0.0603 real_freq=0.0000
|
| 71 |
+
grp featured chi2= 983.3 fake_freq=0.0599 real_freq=0.0000
|
| 72 |
+
like chi2= 983.3 fake_freq=0.5211 real_freq=0.2752
|
| 73 |
+
news chi2= 980.1 fake_freq=0.3171 real_freq=0.1348
|
| 74 |
+
catriona gray chi2= 979.6 fake_freq=0.0599 real_freq=0.0001
|
| 75 |
+
yan chi2= 971.2 fake_freq=0.0989 real_freq=0.0136
|
| 76 |
+
|
| 77 |
+
=================================================================
|
| 78 |
+
TOP 60 WORDS ASSOCIATED WITH *** REAL NEWS ***
|
| 79 |
+
(High-credibility markers ù useful context)
|
| 80 |
+
=================================================================
|
| 81 |
+
said chi2= 30152.0 real_freq=5.3763 fake_freq=0.9817
|
| 82 |
+
said the chi2= 7892.5 real_freq=1.0162 fake_freq=0.0680
|
| 83 |
+
he said chi2= 7048.7 real_freq=1.0146 fake_freq=0.1094
|
| 84 |
+
of the chi2= 6391.5 real_freq=4.0884 fake_freq=2.1730
|
| 85 |
+
was chi2= 6127.3 real_freq=3.1958 fake_freq=1.5541
|
| 86 |
+
had chi2= 4985.5 real_freq=1.2013 fake_freq=0.3359
|
| 87 |
+
were chi2= 4869.7 real_freq=1.4602 fake_freq=0.5025
|
| 88 |
+
for chi2= 3693.8 real_freq=3.9857 fake_freq=2.5171
|
| 89 |
+
percent chi2= 3316.0 real_freq=0.4733 fake_freq=0.0496
|
| 90 |
+
said he chi2= 3054.9 real_freq=0.4525 fake_freq=0.0535
|
| 91 |
+
city chi2= 3015.1 real_freq=0.9589 fake_freq=0.3458
|
| 92 |
+
police chi2= 2951.7 real_freq=0.6889 fake_freq=0.1859
|
| 93 |
+
mr duterte chi2= 2937.8 real_freq=0.3344 fake_freq=0.0068
|
| 94 |
+
would chi2= 2682.3 real_freq=1.1915 fake_freq=0.5339
|
| 95 |
+
added chi2= 2527.3 real_freq=0.4712 fake_freq=0.0931
|
| 96 |
+
in the chi2= 2323.8 real_freq=2.2774 fake_freq=1.3999
|
| 97 |
+
government chi2= 2221.8 real_freq=0.9786 fake_freq=0.4364
|
| 98 |
+
from chi2= 2142.5 real_freq=1.9267 fake_freq=1.1543
|
| 99 |
+
court chi2= 2128.4 real_freq=0.5376 fake_freq=0.1580
|
| 100 |
+
said in chi2= 1914.3 real_freq=0.3323 fake_freq=0.0577
|
| 101 |
+
province chi2= 1836.7 real_freq=0.2501 fake_freq=0.0219
|
| 102 |
+
year chi2= 1725.1 real_freq=0.6088 fake_freq=0.2369
|
| 103 |
+
she said chi2= 1703.1 real_freq=0.2836 fake_freq=0.0451
|
| 104 |
+
sen chi2= 1646.4 real_freq=0.2895 fake_freq=0.0515
|
| 105 |
+
the department chi2= 1624.6 real_freq=0.2532 fake_freq=0.0345
|
| 106 |
+
also chi2= 1619.7 real_freq=1.1369 fake_freq=0.6263
|
| 107 |
+
for the chi2= 1617.7 real_freq=0.9776 fake_freq=0.5076
|
| 108 |
+
rep chi2= 1561.8 real_freq=0.2070 fake_freq=0.0161
|
| 109 |
+
the president chi2= 1555.6 real_freq=0.5825 fake_freq=0.2358
|
| 110 |
+
to the chi2= 1533.1 real_freq=1.4158 fake_freq=0.8552
|
| 111 |
+
department chi2= 1480.6 real_freq=0.3825 fake_freq=0.1150
|
| 112 |
+
he added chi2= 1479.9 real_freq=0.2401 fake_freq=0.0361
|
| 113 |
+
office chi2= 1426.7 real_freq=0.4416 fake_freq=0.1560
|
| 114 |
+
friday chi2= 1420.6 real_freq=0.2201 fake_freq=0.0295
|
| 115 |
+
with chi2= 1404.1 real_freq=2.2094 fake_freq=1.5269
|
| 116 |
+
officials chi2= 1398.2 real_freq=0.3345 fake_freq=0.0928
|
| 117 |
+
military chi2= 1392.7 real_freq=0.3024 fake_freq=0.0747
|
| 118 |
+
monday chi2= 1364.4 real_freq=0.2359 fake_freq=0.0406
|
| 119 |
+
tuesday chi2= 1357.9 real_freq=0.2077 fake_freq=0.0269
|
| 120 |
+
house chi2= 1340.9 real_freq=0.3684 fake_freq=0.1175
|
| 121 |
+
chief chi2= 1329.3 real_freq=0.4226 fake_freq=0.1524
|
| 122 |
+
committee chi2= 1317.5 real_freq=0.2536 fake_freq=0.0528
|
| 123 |
+
the government chi2= 1310.3 real_freq=0.3822 fake_freq=0.1285
|
| 124 |
+
the house chi2= 1290.2 real_freq=0.2091 fake_freq=0.0313
|
| 125 |
+
had been chi2= 1277.4 real_freq=0.1926 fake_freq=0.0240
|
| 126 |
+
on friday chi2= 1269.4 real_freq=0.1663 fake_freq=0.0122
|
| 127 |
+
thursday chi2= 1260.2 real_freq=0.1889 fake_freq=0.0231
|
| 128 |
+
peace chi2= 1233.9 real_freq=0.2489 fake_freq=0.0556
|
| 129 |
+
million chi2= 1231.2 real_freq=0.4199 fake_freq=0.1595
|
| 130 |
+
and the chi2= 1210.5 real_freq=0.7438 fake_freq=0.3889
|
| 131 |
+
at the chi2= 1192.8 real_freq=0.6397 fake_freq=0.3152
|
| 132 |
+
wednesday chi2= 1176.4 real_freq=0.1791 fake_freq=0.0229
|
| 133 |
+
department of chi2= 1141.5 real_freq=0.2878 fake_freq=0.0845
|
| 134 |
+
national chi2= 1125.5 real_freq=0.5410 fake_freq=0.2527
|
| 135 |
+
on monday chi2= 1123.8 real_freq=0.1721 fake_freq=0.0224
|
| 136 |
+
under chi2= 1114.4 real_freq=0.4066 fake_freq=0.1618
|
| 137 |
+
region chi2= 1113.0 real_freq=0.1859 fake_freq=0.0298
|
| 138 |
+
town chi2= 1102.8 real_freq=0.1554 fake_freq=0.0156
|
| 139 |
+
on tuesday chi2= 1101.4 real_freq=0.1529 fake_freq=0.0145
|
| 140 |
+
by the chi2= 1079.0 real_freq=0.7163 fake_freq=0.3863
|
| 141 |
+
|
| 142 |
+
=================================================================
|
| 143 |
+
HOW TO USE THESE RESULTS:
|
| 144 |
+
1. Review the FAKE words list above
|
| 145 |
+
2. Add clearly sensational/biased words to SENSATIONAL_KEYWORDS
|
| 146 |
+
in checker/internal/bias_analyzer.py
|
| 147 |
+
3. Add political framing words to RIGHT/LEFT_LEANING_KEYWORDS
|
| 148 |
+
=================================================================
|
backend/evaluate_model.py
CHANGED
|
@@ -1,820 +1,820 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Model Overfitting Evaluation Script
|
| 3 |
-
=====================================
|
| 4 |
-
Evaluates the Random Forest fake news classifier for overfitting by
|
| 5 |
-
comparing Training vs. Testing performance.
|
| 6 |
-
|
| 7 |
-
Split: 80% Train / 20% Test
|
| 8 |
-
Metrics: classification_report, accuracy_score, confusion matrix plot
|
| 9 |
-
Flag: Overfitting detected if Train Acc > 95% and Test Acc < 70%
|
| 10 |
-
|
| 11 |
-
Usage:
|
| 12 |
-
python backend/evaluate_model.py
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
import sys
|
| 16 |
-
import os
|
| 17 |
-
import re
|
| 18 |
-
import time
|
| 19 |
-
import numpy as np
|
| 20 |
-
from textblob import TextBlob
|
| 21 |
-
import textstat
|
| 22 |
-
|
| 23 |
-
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
-
sys.path.insert(0, PROJECT_ROOT)
|
| 25 |
-
|
| 26 |
-
import pandas as pd
|
| 27 |
-
import matplotlib
|
| 28 |
-
|
| 29 |
-
matplotlib.use("Agg") # Non-interactive backend for saving plots
|
| 30 |
-
import matplotlib.pyplot as plt
|
| 31 |
-
from scipy.sparse import hstack, csr_matrix
|
| 32 |
-
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
|
| 33 |
-
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 34 |
-
from sklearn.ensemble import RandomForestClassifier
|
| 35 |
-
from sklearn.preprocessing import StandardScaler
|
| 36 |
-
from sklearn.metrics import (
|
| 37 |
-
classification_report,
|
| 38 |
-
accuracy_score,
|
| 39 |
-
confusion_matrix,
|
| 40 |
-
ConfusionMatrixDisplay,
|
| 41 |
-
)
|
| 42 |
-
from sentence_transformers import SentenceTransformer
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# ── Paths ──
|
| 46 |
-
DATA_MODELS_DIR = os.path.join(PROJECT_ROOT, "data_models")
|
| 47 |
-
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
# ── MiniLM Model (lazy-loaded singleton) ──
|
| 51 |
-
MINILM_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
|
| 52 |
-
_minilm_model = None
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def get_minilm_model():
|
| 56 |
-
"""Load the multilingual MiniLM model (cached after first call)."""
|
| 57 |
-
global _minilm_model
|
| 58 |
-
if _minilm_model is None:
|
| 59 |
-
print(" Loading MiniLM model...")
|
| 60 |
-
_minilm_model = SentenceTransformer(MINILM_MODEL_NAME)
|
| 61 |
-
return _minilm_model
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# ───────────────────────────────────────────────────────────
|
| 65 |
-
# Text Cleaning (same as train.py)
|
| 66 |
-
# ───────────────────────────────────────────────────────────
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def clean_text(text):
|
| 70 |
-
"""Basic text cleaning for Filipino news articles."""
|
| 71 |
-
if not text or not isinstance(text, str):
|
| 72 |
-
return ""
|
| 73 |
-
text = re.sub(r"<[^>]+>", " ", text)
|
| 74 |
-
text = re.sub(r"https?://\S+", " ", text)
|
| 75 |
-
text = re.sub(r"\s+", " ", text)
|
| 76 |
-
return text.strip()
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
# ───────────────────────────────────────────────────────────
|
| 80 |
-
# Stylometric Features (same as train.py)
|
| 81 |
-
# ───────────────────────────────────────────────────────────
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
# ── Word lists for linguistic features ──
|
| 85 |
-
FIRST_PERSON_PRONOUNS = {
|
| 86 |
-
"i",
|
| 87 |
-
"me",
|
| 88 |
-
"my",
|
| 89 |
-
"mine",
|
| 90 |
-
"myself",
|
| 91 |
-
"we",
|
| 92 |
-
"us",
|
| 93 |
-
"our",
|
| 94 |
-
"ours",
|
| 95 |
-
"ourselves",
|
| 96 |
-
"ako",
|
| 97 |
-
"ko",
|
| 98 |
-
"akin",
|
| 99 |
-
"aking",
|
| 100 |
-
"natin",
|
| 101 |
-
"atin",
|
| 102 |
-
"namin",
|
| 103 |
-
"amin",
|
| 104 |
-
"tayo",
|
| 105 |
-
"kami",
|
| 106 |
-
"ta",
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
AUXILIARY_VERBS = {
|
| 110 |
-
"have",
|
| 111 |
-
"has",
|
| 112 |
-
"had",
|
| 113 |
-
"do",
|
| 114 |
-
"does",
|
| 115 |
-
"did",
|
| 116 |
-
"will",
|
| 117 |
-
"would",
|
| 118 |
-
"shall",
|
| 119 |
-
"should",
|
| 120 |
-
"may",
|
| 121 |
-
"might",
|
| 122 |
-
"can",
|
| 123 |
-
"could",
|
| 124 |
-
"must",
|
| 125 |
-
"am",
|
| 126 |
-
"is",
|
| 127 |
-
"are",
|
| 128 |
-
"was",
|
| 129 |
-
"were",
|
| 130 |
-
"be",
|
| 131 |
-
"been",
|
| 132 |
-
"being",
|
| 133 |
-
"ay",
|
| 134 |
-
"dapat",
|
| 135 |
-
"mayroon",
|
| 136 |
-
"meron",
|
| 137 |
-
"maaari",
|
| 138 |
-
"pwede",
|
| 139 |
-
"kailangan",
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
ANALYTICAL_WORDS = {
|
| 143 |
-
"the",
|
| 144 |
-
"a",
|
| 145 |
-
"an",
|
| 146 |
-
"of",
|
| 147 |
-
"in",
|
| 148 |
-
"on",
|
| 149 |
-
"at",
|
| 150 |
-
"to",
|
| 151 |
-
"for",
|
| 152 |
-
"with",
|
| 153 |
-
"by",
|
| 154 |
-
"from",
|
| 155 |
-
"about",
|
| 156 |
-
"between",
|
| 157 |
-
"through",
|
| 158 |
-
"during",
|
| 159 |
-
"before",
|
| 160 |
-
"after",
|
| 161 |
-
"ang",
|
| 162 |
-
"ng",
|
| 163 |
-
"sa",
|
| 164 |
-
"mga",
|
| 165 |
-
"nang",
|
| 166 |
-
"para",
|
| 167 |
-
"tungkol",
|
| 168 |
-
"mula",
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
CERTAINTY_WORDS = {
|
| 172 |
-
"always",
|
| 173 |
-
"never",
|
| 174 |
-
"absolutely",
|
| 175 |
-
"definitely",
|
| 176 |
-
"certainly",
|
| 177 |
-
"undoubtedly",
|
| 178 |
-
"clearly",
|
| 179 |
-
"obviously",
|
| 180 |
-
"without doubt",
|
| 181 |
-
"guaranteed",
|
| 182 |
-
"proven",
|
| 183 |
-
"fact",
|
| 184 |
-
"undeniable",
|
| 185 |
-
"indisputable",
|
| 186 |
-
"every",
|
| 187 |
-
"all",
|
| 188 |
-
"palagi",
|
| 189 |
-
"sigurado",
|
| 190 |
-
"tiyak",
|
| 191 |
-
"talaga",
|
| 192 |
-
"totoo",
|
| 193 |
-
"lagi",
|
| 194 |
-
"walang duda",
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
TENTATIVE_WORDS = {
|
| 198 |
-
"perhaps",
|
| 199 |
-
"maybe",
|
| 200 |
-
"possibly",
|
| 201 |
-
"might",
|
| 202 |
-
"could",
|
| 203 |
-
"likely",
|
| 204 |
-
"unlikely",
|
| 205 |
-
"suggests",
|
| 206 |
-
"appears",
|
| 207 |
-
"seems",
|
| 208 |
-
"allegedly",
|
| 209 |
-
"reportedly",
|
| 210 |
-
"according",
|
| 211 |
-
"probable",
|
| 212 |
-
"approximately",
|
| 213 |
-
"estimated",
|
| 214 |
-
"siguro",
|
| 215 |
-
"marahil",
|
| 216 |
-
"maaaring",
|
| 217 |
-
"mukhang",
|
| 218 |
-
"parang",
|
| 219 |
-
"umano",
|
| 220 |
-
"diumano",
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
CLOUT_WORDS = {
|
| 224 |
-
"must",
|
| 225 |
-
"demand",
|
| 226 |
-
"require",
|
| 227 |
-
"order",
|
| 228 |
-
"command",
|
| 229 |
-
"insist",
|
| 230 |
-
"decree",
|
| 231 |
-
"mandate",
|
| 232 |
-
"authority",
|
| 233 |
-
"power",
|
| 234 |
-
"control",
|
| 235 |
-
"dominant",
|
| 236 |
-
"superior",
|
| 237 |
-
"we must",
|
| 238 |
-
"you must",
|
| 239 |
-
"kailangan",
|
| 240 |
-
"dapat",
|
| 241 |
-
"utos",
|
| 242 |
-
"kapangyarihan",
|
| 243 |
-
"kontrol",
|
| 244 |
-
"mando",
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
PAST_FOCUS_WORDS = {
|
| 248 |
-
"talked",
|
| 249 |
-
"did",
|
| 250 |
-
"ago",
|
| 251 |
-
"said",
|
| 252 |
-
"was",
|
| 253 |
-
"were",
|
| 254 |
-
"had",
|
| 255 |
-
"went",
|
| 256 |
-
"told",
|
| 257 |
-
"noon",
|
| 258 |
-
"nakaraan",
|
| 259 |
-
"dati",
|
| 260 |
-
"kahapon",
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
PRESENT_FOCUS_WORDS = {
|
| 264 |
-
"now",
|
| 265 |
-
"is",
|
| 266 |
-
"today",
|
| 267 |
-
"are",
|
| 268 |
-
"being",
|
| 269 |
-
"currently",
|
| 270 |
-
"ongoing",
|
| 271 |
-
"ngayon",
|
| 272 |
-
"kasalukuyan",
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
FUTURE_FOCUS_WORDS = {
|
| 276 |
-
"soon",
|
| 277 |
-
"will",
|
| 278 |
-
"may",
|
| 279 |
-
"shall",
|
| 280 |
-
"going",
|
| 281 |
-
"plan",
|
| 282 |
-
"expect",
|
| 283 |
-
"tomorrow",
|
| 284 |
-
"bukas",
|
| 285 |
-
"darating",
|
| 286 |
-
"magiging",
|
| 287 |
-
"gagawin",
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
def extract_stylometric_features(text):
|
| 292 |
-
"""Extract 25 stylometric features from text (matches train.py)."""
|
| 293 |
-
if not text or not isinstance(text, str):
|
| 294 |
-
return [0.0] * 25
|
| 295 |
-
|
| 296 |
-
words = text.split()
|
| 297 |
-
token_count = len(words)
|
| 298 |
-
if token_count == 0:
|
| 299 |
-
return [0.0] * 25
|
| 300 |
-
|
| 301 |
-
words_lower = [w.lower() for w in words]
|
| 302 |
-
text_len = len(text)
|
| 303 |
-
|
| 304 |
-
exclamation_density = text.count("!") / token_count
|
| 305 |
-
question_count = text.count("?")
|
| 306 |
-
|
| 307 |
-
caps_words = sum(1 for w in words if len(w) >= 2 and w.isupper())
|
| 308 |
-
caps_ratio = caps_words / token_count
|
| 309 |
-
|
| 310 |
-
sentences = re.split(r"[.!?]+", text)
|
| 311 |
-
sentences = [s.strip() for s in sentences if s.strip()]
|
| 312 |
-
avg_sentence_length = (
|
| 313 |
-
sum(len(s.split()) for s in sentences) / len(sentences)
|
| 314 |
-
if sentences
|
| 315 |
-
else token_count
|
| 316 |
-
)
|
| 317 |
-
|
| 318 |
-
punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
|
| 319 |
-
punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
|
| 320 |
-
|
| 321 |
-
unique_words = len(set(words_lower))
|
| 322 |
-
unique_word_ratio = unique_words / token_count
|
| 323 |
-
|
| 324 |
-
avg_word_length = sum(len(w) for w in words) / token_count
|
| 325 |
-
|
| 326 |
-
try:
|
| 327 |
-
subjectivity = TextBlob(text).sentiment.subjectivity
|
| 328 |
-
except Exception:
|
| 329 |
-
subjectivity = 0.0
|
| 330 |
-
|
| 331 |
-
try:
|
| 332 |
-
flesch_reading_ease = textstat.flesch_reading_ease(text)
|
| 333 |
-
flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
|
| 334 |
-
coleman_liau_index = textstat.coleman_liau_index(text)
|
| 335 |
-
ari = textstat.automated_readability_index(text)
|
| 336 |
-
except Exception:
|
| 337 |
-
flesch_reading_ease = 0.0
|
| 338 |
-
flesch_kincaid_grade = 0.0
|
| 339 |
-
coleman_liau_index = 0.0
|
| 340 |
-
ari = 0.0
|
| 341 |
-
|
| 342 |
-
first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
|
| 343 |
-
first_person_ratio = first_person_count / token_count
|
| 344 |
-
|
| 345 |
-
aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
|
| 346 |
-
auxiliary_verb_ratio = aux_count / token_count
|
| 347 |
-
|
| 348 |
-
try:
|
| 349 |
-
gunning_fog_index = textstat.gunning_fog(text)
|
| 350 |
-
except Exception:
|
| 351 |
-
gunning_fog_index = 0.0
|
| 352 |
-
|
| 353 |
-
analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
|
| 354 |
-
analytical_thinking = analytical_count / token_count
|
| 355 |
-
|
| 356 |
-
certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
|
| 357 |
-
certainty_score = certainty_count / token_count
|
| 358 |
-
|
| 359 |
-
tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
|
| 360 |
-
tentative_score = tentative_count / token_count
|
| 361 |
-
|
| 362 |
-
clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
|
| 363 |
-
clout_score = clout_count / token_count
|
| 364 |
-
|
| 365 |
-
comma_period_count = text.count(",") + text.count(".")
|
| 366 |
-
comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
|
| 367 |
-
|
| 368 |
-
informal_count = (
|
| 369 |
-
text.count("(")
|
| 370 |
-
+ text.count(")")
|
| 371 |
-
+ text.count("—")
|
| 372 |
-
+ text.count("–")
|
| 373 |
-
+ text.count("-")
|
| 374 |
-
+ text.count("...")
|
| 375 |
-
+ text.count("…")
|
| 376 |
-
)
|
| 377 |
-
informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
|
| 378 |
-
|
| 379 |
-
past_count = sum(1 for w in words_lower if w in PAST_FOCUS_WORDS)
|
| 380 |
-
past_focus_ratio = past_count / token_count
|
| 381 |
-
|
| 382 |
-
present_count = sum(1 for w in words_lower if w in PRESENT_FOCUS_WORDS)
|
| 383 |
-
present_focus_ratio = present_count / token_count
|
| 384 |
-
|
| 385 |
-
future_count = sum(1 for w in words_lower if w in FUTURE_FOCUS_WORDS)
|
| 386 |
-
future_focus_ratio = future_count / token_count
|
| 387 |
-
|
| 388 |
-
return [
|
| 389 |
-
float(exclamation_density),
|
| 390 |
-
float(question_count),
|
| 391 |
-
float(caps_ratio),
|
| 392 |
-
float(avg_sentence_length),
|
| 393 |
-
float(punctuation_density),
|
| 394 |
-
float(token_count),
|
| 395 |
-
float(unique_word_ratio),
|
| 396 |
-
float(avg_word_length),
|
| 397 |
-
float(subjectivity),
|
| 398 |
-
float(flesch_reading_ease),
|
| 399 |
-
float(flesch_kincaid_grade),
|
| 400 |
-
float(coleman_liau_index),
|
| 401 |
-
float(ari),
|
| 402 |
-
float(first_person_ratio),
|
| 403 |
-
float(auxiliary_verb_ratio),
|
| 404 |
-
float(gunning_fog_index),
|
| 405 |
-
float(analytical_thinking),
|
| 406 |
-
float(certainty_score),
|
| 407 |
-
float(tentative_score),
|
| 408 |
-
float(clout_score),
|
| 409 |
-
float(comma_period_density),
|
| 410 |
-
float(informal_punct_density),
|
| 411 |
-
float(past_focus_ratio),
|
| 412 |
-
float(present_focus_ratio),
|
| 413 |
-
float(future_focus_ratio),
|
| 414 |
-
]
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
STYLOMETRIC_FEATURE_NAMES = [
|
| 418 |
-
"exclamation_density",
|
| 419 |
-
"question_count",
|
| 420 |
-
"caps_ratio",
|
| 421 |
-
"avg_sentence_length",
|
| 422 |
-
"punctuation_density",
|
| 423 |
-
"token_count",
|
| 424 |
-
"unique_word_ratio",
|
| 425 |
-
"avg_word_length",
|
| 426 |
-
"subjectivity",
|
| 427 |
-
"flesch_reading_ease",
|
| 428 |
-
"flesch_kincaid_grade",
|
| 429 |
-
"coleman_liau_index",
|
| 430 |
-
"ari",
|
| 431 |
-
"first_person_ratio",
|
| 432 |
-
"auxiliary_verb_ratio",
|
| 433 |
-
"gunning_fog_index",
|
| 434 |
-
"analytical_thinking",
|
| 435 |
-
"certainty_score",
|
| 436 |
-
"tentative_score",
|
| 437 |
-
"clout_score",
|
| 438 |
-
"comma_period_density",
|
| 439 |
-
"informal_punct_density",
|
| 440 |
-
"past_focus_ratio",
|
| 441 |
-
"present_focus_ratio",
|
| 442 |
-
"future_focus_ratio",
|
| 443 |
-
]
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
# ───────────────────────────────────────────────────────────
|
| 447 |
-
# Main Evaluation
|
| 448 |
-
# ──────────────────────────────────────────────────────────
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
def main():
|
| 452 |
-
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 453 |
-
label_names = ["Real", "Fake"]
|
| 454 |
-
|
| 455 |
-
# ── 1. Load Dataset ──
|
| 456 |
-
print("=" * 60)
|
| 457 |
-
print(" MODEL OVERFITTING EVALUATION")
|
| 458 |
-
print("=" * 60)
|
| 459 |
-
|
| 460 |
-
csv_path = os.path.join(
|
| 461 |
-
PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv"
|
| 462 |
-
)
|
| 463 |
-
if not os.path.exists(csv_path):
|
| 464 |
-
print(f"ERROR: Dataset not found at {csv_path}")
|
| 465 |
-
return
|
| 466 |
-
|
| 467 |
-
df = pd.read_csv(csv_path)
|
| 468 |
-
print(f"\nDataset: jcblaise/fake_news_filipino")
|
| 469 |
-
print(f"Total articles: {len(df)}")
|
| 470 |
-
print(f"Distribution:")
|
| 471 |
-
print(f" Real (0): {(df['label'] == 0).sum()}")
|
| 472 |
-
print(f" Fake (1): {(df['label'] == 1).sum()}")
|
| 473 |
-
|
| 474 |
-
# ── 2. Preprocess ──
|
| 475 |
-
print("\nPreprocessing...")
|
| 476 |
-
df = df.dropna(subset=["article"]).copy()
|
| 477 |
-
df = df[df["article"].str.len() > 0].copy()
|
| 478 |
-
df.loc[:, "article_clean"] = df["article"].apply(clean_text)
|
| 479 |
-
|
| 480 |
-
X_texts = df["article_clean"].tolist()
|
| 481 |
-
y_labels = df["label"].tolist()
|
| 482 |
-
print(f" Valid articles: {len(X_texts)}")
|
| 483 |
-
|
| 484 |
-
# ── 3. Split: 80% Train / 20% Test ──
|
| 485 |
-
print("\nSplitting data: 80% Train / 20% Test...")
|
| 486 |
-
X_train, X_test, y_train, y_test = train_test_split(
|
| 487 |
-
X_texts,
|
| 488 |
-
y_labels,
|
| 489 |
-
test_size=0.20,
|
| 490 |
-
random_state=42,
|
| 491 |
-
stratify=y_labels,
|
| 492 |
-
)
|
| 493 |
-
print(f" Training set: {len(X_train)} articles")
|
| 494 |
-
print(f" Testing set: {len(X_test)} articles")
|
| 495 |
-
|
| 496 |
-
# ── 4. Build Hybrid Features ──
|
| 497 |
-
print("\nBuilding hybrid features (TF-IDF + MiniLM + stylometric)...")
|
| 498 |
-
|
| 499 |
-
# TF-IDF
|
| 500 |
-
tfidf = TfidfVectorizer(
|
| 501 |
-
max_features=15000,
|
| 502 |
-
ngram_range=(1, 2),
|
| 503 |
-
min_df=2,
|
| 504 |
-
max_df=0.95,
|
| 505 |
-
sublinear_tf=True,
|
| 506 |
-
)
|
| 507 |
-
X_train_tfidf = tfidf.fit_transform(X_train)
|
| 508 |
-
X_test_tfidf = tfidf.transform(X_test)
|
| 509 |
-
|
| 510 |
-
# MiniLM embeddings
|
| 511 |
-
print(" Encoding texts with MiniLM...")
|
| 512 |
-
minilm = get_minilm_model()
|
| 513 |
-
train_embeddings = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
|
| 514 |
-
test_embeddings = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
|
| 515 |
-
|
| 516 |
-
# Stylometric
|
| 517 |
-
print(" Extracting stylometric features...")
|
| 518 |
-
train_stylo = np.array([extract_stylometric_features(t) for t in X_train])
|
| 519 |
-
test_stylo = np.array([extract_stylometric_features(t) for t in X_test])
|
| 520 |
-
|
| 521 |
-
scaler = StandardScaler()
|
| 522 |
-
train_stylo_scaled = scaler.fit_transform(train_stylo)
|
| 523 |
-
test_stylo_scaled = scaler.transform(test_stylo)
|
| 524 |
-
|
| 525 |
-
# Combine
|
| 526 |
-
X_train_feat = hstack(
|
| 527 |
-
[X_train_tfidf, csr_matrix(train_embeddings), csr_matrix(train_stylo_scaled)]
|
| 528 |
-
)
|
| 529 |
-
X_test_feat = hstack(
|
| 530 |
-
[X_test_tfidf, csr_matrix(test_embeddings), csr_matrix(test_stylo_scaled)]
|
| 531 |
-
)
|
| 532 |
-
|
| 533 |
-
n_tfidf = X_train_tfidf.shape[1]
|
| 534 |
-
n_minilm = 384
|
| 535 |
-
n_stylo = len(STYLOMETRIC_FEATURE_NAMES)
|
| 536 |
-
print(
|
| 537 |
-
f" Feature dimensions: {X_train_feat.shape[1]} "
|
| 538 |
-
f"(TF-IDF: {n_tfidf} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
|
| 539 |
-
)
|
| 540 |
-
|
| 541 |
-
# ── 5. Full 5-Fold Cross-Validation ──
|
| 542 |
-
print("\n" + "=" * 60)
|
| 543 |
-
print(" 5-FOLD CROSS-VALIDATION (Full Dataset)")
|
| 544 |
-
print("=" * 60)
|
| 545 |
-
|
| 546 |
-
# Build features on entire dataset
|
| 547 |
-
print("\nBuilding features on full dataset...")
|
| 548 |
-
tfidf_full = TfidfVectorizer(
|
| 549 |
-
max_features=15000,
|
| 550 |
-
ngram_range=(1, 2),
|
| 551 |
-
min_df=2,
|
| 552 |
-
max_df=0.95,
|
| 553 |
-
sublinear_tf=True,
|
| 554 |
-
)
|
| 555 |
-
X_tfidf_full = tfidf_full.fit_transform(X_texts)
|
| 556 |
-
|
| 557 |
-
print(" Encoding full dataset with MiniLM...")
|
| 558 |
-
full_embeddings = minilm.encode(X_texts, show_progress_bar=True, batch_size=64)
|
| 559 |
-
|
| 560 |
-
stylo_full = np.array([extract_stylometric_features(t) for t in X_texts])
|
| 561 |
-
scaler_full = StandardScaler()
|
| 562 |
-
stylo_full_scaled = scaler_full.fit_transform(stylo_full)
|
| 563 |
-
X_full = hstack(
|
| 564 |
-
[X_tfidf_full, csr_matrix(full_embeddings), csr_matrix(stylo_full_scaled)]
|
| 565 |
-
)
|
| 566 |
-
y_full = np.array(y_labels)
|
| 567 |
-
|
| 568 |
-
print(f" Total samples: {X_full.shape[0]}")
|
| 569 |
-
print(
|
| 570 |
-
f" Feature dimensions: {X_full.shape[1]} "
|
| 571 |
-
f"(TF-IDF: {X_tfidf_full.shape[1]} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
|
| 572 |
-
)
|
| 573 |
-
|
| 574 |
-
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
|
| 575 |
-
|
| 576 |
-
fold_accuracies = []
|
| 577 |
-
fold_precisions = []
|
| 578 |
-
fold_recalls = []
|
| 579 |
-
fold_f1s = []
|
| 580 |
-
fold_train_accs = []
|
| 581 |
-
all_y_true = []
|
| 582 |
-
all_y_pred = []
|
| 583 |
-
|
| 584 |
-
for fold_idx, (train_idx, test_idx) in enumerate(cv.split(X_full, y_full), 1):
|
| 585 |
-
X_fold_train = X_full[train_idx]
|
| 586 |
-
X_fold_test = X_full[test_idx]
|
| 587 |
-
y_fold_train = y_full[train_idx]
|
| 588 |
-
y_fold_test = y_full[test_idx]
|
| 589 |
-
|
| 590 |
-
print(f"\n{'─' * 60}")
|
| 591 |
-
print(f" FOLD {fold_idx}/5 (Train: {len(train_idx)}, Test: {len(test_idx)})")
|
| 592 |
-
print(f"{'─' * 60}")
|
| 593 |
-
|
| 594 |
-
rf_fold = RandomForestClassifier(
|
| 595 |
-
n_estimators=300,
|
| 596 |
-
max_depth=15,
|
| 597 |
-
min_samples_split=5,
|
| 598 |
-
min_samples_leaf=5,
|
| 599 |
-
class_weight="balanced",
|
| 600 |
-
n_jobs=-1,
|
| 601 |
-
random_state=42,
|
| 602 |
-
)
|
| 603 |
-
rf_fold.fit(X_fold_train, y_fold_train)
|
| 604 |
-
|
| 605 |
-
# Predictions
|
| 606 |
-
y_fold_train_pred = rf_fold.predict(X_fold_train)
|
| 607 |
-
y_fold_test_pred = rf_fold.predict(X_fold_test)
|
| 608 |
-
|
| 609 |
-
train_acc = accuracy_score(y_fold_train, y_fold_train_pred)
|
| 610 |
-
test_acc = accuracy_score(y_fold_test, y_fold_test_pred)
|
| 611 |
-
|
| 612 |
-
fold_train_accs.append(train_acc)
|
| 613 |
-
fold_accuracies.append(test_acc)
|
| 614 |
-
|
| 615 |
-
# Per-fold classification report
|
| 616 |
-
report = classification_report(
|
| 617 |
-
y_fold_test,
|
| 618 |
-
y_fold_test_pred,
|
| 619 |
-
target_names=label_names,
|
| 620 |
-
output_dict=True,
|
| 621 |
-
)
|
| 622 |
-
fold_precisions.append(report["weighted avg"]["precision"])
|
| 623 |
-
fold_recalls.append(report["weighted avg"]["recall"])
|
| 624 |
-
fold_f1s.append(report["weighted avg"]["f1-score"])
|
| 625 |
-
|
| 626 |
-
# Collect for final confusion matrix
|
| 627 |
-
all_y_true.extend(y_fold_test)
|
| 628 |
-
all_y_pred.extend(y_fold_test_pred)
|
| 629 |
-
|
| 630 |
-
print(f" Train Accuracy: {train_acc:.4f} ({train_acc:.1%})")
|
| 631 |
-
print(f" Test Accuracy: {test_acc:.4f} ({test_acc:.1%})")
|
| 632 |
-
print(f" Gap: {train_acc - test_acc:.4f}")
|
| 633 |
-
print()
|
| 634 |
-
print(
|
| 635 |
-
classification_report(
|
| 636 |
-
y_fold_test, y_fold_test_pred, target_names=label_names
|
| 637 |
-
)
|
| 638 |
-
)
|
| 639 |
-
|
| 640 |
-
# ── 6. Cross-Fold Summary ──
|
| 641 |
-
fold_accuracies = np.array(fold_accuracies)
|
| 642 |
-
fold_train_accs = np.array(fold_train_accs)
|
| 643 |
-
fold_precisions = np.array(fold_precisions)
|
| 644 |
-
fold_recalls = np.array(fold_recalls)
|
| 645 |
-
fold_f1s = np.array(fold_f1s)
|
| 646 |
-
gaps = fold_train_accs - fold_accuracies
|
| 647 |
-
|
| 648 |
-
print("\n" + "=" * 60)
|
| 649 |
-
print(" CROSS-VALIDATION SUMMARY (5 Folds)")
|
| 650 |
-
print("=" * 60)
|
| 651 |
-
|
| 652 |
-
print(f"\n Per-Fold Test Accuracies:")
|
| 653 |
-
for i, (ta, te) in enumerate(zip(fold_train_accs, fold_accuracies), 1):
|
| 654 |
-
print(f" Fold {i}: Train {ta:.1%} | Test {te:.1%} | Gap {ta - te:.1%}")
|
| 655 |
-
|
| 656 |
-
print(
|
| 657 |
-
f"\n Average Training Accuracy: {fold_train_accs.mean():.4f} "
|
| 658 |
-
f"(+/- {fold_train_accs.std():.4f})"
|
| 659 |
-
)
|
| 660 |
-
print(
|
| 661 |
-
f" Average Testing Accuracy: {fold_accuracies.mean():.4f} "
|
| 662 |
-
f"(+/- {fold_accuracies.std():.4f})"
|
| 663 |
-
)
|
| 664 |
-
print(
|
| 665 |
-
f" Average Precision: {fold_precisions.mean():.4f} "
|
| 666 |
-
f"(+/- {fold_precisions.std():.4f})"
|
| 667 |
-
)
|
| 668 |
-
print(
|
| 669 |
-
f" Average Recall: {fold_recalls.mean():.4f} "
|
| 670 |
-
f"(+/- {fold_recalls.std():.4f})"
|
| 671 |
-
)
|
| 672 |
-
print(
|
| 673 |
-
f" Average F1 Score: {fold_f1s.mean():.4f} "
|
| 674 |
-
f"(+/- {fold_f1s.std():.4f})"
|
| 675 |
-
)
|
| 676 |
-
print(f" Average Gap: {gaps.mean():.4f} " f"(+/- {gaps.std():.4f})")
|
| 677 |
-
|
| 678 |
-
# ── 7. Consistency Check ──
|
| 679 |
-
print("\n" + "=" * 60)
|
| 680 |
-
print(" VERDICT CONSISTENCY & OVERFITTING ANALYSIS")
|
| 681 |
-
print("=" * 60)
|
| 682 |
-
|
| 683 |
-
avg_train = fold_train_accs.mean()
|
| 684 |
-
avg_test = fold_accuracies.mean()
|
| 685 |
-
avg_gap = gaps.mean()
|
| 686 |
-
acc_std = fold_accuracies.std()
|
| 687 |
-
|
| 688 |
-
if avg_train > 0.95 and avg_test < 0.70:
|
| 689 |
-
overfit_status = "OVERFITTING DETECTED"
|
| 690 |
-
print(f"\n *** OVERFITTING DETECTED ***")
|
| 691 |
-
print(f" Average training accuracy ({avg_train:.1%}) is much higher than")
|
| 692 |
-
print(f" average testing accuracy ({avg_test:.1%}).")
|
| 693 |
-
print(f" The model memorizes training data and fails to generalize.")
|
| 694 |
-
elif avg_gap > 0.10:
|
| 695 |
-
overfit_status = "MILD OVERFITTING"
|
| 696 |
-
print(f"\n ** MILD OVERFITTING **")
|
| 697 |
-
print(f" Average gap ({avg_gap:.1%}) exceeds 10%.")
|
| 698 |
-
else:
|
| 699 |
-
overfit_status = "NO OVERFITTING"
|
| 700 |
-
print(f"\n NO OVERFITTING DETECTED")
|
| 701 |
-
print(f" Average gap ({avg_gap:.1%}) is within acceptable range.")
|
| 702 |
-
|
| 703 |
-
if acc_std < 0.01:
|
| 704 |
-
consistency = "HIGHLY CONSISTENT"
|
| 705 |
-
print(f" Verdict Consistency: HIGHLY CONSISTENT (std={acc_std:.4f})")
|
| 706 |
-
print(f" Predictions are very stable across all 5 folds.")
|
| 707 |
-
elif acc_std < 0.03:
|
| 708 |
-
consistency = "CONSISTENT"
|
| 709 |
-
print(f" Verdict Consistency: CONSISTENT (std={acc_std:.4f})")
|
| 710 |
-
print(f" Minor variance across folds — acceptable for production.")
|
| 711 |
-
else:
|
| 712 |
-
consistency = "INCONSISTENT"
|
| 713 |
-
print(f" Verdict Consistency: INCONSISTENT (std={acc_std:.4f})")
|
| 714 |
-
print(f" High variance suggests model stability issues.")
|
| 715 |
-
|
| 716 |
-
# ── 8. Confusion Matrix (aggregated across all folds) ──
|
| 717 |
-
print("\n\nGenerating plots...")
|
| 718 |
-
cm = confusion_matrix(all_y_true, all_y_pred)
|
| 719 |
-
overall_acc = accuracy_score(all_y_true, all_y_pred)
|
| 720 |
-
|
| 721 |
-
fig, ax = plt.subplots(figsize=(8, 6))
|
| 722 |
-
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=label_names)
|
| 723 |
-
disp.plot(ax=ax, cmap="Blues", values_format="d")
|
| 724 |
-
|
| 725 |
-
ax.set_title(
|
| 726 |
-
f"Confusion Matrix — Aggregated 5-Fold CV\n"
|
| 727 |
-
f"Overall Accuracy: {overall_acc:.1%} | {overfit_status}",
|
| 728 |
-
fontsize=14,
|
| 729 |
-
fontweight="bold",
|
| 730 |
-
)
|
| 731 |
-
ax.set_xlabel("Predicted Label", fontsize=12)
|
| 732 |
-
ax.set_ylabel("True Label", fontsize=12)
|
| 733 |
-
|
| 734 |
-
plt.tight_layout()
|
| 735 |
-
cm_path = os.path.join(OUTPUT_DIR, "confusion_matrix.png")
|
| 736 |
-
fig.savefig(cm_path, dpi=150, bbox_inches="tight")
|
| 737 |
-
print(f" Saved: {cm_path}")
|
| 738 |
-
|
| 739 |
-
# ── 9. Per-Fold Accuracy Bar Chart ──
|
| 740 |
-
fig2, ax2 = plt.subplots(figsize=(10, 5))
|
| 741 |
-
|
| 742 |
-
x = np.arange(5)
|
| 743 |
-
width = 0.35
|
| 744 |
-
bars_train = ax2.bar(
|
| 745 |
-
x - width / 2,
|
| 746 |
-
fold_train_accs * 100,
|
| 747 |
-
width,
|
| 748 |
-
label="Training",
|
| 749 |
-
color="#2196F3",
|
| 750 |
-
edgecolor="black",
|
| 751 |
-
linewidth=0.5,
|
| 752 |
-
)
|
| 753 |
-
bars_test = ax2.bar(
|
| 754 |
-
x + width / 2,
|
| 755 |
-
fold_accuracies * 100,
|
| 756 |
-
width,
|
| 757 |
-
label="Testing",
|
| 758 |
-
color="#FF9800",
|
| 759 |
-
edgecolor="black",
|
| 760 |
-
linewidth=0.5,
|
| 761 |
-
)
|
| 762 |
-
|
| 763 |
-
for bar, val in zip(bars_train, fold_train_accs):
|
| 764 |
-
ax2.text(
|
| 765 |
-
bar.get_x() + bar.get_width() / 2,
|
| 766 |
-
bar.get_height() + 0.3,
|
| 767 |
-
f"{val:.1%}",
|
| 768 |
-
ha="center",
|
| 769 |
-
va="bottom",
|
| 770 |
-
fontsize=9,
|
| 771 |
-
fontweight="bold",
|
| 772 |
-
)
|
| 773 |
-
for bar, val in zip(bars_test, fold_accuracies):
|
| 774 |
-
ax2.text(
|
| 775 |
-
bar.get_x() + bar.get_width() / 2,
|
| 776 |
-
bar.get_height() + 0.3,
|
| 777 |
-
f"{val:.1%}",
|
| 778 |
-
ha="center",
|
| 779 |
-
va="bottom",
|
| 780 |
-
fontsize=9,
|
| 781 |
-
fontweight="bold",
|
| 782 |
-
)
|
| 783 |
-
|
| 784 |
-
ax2.set_xticks(x)
|
| 785 |
-
ax2.set_xticklabels([f"Fold {i}" for i in range(1, 6)])
|
| 786 |
-
ax2.set_ylim(0, 105)
|
| 787 |
-
ax2.set_ylabel("Accuracy (%)", fontsize=12)
|
| 788 |
-
ax2.set_title(
|
| 789 |
-
f"Per-Fold Accuracy Comparison\n"
|
| 790 |
-
f"Avg Test: {avg_test:.1%} (+/- {acc_std:.4f}) | {consistency}",
|
| 791 |
-
fontsize=14,
|
| 792 |
-
fontweight="bold",
|
| 793 |
-
)
|
| 794 |
-
ax2.legend(loc="lower right")
|
| 795 |
-
ax2.axhline(y=70, color="red", linestyle="--", alpha=0.5, label="70% threshold")
|
| 796 |
-
|
| 797 |
-
plt.tight_layout()
|
| 798 |
-
bar_path = os.path.join(OUTPUT_DIR, "accuracy_comparison.png")
|
| 799 |
-
fig2.savefig(bar_path, dpi=150, bbox_inches="tight")
|
| 800 |
-
print(f" Saved: {bar_path}")
|
| 801 |
-
|
| 802 |
-
# ── Final Summary ──
|
| 803 |
-
print("\n" + "=" * 60)
|
| 804 |
-
print(" EVALUATION COMPLETE")
|
| 805 |
-
print("=" * 60)
|
| 806 |
-
print(f" Dataset: fake_news_filipino ({len(df)} articles)")
|
| 807 |
-
print(f" Feature set: {X_full.shape[1]} (TF-IDF + 9 stylometric)")
|
| 808 |
-
print(f" Cross-Validation: 5-Fold Stratified")
|
| 809 |
-
print(f" Avg Training Accuracy: {avg_train:.4f} (+/- {fold_train_accs.std():.4f})")
|
| 810 |
-
print(f" Avg Testing Accuracy: {avg_test:.4f} (+/- {acc_std:.4f})")
|
| 811 |
-
print(f" Avg F1 Score: {fold_f1s.mean():.4f} (+/- {fold_f1s.std():.4f})")
|
| 812 |
-
print(f" Avg Gap: {avg_gap:.4f}")
|
| 813 |
-
print(f" Overfitting Status: {overfit_status}")
|
| 814 |
-
print(f" Verdict Consistency: {consistency}")
|
| 815 |
-
print(f" Plots saved to: {OUTPUT_DIR}/")
|
| 816 |
-
print("=" * 60)
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
if __name__ == "__main__":
|
| 820 |
-
main()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Model Overfitting Evaluation Script
|
| 3 |
+
=====================================
|
| 4 |
+
Evaluates the Random Forest fake news classifier for overfitting by
|
| 5 |
+
comparing Training vs. Testing performance.
|
| 6 |
+
|
| 7 |
+
Split: 80% Train / 20% Test
|
| 8 |
+
Metrics: classification_report, accuracy_score, confusion matrix plot
|
| 9 |
+
Flag: Overfitting detected if Train Acc > 95% and Test Acc < 70%
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
python backend/evaluate_model.py
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import sys
|
| 16 |
+
import os
|
| 17 |
+
import re
|
| 18 |
+
import time
|
| 19 |
+
import numpy as np
|
| 20 |
+
from textblob import TextBlob
|
| 21 |
+
import textstat
|
| 22 |
+
|
| 23 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 25 |
+
|
| 26 |
+
import pandas as pd
|
| 27 |
+
import matplotlib
|
| 28 |
+
|
| 29 |
+
matplotlib.use("Agg") # Non-interactive backend for saving plots
|
| 30 |
+
import matplotlib.pyplot as plt
|
| 31 |
+
from scipy.sparse import hstack, csr_matrix
|
| 32 |
+
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
|
| 33 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 34 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 35 |
+
from sklearn.preprocessing import StandardScaler
|
| 36 |
+
from sklearn.metrics import (
|
| 37 |
+
classification_report,
|
| 38 |
+
accuracy_score,
|
| 39 |
+
confusion_matrix,
|
| 40 |
+
ConfusionMatrixDisplay,
|
| 41 |
+
)
|
| 42 |
+
from sentence_transformers import SentenceTransformer
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ── Paths ──
|
| 46 |
+
DATA_MODELS_DIR = os.path.join(PROJECT_ROOT, "data_models")
|
| 47 |
+
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ── MiniLM Model (lazy-loaded singleton) ──
|
| 51 |
+
MINILM_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
|
| 52 |
+
_minilm_model = None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def get_minilm_model():
|
| 56 |
+
"""Load the multilingual MiniLM model (cached after first call)."""
|
| 57 |
+
global _minilm_model
|
| 58 |
+
if _minilm_model is None:
|
| 59 |
+
print(" Loading MiniLM model...")
|
| 60 |
+
_minilm_model = SentenceTransformer(MINILM_MODEL_NAME)
|
| 61 |
+
return _minilm_model
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ───────────────────────────────────────────────────────────
|
| 65 |
+
# Text Cleaning (same as train.py)
|
| 66 |
+
# ───────────────────────────────────────────────────────────
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def clean_text(text):
|
| 70 |
+
"""Basic text cleaning for Filipino news articles."""
|
| 71 |
+
if not text or not isinstance(text, str):
|
| 72 |
+
return ""
|
| 73 |
+
text = re.sub(r"<[^>]+>", " ", text)
|
| 74 |
+
text = re.sub(r"https?://\S+", " ", text)
|
| 75 |
+
text = re.sub(r"\s+", " ", text)
|
| 76 |
+
return text.strip()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ───────────────────────────────────────────────────────────
|
| 80 |
+
# Stylometric Features (same as train.py)
|
| 81 |
+
# ───────────────────────────────────────────────────────────
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ── Word lists for linguistic features ──
|
| 85 |
+
FIRST_PERSON_PRONOUNS = {
|
| 86 |
+
"i",
|
| 87 |
+
"me",
|
| 88 |
+
"my",
|
| 89 |
+
"mine",
|
| 90 |
+
"myself",
|
| 91 |
+
"we",
|
| 92 |
+
"us",
|
| 93 |
+
"our",
|
| 94 |
+
"ours",
|
| 95 |
+
"ourselves",
|
| 96 |
+
"ako",
|
| 97 |
+
"ko",
|
| 98 |
+
"akin",
|
| 99 |
+
"aking",
|
| 100 |
+
"natin",
|
| 101 |
+
"atin",
|
| 102 |
+
"namin",
|
| 103 |
+
"amin",
|
| 104 |
+
"tayo",
|
| 105 |
+
"kami",
|
| 106 |
+
"ta",
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
AUXILIARY_VERBS = {
|
| 110 |
+
"have",
|
| 111 |
+
"has",
|
| 112 |
+
"had",
|
| 113 |
+
"do",
|
| 114 |
+
"does",
|
| 115 |
+
"did",
|
| 116 |
+
"will",
|
| 117 |
+
"would",
|
| 118 |
+
"shall",
|
| 119 |
+
"should",
|
| 120 |
+
"may",
|
| 121 |
+
"might",
|
| 122 |
+
"can",
|
| 123 |
+
"could",
|
| 124 |
+
"must",
|
| 125 |
+
"am",
|
| 126 |
+
"is",
|
| 127 |
+
"are",
|
| 128 |
+
"was",
|
| 129 |
+
"were",
|
| 130 |
+
"be",
|
| 131 |
+
"been",
|
| 132 |
+
"being",
|
| 133 |
+
"ay",
|
| 134 |
+
"dapat",
|
| 135 |
+
"mayroon",
|
| 136 |
+
"meron",
|
| 137 |
+
"maaari",
|
| 138 |
+
"pwede",
|
| 139 |
+
"kailangan",
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
ANALYTICAL_WORDS = {
|
| 143 |
+
"the",
|
| 144 |
+
"a",
|
| 145 |
+
"an",
|
| 146 |
+
"of",
|
| 147 |
+
"in",
|
| 148 |
+
"on",
|
| 149 |
+
"at",
|
| 150 |
+
"to",
|
| 151 |
+
"for",
|
| 152 |
+
"with",
|
| 153 |
+
"by",
|
| 154 |
+
"from",
|
| 155 |
+
"about",
|
| 156 |
+
"between",
|
| 157 |
+
"through",
|
| 158 |
+
"during",
|
| 159 |
+
"before",
|
| 160 |
+
"after",
|
| 161 |
+
"ang",
|
| 162 |
+
"ng",
|
| 163 |
+
"sa",
|
| 164 |
+
"mga",
|
| 165 |
+
"nang",
|
| 166 |
+
"para",
|
| 167 |
+
"tungkol",
|
| 168 |
+
"mula",
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
CERTAINTY_WORDS = {
|
| 172 |
+
"always",
|
| 173 |
+
"never",
|
| 174 |
+
"absolutely",
|
| 175 |
+
"definitely",
|
| 176 |
+
"certainly",
|
| 177 |
+
"undoubtedly",
|
| 178 |
+
"clearly",
|
| 179 |
+
"obviously",
|
| 180 |
+
"without doubt",
|
| 181 |
+
"guaranteed",
|
| 182 |
+
"proven",
|
| 183 |
+
"fact",
|
| 184 |
+
"undeniable",
|
| 185 |
+
"indisputable",
|
| 186 |
+
"every",
|
| 187 |
+
"all",
|
| 188 |
+
"palagi",
|
| 189 |
+
"sigurado",
|
| 190 |
+
"tiyak",
|
| 191 |
+
"talaga",
|
| 192 |
+
"totoo",
|
| 193 |
+
"lagi",
|
| 194 |
+
"walang duda",
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
TENTATIVE_WORDS = {
|
| 198 |
+
"perhaps",
|
| 199 |
+
"maybe",
|
| 200 |
+
"possibly",
|
| 201 |
+
"might",
|
| 202 |
+
"could",
|
| 203 |
+
"likely",
|
| 204 |
+
"unlikely",
|
| 205 |
+
"suggests",
|
| 206 |
+
"appears",
|
| 207 |
+
"seems",
|
| 208 |
+
"allegedly",
|
| 209 |
+
"reportedly",
|
| 210 |
+
"according",
|
| 211 |
+
"probable",
|
| 212 |
+
"approximately",
|
| 213 |
+
"estimated",
|
| 214 |
+
"siguro",
|
| 215 |
+
"marahil",
|
| 216 |
+
"maaaring",
|
| 217 |
+
"mukhang",
|
| 218 |
+
"parang",
|
| 219 |
+
"umano",
|
| 220 |
+
"diumano",
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
CLOUT_WORDS = {
|
| 224 |
+
"must",
|
| 225 |
+
"demand",
|
| 226 |
+
"require",
|
| 227 |
+
"order",
|
| 228 |
+
"command",
|
| 229 |
+
"insist",
|
| 230 |
+
"decree",
|
| 231 |
+
"mandate",
|
| 232 |
+
"authority",
|
| 233 |
+
"power",
|
| 234 |
+
"control",
|
| 235 |
+
"dominant",
|
| 236 |
+
"superior",
|
| 237 |
+
"we must",
|
| 238 |
+
"you must",
|
| 239 |
+
"kailangan",
|
| 240 |
+
"dapat",
|
| 241 |
+
"utos",
|
| 242 |
+
"kapangyarihan",
|
| 243 |
+
"kontrol",
|
| 244 |
+
"mando",
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
PAST_FOCUS_WORDS = {
|
| 248 |
+
"talked",
|
| 249 |
+
"did",
|
| 250 |
+
"ago",
|
| 251 |
+
"said",
|
| 252 |
+
"was",
|
| 253 |
+
"were",
|
| 254 |
+
"had",
|
| 255 |
+
"went",
|
| 256 |
+
"told",
|
| 257 |
+
"noon",
|
| 258 |
+
"nakaraan",
|
| 259 |
+
"dati",
|
| 260 |
+
"kahapon",
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
PRESENT_FOCUS_WORDS = {
|
| 264 |
+
"now",
|
| 265 |
+
"is",
|
| 266 |
+
"today",
|
| 267 |
+
"are",
|
| 268 |
+
"being",
|
| 269 |
+
"currently",
|
| 270 |
+
"ongoing",
|
| 271 |
+
"ngayon",
|
| 272 |
+
"kasalukuyan",
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
FUTURE_FOCUS_WORDS = {
|
| 276 |
+
"soon",
|
| 277 |
+
"will",
|
| 278 |
+
"may",
|
| 279 |
+
"shall",
|
| 280 |
+
"going",
|
| 281 |
+
"plan",
|
| 282 |
+
"expect",
|
| 283 |
+
"tomorrow",
|
| 284 |
+
"bukas",
|
| 285 |
+
"darating",
|
| 286 |
+
"magiging",
|
| 287 |
+
"gagawin",
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
def extract_stylometric_features(text):
|
| 292 |
+
"""Extract 25 stylometric features from text (matches train.py)."""
|
| 293 |
+
if not text or not isinstance(text, str):
|
| 294 |
+
return [0.0] * 25
|
| 295 |
+
|
| 296 |
+
words = text.split()
|
| 297 |
+
token_count = len(words)
|
| 298 |
+
if token_count == 0:
|
| 299 |
+
return [0.0] * 25
|
| 300 |
+
|
| 301 |
+
words_lower = [w.lower() for w in words]
|
| 302 |
+
text_len = len(text)
|
| 303 |
+
|
| 304 |
+
exclamation_density = text.count("!") / token_count
|
| 305 |
+
question_count = text.count("?")
|
| 306 |
+
|
| 307 |
+
caps_words = sum(1 for w in words if len(w) >= 2 and w.isupper())
|
| 308 |
+
caps_ratio = caps_words / token_count
|
| 309 |
+
|
| 310 |
+
sentences = re.split(r"[.!?]+", text)
|
| 311 |
+
sentences = [s.strip() for s in sentences if s.strip()]
|
| 312 |
+
avg_sentence_length = (
|
| 313 |
+
sum(len(s.split()) for s in sentences) / len(sentences)
|
| 314 |
+
if sentences
|
| 315 |
+
else token_count
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
|
| 319 |
+
punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
|
| 320 |
+
|
| 321 |
+
unique_words = len(set(words_lower))
|
| 322 |
+
unique_word_ratio = unique_words / token_count
|
| 323 |
+
|
| 324 |
+
avg_word_length = sum(len(w) for w in words) / token_count
|
| 325 |
+
|
| 326 |
+
try:
|
| 327 |
+
subjectivity = TextBlob(text).sentiment.subjectivity
|
| 328 |
+
except Exception:
|
| 329 |
+
subjectivity = 0.0
|
| 330 |
+
|
| 331 |
+
try:
|
| 332 |
+
flesch_reading_ease = textstat.flesch_reading_ease(text)
|
| 333 |
+
flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
|
| 334 |
+
coleman_liau_index = textstat.coleman_liau_index(text)
|
| 335 |
+
ari = textstat.automated_readability_index(text)
|
| 336 |
+
except Exception:
|
| 337 |
+
flesch_reading_ease = 0.0
|
| 338 |
+
flesch_kincaid_grade = 0.0
|
| 339 |
+
coleman_liau_index = 0.0
|
| 340 |
+
ari = 0.0
|
| 341 |
+
|
| 342 |
+
first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
|
| 343 |
+
first_person_ratio = first_person_count / token_count
|
| 344 |
+
|
| 345 |
+
aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
|
| 346 |
+
auxiliary_verb_ratio = aux_count / token_count
|
| 347 |
+
|
| 348 |
+
try:
|
| 349 |
+
gunning_fog_index = textstat.gunning_fog(text)
|
| 350 |
+
except Exception:
|
| 351 |
+
gunning_fog_index = 0.0
|
| 352 |
+
|
| 353 |
+
analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
|
| 354 |
+
analytical_thinking = analytical_count / token_count
|
| 355 |
+
|
| 356 |
+
certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
|
| 357 |
+
certainty_score = certainty_count / token_count
|
| 358 |
+
|
| 359 |
+
tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
|
| 360 |
+
tentative_score = tentative_count / token_count
|
| 361 |
+
|
| 362 |
+
clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
|
| 363 |
+
clout_score = clout_count / token_count
|
| 364 |
+
|
| 365 |
+
comma_period_count = text.count(",") + text.count(".")
|
| 366 |
+
comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
|
| 367 |
+
|
| 368 |
+
informal_count = (
|
| 369 |
+
text.count("(")
|
| 370 |
+
+ text.count(")")
|
| 371 |
+
+ text.count("—")
|
| 372 |
+
+ text.count("–")
|
| 373 |
+
+ text.count("-")
|
| 374 |
+
+ text.count("...")
|
| 375 |
+
+ text.count("…")
|
| 376 |
+
)
|
| 377 |
+
informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
|
| 378 |
+
|
| 379 |
+
past_count = sum(1 for w in words_lower if w in PAST_FOCUS_WORDS)
|
| 380 |
+
past_focus_ratio = past_count / token_count
|
| 381 |
+
|
| 382 |
+
present_count = sum(1 for w in words_lower if w in PRESENT_FOCUS_WORDS)
|
| 383 |
+
present_focus_ratio = present_count / token_count
|
| 384 |
+
|
| 385 |
+
future_count = sum(1 for w in words_lower if w in FUTURE_FOCUS_WORDS)
|
| 386 |
+
future_focus_ratio = future_count / token_count
|
| 387 |
+
|
| 388 |
+
return [
|
| 389 |
+
float(exclamation_density),
|
| 390 |
+
float(question_count),
|
| 391 |
+
float(caps_ratio),
|
| 392 |
+
float(avg_sentence_length),
|
| 393 |
+
float(punctuation_density),
|
| 394 |
+
float(token_count),
|
| 395 |
+
float(unique_word_ratio),
|
| 396 |
+
float(avg_word_length),
|
| 397 |
+
float(subjectivity),
|
| 398 |
+
float(flesch_reading_ease),
|
| 399 |
+
float(flesch_kincaid_grade),
|
| 400 |
+
float(coleman_liau_index),
|
| 401 |
+
float(ari),
|
| 402 |
+
float(first_person_ratio),
|
| 403 |
+
float(auxiliary_verb_ratio),
|
| 404 |
+
float(gunning_fog_index),
|
| 405 |
+
float(analytical_thinking),
|
| 406 |
+
float(certainty_score),
|
| 407 |
+
float(tentative_score),
|
| 408 |
+
float(clout_score),
|
| 409 |
+
float(comma_period_density),
|
| 410 |
+
float(informal_punct_density),
|
| 411 |
+
float(past_focus_ratio),
|
| 412 |
+
float(present_focus_ratio),
|
| 413 |
+
float(future_focus_ratio),
|
| 414 |
+
]
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
STYLOMETRIC_FEATURE_NAMES = [
|
| 418 |
+
"exclamation_density",
|
| 419 |
+
"question_count",
|
| 420 |
+
"caps_ratio",
|
| 421 |
+
"avg_sentence_length",
|
| 422 |
+
"punctuation_density",
|
| 423 |
+
"token_count",
|
| 424 |
+
"unique_word_ratio",
|
| 425 |
+
"avg_word_length",
|
| 426 |
+
"subjectivity",
|
| 427 |
+
"flesch_reading_ease",
|
| 428 |
+
"flesch_kincaid_grade",
|
| 429 |
+
"coleman_liau_index",
|
| 430 |
+
"ari",
|
| 431 |
+
"first_person_ratio",
|
| 432 |
+
"auxiliary_verb_ratio",
|
| 433 |
+
"gunning_fog_index",
|
| 434 |
+
"analytical_thinking",
|
| 435 |
+
"certainty_score",
|
| 436 |
+
"tentative_score",
|
| 437 |
+
"clout_score",
|
| 438 |
+
"comma_period_density",
|
| 439 |
+
"informal_punct_density",
|
| 440 |
+
"past_focus_ratio",
|
| 441 |
+
"present_focus_ratio",
|
| 442 |
+
"future_focus_ratio",
|
| 443 |
+
]
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
# ───────────────────────────────────────────────────────────
|
| 447 |
+
# Main Evaluation
|
| 448 |
+
# ──��────────────────────────────────────────────────────────
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def main():
|
| 452 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 453 |
+
label_names = ["Real", "Fake"]
|
| 454 |
+
|
| 455 |
+
# ── 1. Load Dataset ──
|
| 456 |
+
print("=" * 60)
|
| 457 |
+
print(" MODEL OVERFITTING EVALUATION")
|
| 458 |
+
print("=" * 60)
|
| 459 |
+
|
| 460 |
+
csv_path = os.path.join(
|
| 461 |
+
PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv"
|
| 462 |
+
)
|
| 463 |
+
if not os.path.exists(csv_path):
|
| 464 |
+
print(f"ERROR: Dataset not found at {csv_path}")
|
| 465 |
+
return
|
| 466 |
+
|
| 467 |
+
df = pd.read_csv(csv_path)
|
| 468 |
+
print(f"\nDataset: jcblaise/fake_news_filipino")
|
| 469 |
+
print(f"Total articles: {len(df)}")
|
| 470 |
+
print(f"Distribution:")
|
| 471 |
+
print(f" Real (0): {(df['label'] == 0).sum()}")
|
| 472 |
+
print(f" Fake (1): {(df['label'] == 1).sum()}")
|
| 473 |
+
|
| 474 |
+
# ── 2. Preprocess ──
|
| 475 |
+
print("\nPreprocessing...")
|
| 476 |
+
df = df.dropna(subset=["article"]).copy()
|
| 477 |
+
df = df[df["article"].str.len() > 0].copy()
|
| 478 |
+
df.loc[:, "article_clean"] = df["article"].apply(clean_text)
|
| 479 |
+
|
| 480 |
+
X_texts = df["article_clean"].tolist()
|
| 481 |
+
y_labels = df["label"].tolist()
|
| 482 |
+
print(f" Valid articles: {len(X_texts)}")
|
| 483 |
+
|
| 484 |
+
# ── 3. Split: 80% Train / 20% Test ──
|
| 485 |
+
print("\nSplitting data: 80% Train / 20% Test...")
|
| 486 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 487 |
+
X_texts,
|
| 488 |
+
y_labels,
|
| 489 |
+
test_size=0.20,
|
| 490 |
+
random_state=42,
|
| 491 |
+
stratify=y_labels,
|
| 492 |
+
)
|
| 493 |
+
print(f" Training set: {len(X_train)} articles")
|
| 494 |
+
print(f" Testing set: {len(X_test)} articles")
|
| 495 |
+
|
| 496 |
+
# ── 4. Build Hybrid Features ──
|
| 497 |
+
print("\nBuilding hybrid features (TF-IDF + MiniLM + stylometric)...")
|
| 498 |
+
|
| 499 |
+
# TF-IDF
|
| 500 |
+
tfidf = TfidfVectorizer(
|
| 501 |
+
max_features=15000,
|
| 502 |
+
ngram_range=(1, 2),
|
| 503 |
+
min_df=2,
|
| 504 |
+
max_df=0.95,
|
| 505 |
+
sublinear_tf=True,
|
| 506 |
+
)
|
| 507 |
+
X_train_tfidf = tfidf.fit_transform(X_train)
|
| 508 |
+
X_test_tfidf = tfidf.transform(X_test)
|
| 509 |
+
|
| 510 |
+
# MiniLM embeddings
|
| 511 |
+
print(" Encoding texts with MiniLM...")
|
| 512 |
+
minilm = get_minilm_model()
|
| 513 |
+
train_embeddings = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
|
| 514 |
+
test_embeddings = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
|
| 515 |
+
|
| 516 |
+
# Stylometric
|
| 517 |
+
print(" Extracting stylometric features...")
|
| 518 |
+
train_stylo = np.array([extract_stylometric_features(t) for t in X_train])
|
| 519 |
+
test_stylo = np.array([extract_stylometric_features(t) for t in X_test])
|
| 520 |
+
|
| 521 |
+
scaler = StandardScaler()
|
| 522 |
+
train_stylo_scaled = scaler.fit_transform(train_stylo)
|
| 523 |
+
test_stylo_scaled = scaler.transform(test_stylo)
|
| 524 |
+
|
| 525 |
+
# Combine
|
| 526 |
+
X_train_feat = hstack(
|
| 527 |
+
[X_train_tfidf, csr_matrix(train_embeddings), csr_matrix(train_stylo_scaled)]
|
| 528 |
+
)
|
| 529 |
+
X_test_feat = hstack(
|
| 530 |
+
[X_test_tfidf, csr_matrix(test_embeddings), csr_matrix(test_stylo_scaled)]
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
n_tfidf = X_train_tfidf.shape[1]
|
| 534 |
+
n_minilm = 384
|
| 535 |
+
n_stylo = len(STYLOMETRIC_FEATURE_NAMES)
|
| 536 |
+
print(
|
| 537 |
+
f" Feature dimensions: {X_train_feat.shape[1]} "
|
| 538 |
+
f"(TF-IDF: {n_tfidf} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
# ── 5. Full 5-Fold Cross-Validation ──
|
| 542 |
+
print("\n" + "=" * 60)
|
| 543 |
+
print(" 5-FOLD CROSS-VALIDATION (Full Dataset)")
|
| 544 |
+
print("=" * 60)
|
| 545 |
+
|
| 546 |
+
# Build features on entire dataset
|
| 547 |
+
print("\nBuilding features on full dataset...")
|
| 548 |
+
tfidf_full = TfidfVectorizer(
|
| 549 |
+
max_features=15000,
|
| 550 |
+
ngram_range=(1, 2),
|
| 551 |
+
min_df=2,
|
| 552 |
+
max_df=0.95,
|
| 553 |
+
sublinear_tf=True,
|
| 554 |
+
)
|
| 555 |
+
X_tfidf_full = tfidf_full.fit_transform(X_texts)
|
| 556 |
+
|
| 557 |
+
print(" Encoding full dataset with MiniLM...")
|
| 558 |
+
full_embeddings = minilm.encode(X_texts, show_progress_bar=True, batch_size=64)
|
| 559 |
+
|
| 560 |
+
stylo_full = np.array([extract_stylometric_features(t) for t in X_texts])
|
| 561 |
+
scaler_full = StandardScaler()
|
| 562 |
+
stylo_full_scaled = scaler_full.fit_transform(stylo_full)
|
| 563 |
+
X_full = hstack(
|
| 564 |
+
[X_tfidf_full, csr_matrix(full_embeddings), csr_matrix(stylo_full_scaled)]
|
| 565 |
+
)
|
| 566 |
+
y_full = np.array(y_labels)
|
| 567 |
+
|
| 568 |
+
print(f" Total samples: {X_full.shape[0]}")
|
| 569 |
+
print(
|
| 570 |
+
f" Feature dimensions: {X_full.shape[1]} "
|
| 571 |
+
f"(TF-IDF: {X_tfidf_full.shape[1]} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
|
| 575 |
+
|
| 576 |
+
fold_accuracies = []
|
| 577 |
+
fold_precisions = []
|
| 578 |
+
fold_recalls = []
|
| 579 |
+
fold_f1s = []
|
| 580 |
+
fold_train_accs = []
|
| 581 |
+
all_y_true = []
|
| 582 |
+
all_y_pred = []
|
| 583 |
+
|
| 584 |
+
for fold_idx, (train_idx, test_idx) in enumerate(cv.split(X_full, y_full), 1):
|
| 585 |
+
X_fold_train = X_full[train_idx]
|
| 586 |
+
X_fold_test = X_full[test_idx]
|
| 587 |
+
y_fold_train = y_full[train_idx]
|
| 588 |
+
y_fold_test = y_full[test_idx]
|
| 589 |
+
|
| 590 |
+
print(f"\n{'─' * 60}")
|
| 591 |
+
print(f" FOLD {fold_idx}/5 (Train: {len(train_idx)}, Test: {len(test_idx)})")
|
| 592 |
+
print(f"{'─' * 60}")
|
| 593 |
+
|
| 594 |
+
rf_fold = RandomForestClassifier(
|
| 595 |
+
n_estimators=300,
|
| 596 |
+
max_depth=15,
|
| 597 |
+
min_samples_split=5,
|
| 598 |
+
min_samples_leaf=5,
|
| 599 |
+
class_weight="balanced",
|
| 600 |
+
n_jobs=-1,
|
| 601 |
+
random_state=42,
|
| 602 |
+
)
|
| 603 |
+
rf_fold.fit(X_fold_train, y_fold_train)
|
| 604 |
+
|
| 605 |
+
# Predictions
|
| 606 |
+
y_fold_train_pred = rf_fold.predict(X_fold_train)
|
| 607 |
+
y_fold_test_pred = rf_fold.predict(X_fold_test)
|
| 608 |
+
|
| 609 |
+
train_acc = accuracy_score(y_fold_train, y_fold_train_pred)
|
| 610 |
+
test_acc = accuracy_score(y_fold_test, y_fold_test_pred)
|
| 611 |
+
|
| 612 |
+
fold_train_accs.append(train_acc)
|
| 613 |
+
fold_accuracies.append(test_acc)
|
| 614 |
+
|
| 615 |
+
# Per-fold classification report
|
| 616 |
+
report = classification_report(
|
| 617 |
+
y_fold_test,
|
| 618 |
+
y_fold_test_pred,
|
| 619 |
+
target_names=label_names,
|
| 620 |
+
output_dict=True,
|
| 621 |
+
)
|
| 622 |
+
fold_precisions.append(report["weighted avg"]["precision"])
|
| 623 |
+
fold_recalls.append(report["weighted avg"]["recall"])
|
| 624 |
+
fold_f1s.append(report["weighted avg"]["f1-score"])
|
| 625 |
+
|
| 626 |
+
# Collect for final confusion matrix
|
| 627 |
+
all_y_true.extend(y_fold_test)
|
| 628 |
+
all_y_pred.extend(y_fold_test_pred)
|
| 629 |
+
|
| 630 |
+
print(f" Train Accuracy: {train_acc:.4f} ({train_acc:.1%})")
|
| 631 |
+
print(f" Test Accuracy: {test_acc:.4f} ({test_acc:.1%})")
|
| 632 |
+
print(f" Gap: {train_acc - test_acc:.4f}")
|
| 633 |
+
print()
|
| 634 |
+
print(
|
| 635 |
+
classification_report(
|
| 636 |
+
y_fold_test, y_fold_test_pred, target_names=label_names
|
| 637 |
+
)
|
| 638 |
+
)
|
| 639 |
+
|
| 640 |
+
# ── 6. Cross-Fold Summary ──
|
| 641 |
+
fold_accuracies = np.array(fold_accuracies)
|
| 642 |
+
fold_train_accs = np.array(fold_train_accs)
|
| 643 |
+
fold_precisions = np.array(fold_precisions)
|
| 644 |
+
fold_recalls = np.array(fold_recalls)
|
| 645 |
+
fold_f1s = np.array(fold_f1s)
|
| 646 |
+
gaps = fold_train_accs - fold_accuracies
|
| 647 |
+
|
| 648 |
+
print("\n" + "=" * 60)
|
| 649 |
+
print(" CROSS-VALIDATION SUMMARY (5 Folds)")
|
| 650 |
+
print("=" * 60)
|
| 651 |
+
|
| 652 |
+
print(f"\n Per-Fold Test Accuracies:")
|
| 653 |
+
for i, (ta, te) in enumerate(zip(fold_train_accs, fold_accuracies), 1):
|
| 654 |
+
print(f" Fold {i}: Train {ta:.1%} | Test {te:.1%} | Gap {ta - te:.1%}")
|
| 655 |
+
|
| 656 |
+
print(
|
| 657 |
+
f"\n Average Training Accuracy: {fold_train_accs.mean():.4f} "
|
| 658 |
+
f"(+/- {fold_train_accs.std():.4f})"
|
| 659 |
+
)
|
| 660 |
+
print(
|
| 661 |
+
f" Average Testing Accuracy: {fold_accuracies.mean():.4f} "
|
| 662 |
+
f"(+/- {fold_accuracies.std():.4f})"
|
| 663 |
+
)
|
| 664 |
+
print(
|
| 665 |
+
f" Average Precision: {fold_precisions.mean():.4f} "
|
| 666 |
+
f"(+/- {fold_precisions.std():.4f})"
|
| 667 |
+
)
|
| 668 |
+
print(
|
| 669 |
+
f" Average Recall: {fold_recalls.mean():.4f} "
|
| 670 |
+
f"(+/- {fold_recalls.std():.4f})"
|
| 671 |
+
)
|
| 672 |
+
print(
|
| 673 |
+
f" Average F1 Score: {fold_f1s.mean():.4f} "
|
| 674 |
+
f"(+/- {fold_f1s.std():.4f})"
|
| 675 |
+
)
|
| 676 |
+
print(f" Average Gap: {gaps.mean():.4f} " f"(+/- {gaps.std():.4f})")
|
| 677 |
+
|
| 678 |
+
# ── 7. Consistency Check ──
|
| 679 |
+
print("\n" + "=" * 60)
|
| 680 |
+
print(" VERDICT CONSISTENCY & OVERFITTING ANALYSIS")
|
| 681 |
+
print("=" * 60)
|
| 682 |
+
|
| 683 |
+
avg_train = fold_train_accs.mean()
|
| 684 |
+
avg_test = fold_accuracies.mean()
|
| 685 |
+
avg_gap = gaps.mean()
|
| 686 |
+
acc_std = fold_accuracies.std()
|
| 687 |
+
|
| 688 |
+
if avg_train > 0.95 and avg_test < 0.70:
|
| 689 |
+
overfit_status = "OVERFITTING DETECTED"
|
| 690 |
+
print(f"\n *** OVERFITTING DETECTED ***")
|
| 691 |
+
print(f" Average training accuracy ({avg_train:.1%}) is much higher than")
|
| 692 |
+
print(f" average testing accuracy ({avg_test:.1%}).")
|
| 693 |
+
print(f" The model memorizes training data and fails to generalize.")
|
| 694 |
+
elif avg_gap > 0.10:
|
| 695 |
+
overfit_status = "MILD OVERFITTING"
|
| 696 |
+
print(f"\n ** MILD OVERFITTING **")
|
| 697 |
+
print(f" Average gap ({avg_gap:.1%}) exceeds 10%.")
|
| 698 |
+
else:
|
| 699 |
+
overfit_status = "NO OVERFITTING"
|
| 700 |
+
print(f"\n NO OVERFITTING DETECTED")
|
| 701 |
+
print(f" Average gap ({avg_gap:.1%}) is within acceptable range.")
|
| 702 |
+
|
| 703 |
+
if acc_std < 0.01:
|
| 704 |
+
consistency = "HIGHLY CONSISTENT"
|
| 705 |
+
print(f" Verdict Consistency: HIGHLY CONSISTENT (std={acc_std:.4f})")
|
| 706 |
+
print(f" Predictions are very stable across all 5 folds.")
|
| 707 |
+
elif acc_std < 0.03:
|
| 708 |
+
consistency = "CONSISTENT"
|
| 709 |
+
print(f" Verdict Consistency: CONSISTENT (std={acc_std:.4f})")
|
| 710 |
+
print(f" Minor variance across folds — acceptable for production.")
|
| 711 |
+
else:
|
| 712 |
+
consistency = "INCONSISTENT"
|
| 713 |
+
print(f" Verdict Consistency: INCONSISTENT (std={acc_std:.4f})")
|
| 714 |
+
print(f" High variance suggests model stability issues.")
|
| 715 |
+
|
| 716 |
+
# ── 8. Confusion Matrix (aggregated across all folds) ──
|
| 717 |
+
print("\n\nGenerating plots...")
|
| 718 |
+
cm = confusion_matrix(all_y_true, all_y_pred)
|
| 719 |
+
overall_acc = accuracy_score(all_y_true, all_y_pred)
|
| 720 |
+
|
| 721 |
+
fig, ax = plt.subplots(figsize=(8, 6))
|
| 722 |
+
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=label_names)
|
| 723 |
+
disp.plot(ax=ax, cmap="Blues", values_format="d")
|
| 724 |
+
|
| 725 |
+
ax.set_title(
|
| 726 |
+
f"Confusion Matrix — Aggregated 5-Fold CV\n"
|
| 727 |
+
f"Overall Accuracy: {overall_acc:.1%} | {overfit_status}",
|
| 728 |
+
fontsize=14,
|
| 729 |
+
fontweight="bold",
|
| 730 |
+
)
|
| 731 |
+
ax.set_xlabel("Predicted Label", fontsize=12)
|
| 732 |
+
ax.set_ylabel("True Label", fontsize=12)
|
| 733 |
+
|
| 734 |
+
plt.tight_layout()
|
| 735 |
+
cm_path = os.path.join(OUTPUT_DIR, "confusion_matrix.png")
|
| 736 |
+
fig.savefig(cm_path, dpi=150, bbox_inches="tight")
|
| 737 |
+
print(f" Saved: {cm_path}")
|
| 738 |
+
|
| 739 |
+
# ── 9. Per-Fold Accuracy Bar Chart ──
|
| 740 |
+
fig2, ax2 = plt.subplots(figsize=(10, 5))
|
| 741 |
+
|
| 742 |
+
x = np.arange(5)
|
| 743 |
+
width = 0.35
|
| 744 |
+
bars_train = ax2.bar(
|
| 745 |
+
x - width / 2,
|
| 746 |
+
fold_train_accs * 100,
|
| 747 |
+
width,
|
| 748 |
+
label="Training",
|
| 749 |
+
color="#2196F3",
|
| 750 |
+
edgecolor="black",
|
| 751 |
+
linewidth=0.5,
|
| 752 |
+
)
|
| 753 |
+
bars_test = ax2.bar(
|
| 754 |
+
x + width / 2,
|
| 755 |
+
fold_accuracies * 100,
|
| 756 |
+
width,
|
| 757 |
+
label="Testing",
|
| 758 |
+
color="#FF9800",
|
| 759 |
+
edgecolor="black",
|
| 760 |
+
linewidth=0.5,
|
| 761 |
+
)
|
| 762 |
+
|
| 763 |
+
for bar, val in zip(bars_train, fold_train_accs):
|
| 764 |
+
ax2.text(
|
| 765 |
+
bar.get_x() + bar.get_width() / 2,
|
| 766 |
+
bar.get_height() + 0.3,
|
| 767 |
+
f"{val:.1%}",
|
| 768 |
+
ha="center",
|
| 769 |
+
va="bottom",
|
| 770 |
+
fontsize=9,
|
| 771 |
+
fontweight="bold",
|
| 772 |
+
)
|
| 773 |
+
for bar, val in zip(bars_test, fold_accuracies):
|
| 774 |
+
ax2.text(
|
| 775 |
+
bar.get_x() + bar.get_width() / 2,
|
| 776 |
+
bar.get_height() + 0.3,
|
| 777 |
+
f"{val:.1%}",
|
| 778 |
+
ha="center",
|
| 779 |
+
va="bottom",
|
| 780 |
+
fontsize=9,
|
| 781 |
+
fontweight="bold",
|
| 782 |
+
)
|
| 783 |
+
|
| 784 |
+
ax2.set_xticks(x)
|
| 785 |
+
ax2.set_xticklabels([f"Fold {i}" for i in range(1, 6)])
|
| 786 |
+
ax2.set_ylim(0, 105)
|
| 787 |
+
ax2.set_ylabel("Accuracy (%)", fontsize=12)
|
| 788 |
+
ax2.set_title(
|
| 789 |
+
f"Per-Fold Accuracy Comparison\n"
|
| 790 |
+
f"Avg Test: {avg_test:.1%} (+/- {acc_std:.4f}) | {consistency}",
|
| 791 |
+
fontsize=14,
|
| 792 |
+
fontweight="bold",
|
| 793 |
+
)
|
| 794 |
+
ax2.legend(loc="lower right")
|
| 795 |
+
ax2.axhline(y=70, color="red", linestyle="--", alpha=0.5, label="70% threshold")
|
| 796 |
+
|
| 797 |
+
plt.tight_layout()
|
| 798 |
+
bar_path = os.path.join(OUTPUT_DIR, "accuracy_comparison.png")
|
| 799 |
+
fig2.savefig(bar_path, dpi=150, bbox_inches="tight")
|
| 800 |
+
print(f" Saved: {bar_path}")
|
| 801 |
+
|
| 802 |
+
# ── Final Summary ──
|
| 803 |
+
print("\n" + "=" * 60)
|
| 804 |
+
print(" EVALUATION COMPLETE")
|
| 805 |
+
print("=" * 60)
|
| 806 |
+
print(f" Dataset: fake_news_filipino ({len(df)} articles)")
|
| 807 |
+
print(f" Feature set: {X_full.shape[1]} (TF-IDF + 9 stylometric)")
|
| 808 |
+
print(f" Cross-Validation: 5-Fold Stratified")
|
| 809 |
+
print(f" Avg Training Accuracy: {avg_train:.4f} (+/- {fold_train_accs.std():.4f})")
|
| 810 |
+
print(f" Avg Testing Accuracy: {avg_test:.4f} (+/- {acc_std:.4f})")
|
| 811 |
+
print(f" Avg F1 Score: {fold_f1s.mean():.4f} (+/- {fold_f1s.std():.4f})")
|
| 812 |
+
print(f" Avg Gap: {avg_gap:.4f}")
|
| 813 |
+
print(f" Overfitting Status: {overfit_status}")
|
| 814 |
+
print(f" Verdict Consistency: {consistency}")
|
| 815 |
+
print(f" Plots saved to: {OUTPUT_DIR}/")
|
| 816 |
+
print("=" * 60)
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
if __name__ == "__main__":
|
| 820 |
+
main()
|
backend/mine_bias_words.py
CHANGED
|
@@ -1,194 +1,194 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Chi-Squared Bias Word Miner
|
| 3 |
-
============================
|
| 4 |
-
Automatically discovers words most statistically associated with
|
| 5 |
-
Fake vs. Real news in your training dataset using chi-squared analysis.
|
| 6 |
-
|
| 7 |
-
Outputs:
|
| 8 |
-
1. Full scored table (word, chi2 score, fake freq, real freq)
|
| 9 |
-
2. Ready-to-paste Python lists for bias_analyzer.py
|
| 10 |
-
|
| 11 |
-
Usage:
|
| 12 |
-
python backend/mine_bias_words.py
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
import sys
|
| 16 |
-
import os
|
| 17 |
-
import pandas as pd
|
| 18 |
-
from sklearn.feature_extraction.text import CountVectorizer
|
| 19 |
-
from sklearn.feature_selection import chi2
|
| 20 |
-
import numpy as np
|
| 21 |
-
|
| 22 |
-
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
-
sys.path.insert(0, PROJECT_ROOT)
|
| 24 |
-
|
| 25 |
-
TOP_N = 60 # How many top words to show per class
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def load_data():
|
| 29 |
-
"""Load the same datasets used in training."""
|
| 30 |
-
frames = []
|
| 31 |
-
|
| 32 |
-
csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
|
| 33 |
-
if os.path.exists(csv1):
|
| 34 |
-
df1 = pd.read_csv(csv1, skiprows=1)[["article", "label"]].copy()
|
| 35 |
-
df1 = df1[~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
|
| 36 |
-
# label column may be StringDtype (git conflict markers) — coerce to int
|
| 37 |
-
df1["label"] = pd.to_numeric(df1["label"], errors="coerce")
|
| 38 |
-
df1 = df1.dropna(subset=["label"])
|
| 39 |
-
df1["label"] = df1["label"].astype(int)
|
| 40 |
-
frames.append(df1)
|
| 41 |
-
print(
|
| 42 |
-
f" jcblaise: {len(df1)} articles (Real={int((df1.label==0).sum())}, Fake={int((df1.label==1).sum())})"
|
| 43 |
-
)
|
| 44 |
-
|
| 45 |
-
csv2 = os.path.join(
|
| 46 |
-
PROJECT_ROOT,
|
| 47 |
-
"data",
|
| 48 |
-
"raw",
|
| 49 |
-
"philippine_corpus",
|
| 50 |
-
"Philippine Fake News Corpus.csv",
|
| 51 |
-
)
|
| 52 |
-
if os.path.exists(csv2):
|
| 53 |
-
df2 = pd.read_csv(csv2, skiprows=1)
|
| 54 |
-
df2 = df2.rename(columns={"Content": "article"})
|
| 55 |
-
df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
|
| 56 |
-
df2 = df2[["article", "label"]].dropna().copy()
|
| 57 |
-
frames.append(df2)
|
| 58 |
-
print(
|
| 59 |
-
f" PH Corpus: {len(df2)} articles (Real={int((df2.label==0).sum())}, Fake={int((df2.label==1).sum())})"
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
df = pd.concat(frames, ignore_index=True).dropna(subset=["article", "label"])
|
| 63 |
-
df = df[df["article"].astype(str).str.len() > 0].copy()
|
| 64 |
-
return df
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def mine_words(df, top_n=TOP_N):
|
| 68 |
-
texts = df["article"].fillna("").astype(str).tolist()
|
| 69 |
-
labels = df["label"].tolist()
|
| 70 |
-
|
| 71 |
-
print(f"\nBuilding vocabulary from {len(texts)} articles...")
|
| 72 |
-
vec = CountVectorizer(
|
| 73 |
-
max_features=20000,
|
| 74 |
-
ngram_range=(1, 2),
|
| 75 |
-
min_df=5,
|
| 76 |
-
max_df=0.85,
|
| 77 |
-
stop_words=None, # keep Filipino/Tagalog words too
|
| 78 |
-
)
|
| 79 |
-
X = vec.fit_transform(texts)
|
| 80 |
-
feature_names = vec.get_feature_names_out()
|
| 81 |
-
|
| 82 |
-
print("Running chi-squared analysis...")
|
| 83 |
-
chi2_scores, _ = chi2(X, labels)
|
| 84 |
-
sorted_idx = np.argsort(chi2_scores)[::-1]
|
| 85 |
-
|
| 86 |
-
X_array = X.toarray()
|
| 87 |
-
real_mask = [l == 0 for l in labels]
|
| 88 |
-
fake_mask = [l == 1 for l in labels]
|
| 89 |
-
real_mean = X_array[real_mask].mean(axis=0)
|
| 90 |
-
fake_mean = X_array[fake_mask].mean(axis=0)
|
| 91 |
-
|
| 92 |
-
fake_words, real_words = [], []
|
| 93 |
-
for idx in sorted_idx[: top_n * 3]:
|
| 94 |
-
word = feature_names[idx]
|
| 95 |
-
score = chi2_scores[idx]
|
| 96 |
-
if word.isdigit() or len(word) <= 2:
|
| 97 |
-
continue
|
| 98 |
-
entry = (word, score, fake_mean[idx], real_mean[idx])
|
| 99 |
-
if fake_mean[idx] > real_mean[idx]:
|
| 100 |
-
fake_words.append(entry)
|
| 101 |
-
else:
|
| 102 |
-
real_words.append((word, score, real_mean[idx], fake_mean[idx]))
|
| 103 |
-
if len(fake_words) >= top_n and len(real_words) >= top_n:
|
| 104 |
-
break
|
| 105 |
-
|
| 106 |
-
return fake_words[:top_n], real_words[:top_n]
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def _py_list(words, var_name, comment):
|
| 110 |
-
"""Format a word list as a ready-to-paste Python block."""
|
| 111 |
-
lines = [f"# {comment}", f"{var_name} = ["]
|
| 112 |
-
for i in range(0, len(words), 5):
|
| 113 |
-
chunk = words[i : i + 5]
|
| 114 |
-
lines.append(" " + ", ".join(f'"{w}"' for w in chunk) + ",")
|
| 115 |
-
lines.append("]")
|
| 116 |
-
return "\n".join(lines)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def main():
|
| 120 |
-
print("=" * 65)
|
| 121 |
-
print(" CHI-SQUARED BIAS WORD MINER")
|
| 122 |
-
print("=" * 65)
|
| 123 |
-
|
| 124 |
-
df = load_data()
|
| 125 |
-
print(
|
| 126 |
-
f"\nTotal: {len(df)} articles | Real: {int((df.label==0).sum())} | Fake: {int((df.label==1).sum())}"
|
| 127 |
-
)
|
| 128 |
-
|
| 129 |
-
fake_words, real_words = mine_words(df)
|
| 130 |
-
|
| 131 |
-
# ── Scored table ───────────────────────────────────────────────────
|
| 132 |
-
print("\n" + "=" * 65)
|
| 133 |
-
print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** FAKE NEWS ***")
|
| 134 |
-
print("=" * 65)
|
| 135 |
-
for word, score, f_avg, r_avg in fake_words:
|
| 136 |
-
bar = "█" * min(20, int(score / 200))
|
| 137 |
-
print(f" {word:<28} χ²={score:>8.0f} {bar}")
|
| 138 |
-
|
| 139 |
-
print("\n" + "=" * 65)
|
| 140 |
-
print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** REAL NEWS ***")
|
| 141 |
-
print("=" * 65)
|
| 142 |
-
for word, score, r_avg, f_avg in real_words:
|
| 143 |
-
bar = "█" * min(20, int(score / 200))
|
| 144 |
-
print(f" {word:<28} χ²={score:>8.0f} {bar}")
|
| 145 |
-
|
| 146 |
-
# ── Ready-to-paste blocks ──────────────────────────────────────────
|
| 147 |
-
fake_terms = [w for w, *_ in fake_words]
|
| 148 |
-
real_terms = [w for w, *_ in real_words]
|
| 149 |
-
|
| 150 |
-
print("\n\n" + "=" * 65)
|
| 151 |
-
print(" ✂ COPY-PASTE INTO bias_analyzer.py")
|
| 152 |
-
print("=" * 65)
|
| 153 |
-
|
| 154 |
-
print(
|
| 155 |
-
"\n"
|
| 156 |
-
+ _py_list(
|
| 157 |
-
fake_terms[:30],
|
| 158 |
-
"SENSATIONAL_ADDITIONS",
|
| 159 |
-
"Paste these into SENSATIONAL_KEYWORDS in bias_analyzer.py",
|
| 160 |
-
)
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
print(
|
| 164 |
-
"\n"
|
| 165 |
-
+ _py_list(
|
| 166 |
-
fake_terms[30:],
|
| 167 |
-
"RIGHT_CANDIDATE_ADDITIONS",
|
| 168 |
-
"Review — move political terms to CONSERVATIVE_PRO / RIGHT_LEANING_KEYWORDS",
|
| 169 |
-
)
|
| 170 |
-
)
|
| 171 |
-
|
| 172 |
-
print(
|
| 173 |
-
"\n"
|
| 174 |
-
+ _py_list(
|
| 175 |
-
real_terms[:30],
|
| 176 |
-
"EVIDENCE_ADDITIONS",
|
| 177 |
-
"Paste into EVIDENCE_BASED_MARKERS in bias_analyzer.py",
|
| 178 |
-
)
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
print("\n" + "=" * 65)
|
| 182 |
-
print(" TIPS:")
|
| 183 |
-
print(" • Words appearing in both tables are not discriminative — skip them")
|
| 184 |
-
print(" • Move politician names to CONSERVATIVE_PRO / LIBERAL_PRO instead")
|
| 185 |
-
print(" • For political faction slang, use this LLM prompt:")
|
| 186 |
-
print()
|
| 187 |
-
print(' "Give me 40 Filipino/Tagalog/Bisaya social media terms used by')
|
| 188 |
-
print(" (1) pro-Duterte DDS, (2) Kakampink/pro-Leni, (3) PBBM/pro-Marcos")
|
| 189 |
-
print(' supporters. Return three plain Python string lists."')
|
| 190 |
-
print("=" * 65)
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
if __name__ == "__main__":
|
| 194 |
-
main()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chi-Squared Bias Word Miner
|
| 3 |
+
============================
|
| 4 |
+
Automatically discovers words most statistically associated with
|
| 5 |
+
Fake vs. Real news in your training dataset using chi-squared analysis.
|
| 6 |
+
|
| 7 |
+
Outputs:
|
| 8 |
+
1. Full scored table (word, chi2 score, fake freq, real freq)
|
| 9 |
+
2. Ready-to-paste Python lists for bias_analyzer.py
|
| 10 |
+
|
| 11 |
+
Usage:
|
| 12 |
+
python backend/mine_bias_words.py
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import sys
|
| 16 |
+
import os
|
| 17 |
+
import pandas as pd
|
| 18 |
+
from sklearn.feature_extraction.text import CountVectorizer
|
| 19 |
+
from sklearn.feature_selection import chi2
|
| 20 |
+
import numpy as np
|
| 21 |
+
|
| 22 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 24 |
+
|
| 25 |
+
TOP_N = 60 # How many top words to show per class
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def load_data():
|
| 29 |
+
"""Load the same datasets used in training."""
|
| 30 |
+
frames = []
|
| 31 |
+
|
| 32 |
+
csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
|
| 33 |
+
if os.path.exists(csv1):
|
| 34 |
+
df1 = pd.read_csv(csv1, skiprows=1)[["article", "label"]].copy()
|
| 35 |
+
df1 = df1[~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
|
| 36 |
+
# label column may be StringDtype (git conflict markers) — coerce to int
|
| 37 |
+
df1["label"] = pd.to_numeric(df1["label"], errors="coerce")
|
| 38 |
+
df1 = df1.dropna(subset=["label"])
|
| 39 |
+
df1["label"] = df1["label"].astype(int)
|
| 40 |
+
frames.append(df1)
|
| 41 |
+
print(
|
| 42 |
+
f" jcblaise: {len(df1)} articles (Real={int((df1.label==0).sum())}, Fake={int((df1.label==1).sum())})"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
csv2 = os.path.join(
|
| 46 |
+
PROJECT_ROOT,
|
| 47 |
+
"data",
|
| 48 |
+
"raw",
|
| 49 |
+
"philippine_corpus",
|
| 50 |
+
"Philippine Fake News Corpus.csv",
|
| 51 |
+
)
|
| 52 |
+
if os.path.exists(csv2):
|
| 53 |
+
df2 = pd.read_csv(csv2, skiprows=1)
|
| 54 |
+
df2 = df2.rename(columns={"Content": "article"})
|
| 55 |
+
df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
|
| 56 |
+
df2 = df2[["article", "label"]].dropna().copy()
|
| 57 |
+
frames.append(df2)
|
| 58 |
+
print(
|
| 59 |
+
f" PH Corpus: {len(df2)} articles (Real={int((df2.label==0).sum())}, Fake={int((df2.label==1).sum())})"
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
df = pd.concat(frames, ignore_index=True).dropna(subset=["article", "label"])
|
| 63 |
+
df = df[df["article"].astype(str).str.len() > 0].copy()
|
| 64 |
+
return df
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def mine_words(df, top_n=TOP_N):
|
| 68 |
+
texts = df["article"].fillna("").astype(str).tolist()
|
| 69 |
+
labels = df["label"].tolist()
|
| 70 |
+
|
| 71 |
+
print(f"\nBuilding vocabulary from {len(texts)} articles...")
|
| 72 |
+
vec = CountVectorizer(
|
| 73 |
+
max_features=20000,
|
| 74 |
+
ngram_range=(1, 2),
|
| 75 |
+
min_df=5,
|
| 76 |
+
max_df=0.85,
|
| 77 |
+
stop_words=None, # keep Filipino/Tagalog words too
|
| 78 |
+
)
|
| 79 |
+
X = vec.fit_transform(texts)
|
| 80 |
+
feature_names = vec.get_feature_names_out()
|
| 81 |
+
|
| 82 |
+
print("Running chi-squared analysis...")
|
| 83 |
+
chi2_scores, _ = chi2(X, labels)
|
| 84 |
+
sorted_idx = np.argsort(chi2_scores)[::-1]
|
| 85 |
+
|
| 86 |
+
X_array = X.toarray()
|
| 87 |
+
real_mask = [l == 0 for l in labels]
|
| 88 |
+
fake_mask = [l == 1 for l in labels]
|
| 89 |
+
real_mean = X_array[real_mask].mean(axis=0)
|
| 90 |
+
fake_mean = X_array[fake_mask].mean(axis=0)
|
| 91 |
+
|
| 92 |
+
fake_words, real_words = [], []
|
| 93 |
+
for idx in sorted_idx[: top_n * 3]:
|
| 94 |
+
word = feature_names[idx]
|
| 95 |
+
score = chi2_scores[idx]
|
| 96 |
+
if word.isdigit() or len(word) <= 2:
|
| 97 |
+
continue
|
| 98 |
+
entry = (word, score, fake_mean[idx], real_mean[idx])
|
| 99 |
+
if fake_mean[idx] > real_mean[idx]:
|
| 100 |
+
fake_words.append(entry)
|
| 101 |
+
else:
|
| 102 |
+
real_words.append((word, score, real_mean[idx], fake_mean[idx]))
|
| 103 |
+
if len(fake_words) >= top_n and len(real_words) >= top_n:
|
| 104 |
+
break
|
| 105 |
+
|
| 106 |
+
return fake_words[:top_n], real_words[:top_n]
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _py_list(words, var_name, comment):
|
| 110 |
+
"""Format a word list as a ready-to-paste Python block."""
|
| 111 |
+
lines = [f"# {comment}", f"{var_name} = ["]
|
| 112 |
+
for i in range(0, len(words), 5):
|
| 113 |
+
chunk = words[i : i + 5]
|
| 114 |
+
lines.append(" " + ", ".join(f'"{w}"' for w in chunk) + ",")
|
| 115 |
+
lines.append("]")
|
| 116 |
+
return "\n".join(lines)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def main():
|
| 120 |
+
print("=" * 65)
|
| 121 |
+
print(" CHI-SQUARED BIAS WORD MINER")
|
| 122 |
+
print("=" * 65)
|
| 123 |
+
|
| 124 |
+
df = load_data()
|
| 125 |
+
print(
|
| 126 |
+
f"\nTotal: {len(df)} articles | Real: {int((df.label==0).sum())} | Fake: {int((df.label==1).sum())}"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
fake_words, real_words = mine_words(df)
|
| 130 |
+
|
| 131 |
+
# ── Scored table ───────────────────────────────────────────────────
|
| 132 |
+
print("\n" + "=" * 65)
|
| 133 |
+
print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** FAKE NEWS ***")
|
| 134 |
+
print("=" * 65)
|
| 135 |
+
for word, score, f_avg, r_avg in fake_words:
|
| 136 |
+
bar = "█" * min(20, int(score / 200))
|
| 137 |
+
print(f" {word:<28} χ²={score:>8.0f} {bar}")
|
| 138 |
+
|
| 139 |
+
print("\n" + "=" * 65)
|
| 140 |
+
print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** REAL NEWS ***")
|
| 141 |
+
print("=" * 65)
|
| 142 |
+
for word, score, r_avg, f_avg in real_words:
|
| 143 |
+
bar = "█" * min(20, int(score / 200))
|
| 144 |
+
print(f" {word:<28} χ²={score:>8.0f} {bar}")
|
| 145 |
+
|
| 146 |
+
# ── Ready-to-paste blocks ──────────────────────────────────────────
|
| 147 |
+
fake_terms = [w for w, *_ in fake_words]
|
| 148 |
+
real_terms = [w for w, *_ in real_words]
|
| 149 |
+
|
| 150 |
+
print("\n\n" + "=" * 65)
|
| 151 |
+
print(" ✂ COPY-PASTE INTO bias_analyzer.py")
|
| 152 |
+
print("=" * 65)
|
| 153 |
+
|
| 154 |
+
print(
|
| 155 |
+
"\n"
|
| 156 |
+
+ _py_list(
|
| 157 |
+
fake_terms[:30],
|
| 158 |
+
"SENSATIONAL_ADDITIONS",
|
| 159 |
+
"Paste these into SENSATIONAL_KEYWORDS in bias_analyzer.py",
|
| 160 |
+
)
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
print(
|
| 164 |
+
"\n"
|
| 165 |
+
+ _py_list(
|
| 166 |
+
fake_terms[30:],
|
| 167 |
+
"RIGHT_CANDIDATE_ADDITIONS",
|
| 168 |
+
"Review — move political terms to CONSERVATIVE_PRO / RIGHT_LEANING_KEYWORDS",
|
| 169 |
+
)
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
print(
|
| 173 |
+
"\n"
|
| 174 |
+
+ _py_list(
|
| 175 |
+
real_terms[:30],
|
| 176 |
+
"EVIDENCE_ADDITIONS",
|
| 177 |
+
"Paste into EVIDENCE_BASED_MARKERS in bias_analyzer.py",
|
| 178 |
+
)
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
print("\n" + "=" * 65)
|
| 182 |
+
print(" TIPS:")
|
| 183 |
+
print(" • Words appearing in both tables are not discriminative — skip them")
|
| 184 |
+
print(" • Move politician names to CONSERVATIVE_PRO / LIBERAL_PRO instead")
|
| 185 |
+
print(" • For political faction slang, use this LLM prompt:")
|
| 186 |
+
print()
|
| 187 |
+
print(' "Give me 40 Filipino/Tagalog/Bisaya social media terms used by')
|
| 188 |
+
print(" (1) pro-Duterte DDS, (2) Kakampink/pro-Leni, (3) PBBM/pro-Marcos")
|
| 189 |
+
print(' supporters. Return three plain Python string lists."')
|
| 190 |
+
print("=" * 65)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
main()
|
backend/tune_rf.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hybrid RF Hyperparameter Tuner
|
| 3 |
+
================================
|
| 4 |
+
Systematically tests different Random Forest configurations on
|
| 5 |
+
the hybrid feature set (TF-IDF + MiniLM + Stylometric) to find
|
| 6 |
+
the settings that beat the Linear SVM baseline (94.89% accuracy).
|
| 7 |
+
|
| 8 |
+
Varies:
|
| 9 |
+
• tfidf_features : number of TF-IDF vocabulary features (2000, 5000, 10000)
|
| 10 |
+
• max_depth : RF tree depth (None=unlimited, 30, 50)
|
| 11 |
+
• max_features_rf : features sampled per split ('sqrt', 'log2', 0.15, 0.25)
|
| 12 |
+
|
| 13 |
+
Output:
|
| 14 |
+
evaluation_results/tune_rf_results.txt — all trial scores
|
| 15 |
+
evaluation_results/tune_rf_heatmap.png — accuracy heatmap
|
| 16 |
+
|
| 17 |
+
Usage:
|
| 18 |
+
python backend/tune_rf.py
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
import sys, os, time, warnings
|
| 22 |
+
warnings.filterwarnings("ignore")
|
| 23 |
+
|
| 24 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 25 |
+
sys.path.insert(0, PROJECT_ROOT)
|
| 26 |
+
|
| 27 |
+
import numpy as np
|
| 28 |
+
import matplotlib
|
| 29 |
+
matplotlib.use("Agg")
|
| 30 |
+
import matplotlib.pyplot as plt
|
| 31 |
+
from scipy.sparse import hstack, csr_matrix
|
| 32 |
+
from sklearn.model_selection import train_test_split
|
| 33 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 34 |
+
from sklearn.preprocessing import StandardScaler
|
| 35 |
+
from sklearn.ensemble import RandomForestClassifier
|
| 36 |
+
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
|
| 37 |
+
|
| 38 |
+
from backend.train import (
|
| 39 |
+
load_fake_news_dataset,
|
| 40 |
+
preprocess,
|
| 41 |
+
extract_stylometric_features,
|
| 42 |
+
get_minilm_model,
|
| 43 |
+
STYLOMETRIC_FEATURE_NAMES,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
|
| 47 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 48 |
+
|
| 49 |
+
SVM_BASELINE_ACC = 0.9489 # target to beat
|
| 50 |
+
SVM_BASELINE_F1 = 0.9489
|
| 51 |
+
|
| 52 |
+
# ── Grid ──────────────────────────────────────────────────────────────────
|
| 53 |
+
TFIDF_SIZES = [2000, 5000, 10000]
|
| 54 |
+
MAX_DEPTHS = [None, 30, 50]
|
| 55 |
+
MAX_FEATURES_RF = ["sqrt", "log2", 0.15, 0.25]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def build_hybrid(X_train, X_test, tfidf_size, feature_multiplier=1.0):
|
| 59 |
+
"""Build TF-IDF + MiniLM + Stylometric features.
|
| 60 |
+
feature_multiplier scales the dense (MiniLM + Stylo) columns so they
|
| 61 |
+
are not drowned out by the sparse TF-IDF block when max_features is low.
|
| 62 |
+
"""
|
| 63 |
+
tfidf = TfidfVectorizer(
|
| 64 |
+
max_features=tfidf_size,
|
| 65 |
+
ngram_range=(1, 3),
|
| 66 |
+
min_df=2,
|
| 67 |
+
max_df=0.95,
|
| 68 |
+
sublinear_tf=True,
|
| 69 |
+
)
|
| 70 |
+
X_tr_tfidf = tfidf.fit_transform(X_train)
|
| 71 |
+
X_te_tfidf = tfidf.transform(X_test)
|
| 72 |
+
|
| 73 |
+
minilm = get_minilm_model()
|
| 74 |
+
emb_tr = minilm.encode(X_train, show_progress_bar=False, batch_size=64)
|
| 75 |
+
emb_te = minilm.encode(X_test, show_progress_bar=False, batch_size=64)
|
| 76 |
+
|
| 77 |
+
stylo_tr = np.array([extract_stylometric_features(t) for t in X_train])
|
| 78 |
+
stylo_te = np.array([extract_stylometric_features(t) for t in X_test])
|
| 79 |
+
scaler = StandardScaler()
|
| 80 |
+
stylo_tr = scaler.fit_transform(stylo_tr)
|
| 81 |
+
stylo_te = scaler.transform(stylo_te)
|
| 82 |
+
|
| 83 |
+
# Scale dense features to give RF a fairer chance of picking them
|
| 84 |
+
dense_tr = np.hstack([emb_tr, stylo_tr]) * feature_multiplier
|
| 85 |
+
dense_te = np.hstack([emb_te, stylo_te]) * feature_multiplier
|
| 86 |
+
|
| 87 |
+
X_tr = hstack([X_tr_tfidf, csr_matrix(dense_tr)])
|
| 88 |
+
X_te = hstack([X_te_tfidf, csr_matrix(dense_te)])
|
| 89 |
+
return X_tr, X_te
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def main():
|
| 93 |
+
print("=" * 65)
|
| 94 |
+
print(" HYBRID RF HYPERPARAMETER TUNER")
|
| 95 |
+
print(f" Target to beat: SVM Accuracy={SVM_BASELINE_ACC:.4f} F1={SVM_BASELINE_F1:.4f}")
|
| 96 |
+
print("=" * 65)
|
| 97 |
+
|
| 98 |
+
# ── Load data once ───────────────────────────────────────────────────
|
| 99 |
+
print("\nLoading dataset (mixed mode) …")
|
| 100 |
+
df = load_fake_news_dataset()
|
| 101 |
+
X_all, y_all = preprocess(df, undersample=False, oversample=True)
|
| 102 |
+
|
| 103 |
+
X_train, X_test, y_train, y_test = train_test_split(
|
| 104 |
+
X_all, y_all, test_size=0.20, random_state=42, stratify=y_all
|
| 105 |
+
)
|
| 106 |
+
y_train_arr = np.array(y_train)
|
| 107 |
+
y_test_arr = np.array(y_test)
|
| 108 |
+
|
| 109 |
+
print(f" Train: {len(X_train):,} | Test: {len(X_test):,}")
|
| 110 |
+
|
| 111 |
+
# ── Encode MiniLM once for the LARGEST tfidf size run —————————————
|
| 112 |
+
# We re-encode per tfidf_size but cache embeddings since they don't change
|
| 113 |
+
print("\nPre-encoding texts with MiniLM (done once, reused across all trials) …")
|
| 114 |
+
minilm = get_minilm_model()
|
| 115 |
+
emb_tr = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
|
| 116 |
+
emb_te = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
|
| 117 |
+
|
| 118 |
+
stylo_tr = np.array([extract_stylometric_features(t) for t in X_train])
|
| 119 |
+
stylo_te = np.array([extract_stylometric_features(t) for t in X_test])
|
| 120 |
+
scaler = StandardScaler()
|
| 121 |
+
stylo_tr = scaler.fit_transform(stylo_tr)
|
| 122 |
+
stylo_te = scaler.transform(stylo_te)
|
| 123 |
+
|
| 124 |
+
print(f" MiniLM embeddings: {emb_tr.shape} | Stylometric: {stylo_tr.shape}")
|
| 125 |
+
|
| 126 |
+
# ── Grid search ──────────────────────────────────────────────────────
|
| 127 |
+
results = []
|
| 128 |
+
trial = 0
|
| 129 |
+
total = len(TFIDF_SIZES) * len(MAX_DEPTHS) * len(MAX_FEATURES_RF)
|
| 130 |
+
|
| 131 |
+
print(f"\nRunning {total} trials …\n")
|
| 132 |
+
print(f" {'#':>3} {'TFIDF':>6} {'MaxDepth':>9} {'MaxFeat':>8} "
|
| 133 |
+
f"{'Accuracy':>9} {'F1(Wtd)':>8} {'AUC':>7} {'Time':>6} Beat?")
|
| 134 |
+
print(" " + "-" * 75)
|
| 135 |
+
|
| 136 |
+
best_acc = 0.0
|
| 137 |
+
best_cfg = {}
|
| 138 |
+
|
| 139 |
+
for tfidf_sz in TFIDF_SIZES:
|
| 140 |
+
# Build TF-IDF for this grid row (embeddings already cached)
|
| 141 |
+
tfidf = TfidfVectorizer(
|
| 142 |
+
max_features=tfidf_sz,
|
| 143 |
+
ngram_range=(1, 3),
|
| 144 |
+
min_df=2,
|
| 145 |
+
max_df=0.95,
|
| 146 |
+
sublinear_tf=True,
|
| 147 |
+
)
|
| 148 |
+
X_tr_tfidf = tfidf.fit_transform(X_train)
|
| 149 |
+
X_te_tfidf = tfidf.transform(X_test)
|
| 150 |
+
|
| 151 |
+
# Combine with cached dense features
|
| 152 |
+
# Feature multiplier: scale dense block so RF sampling
|
| 153 |
+
# has a realistic chance of selecting it
|
| 154 |
+
# Multiplier = sqrt(tfidf_sz / n_dense) approximates equal
|
| 155 |
+
# representation across sqrt-sampling
|
| 156 |
+
n_dense = emb_tr.shape[1] + stylo_tr.shape[1] # 384+25 = 409
|
| 157 |
+
multiplier = max(1.0, (tfidf_sz / n_dense) ** 0.5)
|
| 158 |
+
|
| 159 |
+
dense_tr = np.hstack([emb_tr, stylo_tr]) * multiplier
|
| 160 |
+
dense_te = np.hstack([emb_te, stylo_te]) * multiplier
|
| 161 |
+
|
| 162 |
+
X_tr = hstack([X_tr_tfidf, csr_matrix(dense_tr)])
|
| 163 |
+
X_te = hstack([X_te_tfidf, csr_matrix(dense_te)])
|
| 164 |
+
|
| 165 |
+
for max_depth in MAX_DEPTHS:
|
| 166 |
+
for max_feat in MAX_FEATURES_RF:
|
| 167 |
+
trial += 1
|
| 168 |
+
depth_label = str(max_depth) if max_depth else "None"
|
| 169 |
+
|
| 170 |
+
t0 = time.time()
|
| 171 |
+
rf = RandomForestClassifier(
|
| 172 |
+
n_estimators=300,
|
| 173 |
+
max_depth=max_depth,
|
| 174 |
+
min_samples_split=5,
|
| 175 |
+
min_samples_leaf=3,
|
| 176 |
+
max_features=max_feat,
|
| 177 |
+
class_weight="balanced",
|
| 178 |
+
n_jobs=-1,
|
| 179 |
+
random_state=42,
|
| 180 |
+
)
|
| 181 |
+
rf.fit(X_tr, y_train_arr)
|
| 182 |
+
y_pred = rf.predict(X_te)
|
| 183 |
+
proba = rf.predict_proba(X_te)
|
| 184 |
+
elapsed = time.time() - t0
|
| 185 |
+
|
| 186 |
+
acc = accuracy_score(y_test_arr, y_pred)
|
| 187 |
+
f1 = f1_score(y_test_arr, y_pred, average="weighted", zero_division=0)
|
| 188 |
+
try:
|
| 189 |
+
auc = roc_auc_score(y_test_arr, proba[:, 1])
|
| 190 |
+
except Exception:
|
| 191 |
+
auc = float("nan")
|
| 192 |
+
|
| 193 |
+
beat = "✓ BEAT IT!" if acc > SVM_BASELINE_ACC else ""
|
| 194 |
+
marker = ">>>" if acc > SVM_BASELINE_ACC else " "
|
| 195 |
+
|
| 196 |
+
row = {
|
| 197 |
+
"trial": trial,
|
| 198 |
+
"tfidf_sz": tfidf_sz,
|
| 199 |
+
"max_depth": max_depth,
|
| 200 |
+
"max_feat": max_feat,
|
| 201 |
+
"accuracy": acc,
|
| 202 |
+
"f1": f1,
|
| 203 |
+
"auc": auc,
|
| 204 |
+
"time": elapsed,
|
| 205 |
+
"multiplier": multiplier,
|
| 206 |
+
}
|
| 207 |
+
results.append(row)
|
| 208 |
+
|
| 209 |
+
if acc > best_acc:
|
| 210 |
+
best_acc = acc
|
| 211 |
+
best_cfg = row.copy()
|
| 212 |
+
|
| 213 |
+
mf_str = f"{max_feat:.2f}" if isinstance(max_feat, float) else max_feat
|
| 214 |
+
print(
|
| 215 |
+
f"{marker} {trial:>3} {tfidf_sz:>6,} {depth_label:>9} {mf_str:>8} "
|
| 216 |
+
f"{acc:>9.4f} {f1:>8.4f} {auc:>7.4f} {elapsed:>5.1f}s {beat}"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# ── Summary ──────────────────────────────────────────────────────────
|
| 220 |
+
print("\n" + "=" * 65)
|
| 221 |
+
print(" BEST CONFIGURATION FOUND")
|
| 222 |
+
print("=" * 65)
|
| 223 |
+
depth_label = str(best_cfg["max_depth"]) if best_cfg["max_depth"] else "None"
|
| 224 |
+
mf_label = (f"{best_cfg['max_feat']:.2f}"
|
| 225 |
+
if isinstance(best_cfg["max_feat"], float)
|
| 226 |
+
else best_cfg["max_feat"])
|
| 227 |
+
print(f" TF-IDF features : {best_cfg['tfidf_sz']:,}")
|
| 228 |
+
print(f" max_depth : {depth_label}")
|
| 229 |
+
print(f" max_features : {mf_label}")
|
| 230 |
+
print(f" Dense multiplier: {best_cfg['multiplier']:.2f}")
|
| 231 |
+
print(f" Accuracy : {best_cfg['accuracy']:.4f} "
|
| 232 |
+
f"({'BEATS SVM ✓' if best_cfg['accuracy'] > SVM_BASELINE_ACC else 'below SVM ✗'})")
|
| 233 |
+
print(f" F1 Weighted : {best_cfg['f1']:.4f}")
|
| 234 |
+
print(f" AUC-ROC : {best_cfg['auc']:.4f}")
|
| 235 |
+
|
| 236 |
+
# ── Save text report ─────────────────────────────────────────────────
|
| 237 |
+
lines = []
|
| 238 |
+
lines.append("=" * 65)
|
| 239 |
+
lines.append(" HYBRID RF TUNING RESULTS")
|
| 240 |
+
lines.append(f" Target: SVM Acc={SVM_BASELINE_ACC:.4f} F1={SVM_BASELINE_F1:.4f}")
|
| 241 |
+
lines.append("=" * 65)
|
| 242 |
+
lines.append(f"\n {'#':>3} {'TFIDF':>6} {'MaxDepth':>9} {'MaxFeat':>8} "
|
| 243 |
+
f"{'Accuracy':>9} {'F1(Wtd)':>8} {'AUC':>7} {'Time':>6}")
|
| 244 |
+
lines.append(" " + "-" * 70)
|
| 245 |
+
for r in results:
|
| 246 |
+
d = str(r["max_depth"]) if r["max_depth"] else "None"
|
| 247 |
+
mf = f"{r['max_feat']:.2f}" if isinstance(r["max_feat"], float) else r["max_feat"]
|
| 248 |
+
b = " ← BEST" if r == best_cfg else ""
|
| 249 |
+
lines.append(
|
| 250 |
+
f" {r['trial']:>3} {r['tfidf_sz']:>6,} {d:>9} {mf:>8} "
|
| 251 |
+
f"{r['accuracy']:>9.4f} {r['f1']:>8.4f} {r['auc']:>7.4f} "
|
| 252 |
+
f"{r['time']:>5.1f}s{b}"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
lines.append("\n" + "=" * 65)
|
| 256 |
+
lines.append(" RECOMMENDATION FOR train.py")
|
| 257 |
+
lines.append("=" * 65)
|
| 258 |
+
depth_label = str(best_cfg["max_depth"]) if best_cfg["max_depth"] else "None (unlimited)"
|
| 259 |
+
mf_label = (f"{best_cfg['max_feat']:.2f}"
|
| 260 |
+
if isinstance(best_cfg["max_feat"], float)
|
| 261 |
+
else f"'{best_cfg['max_feat']}'")
|
| 262 |
+
lines.append(f"""
|
| 263 |
+
In backend/train.py → build_features():
|
| 264 |
+
TfidfVectorizer(max_features={best_cfg['tfidf_sz']}, ...)
|
| 265 |
+
|
| 266 |
+
In backend/train.py → train_model():
|
| 267 |
+
RandomForestClassifier(
|
| 268 |
+
n_estimators=500,
|
| 269 |
+
max_depth={best_cfg['max_depth']},
|
| 270 |
+
min_samples_split=5,
|
| 271 |
+
min_samples_leaf=3,
|
| 272 |
+
max_features={mf_label},
|
| 273 |
+
class_weight='balanced',
|
| 274 |
+
n_jobs=-1,
|
| 275 |
+
random_state=42,
|
| 276 |
+
)
|
| 277 |
+
""")
|
| 278 |
+
|
| 279 |
+
txt = "\n".join(lines)
|
| 280 |
+
print(txt)
|
| 281 |
+
out_txt = os.path.join(OUTPUT_DIR, "tune_rf_results.txt")
|
| 282 |
+
with open(out_txt, "w", encoding="utf-8") as f:
|
| 283 |
+
f.write(txt)
|
| 284 |
+
print(f"\n Saved: {out_txt}")
|
| 285 |
+
|
| 286 |
+
# ── Heatmap: Accuracy by (tfidf_size x max_depth) for best max_feat ──
|
| 287 |
+
try:
|
| 288 |
+
best_mf = best_cfg["max_feat"]
|
| 289 |
+
sub = [r for r in results if r["max_feat"] == best_mf]
|
| 290 |
+
depths_u = [None, 30, 50]
|
| 291 |
+
tfidf_u = TFIDF_SIZES
|
| 292 |
+
grid = np.zeros((len(depths_u), len(tfidf_u)))
|
| 293 |
+
for r in sub:
|
| 294 |
+
di = depths_u.index(r["max_depth"])
|
| 295 |
+
ti = tfidf_u.index(r["tfidf_sz"])
|
| 296 |
+
grid[di, ti] = r["accuracy"]
|
| 297 |
+
|
| 298 |
+
fig, ax = plt.subplots(figsize=(8, 5))
|
| 299 |
+
im = ax.imshow(grid, cmap="RdYlGn", vmin=0.85, vmax=1.0)
|
| 300 |
+
ax.set_xticks(range(len(tfidf_u)))
|
| 301 |
+
ax.set_xticklabels([f"{t:,}" for t in tfidf_u])
|
| 302 |
+
ax.set_yticks(range(len(depths_u)))
|
| 303 |
+
ax.set_yticklabels(["None (unlimited)", "30", "50"])
|
| 304 |
+
ax.set_xlabel("TF-IDF Vocabulary Size", fontsize=12)
|
| 305 |
+
ax.set_ylabel("max_depth", fontsize=12)
|
| 306 |
+
mf_str = f"{best_mf:.2f}" if isinstance(best_mf, float) else best_mf
|
| 307 |
+
ax.set_title(
|
| 308 |
+
f"Hybrid RF Accuracy Heatmap (max_features='{mf_str}')\n"
|
| 309 |
+
f"Red < {SVM_BASELINE_ACC:.0%} = below SVM baseline Green = above SVM baseline",
|
| 310 |
+
fontsize=11, fontweight="bold"
|
| 311 |
+
)
|
| 312 |
+
# Annotate cells
|
| 313 |
+
for i in range(len(depths_u)):
|
| 314 |
+
for j in range(len(tfidf_u)):
|
| 315 |
+
v = grid[i, j]
|
| 316 |
+
ax.text(j, i, f"{v:.4f}", ha="center", va="center",
|
| 317 |
+
fontsize=11, fontweight="bold",
|
| 318 |
+
color="white" if v < 0.90 else "black")
|
| 319 |
+
fig.colorbar(im, ax=ax, label="Accuracy")
|
| 320 |
+
plt.axhline(-0.5, color="gray")
|
| 321 |
+
plt.tight_layout()
|
| 322 |
+
hm_path = os.path.join(OUTPUT_DIR, "tune_rf_heatmap.png")
|
| 323 |
+
fig.savefig(hm_path, dpi=150, bbox_inches="tight")
|
| 324 |
+
plt.close(fig)
|
| 325 |
+
print(f" Saved: {hm_path}")
|
| 326 |
+
except Exception as e:
|
| 327 |
+
print(f" (Heatmap skipped: {e})")
|
| 328 |
+
|
| 329 |
+
print("\n All done. Check evaluation_results/ for the full report.")
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
if __name__ == "__main__":
|
| 333 |
+
main()
|
check_app/.flutter-plugins-dependencies
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"jni","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni-1.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni_flutter-1.0.1\\\\","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.3.1\\\\","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.21\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.28\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"jni","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni-1.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.2.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"jni","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni-1.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.4.2\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-04-10 23:20:38.632272","version":"3.41.2","swift_package_manager_enabled":{"ios":false,"macos":false}}
|
check_app/.gitignore
CHANGED
|
@@ -1,45 +1,45 @@
|
|
| 1 |
-
# Miscellaneous
|
| 2 |
-
*.class
|
| 3 |
-
*.log
|
| 4 |
-
*.pyc
|
| 5 |
-
*.swp
|
| 6 |
-
.DS_Store
|
| 7 |
-
.atom/
|
| 8 |
-
.build/
|
| 9 |
-
.buildlog/
|
| 10 |
-
.history
|
| 11 |
-
.svn/
|
| 12 |
-
.swiftpm/
|
| 13 |
-
migrate_working_dir/
|
| 14 |
-
|
| 15 |
-
# IntelliJ related
|
| 16 |
-
*.iml
|
| 17 |
-
*.ipr
|
| 18 |
-
*.iws
|
| 19 |
-
.idea/
|
| 20 |
-
|
| 21 |
-
# The .vscode folder contains launch configuration and tasks you configure in
|
| 22 |
-
# VS Code which you may wish to be included in version control, so this line
|
| 23 |
-
# is commented out by default.
|
| 24 |
-
#.vscode/
|
| 25 |
-
|
| 26 |
-
# Flutter/Dart/Pub related
|
| 27 |
-
**/doc/api/
|
| 28 |
-
**/ios/Flutter/.last_build_id
|
| 29 |
-
.dart_tool/
|
| 30 |
-
.flutter-plugins-dependencies
|
| 31 |
-
.pub-cache/
|
| 32 |
-
.pub/
|
| 33 |
-
/build/
|
| 34 |
-
/coverage/
|
| 35 |
-
|
| 36 |
-
# Symbolication related
|
| 37 |
-
app.*.symbols
|
| 38 |
-
|
| 39 |
-
# Obfuscation related
|
| 40 |
-
app.*.map.json
|
| 41 |
-
|
| 42 |
-
# Android Studio will place build artifacts here
|
| 43 |
-
/android/app/debug
|
| 44 |
-
/android/app/profile
|
| 45 |
-
/android/app/release
|
|
|
|
| 1 |
+
# Miscellaneous
|
| 2 |
+
*.class
|
| 3 |
+
*.log
|
| 4 |
+
*.pyc
|
| 5 |
+
*.swp
|
| 6 |
+
.DS_Store
|
| 7 |
+
.atom/
|
| 8 |
+
.build/
|
| 9 |
+
.buildlog/
|
| 10 |
+
.history
|
| 11 |
+
.svn/
|
| 12 |
+
.swiftpm/
|
| 13 |
+
migrate_working_dir/
|
| 14 |
+
|
| 15 |
+
# IntelliJ related
|
| 16 |
+
*.iml
|
| 17 |
+
*.ipr
|
| 18 |
+
*.iws
|
| 19 |
+
.idea/
|
| 20 |
+
|
| 21 |
+
# The .vscode folder contains launch configuration and tasks you configure in
|
| 22 |
+
# VS Code which you may wish to be included in version control, so this line
|
| 23 |
+
# is commented out by default.
|
| 24 |
+
#.vscode/
|
| 25 |
+
|
| 26 |
+
# Flutter/Dart/Pub related
|
| 27 |
+
**/doc/api/
|
| 28 |
+
**/ios/Flutter/.last_build_id
|
| 29 |
+
.dart_tool/
|
| 30 |
+
.flutter-plugins-dependencies
|
| 31 |
+
.pub-cache/
|
| 32 |
+
.pub/
|
| 33 |
+
/build/
|
| 34 |
+
/coverage/
|
| 35 |
+
|
| 36 |
+
# Symbolication related
|
| 37 |
+
app.*.symbols
|
| 38 |
+
|
| 39 |
+
# Obfuscation related
|
| 40 |
+
app.*.map.json
|
| 41 |
+
|
| 42 |
+
# Android Studio will place build artifacts here
|
| 43 |
+
/android/app/debug
|
| 44 |
+
/android/app/profile
|
| 45 |
+
/android/app/release
|
check_app/.metadata
CHANGED
|
@@ -1,45 +1,45 @@
|
|
| 1 |
-
# This file tracks properties of this Flutter project.
|
| 2 |
-
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
| 3 |
-
#
|
| 4 |
-
# This file should be version controlled and should not be manually edited.
|
| 5 |
-
|
| 6 |
-
version:
|
| 7 |
-
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
| 8 |
-
channel: "stable"
|
| 9 |
-
|
| 10 |
-
project_type: app
|
| 11 |
-
|
| 12 |
-
# Tracks metadata for the flutter migrate command
|
| 13 |
-
migration:
|
| 14 |
-
platforms:
|
| 15 |
-
- platform: root
|
| 16 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 17 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 18 |
-
- platform: android
|
| 19 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 20 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 21 |
-
- platform: ios
|
| 22 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 23 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 24 |
-
- platform: linux
|
| 25 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 26 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 27 |
-
- platform: macos
|
| 28 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 29 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 30 |
-
- platform: web
|
| 31 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 32 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 33 |
-
- platform: windows
|
| 34 |
-
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 35 |
-
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 36 |
-
|
| 37 |
-
# User provided section
|
| 38 |
-
|
| 39 |
-
# List of Local paths (relative to this file) that should be
|
| 40 |
-
# ignored by the migrate tool.
|
| 41 |
-
#
|
| 42 |
-
# Files that are not part of the templates will be ignored by default.
|
| 43 |
-
unmanaged_files:
|
| 44 |
-
- 'lib/main.dart'
|
| 45 |
-
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
|
|
| 1 |
+
# This file tracks properties of this Flutter project.
|
| 2 |
+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
| 3 |
+
#
|
| 4 |
+
# This file should be version controlled and should not be manually edited.
|
| 5 |
+
|
| 6 |
+
version:
|
| 7 |
+
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
| 8 |
+
channel: "stable"
|
| 9 |
+
|
| 10 |
+
project_type: app
|
| 11 |
+
|
| 12 |
+
# Tracks metadata for the flutter migrate command
|
| 13 |
+
migration:
|
| 14 |
+
platforms:
|
| 15 |
+
- platform: root
|
| 16 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 17 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 18 |
+
- platform: android
|
| 19 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 20 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 21 |
+
- platform: ios
|
| 22 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 23 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 24 |
+
- platform: linux
|
| 25 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 26 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 27 |
+
- platform: macos
|
| 28 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 29 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 30 |
+
- platform: web
|
| 31 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 32 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 33 |
+
- platform: windows
|
| 34 |
+
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 35 |
+
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
| 36 |
+
|
| 37 |
+
# User provided section
|
| 38 |
+
|
| 39 |
+
# List of Local paths (relative to this file) that should be
|
| 40 |
+
# ignored by the migrate tool.
|
| 41 |
+
#
|
| 42 |
+
# Files that are not part of the templates will be ignored by default.
|
| 43 |
+
unmanaged_files:
|
| 44 |
+
- 'lib/main.dart'
|
| 45 |
+
- 'ios/Runner.xcodeproj/project.pbxproj'
|
check_app/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
-
# check_app
|
| 2 |
-
|
| 3 |
-
A new Flutter project.
|
| 4 |
-
|
| 5 |
-
## Getting Started
|
| 6 |
-
|
| 7 |
-
This project is a starting point for a Flutter application.
|
| 8 |
-
|
| 9 |
-
A few resources to get you started if this is your first Flutter project:
|
| 10 |
-
|
| 11 |
-
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
| 12 |
-
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
| 13 |
-
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
| 14 |
-
|
| 15 |
-
For help getting started with Flutter development, view the
|
| 16 |
-
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
| 17 |
-
samples, guidance on mobile development, and a full API reference.
|
|
|
|
| 1 |
+
# check_app
|
| 2 |
+
|
| 3 |
+
A new Flutter project.
|
| 4 |
+
|
| 5 |
+
## Getting Started
|
| 6 |
+
|
| 7 |
+
This project is a starting point for a Flutter application.
|
| 8 |
+
|
| 9 |
+
A few resources to get you started if this is your first Flutter project:
|
| 10 |
+
|
| 11 |
+
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
| 12 |
+
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
| 13 |
+
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
| 14 |
+
|
| 15 |
+
For help getting started with Flutter development, view the
|
| 16 |
+
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
| 17 |
+
samples, guidance on mobile development, and a full API reference.
|
check_app/analysis_options.yaml
CHANGED
|
@@ -1,28 +1,28 @@
|
|
| 1 |
-
# This file configures the analyzer, which statically analyzes Dart code to
|
| 2 |
-
# check for errors, warnings, and lints.
|
| 3 |
-
#
|
| 4 |
-
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
| 5 |
-
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
| 6 |
-
# invoked from the command line by running `flutter analyze`.
|
| 7 |
-
|
| 8 |
-
# The following line activates a set of recommended lints for Flutter apps,
|
| 9 |
-
# packages, and plugins designed to encourage good coding practices.
|
| 10 |
-
include: package:flutter_lints/flutter.yaml
|
| 11 |
-
|
| 12 |
-
linter:
|
| 13 |
-
# The lint rules applied to this project can be customized in the
|
| 14 |
-
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
| 15 |
-
# included above or to enable additional rules. A list of all available lints
|
| 16 |
-
# and their documentation is published at https://dart.dev/lints.
|
| 17 |
-
#
|
| 18 |
-
# Instead of disabling a lint rule for the entire project in the
|
| 19 |
-
# section below, it can also be suppressed for a single line of code
|
| 20 |
-
# or a specific dart file by using the `// ignore: name_of_lint` and
|
| 21 |
-
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
| 22 |
-
# producing the lint.
|
| 23 |
-
rules:
|
| 24 |
-
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
| 25 |
-
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
| 26 |
-
|
| 27 |
-
# Additional information about this file can be found at
|
| 28 |
-
# https://dart.dev/guides/language/analysis-options
|
|
|
|
| 1 |
+
# This file configures the analyzer, which statically analyzes Dart code to
|
| 2 |
+
# check for errors, warnings, and lints.
|
| 3 |
+
#
|
| 4 |
+
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
| 5 |
+
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
| 6 |
+
# invoked from the command line by running `flutter analyze`.
|
| 7 |
+
|
| 8 |
+
# The following line activates a set of recommended lints for Flutter apps,
|
| 9 |
+
# packages, and plugins designed to encourage good coding practices.
|
| 10 |
+
include: package:flutter_lints/flutter.yaml
|
| 11 |
+
|
| 12 |
+
linter:
|
| 13 |
+
# The lint rules applied to this project can be customized in the
|
| 14 |
+
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
| 15 |
+
# included above or to enable additional rules. A list of all available lints
|
| 16 |
+
# and their documentation is published at https://dart.dev/lints.
|
| 17 |
+
#
|
| 18 |
+
# Instead of disabling a lint rule for the entire project in the
|
| 19 |
+
# section below, it can also be suppressed for a single line of code
|
| 20 |
+
# or a specific dart file by using the `// ignore: name_of_lint` and
|
| 21 |
+
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
| 22 |
+
# producing the lint.
|
| 23 |
+
rules:
|
| 24 |
+
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
| 25 |
+
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
| 26 |
+
|
| 27 |
+
# Additional information about this file can be found at
|
| 28 |
+
# https://dart.dev/guides/language/analysis-options
|
check_app/android/.gitignore
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
-
gradle-wrapper.jar
|
| 2 |
-
/.gradle
|
| 3 |
-
/captures/
|
| 4 |
-
/gradlew
|
| 5 |
-
/gradlew.bat
|
| 6 |
-
/local.properties
|
| 7 |
-
GeneratedPluginRegistrant.java
|
| 8 |
-
.cxx/
|
| 9 |
-
|
| 10 |
-
# Remember to never publicly share your keystore.
|
| 11 |
-
# See https://flutter.dev/to/reference-keystore
|
| 12 |
-
key.properties
|
| 13 |
-
**/*.keystore
|
| 14 |
-
**/*.jks
|
|
|
|
| 1 |
+
gradle-wrapper.jar
|
| 2 |
+
/.gradle
|
| 3 |
+
/captures/
|
| 4 |
+
/gradlew
|
| 5 |
+
/gradlew.bat
|
| 6 |
+
/local.properties
|
| 7 |
+
GeneratedPluginRegistrant.java
|
| 8 |
+
.cxx/
|
| 9 |
+
|
| 10 |
+
# Remember to never publicly share your keystore.
|
| 11 |
+
# See https://flutter.dev/to/reference-keystore
|
| 12 |
+
key.properties
|
| 13 |
+
**/*.keystore
|
| 14 |
+
**/*.jks
|
check_app/android/.gradle/8.14/checksums/checksums.lock
ADDED
|
Binary file (17 Bytes). View file
|
|
|
check_app/android/.gradle/8.14/checksums/md5-checksums.bin
ADDED
|
Binary file (42 kB). View file
|
|
|
check_app/android/.gradle/8.14/checksums/sha1-checksums.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8f3080ceac71cb4dc7d292b0ec33179e5e7d6de81a9d50c323e33c721005b474
|
| 3 |
+
size 126215
|
check_app/android/.gradle/8.14/executionHistory/executionHistory.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cf6ec48baf09b431f11f0fcc117503713cd528880d5c544427a025f443cd4198
|
| 3 |
+
size 7752405
|
check_app/android/.gradle/8.14/executionHistory/executionHistory.lock
ADDED
|
Binary file (17 Bytes). View file
|
|
|
check_app/android/.gradle/8.14/fileChanges/last-build.bin
ADDED
|
Binary file (1 Bytes). View file
|
|
|
check_app/android/.gradle/8.14/fileHashes/fileHashes.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d360ea65f31f6b8ea2d31eac54024c603025ba1ffca7845faea655d8fca82819
|
| 3 |
+
size 518941
|
check_app/android/.gradle/8.14/fileHashes/fileHashes.lock
ADDED
|
Binary file (17 Bytes). View file
|
|
|
check_app/android/.gradle/8.14/fileHashes/resourceHashesCache.bin
ADDED
|
Binary file (24.5 kB). View file
|
|
|
check_app/android/.gradle/8.14/gc.properties
ADDED
|
File without changes
|
check_app/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
ADDED
|
Binary file (17 Bytes). View file
|
|
|
check_app/android/.gradle/buildOutputCleanup/cache.properties
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#Sun Mar 08 00:19:03 CST 2026
|
| 2 |
+
gradle.version=8.14
|
check_app/android/.gradle/buildOutputCleanup/outputFiles.bin
ADDED
|
Binary file (74.7 kB). View file
|
|
|
check_app/android/.gradle/file-system.probe
ADDED
|
Binary file (8 Bytes). View file
|
|
|
check_app/android/.gradle/noVersion/buildLogic.lock
ADDED
|
Binary file (17 Bytes). View file
|
|
|
check_app/android/.gradle/vcs-1/gc.properties
ADDED
|
File without changes
|
check_app/android/app/build.gradle.kts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
| 1 |
-
plugins {
|
| 2 |
-
id("com.android.application")
|
| 3 |
-
id("kotlin-android")
|
| 4 |
-
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
| 5 |
-
id("dev.flutter.flutter-gradle-plugin")
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
android {
|
| 9 |
-
namespace = "com.
|
| 10 |
-
compileSdk = flutter.compileSdkVersion
|
| 11 |
-
ndkVersion = flutter.ndkVersion
|
| 12 |
-
|
| 13 |
-
compileOptions {
|
| 14 |
-
sourceCompatibility = JavaVersion.VERSION_17
|
| 15 |
-
targetCompatibility = JavaVersion.VERSION_17
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
kotlinOptions {
|
| 19 |
-
jvmTarget = JavaVersion.VERSION_17.toString()
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
defaultConfig {
|
| 23 |
-
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
| 24 |
-
applicationId = "com.
|
| 25 |
-
// You can update the following values to match your application needs.
|
| 26 |
-
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
| 27 |
-
minSdk = flutter.minSdkVersion
|
| 28 |
-
targetSdk = flutter.targetSdkVersion
|
| 29 |
-
versionCode = flutter.versionCode
|
| 30 |
-
versionName = flutter.versionName
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
buildTypes {
|
| 34 |
-
release {
|
| 35 |
-
// TODO: Add your own signing config for the release build.
|
| 36 |
-
// Signing with the debug keys for now, so `flutter run --release` works.
|
| 37 |
-
signingConfig = signingConfigs.getByName("debug")
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
flutter {
|
| 43 |
-
source = "../.."
|
| 44 |
-
}
|
|
|
|
| 1 |
+
plugins {
|
| 2 |
+
id("com.android.application")
|
| 3 |
+
id("kotlin-android")
|
| 4 |
+
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
| 5 |
+
id("dev.flutter.flutter-gradle-plugin")
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
android {
|
| 9 |
+
namespace = "com.bantaypahayag.app"
|
| 10 |
+
compileSdk = flutter.compileSdkVersion
|
| 11 |
+
ndkVersion = flutter.ndkVersion
|
| 12 |
+
|
| 13 |
+
compileOptions {
|
| 14 |
+
sourceCompatibility = JavaVersion.VERSION_17
|
| 15 |
+
targetCompatibility = JavaVersion.VERSION_17
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
kotlinOptions {
|
| 19 |
+
jvmTarget = JavaVersion.VERSION_17.toString()
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
defaultConfig {
|
| 23 |
+
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
| 24 |
+
applicationId = "com.bantaypahayag.app"
|
| 25 |
+
// You can update the following values to match your application needs.
|
| 26 |
+
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
| 27 |
+
minSdk = flutter.minSdkVersion
|
| 28 |
+
targetSdk = flutter.targetSdkVersion
|
| 29 |
+
versionCode = flutter.versionCode
|
| 30 |
+
versionName = flutter.versionName
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
buildTypes {
|
| 34 |
+
release {
|
| 35 |
+
// TODO: Add your own signing config for the release build.
|
| 36 |
+
// Signing with the debug keys for now, so `flutter run --release` works.
|
| 37 |
+
signingConfig = signingConfigs.getByName("debug")
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
flutter {
|
| 43 |
+
source = "../.."
|
| 44 |
+
}
|
check_app/android/app/src/debug/AndroidManifest.xml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 2 |
-
<!-- The INTERNET permission is required for development. Specifically,
|
| 3 |
-
the Flutter tool needs it to communicate with the running application
|
| 4 |
-
to allow setting breakpoints, to provide hot reload, etc.
|
| 5 |
-
-->
|
| 6 |
-
<uses-permission android:name="android.permission.INTERNET"/>
|
| 7 |
-
</manifest>
|
|
|
|
| 1 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 2 |
+
<!-- The INTERNET permission is required for development. Specifically,
|
| 3 |
+
the Flutter tool needs it to communicate with the running application
|
| 4 |
+
to allow setting breakpoints, to provide hot reload, etc.
|
| 5 |
+
-->
|
| 6 |
+
<uses-permission android:name="android.permission.INTERNET"/>
|
| 7 |
+
</manifest>
|
check_app/android/app/src/main/AndroidManifest.xml
CHANGED
|
@@ -1,45 +1,46 @@
|
|
| 1 |
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 2 |
-
<
|
| 3 |
-
|
| 4 |
-
android:
|
| 5 |
-
android:
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
android:
|
| 9 |
-
android:
|
| 10 |
-
android:
|
| 11 |
-
android:
|
| 12 |
-
android:
|
| 13 |
-
android:
|
| 14 |
-
android:
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
android:
|
| 22 |
-
/
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
<
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
<
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
android:
|
| 33 |
-
|
| 34 |
-
<
|
| 35 |
-
|
| 36 |
-
https://developer.android.com/
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
<
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
</
|
|
|
|
|
|
| 1 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 2 |
+
<uses-permission android:name="android.permission.INTERNET"/>
|
| 3 |
+
<application
|
| 4 |
+
android:label="BantayPahayag"
|
| 5 |
+
android:name="${applicationName}"
|
| 6 |
+
android:icon="@mipmap/ic_launcher">
|
| 7 |
+
<activity
|
| 8 |
+
android:name=".MainActivity"
|
| 9 |
+
android:exported="true"
|
| 10 |
+
android:launchMode="singleTop"
|
| 11 |
+
android:taskAffinity=""
|
| 12 |
+
android:theme="@style/LaunchTheme"
|
| 13 |
+
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
| 14 |
+
android:hardwareAccelerated="true"
|
| 15 |
+
android:windowSoftInputMode="adjustResize">
|
| 16 |
+
<!-- Specifies an Android theme to apply to this Activity as soon as
|
| 17 |
+
the Android process has started. This theme is visible to the user
|
| 18 |
+
while the Flutter UI initializes. After that, this theme continues
|
| 19 |
+
to determine the Window background behind the Flutter UI. -->
|
| 20 |
+
<meta-data
|
| 21 |
+
android:name="io.flutter.embedding.android.NormalTheme"
|
| 22 |
+
android:resource="@style/NormalTheme"
|
| 23 |
+
/>
|
| 24 |
+
<intent-filter>
|
| 25 |
+
<action android:name="android.intent.action.MAIN"/>
|
| 26 |
+
<category android:name="android.intent.category.LAUNCHER"/>
|
| 27 |
+
</intent-filter>
|
| 28 |
+
</activity>
|
| 29 |
+
<!-- Don't delete the meta-data below.
|
| 30 |
+
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
| 31 |
+
<meta-data
|
| 32 |
+
android:name="flutterEmbedding"
|
| 33 |
+
android:value="2" />
|
| 34 |
+
</application>
|
| 35 |
+
<!-- Required to query activities that can process text, see:
|
| 36 |
+
https://developer.android.com/training/package-visibility and
|
| 37 |
+
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
| 38 |
+
|
| 39 |
+
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
| 40 |
+
<queries>
|
| 41 |
+
<intent>
|
| 42 |
+
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
| 43 |
+
<data android:mimeType="text/plain"/>
|
| 44 |
+
</intent>
|
| 45 |
+
</queries>
|
| 46 |
+
</manifest>
|
check_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package io.flutter.plugins;
|
| 2 |
+
|
| 3 |
+
import androidx.annotation.Keep;
|
| 4 |
+
import androidx.annotation.NonNull;
|
| 5 |
+
import io.flutter.Log;
|
| 6 |
+
|
| 7 |
+
import io.flutter.embedding.engine.FlutterEngine;
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Generated file. Do not edit.
|
| 11 |
+
* This file is generated by the Flutter tool based on the
|
| 12 |
+
* plugins that support the Android platform.
|
| 13 |
+
*/
|
| 14 |
+
@Keep
|
| 15 |
+
public final class GeneratedPluginRegistrant {
|
| 16 |
+
private static final String TAG = "GeneratedPluginRegistrant";
|
| 17 |
+
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
|
| 18 |
+
try {
|
| 19 |
+
flutterEngine.getPlugins().add(new com.github.dart_lang.jni.JniPlugin());
|
| 20 |
+
} catch (Exception e) {
|
| 21 |
+
Log.e(TAG, "Error registering plugin jni, com.github.dart_lang.jni.JniPlugin", e);
|
| 22 |
+
}
|
| 23 |
+
try {
|
| 24 |
+
flutterEngine.getPlugins().add(new com.github.dart_lang.jni_flutter.JniFlutterPlugin());
|
| 25 |
+
} catch (Exception e) {
|
| 26 |
+
Log.e(TAG, "Error registering plugin jni_flutter, com.github.dart_lang.jni_flutter.JniFlutterPlugin", e);
|
| 27 |
+
}
|
| 28 |
+
try {
|
| 29 |
+
flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
|
| 30 |
+
} catch (Exception e) {
|
| 31 |
+
Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
|
| 32 |
+
}
|
| 33 |
+
try {
|
| 34 |
+
flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
|
| 35 |
+
} catch (Exception e) {
|
| 36 |
+
Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
check_app/android/app/src/main/kotlin/com/bantaypahayag/app/MainActivity.kt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.bantaypahayag.app
|
| 2 |
+
|
| 3 |
+
import io.flutter.embedding.android.FlutterActivity
|
| 4 |
+
|
| 5 |
+
class MainActivity : FlutterActivity()
|
check_app/android/app/src/main/res/drawable-v21/launch_background.xml
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
-
<!-- Modify this file to customize your launch splash screen -->
|
| 3 |
-
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
| 4 |
-
<item android:drawable="?android:colorBackground" />
|
| 5 |
-
|
| 6 |
-
<!-- You can insert your own image assets here -->
|
| 7 |
-
<!-- <item>
|
| 8 |
-
<bitmap
|
| 9 |
-
android:gravity="center"
|
| 10 |
-
android:src="@mipmap/launch_image" />
|
| 11 |
-
</item> -->
|
| 12 |
-
</layer-list>
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<!-- Modify this file to customize your launch splash screen -->
|
| 3 |
+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
| 4 |
+
<item android:drawable="?android:colorBackground" />
|
| 5 |
+
|
| 6 |
+
<!-- You can insert your own image assets here -->
|
| 7 |
+
<!-- <item>
|
| 8 |
+
<bitmap
|
| 9 |
+
android:gravity="center"
|
| 10 |
+
android:src="@mipmap/launch_image" />
|
| 11 |
+
</item> -->
|
| 12 |
+
</layer-list>
|
check_app/android/app/src/main/res/drawable/launch_background.xml
CHANGED
|
@@ -1,12 +1,12 @@
|
|
| 1 |
-
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
-
<!-- Modify this file to customize your launch splash screen -->
|
| 3 |
-
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
| 4 |
-
<item android:drawable="@android:color/white" />
|
| 5 |
-
|
| 6 |
-
<!-- You can insert your own image assets here -->
|
| 7 |
-
<!-- <item>
|
| 8 |
-
<bitmap
|
| 9 |
-
android:gravity="center"
|
| 10 |
-
android:src="@mipmap/launch_image" />
|
| 11 |
-
</item> -->
|
| 12 |
-
</layer-list>
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<!-- Modify this file to customize your launch splash screen -->
|
| 3 |
+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
| 4 |
+
<item android:drawable="@android:color/white" />
|
| 5 |
+
|
| 6 |
+
<!-- You can insert your own image assets here -->
|
| 7 |
+
<!-- <item>
|
| 8 |
+
<bitmap
|
| 9 |
+
android:gravity="center"
|
| 10 |
+
android:src="@mipmap/launch_image" />
|
| 11 |
+
</item> -->
|
| 12 |
+
</layer-list>
|
check_app/android/app/src/main/res/values-night/styles.xml
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
-
<resources>
|
| 3 |
-
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
| 4 |
-
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
| 5 |
-
<!-- Show a splash screen on the activity. Automatically removed when
|
| 6 |
-
the Flutter engine draws its first frame -->
|
| 7 |
-
<item name="android:windowBackground">@drawable/launch_background</item>
|
| 8 |
-
</style>
|
| 9 |
-
<!-- Theme applied to the Android Window as soon as the process has started.
|
| 10 |
-
This theme determines the color of the Android Window while your
|
| 11 |
-
Flutter UI initializes, as well as behind your Flutter UI while its
|
| 12 |
-
running.
|
| 13 |
-
|
| 14 |
-
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
| 15 |
-
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
| 16 |
-
<item name="android:windowBackground">?android:colorBackground</item>
|
| 17 |
-
</style>
|
| 18 |
-
</resources>
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<resources>
|
| 3 |
+
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
| 4 |
+
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
| 5 |
+
<!-- Show a splash screen on the activity. Automatically removed when
|
| 6 |
+
the Flutter engine draws its first frame -->
|
| 7 |
+
<item name="android:windowBackground">@drawable/launch_background</item>
|
| 8 |
+
</style>
|
| 9 |
+
<!-- Theme applied to the Android Window as soon as the process has started.
|
| 10 |
+
This theme determines the color of the Android Window while your
|
| 11 |
+
Flutter UI initializes, as well as behind your Flutter UI while its
|
| 12 |
+
running.
|
| 13 |
+
|
| 14 |
+
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
| 15 |
+
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
| 16 |
+
<item name="android:windowBackground">?android:colorBackground</item>
|
| 17 |
+
</style>
|
| 18 |
+
</resources>
|
check_app/android/app/src/main/res/values/styles.xml
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
-
<resources>
|
| 3 |
-
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
| 4 |
-
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
| 5 |
-
<!-- Show a splash screen on the activity. Automatically removed when
|
| 6 |
-
the Flutter engine draws its first frame -->
|
| 7 |
-
<item name="android:windowBackground">@drawable/launch_background</item>
|
| 8 |
-
</style>
|
| 9 |
-
<!-- Theme applied to the Android Window as soon as the process has started.
|
| 10 |
-
This theme determines the color of the Android Window while your
|
| 11 |
-
Flutter UI initializes, as well as behind your Flutter UI while its
|
| 12 |
-
running.
|
| 13 |
-
|
| 14 |
-
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
| 15 |
-
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
| 16 |
-
<item name="android:windowBackground">?android:colorBackground</item>
|
| 17 |
-
</style>
|
| 18 |
-
</resources>
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<resources>
|
| 3 |
+
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
| 4 |
+
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
| 5 |
+
<!-- Show a splash screen on the activity. Automatically removed when
|
| 6 |
+
the Flutter engine draws its first frame -->
|
| 7 |
+
<item name="android:windowBackground">@drawable/launch_background</item>
|
| 8 |
+
</style>
|
| 9 |
+
<!-- Theme applied to the Android Window as soon as the process has started.
|
| 10 |
+
This theme determines the color of the Android Window while your
|
| 11 |
+
Flutter UI initializes, as well as behind your Flutter UI while its
|
| 12 |
+
running.
|
| 13 |
+
|
| 14 |
+
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
| 15 |
+
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
| 16 |
+
<item name="android:windowBackground">?android:colorBackground</item>
|
| 17 |
+
</style>
|
| 18 |
+
</resources>
|
check_app/android/app/src/profile/AndroidManifest.xml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 2 |
-
<!-- The INTERNET permission is required for development. Specifically,
|
| 3 |
-
the Flutter tool needs it to communicate with the running application
|
| 4 |
-
to allow setting breakpoints, to provide hot reload, etc.
|
| 5 |
-
-->
|
| 6 |
-
<uses-permission android:name="android.permission.INTERNET"/>
|
| 7 |
-
</manifest>
|
|
|
|
| 1 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 2 |
+
<!-- The INTERNET permission is required for development. Specifically,
|
| 3 |
+
the Flutter tool needs it to communicate with the running application
|
| 4 |
+
to allow setting breakpoints, to provide hot reload, etc.
|
| 5 |
+
-->
|
| 6 |
+
<uses-permission android:name="android.permission.INTERNET"/>
|
| 7 |
+
</manifest>
|
check_app/android/build.gradle.kts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
| 1 |
-
allprojects {
|
| 2 |
-
repositories {
|
| 3 |
-
google()
|
| 4 |
-
mavenCentral()
|
| 5 |
-
}
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
val newBuildDir: Directory =
|
| 9 |
-
rootProject.layout.buildDirectory
|
| 10 |
-
.dir("../../build")
|
| 11 |
-
.get()
|
| 12 |
-
rootProject.layout.buildDirectory.value(newBuildDir)
|
| 13 |
-
|
| 14 |
-
subprojects {
|
| 15 |
-
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
| 16 |
-
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
| 17 |
-
}
|
| 18 |
-
subprojects {
|
| 19 |
-
project.evaluationDependsOn(":app")
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
tasks.register<Delete>("clean") {
|
| 23 |
-
delete(rootProject.layout.buildDirectory)
|
| 24 |
-
}
|
|
|
|
| 1 |
+
allprojects {
|
| 2 |
+
repositories {
|
| 3 |
+
google()
|
| 4 |
+
mavenCentral()
|
| 5 |
+
}
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
val newBuildDir: Directory =
|
| 9 |
+
rootProject.layout.buildDirectory
|
| 10 |
+
.dir("../../build")
|
| 11 |
+
.get()
|
| 12 |
+
rootProject.layout.buildDirectory.value(newBuildDir)
|
| 13 |
+
|
| 14 |
+
subprojects {
|
| 15 |
+
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
| 16 |
+
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
| 17 |
+
}
|
| 18 |
+
subprojects {
|
| 19 |
+
project.evaluationDependsOn(":app")
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
tasks.register<Delete>("clean") {
|
| 23 |
+
delete(rootProject.layout.buildDirectory)
|
| 24 |
+
}
|
check_app/android/build/reports/problems/problems-report.html
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
check_app/android/gradle.properties
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
| 2 |
-
android.useAndroidX=true
|
|
|
|
| 1 |
+
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
| 2 |
+
android.useAndroidX=true
|
check_app/android/gradle/wrapper/gradle-wrapper.jar
ADDED
|
Binary file (53.6 kB). View file
|
|
|
check_app/android/gradle/wrapper/gradle-wrapper.properties
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
distributionBase=GRADLE_USER_HOME
|
| 2 |
-
distributionPath=wrapper/dists
|
| 3 |
-
zipStoreBase=GRADLE_USER_HOME
|
| 4 |
-
zipStorePath=wrapper/dists
|
| 5 |
-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
|
|
|
| 1 |
+
distributionBase=GRADLE_USER_HOME
|
| 2 |
+
distributionPath=wrapper/dists
|
| 3 |
+
zipStoreBase=GRADLE_USER_HOME
|
| 4 |
+
zipStorePath=wrapper/dists
|
| 5 |
+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|