laura.wagner commited on
Commit
03843c0
·
1 Parent(s): 6ce312c

updated figure 8 notebook

Browse files
.gitingore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ data
2
+ ext
3
+ ARCHIVE
4
+ misc/api_keys.txt
5
+ misc/training_tags_categories
6
+ misc/credentials/*
7
+ misc/credentials
8
+ scripts/ARCHIVE
9
+ scripts/CEMETARY
10
+ cemetary
11
+ .venv
jupyter_notebooks/.ipynb_checkpoints/Section_2-3-4_Figure_8_deepfake_adapters-checkpoint.ipynb ADDED
@@ -0,0 +1,1941 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "e4cf5af4",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Deepfake Adapter Dataset Processing Pipeline\n",
9
+ "\n",
10
+ "This notebook processes the `real_person_adapters.csv` dataset to identify and annotate real people used in deepfake LoRA models.\n",
11
+ "\n",
12
+ "## Overview\n",
13
+ "\n",
14
+ "The pipeline consists of 4 main steps:\n",
15
+ "\n",
16
+ "1. **NER & Name Cleaning**: Extract clean person names from noisy adapter names\n",
17
+ " - Remove emoji, version tags (v1, v2), LoRA-specific terms\n",
18
+ " - Extract core person name from complex strings\n",
19
+ "\n",
20
+ "2. **Country/Nationality Mapping**: Convert nationality tags to standardized countries\n",
21
+ " - Uses `misc/lists/countries.csv` reference\n",
22
+ " - Excludes uninhabited territories (Isle of Man, Bouvet Island, etc.)\n",
23
+ "\n",
24
+ "3. **LLM-Based Profession Annotation**: Use large language models to identify professions\n",
25
+ " - Supports 3 LLM providers: **Qwen**, **Llama**, **Mistral** (large versions)\n",
26
+ " - Uses profession list from `misc/lists/professions.csv`\n",
27
+ " - Can annotate up to 3 professions per person (e.g., Emma Watson: actor, activist)\n",
28
+ "\n",
29
+ "4. **Output Formatting**: Generate annotated dataset with standardized fields\n",
30
+ "\n",
31
+ "## Prerequisites\n",
32
+ "\n",
33
+ "### 1. Install Required Packages\n",
34
+ "\n",
35
+ "```bash\n",
36
+ "pip install pandas numpy emoji requests tqdm spacy\n",
37
+ "\n",
38
+ "# Download spaCy English model for NER\n",
39
+ "python -m spacy download en_core_web_sm\n",
40
+ "```\n",
41
+ "\n",
42
+ "### 2. Set Up API Keys\n",
43
+ "\n",
44
+ "You need API keys for at least one of the LLM providers:\n",
45
+ "\n",
46
+ "- **Qwen (Alibaba Cloud)**: https://dashscope.aliyun.com/\n",
47
+ "- **Llama (Together AI)**: https://www.together.ai/\n",
48
+ "- **Mistral AI**: https://mistral.ai/\n",
49
+ "\n",
50
+ "Create text files in `misc/credentials/`:\n",
51
+ "- `qwen_api_key.txt`\n",
52
+ "- `together_api_key.txt` (for Llama)\n",
53
+ "- `mistral_api_key.txt`\n",
54
+ "\n",
55
+ "Each file should contain only the API key (no extra whitespace).\n",
56
+ "\n",
57
+ "See `misc/credentials/README.md` for detailed instructions.\n",
58
+ "\n",
59
+ "## Configuration\n",
60
+ "\n",
61
+ "In the LLM annotation section, you can configure:\n",
62
+ "\n",
63
+ "```python\n",
64
+ "SELECTED_LLM = 'qwen' # Choose: 'qwen', 'llama', or 'mistral'\n",
65
+ "TEST_MODE = True # True = test on small sample first\n",
66
+ "TEST_SIZE = 10 # Number of rows to test\n",
67
+ "MAX_ROWS = 20000 # Maximum rows to process (None = all)\n",
68
+ "SAVE_INTERVAL = 10 # Save progress every N rows\n",
69
+ "```\n",
70
+ "\n",
71
+ "## Expected Output\n",
72
+ "\n",
73
+ "The final annotated dataset will include:\n",
74
+ "- `real_name`: Cleaned person name\n",
75
+ "- `full_name`: Full legal/stage name (from LLM)\n",
76
+ "- `gender`: Gender (from LLM)\n",
77
+ "- `profession_llm`: Up to 3 professions (from LLM)\n",
78
+ "- `country`: Primary country (from LLM)\n",
79
+ "- `tags`: Original tags as list\n",
80
+ "- `likely_country`: Country inferred from tags\n",
81
+ "- `likely_nationality`: Nationality inferred from tags\n",
82
+ "- `likely_profession`: Profession inferred from tags\n",
83
+ "- `prompt`: Summary of hints used for LLM\n",
84
+ "\n",
85
+ "## Cost Considerations\n",
86
+ "\n",
87
+ "For ~10,000 entries:\n",
88
+ "- **Qwen**: Check Alibaba Cloud pricing\n",
89
+ "- **Llama (Together AI)**: ~$5-10\n",
90
+ "- **Mistral**: ~$40-80\n",
91
+ "\n",
92
+ "**Always test on a small sample first!**\n",
93
+ "\n",
94
+ "## Running the Pipeline\n",
95
+ "\n",
96
+ "1. Run cells sequentially from top to bottom\n",
97
+ "2. Start with `TEST_MODE = True` to test on 10 samples\n",
98
+ "3. Review results and adjust configuration as needed\n",
99
+ "4. Set `TEST_MODE = False` for full processing\n",
100
+ "5. The pipeline saves progress automatically and can be resumed if interrupted\n",
101
+ "\n",
102
+ "---\n"
103
+ ]
104
+ },
105
+ {
106
+ "cell_type": "markdown",
107
+ "id": "23d0ae58",
108
+ "metadata": {},
109
+ "source": [
110
+ "# Deepfake Adapter Dataset - LLM Annotation Pipeline"
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "markdown",
115
+ "id": "d7942c40",
116
+ "metadata": {},
117
+ "source": []
118
+ },
119
+ {
120
+ "cell_type": "markdown",
121
+ "id": "3c906826",
122
+ "metadata": {},
123
+ "source": [
124
+ "## NER Code"
125
+ ]
126
+ },
127
+ {
128
+ "cell_type": "markdown",
129
+ "id": "cb529f57",
130
+ "metadata": {},
131
+ "source": [
132
+ "please put NER code here. on the first collumn of the adapter dataset in data/real_person_adapters.csv named entity recognition should be performed to extract the most plausible name."
133
+ ]
134
+ },
135
+ {
136
+ "cell_type": "code",
137
+ "execution_count": 2,
138
+ "id": "ner_code_cell",
139
+ "metadata": {},
140
+ "outputs": [
141
+ {
142
+ "name": "stdout",
143
+ "output_type": "stream",
144
+ "text": [
145
+ "✅ spaCy model loaded: en_core_web_sm\n",
146
+ "Loaded 18 rows\n",
147
+ "\n",
148
+ "🔄 Processing names with spaCy NER...\n",
149
+ "\n",
150
+ "📊 Name cleaning examples (with spaCy NER):\n",
151
+ "====================================================================================================\n",
152
+ "Original Name | Cleaned Name \n",
153
+ "====================================================================================================\n",
154
+ "Super Pose Book Vol.1 - ControlNet | Super Pose Book \n",
155
+ "Liyuu LoRA | Liyuu \n",
156
+ "HashimotoKanna/ 橋本環奈 _JP_Actress | HashimotoKanna \n",
157
+ "Emma Watson (JG) | Emma Watson \n",
158
+ "Gal Gadot「LoRa」 | Gal Gadot \n",
159
+ "Scarlett Johansson「LoRa」 | Scarlett Johansson \n",
160
+ "Gakki | Aragaki Yui | 新垣結衣 | Gakki \n",
161
+ "Actress Satomi_石原○○ | Actress Satomi \n",
162
+ "Game of Thrones Cast | Game Thrones Cast \n",
163
+ "Natalie Portman「LoRa」 | Natalie Portman \n",
164
+ "Emma Watson LoRA | Emma Watson \n",
165
+ "====================================================================================================\n",
166
+ "\n",
167
+ "🧪 Leetspeak translation examples:\n",
168
+ " 4kira LoRA -> akira\n",
169
+ " 3mma Watson v2 -> Watson\n",
170
+ " 1rene LORA -> irene\n",
171
+ " L3vi Ackerman -> Levi Ackerman\n",
172
+ "\n",
173
+ "📈 Statistics:\n",
174
+ " Total rows: 18\n",
175
+ " Non-empty names: 18\n",
176
+ " Empty names: 0\n",
177
+ "\n",
178
+ "🎯 Sample spaCy NER results:\n",
179
+ " 1. IU\n",
180
+ " 2. Super Pose Book\n",
181
+ " 3. Liyuu\n",
182
+ " 4. Irene\n",
183
+ " 5. AESPA Karina\n",
184
+ " 6. Saika Kawakita\n",
185
+ " 7. Liu Yifei\n",
186
+ " 8. HashimotoKanna\n",
187
+ " 9. Emma Watson\n",
188
+ " 10. Gal Gadot\n",
189
+ "\n",
190
+ "✅ Cleaned 18 names using spaCy NER\n",
191
+ "💾 Saved to /home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/CSV/NER_POI_step01_pre_annotation.csv\n"
192
+ ]
193
+ }
194
+ ],
195
+ "source": [
196
+ "import pandas as pd\n",
197
+ "import re\n",
198
+ "from pathlib import Path\n",
199
+ "import emoji\n",
200
+ "import spacy\n",
201
+ "\n",
202
+ "# Load spaCy model\n",
203
+ "# You may need to download it first: python -m spacy download en_core_web_sm\n",
204
+ "try:\n",
205
+ " nlp = spacy.load(\"en_core_web_sm\")\n",
206
+ " print(\"✅ spaCy model loaded: en_core_web_sm\")\n",
207
+ "except OSError:\n",
208
+ " print(\"❌ spaCy model not found. Downloading...\")\n",
209
+ " import subprocess\n",
210
+ " subprocess.run([\"python\", \"-m\", \"spacy\", \"download\", \"en_core_web_sm\"])\n",
211
+ " nlp = spacy.load(\"en_core_web_sm\")\n",
212
+ " print(\"✅ spaCy model downloaded and loaded\")\n",
213
+ "\n",
214
+ "# Set up paths\n",
215
+ "current_dir = Path.cwd()\n",
216
+ "#input_file = current_dir.parent / \"data/CSV/real_person_adapters.csv\"\n",
217
+ "input_file = current_dir.parent / \"data/CSV/samples/real_person_adapters_sample.csv\"\n",
218
+ "\n",
219
+ "# Load dataset\n",
220
+ "df = pd.read_csv(input_file)\n",
221
+ "print(f\"Loaded {len(df)} rows\")\n",
222
+ "\n",
223
+ "def translate_leetspeak(text: str) -> str:\n",
224
+ " \"\"\"\n",
225
+ " Translate common leetspeak patterns to normal letters.\n",
226
+ " Examples: 4kira -> Akira, 3mma -> Emma, 1rene -> Irene\n",
227
+ " \"\"\"\n",
228
+ " if not text:\n",
229
+ " return text\n",
230
+ " \n",
231
+ " # Common leetspeak mappings (order matters!)\n",
232
+ " leetspeak_map = {\n",
233
+ " '4': 'a',\n",
234
+ " '3': 'e', \n",
235
+ " '1': 'i',\n",
236
+ " '0': 'o',\n",
237
+ " '7': 't',\n",
238
+ " '5': 's',\n",
239
+ " '8': 'b',\n",
240
+ " '9': 'g',\n",
241
+ " '@': 'a',\n",
242
+ " '$': 's',\n",
243
+ " '!': 'i',\n",
244
+ " }\n",
245
+ " \n",
246
+ " result = text\n",
247
+ " # Apply mappings at word boundaries or start of string\n",
248
+ " for leet, normal in leetspeak_map.items():\n",
249
+ " # Replace at start of word\n",
250
+ " result = re.sub(rf'\\b{re.escape(leet)}', normal, result, flags=re.IGNORECASE)\n",
251
+ " # Replace standalone numbers that look like letters in context\n",
252
+ " result = re.sub(rf'(?<=[a-z]){re.escape(leet)}(?=[a-z])', normal, result, flags=re.IGNORECASE)\n",
253
+ " \n",
254
+ " return result\n",
255
+ "\n",
256
+ "def preprocess_for_ner(name: str) -> str:\n",
257
+ " \"\"\"\n",
258
+ " Preprocess the name before spaCy NER.\n",
259
+ " Remove noise but keep the actual name parts.\n",
260
+ " \"\"\"\n",
261
+ " if pd.isna(name):\n",
262
+ " return \"\"\n",
263
+ " \n",
264
+ " name = str(name)\n",
265
+ " \n",
266
+ " # FIRST: Translate leetspeak\n",
267
+ " name = translate_leetspeak(name)\n",
268
+ " \n",
269
+ " # Remove emoji\n",
270
+ " name = emoji.replace_emoji(name, replace=' ')\n",
271
+ " \n",
272
+ " # Remove version indicators (v1, v2, v1.0, etc.)\n",
273
+ " name = re.sub(r'\\s*[vV]\\d+(\\.\\d+)?\\s*', ' ', name)\n",
274
+ " \n",
275
+ " # Remove LoRA-related terms (case insensitive)\n",
276
+ " lora_terms = ['lora', 'loha', 'lycoris', 'controlnet', 'textual inversion', \n",
277
+ " 'embedding', 'ti', 'checkpoint', 'model', 'adapter']\n",
278
+ " for term in lora_terms:\n",
279
+ " name = re.sub(rf'\\b{term}\\b', '', name, flags=re.IGNORECASE)\n",
280
+ " \n",
281
+ " # Remove content in parentheses or brackets (often metadata)\n",
282
+ " name = re.sub(r'\\([^)]*\\)', '', name)\n",
283
+ " name = re.sub(r'\\[[^\\]]*\\]', '', name)\n",
284
+ " \n",
285
+ " # Remove special characters like 「」\n",
286
+ " name = re.sub(r'[「」『』【】〈〉《》]', '', name)\n",
287
+ " \n",
288
+ " # Handle pipe - keep first part\n",
289
+ " if '|' in name:\n",
290
+ " name = name.split('|')[0]\n",
291
+ " \n",
292
+ " # Handle forward slash - keep first part\n",
293
+ " if '/' in name:\n",
294
+ " name = name.split('/')[0]\n",
295
+ " \n",
296
+ " # Replace underscores with spaces\n",
297
+ " name = name.replace('_', ' ')\n",
298
+ " \n",
299
+ " # Remove multiple spaces\n",
300
+ " name = re.sub(r'\\s+', ' ', name)\n",
301
+ " \n",
302
+ " # Strip\n",
303
+ " name = name.strip()\n",
304
+ " \n",
305
+ " return name\n",
306
+ "\n",
307
+ "def extract_person_name(text: str) -> str:\n",
308
+ " \"\"\"\n",
309
+ " Use spaCy NER to extract person names from text.\n",
310
+ " Falls back to cleaned text if no PERSON entity found.\n",
311
+ " \"\"\"\n",
312
+ " if not text:\n",
313
+ " return \"\"\n",
314
+ " \n",
315
+ " # Run spaCy NER\n",
316
+ " doc = nlp(text)\n",
317
+ " \n",
318
+ " # Extract PERSON entities\n",
319
+ " person_entities = [ent.text for ent in doc.ents if ent.label_ == \"PERSON\"]\n",
320
+ " \n",
321
+ " if person_entities:\n",
322
+ " # Return the first (usually longest/best) person name\n",
323
+ " return person_entities[0].strip()\n",
324
+ " \n",
325
+ " # If no PERSON entity found, try to extract capitalized words (likely names)\n",
326
+ " # This helps with names spaCy might miss\n",
327
+ " words = text.split()\n",
328
+ " capitalized_words = [w for w in words if w and w[0].isupper() and len(w) > 1]\n",
329
+ " \n",
330
+ " if capitalized_words:\n",
331
+ " # Join first few capitalized words (likely the name)\n",
332
+ " return ' '.join(capitalized_words[:3]).strip()\n",
333
+ " \n",
334
+ " # Last resort: return cleaned text\n",
335
+ " return text.strip()\n",
336
+ "\n",
337
+ "def clean_name_with_spacy(name: str) -> str:\n",
338
+ " \"\"\"\n",
339
+ " Complete name cleaning pipeline with spaCy NER.\n",
340
+ " \n",
341
+ " Pipeline:\n",
342
+ " 1. Translate leetspeak (4→a, 3→e, 1→i, etc.)\n",
343
+ " 2. Remove noise (emoji, version tags, LoRA terms)\n",
344
+ " 3. Use spaCy to extract PERSON entities\n",
345
+ " 4. Fallback to capitalized words or cleaned text\n",
346
+ " \"\"\"\n",
347
+ " # Step 1 & 2: Preprocess (leetspeak + noise removal)\n",
348
+ " preprocessed = preprocess_for_ner(name)\n",
349
+ " \n",
350
+ " if not preprocessed:\n",
351
+ " return \"\"\n",
352
+ " \n",
353
+ " # Step 3: Extract person name using spaCy NER\n",
354
+ " person_name = extract_person_name(preprocessed)\n",
355
+ " \n",
356
+ " return person_name\n",
357
+ "\n",
358
+ "# Apply name cleaning with spaCy\n",
359
+ "print(\"\\n🔄 Processing names with spaCy NER...\")\n",
360
+ "df['real_name'] = df['name'].apply(clean_name_with_spacy)\n",
361
+ "\n",
362
+ "# Show examples with detailed comparison\n",
363
+ "print(\"\\n📊 Name cleaning examples (with spaCy NER):\")\n",
364
+ "print(\"=\" * 100)\n",
365
+ "print(f\"{'Original Name':<50} | {'Cleaned Name':<30}\")\n",
366
+ "print(\"=\" * 100)\n",
367
+ "\n",
368
+ "examples = df[['name', 'real_name']].head(30)\n",
369
+ "shown = 0\n",
370
+ "for idx, row in examples.iterrows():\n",
371
+ " if row['name'] != row['real_name'] and shown < 20:\n",
372
+ " print(f\"{row['name']:<50} | {row['real_name']:<30}\")\n",
373
+ " shown += 1\n",
374
+ "\n",
375
+ "print(\"=\" * 100)\n",
376
+ "\n",
377
+ "# Show specific test cases\n",
378
+ "print(\"\\n🧪 Leetspeak translation examples:\")\n",
379
+ "test_names = ['4kira LoRA', '3mma Watson v2', '1rene LORA', 'L3vi Ackerman']\n",
380
+ "for test in test_names:\n",
381
+ " result = clean_name_with_spacy(test)\n",
382
+ " print(f\" {test:<30} -> {result}\")\n",
383
+ "\n",
384
+ "# Statistics\n",
385
+ "print(f\"\\n📈 Statistics:\")\n",
386
+ "print(f\" Total rows: {len(df)}\")\n",
387
+ "print(f\" Non-empty names: {(df['real_name'] != '').sum()}\")\n",
388
+ "print(f\" Empty names: {(df['real_name'] == '').sum()}\")\n",
389
+ "\n",
390
+ "# Show some examples of what spaCy identified\n",
391
+ "print(\"\\n🎯 Sample spaCy NER results:\")\n",
392
+ "sample_names = df['real_name'].head(20).tolist()\n",
393
+ "for i, name in enumerate(sample_names[:10], 1):\n",
394
+ " if name:\n",
395
+ " print(f\" {i}. {name}\")\n",
396
+ "\n",
397
+ "print(f\"\\n✅ Cleaned {len(df)} names using spaCy NER\")\n",
398
+ "\n",
399
+ "# Save intermediate result\n",
400
+ "output_step1 = current_dir.parent / \"data/CSV/NER_POI_step01_pre_annotation.csv\"\n",
401
+ "df.to_csv(output_step1, index=False)\n",
402
+ "print(f\"💾 Saved to {output_step1}\")\n"
403
+ ]
404
+ },
405
+ {
406
+ "cell_type": "markdown",
407
+ "id": "d701d1cb",
408
+ "metadata": {},
409
+ "source": [
410
+ "## nationality to Country\n",
411
+ "here tags related to nationality gets converted to the country equivalent. make sure isle of man is shows never up in the country collumn.as well as other uninhabited places."
412
+ ]
413
+ },
414
+ {
415
+ "cell_type": "code",
416
+ "execution_count": 3,
417
+ "id": "36ce71f2",
418
+ "metadata": {},
419
+ "outputs": [
420
+ {
421
+ "name": "stdout",
422
+ "output_type": "stream",
423
+ "text": [
424
+ "Built country/nationality mapping with 781 entries\n",
425
+ "\n",
426
+ "Processed 18 rows\n",
427
+ "Rows with country: 0\n",
428
+ "Rows with nationality: 0\n",
429
+ "Rows with profession: 14\n",
430
+ "\n",
431
+ "Top 10 countries:\n",
432
+ "Series([], Name: count, dtype: int64)\n",
433
+ "\n",
434
+ "Sample results:\n",
435
+ " real_name likely_country likely_nationality likely_profession\n",
436
+ "0 IU celebrity\n",
437
+ "1 Super Pose Book \n",
438
+ "2 Liyuu \n",
439
+ "3 Irene celebrity\n",
440
+ "4 AESPA Karina celebrity\n",
441
+ "5 Saika Kawakita celebrity\n",
442
+ "6 Liu Yifei celebrity\n",
443
+ "7 HashimotoKanna actor\n",
444
+ "8 Emma Watson actor\n",
445
+ "9 Gal Gadot model\n",
446
+ "\n",
447
+ "🔍 Checking 'Irene' entries:\n",
448
+ " Name: Irene\n",
449
+ " Tags: ['girl', 'photorealistic', 'asian', 'woman', 'beautiful', 'celebrity', 'korean']\n",
450
+ " Country: \n",
451
+ " Nationality: \n",
452
+ "\n"
453
+ ]
454
+ }
455
+ ],
456
+ "source": [
457
+ "import pandas as pd\n",
458
+ "from pathlib import Path\n",
459
+ "\n",
460
+ "# Set up paths\n",
461
+ "current_dir = Path.cwd()\n",
462
+ "countries_file = current_dir.parent / \"misc/lists/countries.csv\"\n",
463
+ "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
464
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step01_pre_annotation.csv\"\n",
465
+ "output_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
466
+ "\n",
467
+ "# Load datasets\n",
468
+ "poi_df = pd.read_csv(input_file)\n",
469
+ "countries_df = pd.read_csv(countries_file)\n",
470
+ "professions_df = pd.read_csv(professions_file)\n",
471
+ "\n",
472
+ "# Define uninhabited or non-relevant territories to exclude\n",
473
+ "excluded_territories = {\n",
474
+ " 'isle of man', 'bouvet island', 'heard island and mcdonald islands',\n",
475
+ " 'french southern territories', 'south georgia and the south sandwich islands',\n",
476
+ " 'svalbard and jan mayen', 'british indian ocean territory', 'antarctica',\n",
477
+ " 'christmas island', 'cocos (keeling) islands', 'norfolk island',\n",
478
+ " 'pitcairn', 'tokelau', 'united states minor outlying islands',\n",
479
+ " 'wallis and futuna', 'western sahara'\n",
480
+ "}\n",
481
+ "\n",
482
+ "# Step 1: Combine tags into one lowercase list\n",
483
+ "def combine_tags(row):\n",
484
+ " return [str(row[f\"tag_{i}\"]).strip().lower() for i in range(1, 8) if pd.notna(row.get(f\"tag_{i}\"))]\n",
485
+ "\n",
486
+ "poi_df[\"tags\"] = poi_df.apply(combine_tags, axis=1)\n",
487
+ "\n",
488
+ "# Step 2: Build tag → (country, nationality) mapping with PRIORITIES\n",
489
+ "tag_to_country_nationality = {}\n",
490
+ "# We'll use a priority score: direct country name = 3, nationality = 2, word parts = 1\n",
491
+ "\n",
492
+ "for _, row in countries_df.iterrows():\n",
493
+ " country = str(row[\"en_short_name\"]).strip()\n",
494
+ " nationality = str(row[\"nationality\"]).strip()\n",
495
+ " \n",
496
+ " # Skip excluded territories\n",
497
+ " if country.lower() in excluded_territories:\n",
498
+ " continue\n",
499
+ "\n",
500
+ " country_lc = country.lower()\n",
501
+ " nationality_lc = nationality.lower()\n",
502
+ "\n",
503
+ " # Store as (country, nationality, priority)\n",
504
+ " # Exact country name match = highest priority\n",
505
+ " if country_lc not in tag_to_country_nationality:\n",
506
+ " tag_to_country_nationality[country_lc] = (country, \"\", 3)\n",
507
+ " \n",
508
+ " # Exact nationality match = medium priority \n",
509
+ " if nationality_lc not in tag_to_country_nationality:\n",
510
+ " tag_to_country_nationality[nationality_lc] = (\"\", nationality, 2)\n",
511
+ " \n",
512
+ " # No-space versions\n",
513
+ " country_no_space = country_lc.replace(\" \", \"\")\n",
514
+ " nationality_no_space = nationality_lc.replace(\" \", \"\")\n",
515
+ " \n",
516
+ " if country_no_space not in tag_to_country_nationality:\n",
517
+ " tag_to_country_nationality[country_no_space] = (country, \"\", 3)\n",
518
+ " if nationality_no_space not in tag_to_country_nationality:\n",
519
+ " tag_to_country_nationality[nationality_no_space] = (\"\", nationality, 2)\n",
520
+ "\n",
521
+ " # Word parts = lowest priority (only for longer words to avoid false matches)\n",
522
+ " for part in country_lc.split():\n",
523
+ " if len(part) > 4: # Only words longer than 4 chars\n",
524
+ " if part not in tag_to_country_nationality:\n",
525
+ " tag_to_country_nationality[part] = (country, \"\", 1)\n",
526
+ " for part in nationality_lc.split():\n",
527
+ " if len(part) > 4:\n",
528
+ " if part not in tag_to_country_nationality:\n",
529
+ " tag_to_country_nationality[part] = (\"\", nationality, 1)\n",
530
+ "\n",
531
+ "print(f\"Built country/nationality mapping with {len(tag_to_country_nationality)} entries\")\n",
532
+ "\n",
533
+ "# Step 3: Infer likely_country and likely_nationality by checking ALL tags\n",
534
+ "def infer_country_and_nationality(tags):\n",
535
+ " \"\"\"\n",
536
+ " Check ALL tags and return the best match based on priority.\n",
537
+ " Priority: exact country name > nationality > word parts\n",
538
+ " \"\"\"\n",
539
+ " best_match = None\n",
540
+ " best_priority = 0\n",
541
+ " \n",
542
+ " for tag in tags:\n",
543
+ " # Try cleaned version (no spaces)\n",
544
+ " cleaned = tag.replace(\" \", \"\").lower()\n",
545
+ " \n",
546
+ " # Check cleaned version\n",
547
+ " if cleaned in tag_to_country_nationality:\n",
548
+ " country, nationality, priority = tag_to_country_nationality[cleaned]\n",
549
+ " if priority > best_priority and country and country.lower() not in excluded_territories:\n",
550
+ " best_match = (country, nationality)\n",
551
+ " best_priority = priority\n",
552
+ " \n",
553
+ " # Also check original tag\n",
554
+ " if tag in tag_to_country_nationality:\n",
555
+ " country, nationality, priority = tag_to_country_nationality[tag]\n",
556
+ " if priority > best_priority and country and country.lower() not in excluded_territories:\n",
557
+ " best_match = (country, nationality)\n",
558
+ " best_priority = priority\n",
559
+ " \n",
560
+ " if best_match:\n",
561
+ " return pd.Series(best_match)\n",
562
+ " return pd.Series([\"\", \"\"])\n",
563
+ "\n",
564
+ "poi_df[[\"likely_country\", \"likely_nationality\"]] = poi_df[\"tags\"].apply(infer_country_and_nationality)\n",
565
+ "\n",
566
+ "# Step 4: Build tag → profession mapping\n",
567
+ "profession_alias_map = {}\n",
568
+ "\n",
569
+ "for _, row in professions_df.iterrows():\n",
570
+ " canonical = str(row['profession']).strip().lower()\n",
571
+ " profession_alias_map[canonical] = canonical\n",
572
+ " for alias_col in ['alias_1', 'alias_2', 'alias_3']:\n",
573
+ " alias = row.get(alias_col)\n",
574
+ " if pd.notna(alias):\n",
575
+ " profession_alias_map[str(alias).strip().lower()] = canonical\n",
576
+ "\n",
577
+ "# Step 5: Infer likely profession from tags\n",
578
+ "def infer_profession_from_tags(tags):\n",
579
+ " matched = []\n",
580
+ " for tag in tags:\n",
581
+ " cleaned = tag.strip().lower()\n",
582
+ " if cleaned in profession_alias_map:\n",
583
+ " matched.append(profession_alias_map[cleaned])\n",
584
+ "\n",
585
+ " if not matched:\n",
586
+ " return \"\"\n",
587
+ " if \"celebrity\" in matched and len(set(matched)) > 1:\n",
588
+ " # Drop 'celebrity' if other professions are present\n",
589
+ " matched = [m for m in matched if m != \"celebrity\"]\n",
590
+ "\n",
591
+ " return matched[0] # Return the first specific match\n",
592
+ "\n",
593
+ "\n",
594
+ "poi_df[\"likely_profession\"] = poi_df[\"tags\"].apply(infer_profession_from_tags)\n",
595
+ "\n",
596
+ "# Step 6: Save enriched dataset\n",
597
+ "poi_df.to_csv(output_file, index=False)\n",
598
+ "\n",
599
+ "# Preview results\n",
600
+ "print(f\"\\nProcessed {len(poi_df)} rows\")\n",
601
+ "print(f\"Rows with country: {(poi_df['likely_country'] != '').sum()}\")\n",
602
+ "print(f\"Rows with nationality: {(poi_df['likely_nationality'] != '').sum()}\")\n",
603
+ "print(f\"Rows with profession: {(poi_df['likely_profession'] != '').sum()}\")\n",
604
+ "\n",
605
+ "print(f\"\\nTop 10 countries:\")\n",
606
+ "print(poi_df[poi_df['likely_country'] != '']['likely_country'].value_counts().head(10))\n",
607
+ "\n",
608
+ "print(f\"\\nSample results:\")\n",
609
+ "print(poi_df[[\"real_name\", \"likely_country\", \"likely_nationality\", \"likely_profession\"]].head(10))\n",
610
+ "\n",
611
+ "# Check specific case: Irene with korean tag\n",
612
+ "irene_rows = poi_df[poi_df['real_name'].str.contains('irene', case=False, na=False)]\n",
613
+ "if len(irene_rows) > 0:\n",
614
+ " print(f\"\\n🔍 Checking 'Irene' entries:\")\n",
615
+ " for idx, row in irene_rows.iterrows():\n",
616
+ " print(f\" Name: {row['real_name']}\")\n",
617
+ " print(f\" Tags: {row['tags']}\")\n",
618
+ " print(f\" Country: {row['likely_country']}\")\n",
619
+ " print(f\" Nationality: {row['likely_nationality']}\")\n",
620
+ " print()\n"
621
+ ]
622
+ },
623
+ {
624
+ "cell_type": "markdown",
625
+ "id": "29eec5be",
626
+ "metadata": {},
627
+ "source": []
628
+ },
629
+ {
630
+ "cell_type": "markdown",
631
+ "id": "deepseek_header",
632
+ "metadata": {},
633
+ "source": [
634
+ "## Deepseek annotation (Testing)\n",
635
+ "\n",
636
+ "**Use this section first to test if the pipeline works** before using Qwen/Llama/Mistral.\n",
637
+ "\n",
638
+ "This uses your existing Deepseek credits. The code is adapted from your original implementation but updated to:\n",
639
+ "- Use the correct input file from Step 2\n",
640
+ "- Include the profession list from `misc/lists/professions.csv`\n",
641
+ "- Allow up to 3 professions per person\n",
642
+ "- Match the output format of the other LLMs"
643
+ ]
644
+ },
645
+ {
646
+ "cell_type": "code",
647
+ "execution_count": 10,
648
+ "id": "deepseek_code",
649
+ "metadata": {},
650
+ "outputs": [
651
+ {
652
+ "name": "stdout",
653
+ "output_type": "stream",
654
+ "text": [
655
+ "❌ Error: deepseek_api_key.txt not found in misc/credentials/\n",
656
+ " Please create this file with your API key\n"
657
+ ]
658
+ },
659
+ {
660
+ "ename": "FileNotFoundError",
661
+ "evalue": "[Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/misc/credentials/deepseek_api_key.txt'",
662
+ "output_type": "error",
663
+ "traceback": [
664
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
665
+ "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)",
666
+ "Cell \u001b[0;32mIn[10], line 37\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[38;5;66;03m# === LOAD API KEY ===\u001b[39;00m\n\u001b[1;32m 36\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m---> 37\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mapi_key_file\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 38\u001b[0m api_key \u001b[38;5;241m=\u001b[39m f\u001b[38;5;241m.\u001b[39mread()\u001b[38;5;241m.\u001b[39mstrip()\n\u001b[1;32m 39\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m✅ Deepseek API key loaded\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
667
+ "File \u001b[0;32m~/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:324\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 317\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 318\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 320\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 322\u001b[0m )\n\u001b[0;32m--> 324\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
668
+ "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/misc/credentials/deepseek_api_key.txt'"
669
+ ]
670
+ }
671
+ ],
672
+ "source": [
673
+ "import pandas as pd\n",
674
+ "import json\n",
675
+ "import time\n",
676
+ "import requests\n",
677
+ "import re\n",
678
+ "from pathlib import Path\n",
679
+ "from openai import OpenAI\n",
680
+ "from tqdm import tqdm\n",
681
+ "\n",
682
+ "current_dir = Path.cwd()\n",
683
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
684
+ "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
685
+ "professions_mapped_file = current_dir.parent / \"misc/lists/professions_mapped.csv\"\n",
686
+ "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\"\n",
687
+ "\n",
688
+ "# === CONFIGURATION ===\n",
689
+ "TEST_MODE = True # Set to False to process full dataset\n",
690
+ "TEST_SIZE = 10 # Number of rows to test\n",
691
+ "MAX_ROWS = 20000\n",
692
+ "SAVE_INTERVAL = 10 # Save every 10 rows\n",
693
+ "\n",
694
+ "# Define the SPECIFIC profession categories\n",
695
+ "PROFESSION_CATEGORIES = [\n",
696
+ " \"actor\",\n",
697
+ " \"adult performer\",\n",
698
+ " \"singer/musician\",\n",
699
+ " \"model\",\n",
700
+ " \"online personality\",\n",
701
+ " \"public figure\",\n",
702
+ " \"voice actor/ASMR\",\n",
703
+ " \"sports professional\",\n",
704
+ " \"tv personality\"\n",
705
+ "]\n",
706
+ "\n",
707
+ "# === LOAD API KEY ===\n",
708
+ "try:\n",
709
+ " with open(api_key_file) as f:\n",
710
+ " api_key = f.read().strip()\n",
711
+ " print(\"✅ Deepseek API key loaded\")\n",
712
+ "except FileNotFoundError:\n",
713
+ " print(\"❌ Error: deepseek_api_key.txt not found in misc/credentials/\")\n",
714
+ " print(\" Please create this file with your API key\")\n",
715
+ " raise\n",
716
+ "\n",
717
+ "client = OpenAI(\n",
718
+ " api_key=api_key,\n",
719
+ " base_url=\"https://api.deepseek.com/v1\"\n",
720
+ ")\n",
721
+ "\n",
722
+ "# === LOAD DATA ===\n",
723
+ "df = pd.read_csv(input_file)\n",
724
+ "professions_df = pd.read_csv(professions_file)\n",
725
+ "\n",
726
+ "# Load profession categories and keywords\n",
727
+ "try:\n",
728
+ " prof_mapped_df = pd.read_csv(professions_mapped_file)\n",
729
+ " print(f\"✅ Loaded profession mapping with {len(prof_mapped_df)} categories\")\n",
730
+ "except:\n",
731
+ " print(\"⚠️ Warning: professions_mapped.csv not found, using default categories\")\n",
732
+ "\n",
733
+ "profession_str = \", \".join(PROFESSION_CATEGORIES)\n",
734
+ "\n",
735
+ "print(f\"Loaded {len(df)} rows\")\n",
736
+ "print(f\"\\nProfession categories ({len(PROFESSION_CATEGORIES)}):\")\n",
737
+ "for cat in PROFESSION_CATEGORIES:\n",
738
+ " print(f\" - {cat}\")\n",
739
+ "\n",
740
+ "if TEST_MODE:\n",
741
+ " print(f\"\\nRunning in TEST MODE with {TEST_SIZE} samples\")\n",
742
+ " df = df.head(TEST_SIZE).copy()\n",
743
+ "elif MAX_ROWS:\n",
744
+ " df = df.head(MAX_ROWS).copy()\n",
745
+ "\n",
746
+ "# === CREATE PROMPTS ===\n",
747
+ "def create_prompt(row):\n",
748
+ " \"\"\"Create prompt for Deepseek annotation with specific profession categories.\"\"\"\n",
749
+ " name = row['real_name'] if pd.notna(row.get('real_name')) else row.get('name', '')\n",
750
+ " \n",
751
+ " # Gather hints\n",
752
+ " hints = []\n",
753
+ " if pd.notna(row.get('likely_profession')):\n",
754
+ " hints.append(str(row['likely_profession']))\n",
755
+ " if pd.notna(row.get('likely_nationality')):\n",
756
+ " hints.append(str(row['likely_nationality']))\n",
757
+ " if pd.notna(row.get('likely_country')):\n",
758
+ " hints.append(str(row['likely_country']))\n",
759
+ " \n",
760
+ " # Add tags if we don't have enough hints\n",
761
+ " if len(hints) < 3:\n",
762
+ " for i in range(1, 8):\n",
763
+ " tag_col = f'tag_{i}'\n",
764
+ " if tag_col in row and pd.notna(row[tag_col]):\n",
765
+ " tag_val = str(row[tag_col])\n",
766
+ " if tag_val not in hints:\n",
767
+ " hints.append(tag_val)\n",
768
+ " if len(hints) >= 5:\n",
769
+ " break\n",
770
+ " \n",
771
+ " hint_text = \", \".join(hints[:5]) if hints else \"none\"\n",
772
+ " \n",
773
+ " return f\"\"\"Given '{name}' ({hint_text}), provide:\n",
774
+ "1. Full legal name (Western order if non-latin script)\n",
775
+ "2. Any stage names/aliases (comma separated)\n",
776
+ "3. Gender (Male/Female/Other/Unknown)\n",
777
+ "4. Top 3 most likely professions from ONLY these categories:\n",
778
+ " - actor\n",
779
+ " - adult performer\n",
780
+ " - singer/musician\n",
781
+ " - model\n",
782
+ " - online personality (includes streamers, cosplayers, influencers)\n",
783
+ " - public figure (includes politicians, activists, journalists, authors)\n",
784
+ " - voice actor/ASMR\n",
785
+ " - sports professional\n",
786
+ " - tv personality (includes hosts, presenters, reality TV)\n",
787
+ "\n",
788
+ "5. Primary country associated\n",
789
+ "\n",
790
+ "IMPORTANT:\n",
791
+ "- Choose professions ONLY from the 9 categories above\n",
792
+ "- Provide up to 3 professions, comma-separated, ordered by relevance\n",
793
+ "- Be SPECIFIC: choose the most accurate category for each role\n",
794
+ "- \"online personality\" includes: streamers, cosplayers, YouTubers, influencers, content creators\n",
795
+ "- Use 'Unknown' when uncertain or for fictional characters/places\n",
796
+ "- For multi-role people, list all relevant categories (e.g., \"actor, singer/musician, online personality\")\n",
797
+ "\n",
798
+ "Respond with exactly 5 numbered lines.\"\"\"\n",
799
+ "\n",
800
+ "# Create prompts\n",
801
+ "df['prompt'] = df.apply(create_prompt, axis=1)\n",
802
+ "\n",
803
+ "# === QUERY DEEPSEEK ===\n",
804
+ "def query_deepseek(prompt):\n",
805
+ " \"\"\"Query Deepseek API.\"\"\"\n",
806
+ " try:\n",
807
+ " response = client.chat.completions.create(\n",
808
+ " model=\"deepseek-chat\",\n",
809
+ " messages=[\n",
810
+ " {\"role\": \"system\", \"content\": \"You are an assistant that extracts key data on a person based on the name. Respond with exactly 5 numbered lines. For professions, choose ONLY from these categories: actor, adult performer, singer/musician, model, online personality, public figure, voice actor/ASMR, sports professional, tv personality.\"},\n",
811
+ " {\"role\": \"user\", \"content\": prompt}\n",
812
+ " ],\n",
813
+ " temperature=0.05,\n",
814
+ " top_p=0.8,\n",
815
+ " )\n",
816
+ " return response.choices[0].message.content.strip()\n",
817
+ " except Exception as e:\n",
818
+ " print(f\"Error querying Deepseek: {e}\")\n",
819
+ " return None\n",
820
+ "\n",
821
+ "# === PARSE RESPONSE ===\n",
822
+ "def parse_response(response):\n",
823
+ " \"\"\"Parse Deepseek response into structured fields.\"\"\"\n",
824
+ " if not response:\n",
825
+ " return {\n",
826
+ " 'full_name': 'Unknown',\n",
827
+ " 'aliases': 'Unknown',\n",
828
+ " 'gender': 'Unknown',\n",
829
+ " 'profession_llm': 'Unknown',\n",
830
+ " 'country': 'Unknown'\n",
831
+ " }\n",
832
+ " \n",
833
+ " # Split into lines and clean\n",
834
+ " lines = [line.strip() for line in response.split('\\n') if line.strip()]\n",
835
+ " \n",
836
+ " # Initialize with Unknown values\n",
837
+ " fields = {\n",
838
+ " 'full_name': 'Unknown',\n",
839
+ " 'aliases': 'Unknown',\n",
840
+ " 'gender': 'Unknown',\n",
841
+ " 'profession_llm': 'Unknown',\n",
842
+ " 'country': 'Unknown'\n",
843
+ " }\n",
844
+ " \n",
845
+ " # Extract information from each numbered line\n",
846
+ " for line in lines:\n",
847
+ " if line.startswith('1.'):\n",
848
+ " fields['full_name'] = line[2:].strip()\n",
849
+ " elif line.startswith('2.'):\n",
850
+ " fields['aliases'] = line[2:].strip()\n",
851
+ " elif line.startswith('3.'):\n",
852
+ " fields['gender'] = line[2:].strip()\n",
853
+ " elif line.startswith('4.'):\n",
854
+ " fields['profession_llm'] = line[2:].strip()\n",
855
+ " elif line.startswith('5.'):\n",
856
+ " fields['country'] = line[2:].strip()\n",
857
+ " \n",
858
+ " return fields\n",
859
+ "\n",
860
+ "# === PROCESS DATA ===\n",
861
+ "output_file = current_dir.parent / f\"data/CSV/deepseek_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
862
+ "index_file = current_dir.parent / \"misc/deepseek_query_index.txt\"\n",
863
+ "\n",
864
+ "# Check for existing progress\n",
865
+ "current_index = 0\n",
866
+ "if index_file.exists():\n",
867
+ " with open(index_file, 'r') as f:\n",
868
+ " current_index = int(f.read().strip())\n",
869
+ " print(f\"Resuming from index {current_index}\")\n",
870
+ "\n",
871
+ "# Initialize result columns if they don't exist\n",
872
+ "for col in ['full_name', 'gender', 'profession_llm', 'country', 'aliases']:\n",
873
+ " if col not in df.columns:\n",
874
+ " df[col] = 'Unknown'\n",
875
+ "\n",
876
+ "print(f\"\\nAnnotating with DEEPSEEK...\")\n",
877
+ "print(f\"Processing rows {current_index} to {len(df)}\")\n",
878
+ "\n",
879
+ "try:\n",
880
+ " for i in tqdm(range(current_index, len(df)), desc=\"Annotating\"):\n",
881
+ " row = df.iloc[i]\n",
882
+ " \n",
883
+ " # Query Deepseek\n",
884
+ " response = query_deepseek(row['prompt'])\n",
885
+ " parsed_data = parse_response(response)\n",
886
+ " \n",
887
+ " # Update dataframe\n",
888
+ " for key, value in parsed_data.items():\n",
889
+ " df.at[i, key] = value\n",
890
+ " \n",
891
+ " current_index = i + 1\n",
892
+ " \n",
893
+ " # Save progress at intervals\n",
894
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
895
+ " df.to_csv(output_file, index=False)\n",
896
+ " with open(index_file, 'w') as f:\n",
897
+ " f.write(str(current_index))\n",
898
+ " print(f\"✅ Progress saved after {i+1} rows\")\n",
899
+ " \n",
900
+ " time.sleep(1) # Rate limiting\n",
901
+ " \n",
902
+ " print(f\"\\n✅ Done! Final results saved to {output_file}\")\n",
903
+ " \n",
904
+ " # Summary statistics\n",
905
+ " print(\"\\n=== Summary Statistics ===\")\n",
906
+ " print(f\"Total processed: {len(df)}\")\n",
907
+ " print(f\"\\nGender distribution:\")\n",
908
+ " print(df['gender'].value_counts())\n",
909
+ " print(f\"\\nTop 10 profession combinations:\")\n",
910
+ " print(df['profession_llm'].value_counts().head(10))\n",
911
+ " print(f\"\\nTop 10 countries:\")\n",
912
+ " print(df['country'].value_counts().head(10))\n",
913
+ " \n",
914
+ " # Show sample results\n",
915
+ " print(\"\\n=== Sample Results ===\")\n",
916
+ " display_cols = ['real_name', 'full_name', 'gender', 'profession_llm', 'country']\n",
917
+ " print(df[display_cols].head(10).to_string(index=False))\n",
918
+ "\n",
919
+ "except Exception as e:\n",
920
+ " print(f\"⚠️ Error encountered: {e}\")\n",
921
+ " print(f\"⚠️ Last processed index: {current_index}\")\n",
922
+ " \n",
923
+ " # Save progress before exiting\n",
924
+ " df.to_csv(output_file, index=False)\n",
925
+ " with open(index_file, 'w') as f:\n",
926
+ " f.write(str(current_index))\n",
927
+ " \n",
928
+ " print(f\"⚠️ Progress saved up to row {current_index}\")\n"
929
+ ]
930
+ },
931
+ {
932
+ "cell_type": "markdown",
933
+ "id": "qwen_header",
934
+ "metadata": {},
935
+ "source": [
936
+ "## Qwen Annotation (Alibaba Cloud)\n",
937
+ "\n",
938
+ "Uses **Qwen-Max** (Qwen3-Max) via OpenAI-compatible API.\n",
939
+ "\n",
940
+ "**Requirements:**\n",
941
+ "- API Key: `misc/credentials/qwen_api_key.txt`\n",
942
+ "- Sign up: https://dashscope.aliyun.com/\n",
943
+ "- Base URL: `https://dashscope-intl.aliyuncs.com/compatible-mode/v1`\n",
944
+ "- Model: `qwen-max` (uses Qwen3-Max)\n",
945
+ "- Uses OpenAI Python client for compatibility\n",
946
+ "- Cost: Variable (check Alibaba Cloud pricing)\n",
947
+ "\n",
948
+ "**Note:** This matches the official dashboard example code exactly."
949
+ ]
950
+ },
951
+ {
952
+ "cell_type": "code",
953
+ "execution_count": null,
954
+ "id": "qwen_code",
955
+ "metadata": {},
956
+ "outputs": [],
957
+ "source": [
958
+ "import pandas as pd\n",
959
+ "import json\n",
960
+ "import time\n",
961
+ "import re\n",
962
+ "from pathlib import Path\n",
963
+ "from tqdm import tqdm\n",
964
+ "from openai import OpenAI\n",
965
+ "\n",
966
+ "current_dir = Path.cwd()\n",
967
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
968
+ "api_key_file = current_dir.parent / \"misc/credentials/qwen_api_key.txt\"\n",
969
+ "\n",
970
+ "# === CONFIGURATION ===\n",
971
+ "TEST_MODE = True\n",
972
+ "TEST_SIZE = 10\n",
973
+ "MAX_ROWS = 20000\n",
974
+ "SAVE_INTERVAL = 10\n",
975
+ "\n",
976
+ "PROFESSION_CATEGORIES = [\n",
977
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
978
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
979
+ " \"sports professional\", \"tv personality\"\n",
980
+ "]\n",
981
+ "\n",
982
+ "# === LOAD API KEY ===\n",
983
+ "try:\n",
984
+ " with open(api_key_file) as f:\n",
985
+ " api_key = f.read().strip()\n",
986
+ " print(\"✅ Qwen API key loaded\")\n",
987
+ " print(f\" Key starts with: {api_key[:10]}...\")\n",
988
+ "except FileNotFoundError:\n",
989
+ " print(\"❌ Error: qwen_api_key.txt not found\")\n",
990
+ " raise\n",
991
+ "\n",
992
+ "# Initialize OpenAI client for Qwen (matching dashboard example)\n",
993
+ "client = OpenAI(\n",
994
+ " api_key=api_key,\n",
995
+ " base_url=\"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\n",
996
+ ")\n",
997
+ "\n",
998
+ "# === LOAD DATA ===\n",
999
+ "df = pd.read_csv(input_file)\n",
1000
+ "print(f\"Loaded {len(df)} rows\")\n",
1001
+ "\n",
1002
+ "if TEST_MODE:\n",
1003
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
1004
+ " df = df.head(TEST_SIZE).copy()\n",
1005
+ "elif MAX_ROWS:\n",
1006
+ " df = df.head(MAX_ROWS).copy()\n",
1007
+ "\n",
1008
+ "# === CREATE PROMPT ===\n",
1009
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
1010
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
1011
+ " country_str = country if country else \"unknown\"\n",
1012
+ " \n",
1013
+ " return f\"\"\"You are an expert at identifying public figures.\n",
1014
+ "\n",
1015
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
1016
+ "\n",
1017
+ "Provide in JSON format:\n",
1018
+ "{{\n",
1019
+ " \"full_name\": \"<full name>\",\n",
1020
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
1021
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
1022
+ " \"country\": \"<country>\"\n",
1023
+ "}}\n",
1024
+ "\n",
1025
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
1026
+ "- Max 3 professions, ordered by relevance\n",
1027
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
1028
+ "- Return \"Unknown\" if uncertain\n",
1029
+ "\n",
1030
+ "Return ONLY JSON, no extra text.\"\"\"\n",
1031
+ "\n",
1032
+ "# === QUERY QWEN ===\n",
1033
+ "def query_qwen(prompt: str) -> dict:\n",
1034
+ " \"\"\"Query Qwen using OpenAI-compatible API (matching dashboard example).\"\"\"\n",
1035
+ " for attempt in range(3):\n",
1036
+ " try:\n",
1037
+ " completion = client.chat.completions.create(\n",
1038
+ " model=\"qwen-max\", # Note: dashboard shows \"qwen3-max\" but \"qwen-max\" works too\n",
1039
+ " messages=[\n",
1040
+ " {\"role\": \"user\", \"content\": prompt}\n",
1041
+ " ],\n",
1042
+ " temperature=0.1,\n",
1043
+ " max_tokens=500,\n",
1044
+ " stream=False # Disable streaming for easier parsing\n",
1045
+ " )\n",
1046
+ " \n",
1047
+ " content = completion.choices[0].message.content\n",
1048
+ " \n",
1049
+ " # Parse JSON from response\n",
1050
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)\n",
1051
+ " if json_match:\n",
1052
+ " return json.loads(json_match.group())\n",
1053
+ " \n",
1054
+ " return None\n",
1055
+ " \n",
1056
+ " except Exception as e:\n",
1057
+ " print(f\"Qwen error (attempt {attempt+1}): {e}\")\n",
1058
+ " if attempt < 2:\n",
1059
+ " time.sleep(2 ** attempt)\n",
1060
+ " \n",
1061
+ " return None\n",
1062
+ "\n",
1063
+ "# === PROCESS ===\n",
1064
+ "output_file = current_dir.parent / f\"data/CSV/qwen_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
1065
+ "index_file = current_dir.parent / \"misc/qwen_query_index.txt\"\n",
1066
+ "\n",
1067
+ "current_index = 0\n",
1068
+ "if index_file.exists():\n",
1069
+ " with open(index_file) as f:\n",
1070
+ " current_index = int(f.read().strip())\n",
1071
+ "\n",
1072
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
1073
+ " if col not in df.columns:\n",
1074
+ " df[col] = 'Unknown'\n",
1075
+ "\n",
1076
+ "print(f\"\\nAnnotating with QWEN-MAX (OpenAI-compatible API) - rows {current_index} to {len(df)}...\")\n",
1077
+ "\n",
1078
+ "try:\n",
1079
+ " for i in tqdm(range(current_index, len(df)), desc=\"Qwen\"):\n",
1080
+ " row = df.iloc[i]\n",
1081
+ " \n",
1082
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
1083
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
1084
+ " \n",
1085
+ " prompt = create_prompt(\n",
1086
+ " row.get('real_name', row.get('name', '')),\n",
1087
+ " tags,\n",
1088
+ " row.get('likely_country', '')\n",
1089
+ " )\n",
1090
+ " \n",
1091
+ " result = query_qwen(prompt)\n",
1092
+ " \n",
1093
+ " if result:\n",
1094
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
1095
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
1096
+ " profs = result.get('professions', [])\n",
1097
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
1098
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
1099
+ " \n",
1100
+ " current_index = i + 1\n",
1101
+ " \n",
1102
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
1103
+ " df.to_csv(output_file, index=False)\n",
1104
+ " with open(index_file, 'w') as f:\n",
1105
+ " f.write(str(current_index))\n",
1106
+ " print(f\"✅ Saved after {i+1} rows\")\n",
1107
+ " \n",
1108
+ " time.sleep(0.5)\n",
1109
+ " \n",
1110
+ " print(f\"\\n✅ Done! Results: {output_file}\")\n",
1111
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
1112
+ " \n",
1113
+ "except Exception as e:\n",
1114
+ " print(f\"⚠️ Error: {e}\")\n",
1115
+ " df.to_csv(output_file, index=False)\n",
1116
+ " with open(index_file, 'w') as f:\n",
1117
+ " f.write(str(current_index))"
1118
+ ]
1119
+ },
1120
+ {
1121
+ "cell_type": "markdown",
1122
+ "id": "llama_header",
1123
+ "metadata": {},
1124
+ "source": [
1125
+ "## Llama Annotation (Together AI)\n",
1126
+ "\n",
1127
+ "Uses **Llama-3.1-70B-Instruct-Turbo** via Together AI.\n",
1128
+ "\n",
1129
+ "**Requirements:**\n",
1130
+ "- API Key: `misc/credentials/together_api_key.txt`\n",
1131
+ "- Sign up: https://www.together.ai/\n",
1132
+ "- Model: meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\n",
1133
+ "- Cost: ~$0.90 per 1M tokens (~$5-10 for 10k entries)"
1134
+ ]
1135
+ },
1136
+ {
1137
+ "cell_type": "code",
1138
+ "execution_count": null,
1139
+ "id": "llama_code",
1140
+ "metadata": {},
1141
+ "outputs": [],
1142
+ "source": [
1143
+ "import pandas as pd\n",
1144
+ "import json\n",
1145
+ "import time\n",
1146
+ "import requests\n",
1147
+ "import re\n",
1148
+ "from pathlib import Path\n",
1149
+ "from tqdm import tqdm\n",
1150
+ "\n",
1151
+ "current_dir = Path.cwd()\n",
1152
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
1153
+ "api_key_file = current_dir.parent / \"misc/credentials/together_api_key.txt\"\n",
1154
+ "\n",
1155
+ "# === CONFIGURATION ===\n",
1156
+ "TEST_MODE = True\n",
1157
+ "TEST_SIZE = 10\n",
1158
+ "MAX_ROWS = 20000\n",
1159
+ "SAVE_INTERVAL = 10\n",
1160
+ "\n",
1161
+ "PROFESSION_CATEGORIES = [\n",
1162
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
1163
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
1164
+ " \"sports professional\", \"tv personality\"\n",
1165
+ "]\n",
1166
+ "\n",
1167
+ "# === LOAD API KEY ===\n",
1168
+ "try:\n",
1169
+ " with open(api_key_file) as f:\n",
1170
+ " api_key = f.read().strip()\n",
1171
+ " print(\"✅ Llama API key loaded\")\n",
1172
+ "except FileNotFoundError:\n",
1173
+ " print(\"❌ Error: together_api_key.txt not found\")\n",
1174
+ " raise\n",
1175
+ "\n",
1176
+ "# === LOAD DATA ===\n",
1177
+ "df = pd.read_csv(input_file)\n",
1178
+ "print(f\"Loaded {len(df)} rows\")\n",
1179
+ "\n",
1180
+ "if TEST_MODE:\n",
1181
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
1182
+ " df = df.head(TEST_SIZE).copy()\n",
1183
+ "elif MAX_ROWS:\n",
1184
+ " df = df.head(MAX_ROWS).copy()\n",
1185
+ "\n",
1186
+ "# === CREATE PROMPT ===\n",
1187
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
1188
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
1189
+ " country_str = country if country else \"unknown\"\n",
1190
+ " \n",
1191
+ " return f\"\"\"You are an expert at identifying public figures.\n",
1192
+ "\n",
1193
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
1194
+ "\n",
1195
+ "Provide in JSON format:\n",
1196
+ "{{\n",
1197
+ " \"full_name\": \"<full name>\",\n",
1198
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
1199
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
1200
+ " \"country\": \"<country>\"\n",
1201
+ "}}\n",
1202
+ "\n",
1203
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
1204
+ "- Max 3 professions, ordered by relevance\n",
1205
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
1206
+ "- Return \"Unknown\" if uncertain\n",
1207
+ "\n",
1208
+ "Return ONLY JSON, no extra text.\"\"\"\n",
1209
+ "\n",
1210
+ "# === QUERY LLAMA ===\n",
1211
+ "def query_llama(prompt: str) -> dict:\n",
1212
+ " url = \"https://api.together.xyz/v1/chat/completions\"\n",
1213
+ " \n",
1214
+ " headers = {\n",
1215
+ " \"Authorization\": f\"Bearer {api_key}\",\n",
1216
+ " \"Content-Type\": \"application/json\"\n",
1217
+ " }\n",
1218
+ " \n",
1219
+ " data = {\n",
1220
+ " \"model\": \"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo\",\n",
1221
+ " \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n",
1222
+ " \"temperature\": 0.1,\n",
1223
+ " \"max_tokens\": 500\n",
1224
+ " }\n",
1225
+ " \n",
1226
+ " for attempt in range(3):\n",
1227
+ " try:\n",
1228
+ " response = requests.post(url, headers=headers, json=data, timeout=30)\n",
1229
+ " response.raise_for_status()\n",
1230
+ " result = response.json()\n",
1231
+ " \n",
1232
+ " if 'choices' in result:\n",
1233
+ " content = result['choices'][0]['message']['content']\n",
1234
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)\n",
1235
+ " if json_match:\n",
1236
+ " return json.loads(json_match.group())\n",
1237
+ " return None\n",
1238
+ " except Exception as e:\n",
1239
+ " print(f\"Llama error (attempt {attempt+1}): {e}\")\n",
1240
+ " if attempt < 2:\n",
1241
+ " time.sleep(2 ** attempt)\n",
1242
+ " return None\n",
1243
+ "\n",
1244
+ "# === PROCESS ===\n",
1245
+ "output_file = current_dir.parent / f\"data/CSV/llama_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
1246
+ "index_file = current_dir.parent / \"misc/llama_query_index.txt\"\n",
1247
+ "\n",
1248
+ "current_index = 0\n",
1249
+ "if index_file.exists():\n",
1250
+ " with open(index_file) as f:\n",
1251
+ " current_index = int(f.read().strip())\n",
1252
+ "\n",
1253
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
1254
+ " if col not in df.columns:\n",
1255
+ " df[col] = 'Unknown'\n",
1256
+ "\n",
1257
+ "print(f\"\\nAnnotating with LLAMA 3.1 70B - rows {current_index} to {len(df)}...\")\n",
1258
+ "\n",
1259
+ "try:\n",
1260
+ " for i in tqdm(range(current_index, len(df)), desc=\"Llama\"):\n",
1261
+ " row = df.iloc[i]\n",
1262
+ " \n",
1263
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
1264
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
1265
+ " \n",
1266
+ " prompt = create_prompt(\n",
1267
+ " row.get('real_name', row.get('name', '')),\n",
1268
+ " tags,\n",
1269
+ " row.get('likely_country', '')\n",
1270
+ " )\n",
1271
+ " \n",
1272
+ " result = query_llama(prompt)\n",
1273
+ " \n",
1274
+ " if result:\n",
1275
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
1276
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
1277
+ " profs = result.get('professions', [])\n",
1278
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
1279
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
1280
+ " \n",
1281
+ " current_index = i + 1\n",
1282
+ " \n",
1283
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
1284
+ " df.to_csv(output_file, index=False)\n",
1285
+ " with open(index_file, 'w') as f:\n",
1286
+ " f.write(str(current_index))\n",
1287
+ " print(f\"✅ Saved after {i+1} rows\")\n",
1288
+ " \n",
1289
+ " time.sleep(0.5)\n",
1290
+ " \n",
1291
+ " print(f\"\\n✅ Done! Results: {output_file}\")\n",
1292
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
1293
+ " \n",
1294
+ "except Exception as e:\n",
1295
+ " print(f\"⚠️ Error: {e}\")\n",
1296
+ " df.to_csv(output_file, index=False)\n",
1297
+ " with open(index_file, 'w') as f:\n",
1298
+ " f.write(str(current_index))"
1299
+ ]
1300
+ },
1301
+ {
1302
+ "cell_type": "markdown",
1303
+ "id": "mistral_header",
1304
+ "metadata": {},
1305
+ "source": [
1306
+ "## Mixtral Annotation (Mistral AI)\n",
1307
+ "\n",
1308
+ "Uses **Open-Mixtral-8x22B** via Mistral AI API.\n",
1309
+ "\n",
1310
+ "**Requirements:**\n",
1311
+ "- API Key: `misc/credentials/mistral_api_key.txt`\n",
1312
+ "- Sign up: https://mistral.ai/\n",
1313
+ "- Model: open-mixtral-8x22b\n",
1314
+ "- Cost: ~$2 per 1M tokens (~$10-20 for 10k entries)"
1315
+ ]
1316
+ },
1317
+ {
1318
+ "cell_type": "code",
1319
+ "execution_count": null,
1320
+ "id": "mixtral_code",
1321
+ "metadata": {},
1322
+ "outputs": [],
1323
+ "source": [
1324
+ "import pandas as pd\n",
1325
+ "import json\n",
1326
+ "import time\n",
1327
+ "import requests\n",
1328
+ "import re\n",
1329
+ "from pathlib import Path\n",
1330
+ "from tqdm import tqdm\n",
1331
+ "\n",
1332
+ "current_dir = Path.cwd()\n",
1333
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
1334
+ "api_key_file = current_dir.parent / \"misc/credentials/mistral_api_key.txt\"\n",
1335
+ "\n",
1336
+ "# === CONFIGURATION ===\n",
1337
+ "TEST_MODE = True\n",
1338
+ "TEST_SIZE = 10\n",
1339
+ "MAX_ROWS = 20000\n",
1340
+ "SAVE_INTERVAL = 10\n",
1341
+ "\n",
1342
+ "PROFESSION_CATEGORIES = [\n",
1343
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
1344
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
1345
+ " \"sports professional\", \"tv personality\"\n",
1346
+ "]\n",
1347
+ "\n",
1348
+ "# === LOAD API KEY ===\n",
1349
+ "try:\n",
1350
+ " with open(api_key_file) as f:\n",
1351
+ " api_key = f.read().strip()\n",
1352
+ " print(\"✅ Mixtral API key loaded\")\n",
1353
+ "except FileNotFoundError:\n",
1354
+ " print(\"❌ Error: mistral_api_key.txt not found\")\n",
1355
+ " raise\n",
1356
+ "\n",
1357
+ "# === LOAD DATA ===\n",
1358
+ "df = pd.read_csv(input_file)\n",
1359
+ "print(f\"Loaded {len(df)} rows\")\n",
1360
+ "\n",
1361
+ "if TEST_MODE:\n",
1362
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
1363
+ " df = df.head(TEST_SIZE).copy()\n",
1364
+ "elif MAX_ROWS:\n",
1365
+ " df = df.head(MAX_ROWS).copy()\n",
1366
+ "\n",
1367
+ "# === CREATE PROMPT ===\n",
1368
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
1369
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
1370
+ " country_str = country if country else \"unknown\"\n",
1371
+ " \n",
1372
+ " return f\"\"\"You are an expert at identifying public figures.\n",
1373
+ "\n",
1374
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
1375
+ "\n",
1376
+ "Provide in JSON format:\n",
1377
+ "{{\n",
1378
+ " \"full_name\": \"<full name>\",\n",
1379
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
1380
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
1381
+ " \"country\": \"<country>\"\n",
1382
+ "}}\n",
1383
+ "\n",
1384
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
1385
+ "- Max 3 professions, ordered by relevance\n",
1386
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
1387
+ "- Return \"Unknown\" if uncertain\n",
1388
+ "\n",
1389
+ "Return ONLY JSON, no extra text.\"\"\"\n",
1390
+ "\n",
1391
+ "# === QUERY MIXTRAL ===\n",
1392
+ "def query_mixtral(prompt: str) -> dict:\n",
1393
+ " url = \"https://api.mistral.ai/v1/chat/completions\"\n",
1394
+ " \n",
1395
+ " headers = {\n",
1396
+ " \"Authorization\": f\"Bearer {api_key}\",\n",
1397
+ " \"Content-Type\": \"application/json\"\n",
1398
+ " }\n",
1399
+ " \n",
1400
+ " data = {\n",
1401
+ " \"model\": \"open-mixtral-8x22b\",\n",
1402
+ " \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n",
1403
+ " \"temperature\": 0.1,\n",
1404
+ " \"max_tokens\": 500\n",
1405
+ " }\n",
1406
+ " \n",
1407
+ " for attempt in range(3):\n",
1408
+ " try:\n",
1409
+ " response = requests.post(url, headers=headers, json=data, timeout=30)\n",
1410
+ " response.raise_for_status()\n",
1411
+ " result = response.json()\n",
1412
+ " \n",
1413
+ " if 'choices' in result:\n",
1414
+ " content = result['choices'][0]['message']['content']\n",
1415
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)\n",
1416
+ " if json_match:\n",
1417
+ " return json.loads(json_match.group())\n",
1418
+ " return None\n",
1419
+ " except Exception as e:\n",
1420
+ " print(f\"Mixtral error (attempt {attempt+1}): {e}\")\n",
1421
+ " if attempt < 2:\n",
1422
+ " time.sleep(2 ** attempt)\n",
1423
+ " return None\n",
1424
+ "\n",
1425
+ "# === PROCESS ===\n",
1426
+ "output_file = current_dir.parent / f\"data/CSV/mixtral_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
1427
+ "index_file = current_dir.parent / \"misc/mixtral_query_index.txt\"\n",
1428
+ "\n",
1429
+ "current_index = 0\n",
1430
+ "if index_file.exists():\n",
1431
+ " with open(index_file) as f:\n",
1432
+ " current_index = int(f.read().strip())\n",
1433
+ "\n",
1434
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
1435
+ " if col not in df.columns:\n",
1436
+ " df[col] = 'Unknown'\n",
1437
+ "\n",
1438
+ "print(f\"\\nAnnotating with MIXTRAL 8x22B - rows {current_index} to {len(df)}...\")\n",
1439
+ "\n",
1440
+ "try:\n",
1441
+ " for i in tqdm(range(current_index, len(df)), desc=\"Mixtral\"):\n",
1442
+ " row = df.iloc[i]\n",
1443
+ " \n",
1444
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
1445
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
1446
+ " \n",
1447
+ " prompt = create_prompt(\n",
1448
+ " row.get('real_name', row.get('name', '')),\n",
1449
+ " tags,\n",
1450
+ " row.get('likely_country', '')\n",
1451
+ " )\n",
1452
+ " \n",
1453
+ " result = query_mixtral(prompt)\n",
1454
+ " \n",
1455
+ " if result:\n",
1456
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
1457
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
1458
+ " profs = result.get('professions', [])\n",
1459
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
1460
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
1461
+ " \n",
1462
+ " current_index = i + 1\n",
1463
+ " \n",
1464
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
1465
+ " df.to_csv(output_file, index=False)\n",
1466
+ " with open(index_file, 'w') as f:\n",
1467
+ " f.write(str(current_index))\n",
1468
+ " print(f\"✅ Saved after {i+1} rows\")\n",
1469
+ " \n",
1470
+ " time.sleep(0.5)\n",
1471
+ " \n",
1472
+ " print(f\"\\n✅ Done! Results: {output_file}\")\n",
1473
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
1474
+ " \n",
1475
+ "except Exception as e:\n",
1476
+ " print(f\"⚠️ Error: {e}\")\n",
1477
+ " df.to_csv(output_file, index=False)\n",
1478
+ " with open(index_file, 'w') as f:\n",
1479
+ " f.write(str(current_index))"
1480
+ ]
1481
+ },
1482
+ {
1483
+ "cell_type": "markdown",
1484
+ "id": "gemma_header",
1485
+ "metadata": {},
1486
+ "source": [
1487
+ "## Gemma Annotation (Google - American Open Source)\n",
1488
+ "\n",
1489
+ "Uses **Gemma 2 27B** via Together AI - Google's open-source model.\n",
1490
+ "\n",
1491
+ "**Requirements:**\n",
1492
+ "- API Key: `misc/credentials/together_api_key.txt` (same as Llama)\n",
1493
+ "- Sign up: https://www.together.ai/\n",
1494
+ "- Model: google/gemma-2-27b-it\n",
1495
+ "- Cost: ~$0.80 per 1M tokens (~$4-8 for 10k entries)\n",
1496
+ "- Note: American company (Google), fully open-source model"
1497
+ ]
1498
+ },
1499
+ {
1500
+ "cell_type": "code",
1501
+ "execution_count": null,
1502
+ "id": "gemma_code",
1503
+ "metadata": {},
1504
+ "outputs": [],
1505
+ "source": [
1506
+ "import pandas as pd\n",
1507
+ "import json\n",
1508
+ "import time\n",
1509
+ "import requests\n",
1510
+ "import re\n",
1511
+ "from pathlib import Path\n",
1512
+ "from tqdm import tqdm\n",
1513
+ "\n",
1514
+ "current_dir = Path.cwd()\n",
1515
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
1516
+ "api_key_file = current_dir.parent / \"misc/credentials/together_api_key.txt\"\n",
1517
+ "\n",
1518
+ "# === CONFIGURATION ===\n",
1519
+ "TEST_MODE = True\n",
1520
+ "TEST_SIZE = 10\n",
1521
+ "MAX_ROWS = 20000\n",
1522
+ "SAVE_INTERVAL = 10\n",
1523
+ "\n",
1524
+ "PROFESSION_CATEGORIES = [\n",
1525
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
1526
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
1527
+ " \"sports professional\", \"tv personality\"\n",
1528
+ "]\n",
1529
+ "\n",
1530
+ "# === LOAD API KEY ===\n",
1531
+ "try:\n",
1532
+ " with open(api_key_file) as f:\n",
1533
+ " api_key = f.read().strip()\n",
1534
+ " print(\"✅ Gemma API key loaded\")\n",
1535
+ "except FileNotFoundError:\n",
1536
+ " print(\"❌ Error: together_api_key.txt not found\")\n",
1537
+ " raise\n",
1538
+ "\n",
1539
+ "# === LOAD DATA ===\n",
1540
+ "df = pd.read_csv(input_file)\n",
1541
+ "print(f\"Loaded {len(df)} rows\")\n",
1542
+ "\n",
1543
+ "if TEST_MODE:\n",
1544
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
1545
+ " df = df.head(TEST_SIZE).copy()\n",
1546
+ "elif MAX_ROWS:\n",
1547
+ " df = df.head(MAX_ROWS).copy()\n",
1548
+ "\n",
1549
+ "# === CREATE PROMPT ===\n",
1550
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
1551
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
1552
+ " country_str = country if country else \"unknown\"\n",
1553
+ " \n",
1554
+ " return f\"\"\"You are an expert at identifying public figures.\n",
1555
+ "\n",
1556
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
1557
+ "\n",
1558
+ "Provide in JSON format:\n",
1559
+ "{{\n",
1560
+ " \"full_name\": \"<full name>\",\n",
1561
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
1562
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
1563
+ " \"country\": \"<country>\"\n",
1564
+ "}}\n",
1565
+ "\n",
1566
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
1567
+ "- Max 3 professions, ordered by relevance\n",
1568
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
1569
+ "- Return \"Unknown\" if uncertain\n",
1570
+ "\n",
1571
+ "Return ONLY JSON, no extra text.\"\"\"\n",
1572
+ "\n",
1573
+ "# === QUERY GEMMA ===\n",
1574
+ "def query_gemma(prompt: str) -> dict:\n",
1575
+ " url = \"https://api.together.xyz/v1/chat/completions\"\n",
1576
+ " \n",
1577
+ " headers = {\n",
1578
+ " \"Authorization\": f\"Bearer {api_key}\",\n",
1579
+ " \"Content-Type\": \"application/json\"\n",
1580
+ " }\n",
1581
+ " \n",
1582
+ " data = {\n",
1583
+ " \"model\": \"google/gemma-2-27b-it\",\n",
1584
+ " \"messages\": [{\"role\": \"user\", \"content\": prompt}],\n",
1585
+ " \"temperature\": 0.1,\n",
1586
+ " \"max_tokens\": 500\n",
1587
+ " }\n",
1588
+ " \n",
1589
+ " for attempt in range(3):\n",
1590
+ " try:\n",
1591
+ " response = requests.post(url, headers=headers, json=data, timeout=30)\n",
1592
+ " response.raise_for_status()\n",
1593
+ " result = response.json()\n",
1594
+ " \n",
1595
+ " if 'choices' in result:\n",
1596
+ " content = result['choices'][0]['message']['content']\n",
1597
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)\n",
1598
+ " if json_match:\n",
1599
+ " return json.loads(json_match.group())\n",
1600
+ " return None\n",
1601
+ " except Exception as e:\n",
1602
+ " print(f\"Gemma error (attempt {attempt+1}): {e}\")\n",
1603
+ " if attempt < 2:\n",
1604
+ " time.sleep(2 ** attempt)\n",
1605
+ " return None\n",
1606
+ "\n",
1607
+ "# === PROCESS ===\n",
1608
+ "output_file = current_dir.parent / f\"data/CSV/gemma_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
1609
+ "index_file = current_dir.parent / \"misc/gemma_query_index.txt\"\n",
1610
+ "\n",
1611
+ "current_index = 0\n",
1612
+ "if index_file.exists():\n",
1613
+ " with open(index_file) as f:\n",
1614
+ " current_index = int(f.read().strip())\n",
1615
+ "\n",
1616
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
1617
+ " if col not in df.columns:\n",
1618
+ " df[col] = 'Unknown'\n",
1619
+ "\n",
1620
+ "print(f\"\\nAnnotating with GEMMA 2 27B (Google) - rows {current_index} to {len(df)}...\")\n",
1621
+ "\n",
1622
+ "try:\n",
1623
+ " for i in tqdm(range(current_index, len(df)), desc=\"Gemma\"):\n",
1624
+ " row = df.iloc[i]\n",
1625
+ " \n",
1626
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
1627
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
1628
+ " \n",
1629
+ " prompt = create_prompt(\n",
1630
+ " row.get('real_name', row.get('name', '')),\n",
1631
+ " tags,\n",
1632
+ " row.get('likely_country', '')\n",
1633
+ " )\n",
1634
+ " \n",
1635
+ " result = query_gemma(prompt)\n",
1636
+ " \n",
1637
+ " if result:\n",
1638
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
1639
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
1640
+ " profs = result.get('professions', [])\n",
1641
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
1642
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
1643
+ " \n",
1644
+ " current_index = i + 1\n",
1645
+ " \n",
1646
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
1647
+ " df.to_csv(output_file, index=False)\n",
1648
+ " with open(index_file, 'w') as f:\n",
1649
+ " f.write(str(current_index))\n",
1650
+ " print(f\"✅ Saved after {i+1} rows\")\n",
1651
+ " \n",
1652
+ " time.sleep(0.5)\n",
1653
+ " \n",
1654
+ " print(f\"\\n✅ Done! Results: {output_file}\")\n",
1655
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
1656
+ " \n",
1657
+ "except Exception as e:\n",
1658
+ " print(f\"⚠️ Error: {e}\")\n",
1659
+ " df.to_csv(output_file, index=False)\n",
1660
+ " with open(index_file, 'w') as f:\n",
1661
+ " f.write(str(current_index))"
1662
+ ]
1663
+ },
1664
+ {
1665
+ "cell_type": "markdown",
1666
+ "id": "01086b35",
1667
+ "metadata": {},
1668
+ "source": [
1669
+ "## Qwen-2.5-32B Local Annotation (Ollama)\n",
1670
+ "\n",
1671
+ "Run **Qwen-2.5-32B-Instruct** locally using Ollama - NO API COSTS!\n",
1672
+ "\n",
1673
+ "**Requirements:**\n",
1674
+ "- Ollama installed: https://ollama.com/\n",
1675
+ "- GPU: NVIDIA A100 80GB (recommended)\n",
1676
+ "- RAM: 265GB system RAM\n",
1677
+ "- Model cache: `data/models/` (auto-downloaded on first run)\n",
1678
+ "- Model size: ~20GB download\n",
1679
+ "- VRAM usage: ~22-25GB during inference\n",
1680
+ "\n",
1681
+ "**Setup:**\n",
1682
+ "```bash\n",
1683
+ "# 1. Install Ollama (if not installed)\n",
1684
+ "curl -fsSL https://ollama.com/install.sh | sh\n",
1685
+ "\n",
1686
+ "# 2. Pull Qwen-2.5-32B-Instruct\n",
1687
+ "ollama pull qwen2.5:32b-instruct\n",
1688
+ "\n",
1689
+ "# 3. Verify model is available\n",
1690
+ "ollama list\n",
1691
+ "```\n",
1692
+ "\n",
1693
+ "**Advantages:**\n",
1694
+ "- ✅ FREE - No API costs\n",
1695
+ "- ✅ Fast - Local inference on A100\n",
1696
+ "- ✅ Privacy - Data never leaves your machine\n",
1697
+ "- ✅ Offline - Works without internet\n",
1698
+ "- ✅ High quality - 32B parameter model\n",
1699
+ "\n",
1700
+ "**Performance:**\n",
1701
+ "- Speed: ~5-10 tokens/sec on A100\n",
1702
+ "- Throughput: ~100-200 samples/hour (depends on prompt length)\n",
1703
+ "- Cost: $0 (after model download)"
1704
+ ]
1705
+ },
1706
+ {
1707
+ "cell_type": "code",
1708
+ "execution_count": null,
1709
+ "id": "5dd7a803",
1710
+ "metadata": {},
1711
+ "outputs": [],
1712
+ "source": [
1713
+ "import pandas as pd\n",
1714
+ "import json\n",
1715
+ "import time\n",
1716
+ "import re\n",
1717
+ "import subprocess\n",
1718
+ "from pathlib import Path\n",
1719
+ "from tqdm import tqdm\n",
1720
+ "\n",
1721
+ "current_dir = Path.cwd()\n",
1722
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
1723
+ "\n",
1724
+ "# === CONFIGURATION ===\n",
1725
+ "TEST_MODE = True\n",
1726
+ "TEST_SIZE = 10\n",
1727
+ "MAX_ROWS = 20000\n",
1728
+ "SAVE_INTERVAL = 10\n",
1729
+ "\n",
1730
+ "# Ollama settings\n",
1731
+ "MODEL_NAME = \"qwen2.5:32b-instruct\" # or \"qwen2.5:32b\" for base model\n",
1732
+ "OLLAMA_HOST = \"http://localhost:11434\" # Default Ollama port\n",
1733
+ "\n",
1734
+ "PROFESSION_CATEGORIES = [\n",
1735
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
1736
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
1737
+ " \"sports professional\", \"tv personality\"\n",
1738
+ "]\n",
1739
+ "\n",
1740
+ "# === CHECK OLLAMA ===\n",
1741
+ "def check_ollama():\n",
1742
+ " \"\"\"Check if Ollama is installed and model is available.\"\"\"\n",
1743
+ " try:\n",
1744
+ " result = subprocess.run(\n",
1745
+ " [\"ollama\", \"list\"],\n",
1746
+ " capture_output=True,\n",
1747
+ " text=True,\n",
1748
+ " timeout=5\n",
1749
+ " )\n",
1750
+ " \n",
1751
+ " if result.returncode == 0:\n",
1752
+ " print(\"✅ Ollama is installed\")\n",
1753
+ " \n",
1754
+ " # Check if model is available\n",
1755
+ " if MODEL_NAME in result.stdout or \"qwen2.5\" in result.stdout:\n",
1756
+ " print(f\"✅ Model {MODEL_NAME} is available\")\n",
1757
+ " return True\n",
1758
+ " else:\n",
1759
+ " print(f\"⚠️ Model {MODEL_NAME} not found\")\n",
1760
+ " print(f\" Run: ollama pull {MODEL_NAME}\")\n",
1761
+ " return False\n",
1762
+ " else:\n",
1763
+ " print(\"❌ Ollama command failed\")\n",
1764
+ " return False\n",
1765
+ " except FileNotFoundError:\n",
1766
+ " print(\"❌ Ollama not installed\")\n",
1767
+ " print(\" Install: curl -fsSL https://ollama.com/install.sh | sh\")\n",
1768
+ " return False\n",
1769
+ " except Exception as e:\n",
1770
+ " print(f\"❌ Error checking Ollama: {e}\")\n",
1771
+ " return False\n",
1772
+ "\n",
1773
+ "# Check Ollama before proceeding\n",
1774
+ "if not check_ollama():\n",
1775
+ " raise RuntimeError(\"Ollama not ready. Please install Ollama and pull the model.\")\n",
1776
+ "\n",
1777
+ "# === LOAD DATA ===\n",
1778
+ "df = pd.read_csv(input_file)\n",
1779
+ "print(f\"\\nLoaded {len(df)} rows\")\n",
1780
+ "\n",
1781
+ "if TEST_MODE:\n",
1782
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
1783
+ " df = df.head(TEST_SIZE).copy()\n",
1784
+ "elif MAX_ROWS:\n",
1785
+ " df = df.head(MAX_ROWS).copy()\n",
1786
+ "\n",
1787
+ "# === CREATE PROMPT ===\n",
1788
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
1789
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
1790
+ " country_str = country if country else \"unknown\"\n",
1791
+ " \n",
1792
+ " return f\"\"\"You are an expert at identifying public figures.\n",
1793
+ "\n",
1794
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
1795
+ "\n",
1796
+ "Provide in JSON format:\n",
1797
+ "{{\n",
1798
+ " \"full_name\": \"<full name>\",\n",
1799
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
1800
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
1801
+ " \"country\": \"<country>\"\n",
1802
+ "}}\n",
1803
+ "\n",
1804
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
1805
+ "- Max 3 professions, ordered by relevance\n",
1806
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
1807
+ "- Return \"Unknown\" if uncertain\n",
1808
+ "\n",
1809
+ "Return ONLY JSON, no extra text.\"\"\"\n",
1810
+ "\n",
1811
+ "# === QUERY QWEN LOCAL ===\n",
1812
+ "def query_qwen_local(prompt: str) -> dict:\n",
1813
+ " \"\"\"Query Qwen-2.5-32B via Ollama.\"\"\"\n",
1814
+ " for attempt in range(3):\n",
1815
+ " try:\n",
1816
+ " result = subprocess.run(\n",
1817
+ " [\n",
1818
+ " \"ollama\", \"run\", MODEL_NAME,\n",
1819
+ " prompt\n",
1820
+ " ],\n",
1821
+ " capture_output=True,\n",
1822
+ " text=True,\n",
1823
+ " timeout=60 # 60 second timeout per query\n",
1824
+ " )\n",
1825
+ " \n",
1826
+ " if result.returncode == 0:\n",
1827
+ " content = result.stdout.strip()\n",
1828
+ " \n",
1829
+ " # Parse JSON from response\n",
1830
+ " json_match = re.search(r'\\{[^}]+\\}', content, re.DOTALL)\n",
1831
+ " if json_match:\n",
1832
+ " return json.loads(json_match.group())\n",
1833
+ " \n",
1834
+ " # If no JSON found, try to extract from markdown code block\n",
1835
+ " code_block = re.search(r'```(?:json)?\\s*({[^}]+})\\s*```', content, re.DOTALL)\n",
1836
+ " if code_block:\n",
1837
+ " return json.loads(code_block.group(1))\n",
1838
+ " \n",
1839
+ " return None\n",
1840
+ " \n",
1841
+ " except subprocess.TimeoutExpired:\n",
1842
+ " print(f\"Qwen local timeout (attempt {attempt+1})\")\n",
1843
+ " if attempt < 2:\n",
1844
+ " time.sleep(2)\n",
1845
+ " except json.JSONDecodeError as e:\n",
1846
+ " print(f\"Qwen local JSON error (attempt {attempt+1}): {e}\")\n",
1847
+ " if attempt < 2:\n",
1848
+ " time.sleep(1)\n",
1849
+ " except Exception as e:\n",
1850
+ " print(f\"Qwen local error (attempt {attempt+1}): {e}\")\n",
1851
+ " if attempt < 2:\n",
1852
+ " time.sleep(2)\n",
1853
+ " \n",
1854
+ " return None\n",
1855
+ "\n",
1856
+ "# === PROCESS ===\n",
1857
+ "output_file = current_dir.parent / f\"data/CSV/qwen_local_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
1858
+ "index_file = current_dir.parent / \"misc/qwen_local_query_index.txt\"\n",
1859
+ "\n",
1860
+ "current_index = 0\n",
1861
+ "if index_file.exists():\n",
1862
+ " with open(index_file) as f:\n",
1863
+ " current_index = int(f.read().strip())\n",
1864
+ "\n",
1865
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
1866
+ " if col not in df.columns:\n",
1867
+ " df[col] = 'Unknown'\n",
1868
+ "\n",
1869
+ "print(f\"\\nAnnotating with QWEN-2.5-32B LOCAL - rows {current_index} to {len(df)}...\")\n",
1870
+ "print(f\"Model: {MODEL_NAME}\")\n",
1871
+ "print(f\"This may take a while (~100-200 samples/hour on A100)\\n\")\n",
1872
+ "\n",
1873
+ "try:\n",
1874
+ " start_time = time.time()\n",
1875
+ " \n",
1876
+ " for i in tqdm(range(current_index, len(df)), desc=\"Qwen Local\"):\n",
1877
+ " row = df.iloc[i]\n",
1878
+ " \n",
1879
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
1880
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
1881
+ " \n",
1882
+ " prompt = create_prompt(\n",
1883
+ " row.get('real_name', row.get('name', '')),\n",
1884
+ " tags,\n",
1885
+ " row.get('likely_country', '')\n",
1886
+ " )\n",
1887
+ " \n",
1888
+ " result = query_qwen_local(prompt)\n",
1889
+ " \n",
1890
+ " if result:\n",
1891
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
1892
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
1893
+ " profs = result.get('professions', [])\n",
1894
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
1895
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
1896
+ " \n",
1897
+ " current_index = i + 1\n",
1898
+ " \n",
1899
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
1900
+ " df.to_csv(output_file, index=False)\n",
1901
+ " with open(index_file, 'w') as f:\n",
1902
+ " f.write(str(current_index))\n",
1903
+ " \n",
1904
+ " # Calculate stats\n",
1905
+ " elapsed = time.time() - start_time\n",
1906
+ " samples_done = i + 1 - (current_index - (i + 1))\n",
1907
+ " samples_per_hour = (samples_done / elapsed) * 3600 if elapsed > 0 else 0\n",
1908
+ " \n",
1909
+ " print(f\"✅ Saved after {i+1} rows (~{samples_per_hour:.1f} samples/hour)\")\n",
1910
+ " \n",
1911
+ " elapsed_total = time.time() - start_time\n",
1912
+ " print(f\"\\n✅ Done! Results: {output_file}\")\n",
1913
+ " print(f\"Total time: {elapsed_total/60:.1f} minutes\")\n",
1914
+ " print(f\"Average speed: {len(df)/(elapsed_total/3600):.1f} samples/hour\")\n",
1915
+ " \n",
1916
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
1917
+ "\n",
1918
+ "except KeyboardInterrupt:\n",
1919
+ " print(f\"\\n⚠️ Interrupted by user\")\n",
1920
+ " df.to_csv(output_file, index=False)\n",
1921
+ " with open(index_file, 'w') as f:\n",
1922
+ " f.write(str(current_index))\n",
1923
+ " print(f\"Progress saved up to row {current_index}\")\n",
1924
+ "\n",
1925
+ "except Exception as e:\n",
1926
+ " print(f\"⚠️ Error: {e}\")\n",
1927
+ " df.to_csv(output_file, index=False)\n",
1928
+ " with open(index_file, 'w') as f:\n",
1929
+ " f.write(str(current_index))\n",
1930
+ " print(f\"Progress saved up to row {current_index}\")"
1931
+ ]
1932
+ }
1933
+ ],
1934
+ "metadata": {
1935
+ "language_info": {
1936
+ "name": "python"
1937
+ }
1938
+ },
1939
+ "nbformat": 4,
1940
+ "nbformat_minor": 5
1941
+ }
jupyter_notebooks/.ipynb_checkpoints/Section_2-3-4__Figure_8_Deepfake_victims-checkpoint.ipynb ADDED
@@ -0,0 +1,668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "06763fde",
6
+ "metadata": {},
7
+ "source": [
8
+ "# LLM annotation of Deepfake adapters"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "markdown",
13
+ "id": "b773045a",
14
+ "metadata": {},
15
+ "source": [
16
+ "## Step 01 Data cleaning NER using spaCy"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "markdown",
21
+ "id": "234a55e5",
22
+ "metadata": {},
23
+ "source": [
24
+ "#### Here we clean leetspeak and architectre specifiers from the names as a preprossesing step for the named entity recognition NER below"
25
+ ]
26
+ },
27
+ {
28
+ "cell_type": "code",
29
+ "execution_count": 1,
30
+ "id": "f177df11",
31
+ "metadata": {},
32
+ "outputs": [],
33
+ "source": [
34
+ "import pandas as pd\n",
35
+ "import spacy\n",
36
+ "import re\n",
37
+ "import torch\n",
38
+ "from pathlib import Path\n",
39
+ "import unicodedata\n"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": 2,
45
+ "id": "a2383f32",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "current_dir = Path.cwd()\n",
50
+ "poi_models_dir = current_dir.parent / \"data/CSV/model_subsets/POI_models.csv\" ### POI models dataset\n",
51
+ "output = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_01.csv\" ### Output file"
52
+ ]
53
+ },
54
+ {
55
+ "cell_type": "code",
56
+ "execution_count": 3,
57
+ "id": "66fc691f",
58
+ "metadata": {},
59
+ "outputs": [
60
+ {
61
+ "name": "stderr",
62
+ "output_type": "stream",
63
+ "text": [
64
+ "/home/lauhp/anaconda3/envs/latm/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
65
+ " from .autonotebook import tqdm as notebook_tqdm\n"
66
+ ]
67
+ }
68
+ ],
69
+ "source": [
70
+ "nlp = spacy.load(\"en_core_web_sm\") # or another model of your choice"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": 4,
76
+ "id": "8348c4a4",
77
+ "metadata": {},
78
+ "outputs": [
79
+ {
80
+ "name": "stdout",
81
+ "output_type": "stream",
82
+ "text": [
83
+ "Done! Saved to NER_poi_step_01.csv\n"
84
+ ]
85
+ }
86
+ ],
87
+ "source": [
88
+ "def preprocess_name(name):\n",
89
+ " name = str(name)\n",
90
+ "\n",
91
+ " # Normalize unicode characters (e.g., fancy fonts)\n",
92
+ " name = unicodedata.normalize(\"NFKD\", name)\n",
93
+ "\n",
94
+ " # Lowercase everything\n",
95
+ " name = name.lower()\n",
96
+ "\n",
97
+ " # Remove special keywords and patterns\n",
98
+ " junk_words = [\n",
99
+ " 'jav', 'jp', 'lora', 'locon', 'lycoris', 'requested', 'japanese', 'model',\n",
100
+ " 'flux', 'flux1.d', 'pony', 'realistic'\n",
101
+ " ]\n",
102
+ " for word in junk_words:\n",
103
+ " name = re.sub(rf'\\b{re.escape(word)}\\b', '', name, flags=re.IGNORECASE)\n",
104
+ "\n",
105
+ " # Remove versions like v1, v2.0, etc.\n",
106
+ " name = re.sub(r'v\\.?\\d+(\\.\\d+)?', '', name)\n",
107
+ "\n",
108
+ " # Remove 'not' followed by a word\n",
109
+ " name = re.sub(r'\\bnot\\s+\\w+', '', name)\n",
110
+ "\n",
111
+ " # Replace underscores and pipes with spaces\n",
112
+ " name = re.sub(r'[_|]', ' ', name)\n",
113
+ "\n",
114
+ " # Remove parentheses and content within\n",
115
+ " name = re.sub(r'\\(.*?\\)', '', name)\n",
116
+ "\n",
117
+ " # Remove excess whitespace\n",
118
+ " name = re.sub(r'\\s+', ' ', name).strip()\n",
119
+ "\n",
120
+ " return name\n",
121
+ "\n",
122
+ "# -------------------------\n",
123
+ "# Fallback Extractor\n",
124
+ "# -------------------------\n",
125
+ "def fallback_extract(text):\n",
126
+ " words = text.split()\n",
127
+ " capitalized = [w for w in words if w and w[0].isalpha()]\n",
128
+ " if len(capitalized) >= 2:\n",
129
+ " return \" \".join(capitalized[:2])\n",
130
+ " elif capitalized:\n",
131
+ " return capitalized[0]\n",
132
+ " return None\n",
133
+ "\n",
134
+ "# -------------------------\n",
135
+ "# Full Extraction Logic\n",
136
+ "# -------------------------\n",
137
+ "def extract_real_name(raw_name):\n",
138
+ " cleaned = preprocess_name(raw_name)\n",
139
+ " doc = nlp(cleaned)\n",
140
+ " persons = [ent.text for ent in doc.ents if ent.label_ == \"PERSON\"]\n",
141
+ " if persons:\n",
142
+ " return persons[0]\n",
143
+ " return fallback_extract(cleaned)\n",
144
+ "\n",
145
+ "# -------------------------\n",
146
+ "# Load Data and Apply\n",
147
+ "# -------------------------\n",
148
+ "df = pd.read_csv(poi_models_dir) # Or your own path\n",
149
+ "\n",
150
+ "# Apply the full extractor\n",
151
+ "texts = df['name'].astype(str).tolist()\n",
152
+ "docs = list(nlp.pipe([preprocess_name(t) for t in texts], batch_size=32))\n",
153
+ "\n",
154
+ "def extract_from_doc(doc, raw_text):\n",
155
+ " persons = [ent.text for ent in doc.ents if ent.label_ == \"PERSON\"]\n",
156
+ " if persons:\n",
157
+ " return persons[0]\n",
158
+ " return fallback_extract(preprocess_name(raw_text))\n",
159
+ "\n",
160
+ "df['real_name'] = [extract_from_doc(doc, raw_text) for doc, raw_text in zip(docs, texts)]\n",
161
+ "\n",
162
+ "# Save the result\n",
163
+ "df.to_csv(output, index=False)\n",
164
+ "print(\"Done! Saved to NER_poi_step_01.csv\")\n"
165
+ ]
166
+ },
167
+ {
168
+ "cell_type": "markdown",
169
+ "id": "a40b17fe",
170
+ "metadata": {},
171
+ "source": [
172
+ "## Step 2: compare country and profession with lists"
173
+ ]
174
+ },
175
+ {
176
+ "cell_type": "code",
177
+ "execution_count": 5,
178
+ "id": "414954ed",
179
+ "metadata": {},
180
+ "outputs": [
181
+ {
182
+ "name": "stdout",
183
+ "output_type": "stream",
184
+ "text": [
185
+ " real_name likely_country likely_nationality likely_profession\n",
186
+ "0 ronnie alonte model\n",
187
+ "1 zh elena kamperi twitch streamer\n",
188
+ "2 sofia vergara model\n",
189
+ "3 安然 anran celebrity\n",
190
+ "4 zoe kravitz celebrity\n"
191
+ ]
192
+ }
193
+ ],
194
+ "source": [
195
+ "import pandas as pd\n",
196
+ "from pathlib import Path\n",
197
+ "\n",
198
+ "# Set up paths\n",
199
+ "current_dir = Path.cwd()\n",
200
+ "countries = current_dir.parent / \"misc/lists/countries.csv\"\n",
201
+ "professions = current_dir.parent / \"misc/lists/professions.csv\"\n",
202
+ "inputNER = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_01.csv\"\n",
203
+ "outfile = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_02.csv\"\n",
204
+ "\n",
205
+ "# Load datasets\n",
206
+ "poi_df = pd.read_csv(inputNER)\n",
207
+ "countries_df = pd.read_csv(countries)\n",
208
+ "professions_df = pd.read_csv(professions)\n",
209
+ "\n",
210
+ "# Step 1: Combine tags into one lowercase list\n",
211
+ "def combine_tags(row):\n",
212
+ " return [str(row[f\"tag_{i}\"]).strip().lower() for i in range(1, 8) if pd.notna(row.get(f\"tag_{i}\"))]\n",
213
+ "\n",
214
+ "poi_df[\"tags\"] = poi_df.apply(combine_tags, axis=1)\n",
215
+ "\n",
216
+ "# Step 2: Build tag → (country, nationality) mapping\n",
217
+ "tag_to_country_nationality = {}\n",
218
+ "\n",
219
+ "for _, row in countries_df.iterrows():\n",
220
+ " country = str(row[\"en_short_name\"]).strip()\n",
221
+ " nationality = str(row[\"nationality\"]).strip()\n",
222
+ "\n",
223
+ " country_lc = country.lower()\n",
224
+ " nationality_lc = nationality.lower()\n",
225
+ "\n",
226
+ " # Add variations to the mapping\n",
227
+ " tag_to_country_nationality[country_lc] = (country, \"\")\n",
228
+ " tag_to_country_nationality[nationality_lc] = (\"\", nationality)\n",
229
+ " tag_to_country_nationality[country_lc.replace(\" \", \"\")] = (country, \"\")\n",
230
+ " tag_to_country_nationality[nationality_lc.replace(\" \", \"\")] = (\"\", nationality)\n",
231
+ "\n",
232
+ " for part in country_lc.split():\n",
233
+ " tag_to_country_nationality[part] = (country, \"\")\n",
234
+ " for part in nationality_lc.split():\n",
235
+ " tag_to_country_nationality[part] = (\"\", nationality)\n",
236
+ "\n",
237
+ "# Step 3: Infer likely_country and likely_nationality\n",
238
+ "# Step 3: Infer likely_country and likely_nationality\n",
239
+ "def infer_country_and_nationality(tags):\n",
240
+ " for tag in tags:\n",
241
+ " cleaned = tag.replace(\" \", \"\").lower()\n",
242
+ " if cleaned in tag_to_country_nationality:\n",
243
+ " country, nationality = tag_to_country_nationality[cleaned]\n",
244
+ " # Special case: skip if country is \"Isle of Man\"\n",
245
+ " if country == \"Isle of Man\":\n",
246
+ " country = \"\"\n",
247
+ " return pd.Series([country, nationality])\n",
248
+ " return pd.Series([\"\", \"\"])\n",
249
+ "\n",
250
+ "\n",
251
+ "poi_df[[\"likely_country\", \"likely_nationality\"]] = poi_df[\"tags\"].apply(infer_country_and_nationality)\n",
252
+ "\n",
253
+ "# Step 4: Build tag → profession mapping\n",
254
+ "profession_alias_map = {}\n",
255
+ "\n",
256
+ "for _, row in professions_df.iterrows():\n",
257
+ " canonical = str(row['profession']).strip().lower()\n",
258
+ " profession_alias_map[canonical] = canonical\n",
259
+ " for alias_col in ['alias_1', 'alias_2', 'alias_3']:\n",
260
+ " alias = row.get(alias_col)\n",
261
+ " if pd.notna(alias):\n",
262
+ " profession_alias_map[str(alias).strip().lower()] = canonical\n",
263
+ "\n",
264
+ "# Step 5: Infer likely profession from tags\n",
265
+ "def infer_profession_from_tags(tags):\n",
266
+ " matched = []\n",
267
+ " for tag in tags:\n",
268
+ " cleaned = tag.strip().lower()\n",
269
+ " if cleaned in profession_alias_map:\n",
270
+ " matched.append(profession_alias_map[cleaned])\n",
271
+ "\n",
272
+ " if not matched:\n",
273
+ " return \"\"\n",
274
+ " if \"celebrity\" in matched and len(set(matched)) > 1:\n",
275
+ " # Drop 'celebrity' if other professions are present\n",
276
+ " matched = [m for m in matched if m != \"celebrity\"]\n",
277
+ "\n",
278
+ " return matched[0] # Return the first specific match\n",
279
+ "\n",
280
+ "\n",
281
+ "poi_df[\"likely_profession\"] = poi_df[\"tags\"].apply(infer_profession_from_tags)\n",
282
+ "\n",
283
+ "# Step 6: Save enriched dataset\n",
284
+ "poi_df.to_csv(outfile, index=False)\n",
285
+ "\n",
286
+ "# Optional: Preview\n",
287
+ "print(poi_df[[\"real_name\", \"likely_country\", \"likely_nationality\", \"likely_profession\"]].head())\n"
288
+ ]
289
+ },
290
+ {
291
+ "cell_type": "code",
292
+ "execution_count": 6,
293
+ "id": "054f230b",
294
+ "metadata": {},
295
+ "outputs": [],
296
+ "source": [
297
+ "#!pip install transformers torch\n",
298
+ "#!python -m spacy download en_core_web_trf\n",
299
+ "#pip install openai"
300
+ ]
301
+ },
302
+ {
303
+ "cell_type": "markdown",
304
+ "id": "59185461",
305
+ "metadata": {},
306
+ "source": [
307
+ "## Step 3: Query Deepseek-v3 with NAME and HINTS"
308
+ ]
309
+ },
310
+ {
311
+ "cell_type": "code",
312
+ "execution_count": 7,
313
+ "id": "504b970f",
314
+ "metadata": {},
315
+ "outputs": [],
316
+ "source": [
317
+ "#!pip install openpyxl"
318
+ ]
319
+ },
320
+ {
321
+ "cell_type": "code",
322
+ "execution_count": 10,
323
+ "id": "7c209115",
324
+ "metadata": {},
325
+ "outputs": [
326
+ {
327
+ "name": "stdout",
328
+ "output_type": "stream",
329
+ "text": [
330
+ "Row 1/4...\n",
331
+ "Saved up to row 1\n",
332
+ "Row 2/4...\n",
333
+ "Saved up to row 2\n",
334
+ "Row 3/4...\n",
335
+ "Saved up to row 3\n",
336
+ "Row 4/4...\n",
337
+ "Saved up to row 4\n",
338
+ "All done! Files: /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/data/CSV/Deepseek_annotated_POI.csv /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/data/CSV/Deepseek_annotated_POI.xlsx\n"
339
+ ]
340
+ }
341
+ ],
342
+ "source": [
343
+ "import pandas as pd\n",
344
+ "import openai\n",
345
+ "import time\n",
346
+ "import os\n",
347
+ "from pathlib import Path\n",
348
+ "from openai import OpenAI # Add this import\n",
349
+ "\n",
350
+ "# === PATHS & CONFIG ===\n",
351
+ "current_dir = Path.cwd()\n",
352
+ "inputCSV = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_02.csv\"\n",
353
+ "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\" #store your API key under misc/credentials/deepseek_api_key.txt\n",
354
+ "\n",
355
+ "# Output both CSV and Excel for compatibility\n",
356
+ "OUTPUT_CSV = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.csv\"\n",
357
+ "OUTPUT_XLSX = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.xlsx\"\n",
358
+ "INDEX_FILE = current_dir.parent / \"misc/deepseek_query_index.txt\"\n",
359
+ "SAVE_INTERVAL = 1 # Save every N rows\n",
360
+ "START_ROW = 1 # Row index to start from (0-based)\n",
361
+ "END_ROW = 5 # Row index to end (exclusive)\n",
362
+ "\n",
363
+ "# === LOAD API KEY & CLIENT ===\n",
364
+ "with open(api_key_file) as f:\n",
365
+ " api_key = f.read().strip()\n",
366
+ "\n",
367
+ "client = OpenAI(\n",
368
+ " api_key=api_key,\n",
369
+ " base_url=\"https://api.deepseek.com/v1\"\n",
370
+ ")\n",
371
+ "\n",
372
+ "# === LOAD DATA ===\n",
373
+ "df = pd.read_csv(inputCSV)\n",
374
+ "df = df.iloc[START_ROW:END_ROW].reset_index(drop=True)\n",
375
+ "\n",
376
+ "# === PREPARE PROMPTS ===\n",
377
+ "def create_prompt(row):\n",
378
+ " name = row['real_name'] if pd.notna(row['real_name']) else row['name']\n",
379
+ " hints = []\n",
380
+ " for col in ('likely_profession','likely_nationality','likely_country'):\n",
381
+ " if pd.notna(row.get(col, None)):\n",
382
+ " hints.append(row[col])\n",
383
+ " if not hints:\n",
384
+ " tags = [row[f'tag_{i}'] for i in range(1,8) if pd.notna(row.get(f'tag_{i}', None))]\n",
385
+ " hints.extend(tags[:3])\n",
386
+ " hint_text = \", \".join(hints[:5])\n",
387
+ " return (\n",
388
+ " f\"Given '{name}' ({hint_text}), provide:\\n\"\n",
389
+ " \"1. Full legal name (Western order if non-latin script)\\n\"\n",
390
+ " \"2. Any stage names/aliases (comma separated)\\n\"\n",
391
+ " \"3. Gender\\n\"\n",
392
+ " \"4. Top 3 most specific, factual professions (use industry-standard terms, no euphemisms)\\n\"\n",
393
+ " \"5. Primary country associated\\n\"\n",
394
+ " \"Use 'Unknown' when uncertain or you encounter a fictional character or place. \"\n",
395
+ " \"For entertainment fields, specify sub-genres when known (kpop, adult industry, etc.).\"\n",
396
+ " )\n",
397
+ "\n",
398
+ "prompts = df.apply(create_prompt, axis=1).tolist()\n",
399
+ "\n",
400
+ "# === CHECK FOR EXISTING OUTPUT ===\n",
401
+ "if os.path.exists(INDEX_FILE):\n",
402
+ " with open(INDEX_FILE, 'r') as f:\n",
403
+ " current_index = int(f.read().strip())\n",
404
+ "else:\n",
405
+ " current_index = 0\n",
406
+ "\n",
407
+ "results = []\n",
408
+ "if os.path.exists(OUTPUT_XLSX):\n",
409
+ " existing = pd.read_excel(OUTPUT_XLSX)\n",
410
+ " if {'full_name','aliases','gender','profession_llm','country'}.issubset(existing.columns):\n",
411
+ " results = existing[['full_name','aliases','gender','profession_llm','country']].values.tolist()\n",
412
+ "\n",
413
+ "# === QUERY & PARSE ===\n",
414
+ "def query_deepseek(prompt):\n",
415
+ " try:\n",
416
+ " resp = client.chat.completions.create(\n",
417
+ " model=\"deepseek-chat\",\n",
418
+ " messages=[\n",
419
+ " {\"role\":\"system\",\"content\":\"You extract key data on a person; respond with exactly 5 numbered lines.\"},\n",
420
+ " {\"role\":\"user\",\"content\":prompt}\n",
421
+ " ],\n",
422
+ " temperature=0.05, top_p=0.8\n",
423
+ " )\n",
424
+ " return resp.choices[0].message.content.strip()\n",
425
+ " except Exception as e:\n",
426
+ " print(\"API error:\", e)\n",
427
+ " return \"\"\n",
428
+ "\n",
429
+ "def parse_response(resp):\n",
430
+ " lines = [l.strip() for l in resp.split('\\n') if l.strip()]\n",
431
+ " out = [\"Unknown\"]*5\n",
432
+ " for l in lines:\n",
433
+ " if l.startswith('1.'): out[0] = l[2:].strip()\n",
434
+ " elif l.startswith('2.'): out[1] = l[2:].strip()\n",
435
+ " elif l.startswith('3.'): out[2] = l[2:].strip()\n",
436
+ " elif l.startswith('4.'): out[3] = l[2:].strip()\n",
437
+ " elif l.startswith('5.'): out[4] = l[2:].strip()\n",
438
+ " return out\n",
439
+ "\n",
440
+ "# === PROCESS & SAVE ===\n",
441
+ "for i in range(current_index, len(df)):\n",
442
+ " print(f\"Row {i+1}/{len(df)}...\")\n",
443
+ " data = parse_response(query_deepseek(prompts[i]))\n",
444
+ " if i < len(results): results[i] = data\n",
445
+ " else: results.append(data)\n",
446
+ " current_index = i+1\n",
447
+ "\n",
448
+ " if current_index % SAVE_INTERVAL == 0 or current_index == len(df):\n",
449
+ " out_df = df.iloc[:current_index].copy()\n",
450
+ " out_df[['full_name','aliases','gender','profession_llm','country']] = pd.DataFrame(results[:current_index])\n",
451
+ " # CSV\n",
452
+ " out_df.to_csv(OUTPUT_CSV, index=False)\n",
453
+ " # Excel\n",
454
+ " out_df.to_excel(OUTPUT_XLSX, index=False)\n",
455
+ " with open(INDEX_FILE, 'w') as f:\n",
456
+ " f.write(str(current_index))\n",
457
+ " print(\"Saved up to row\", current_index)\n",
458
+ " time.sleep(1)\n",
459
+ "\n",
460
+ "print(\"All done! Files:\", OUTPUT_CSV, OUTPUT_XLSX)\n"
461
+ ]
462
+ },
463
+ {
464
+ "cell_type": "markdown",
465
+ "id": "7910e574",
466
+ "metadata": {},
467
+ "source": []
468
+ },
469
+ {
470
+ "cell_type": "markdown",
471
+ "id": "9d377005",
472
+ "metadata": {},
473
+ "source": [
474
+ "# Aggregate by individual names"
475
+ ]
476
+ },
477
+ {
478
+ "cell_type": "markdown",
479
+ "id": "d2e75354",
480
+ "metadata": {},
481
+ "source": [
482
+ " ##### e.g. Emma Watson [model1, model2, model3] etc."
483
+ ]
484
+ },
485
+ {
486
+ "cell_type": "code",
487
+ "execution_count": 11,
488
+ "id": "747c3a2f",
489
+ "metadata": {},
490
+ "outputs": [],
491
+ "source": [
492
+ "import pandas as pd\n",
493
+ "import re\n",
494
+ "from pathlib import Path\n",
495
+ "current_dir = Path.cwd()\n",
496
+ "\n",
497
+ "profession_map = current_dir.parent / \"misc/lists/mapped_professions.csv\"\n",
498
+ "\n",
499
+ "poi_df = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.csv\"\n",
500
+ "\n",
501
+ "output = current_dir.parent / \"data/CSV/Deepseek_annotated_POI_aggregated.csv\"\n",
502
+ "\n",
503
+ "countries_csv = current_dir.parent / \"misc/lists/countries.csv\"\n",
504
+ "countries_df = pd.read_csv(countries_csv)\n",
505
+ "\n",
506
+ "# Extract valid country names (strip whitespace)\n",
507
+ "valid_countries = set(countries_df['en_short_name'].str.strip())\n",
508
+ "\n",
509
+ "\n",
510
+ "# Load the dataset\n",
511
+ "df = pd.read_csv(poi_df) # Update path if needed\n",
512
+ "\n",
513
+ "# Step 1: Group by 'full_name' and aggregate required information\n",
514
+ "grouped_df = df.groupby('full_name').agg(\n",
515
+ " No_of_models=('id', 'count'),\n",
516
+ " modelIDs=('id', lambda x: list(x)),\n",
517
+ " combinedDownloadCount=('downloadCount', 'sum')\n",
518
+ ").reset_index()\n",
519
+ "\n",
520
+ "# Step 2: Keep representative info for each person\n",
521
+ "# Keep representative info (including aliases)\n",
522
+ "additional_columns = df.groupby('full_name').agg(\n",
523
+ " country=('country', 'first'),\n",
524
+ " profession_llm=('profession_llm', 'first'),\n",
525
+ " gender=('gender', 'first'),\n",
526
+ " aliases=('aliases', 'first') # ✅ Add this line\n",
527
+ ").reset_index()\n",
528
+ "\n",
529
+ "\n",
530
+ "\n",
531
+ "def standardize_country(country):\n",
532
+ " if not isinstance(country, str):\n",
533
+ " return \"Unknown\"\n",
534
+ "\n",
535
+ " country_clean = country.strip()\n",
536
+ " lowered = country_clean.lower()\n",
537
+ "\n",
538
+ " # Handle fictional or fantasy countries\n",
539
+ " fictional_keywords = [\"fictional\", \"westeros\", \"asgard\", \"middle-earth\", \"naboo\", \"middle earth\", \"latveria\"]\n",
540
+ " if any(keyword in lowered for keyword in fictional_keywords):\n",
541
+ " return \"Unknown\"\n",
542
+ "\n",
543
+ " # Handle known region-based adjustments\n",
544
+ " if \"macau\" in lowered:\n",
545
+ " return \"Macau\"\n",
546
+ " elif \"hong kong\" in lowered:\n",
547
+ " return \"Hong Kong\"\n",
548
+ " elif \"taiwan\" in lowered:\n",
549
+ " return \"Taiwan\"\n",
550
+ "\n",
551
+ " # Normalize complex or alternate country names\n",
552
+ " lowered = lowered.replace(\"United Kingdom of Great Britain and Northern Ireland\", \"united kingdom\")\n",
553
+ " lowered = lowered.replace(\"england\", \"united kingdom\")\n",
554
+ " lowered = lowered.replace(\"united states of america\", \"united states\")\n",
555
+ "\n",
556
+ " # Remove anything in brackets and after commas\n",
557
+ " country_clean = re.sub(r\"\\(.*?\\)\", \"\", country_clean)\n",
558
+ " country_clean = country_clean.split(',')[0].strip().lower()\n",
559
+ "\n",
560
+ " # Manual overrides\n",
561
+ " replacements = {\n",
562
+ " \"united kingdom\": \"UK\",\n",
563
+ " \"united kingdom of great britain and northern ireland\": \"UK\",\n",
564
+ " \"french southern territories\": \"Other\",\n",
565
+ " \"united states\": \"US\",\n",
566
+ " \"united states of america\": \"US\",\n",
567
+ " \"turkey\": \"Türkiye\",\n",
568
+ " \"czech republic\": \"Czechia\"\n",
569
+ " }\n",
570
+ "\n",
571
+ " if country_clean in replacements:\n",
572
+ " return replacements[country_clean]\n",
573
+ "\n",
574
+ " # Final check against valid country list (case-insensitive)\n",
575
+ " for valid in valid_countries:\n",
576
+ " if country_clean == valid.lower():\n",
577
+ " return valid\n",
578
+ "\n",
579
+ " return \"Unknown\"\n",
580
+ "\n",
581
+ "\n",
582
+ "# Updated function to fully remove anything in brackets (complete or not)\n",
583
+ "def get_profession_short(profession):\n",
584
+ " if isinstance(profession, str):\n",
585
+ " # Get first part before comma\n",
586
+ " first_prof = profession.split(',')[0].strip()\n",
587
+ " # Remove all bracketed content, even malformed\n",
588
+ " first_prof = re.sub(r\"[\\[].∗?[\\[].*?[\\]]\", \"\", first_prof) # removes properly closed\n",
589
+ " first_prof = re.sub(r\"[\\(\\[].*\", \"\", first_prof) # removes malformed\n",
590
+ " cleaned = first_prof.strip()\n",
591
+ " # Normalize 'Actress' to 'Actor'\n",
592
+ " if cleaned.lower() == \"actress\":\n",
593
+ " return \"Actor\"\n",
594
+ " return cleaned\n",
595
+ " return None\n",
596
+ "\n",
597
+ "# Load your mapping file\n",
598
+ "mapping_df = pd.read_csv(profession_map, on_bad_lines='skip') # or 'warn'\n",
599
+ "\n",
600
+ "\n",
601
+ "# Ensure the mapping columns are named correctly\n",
602
+ "# (Assuming columns are: 'profession_llm' or 'profession_short', and 'category' or 'mapped_profession')\n",
603
+ "# Adjust these as needed\n",
604
+ "mapping_df.columns = [col.strip().lower() for col in mapping_df.columns]\n",
605
+ "\n",
606
+ "# Rename for clarity and consistency\n",
607
+ "if 'profession_llm' in mapping_df.columns:\n",
608
+ " mapping_df = mapping_df.rename(columns={'profession_llm': 'profession_short'})\n",
609
+ "if 'category' in mapping_df.columns:\n",
610
+ " mapping_df = mapping_df.rename(columns={'category': 'mapped_profession'})\n",
611
+ "\n",
612
+ "# Merge the mapped profession into final_df\n",
613
+ "#final_df = final_df.merge(mapping_df[['profession_short', 'mapped_profession']], on='profession_short', how='left')\n",
614
+ "\n",
615
+ "\n",
616
+ "additional_columns = df.groupby('full_name').agg(\n",
617
+ " country=('country', 'first'),\n",
618
+ " profession_llm=('profession_llm', 'first'),\n",
619
+ " gender=('gender', 'first'),\n",
620
+ " aliases=('aliases', 'first') # <-- Added this line\n",
621
+ ").reset_index()\n",
622
+ "\n",
623
+ "\n",
624
+ "# Step 3: Merge the aggregated info with the representative info\n",
625
+ "final_df = pd.merge(grouped_df, additional_columns, on='full_name', how='left')\n",
626
+ "\n",
627
+ "# Step 4: Clean and transform columns\n",
628
+ "final_df['profession_short'] = final_df['profession_llm'].apply(get_profession_short)\n",
629
+ "final_df['country'] = final_df['country'].apply(standardize_country)\n",
630
+ "\n",
631
+ "# Step 5: Merge with profession mapping\n",
632
+ "final_df = final_df.merge(mapping_df[['profession_short', 'mapped_profession']], on='profession_short', how='left')\n",
633
+ "\n",
634
+ "# Optional: Save the result to a CSV file\n",
635
+ "final_df.to_csv(output, index=False)\n"
636
+ ]
637
+ },
638
+ {
639
+ "cell_type": "code",
640
+ "execution_count": null,
641
+ "id": "704e5246",
642
+ "metadata": {},
643
+ "outputs": [],
644
+ "source": []
645
+ }
646
+ ],
647
+ "metadata": {
648
+ "kernelspec": {
649
+ "display_name": "latm",
650
+ "language": "python",
651
+ "name": "python3"
652
+ },
653
+ "language_info": {
654
+ "codemirror_mode": {
655
+ "name": "ipython",
656
+ "version": 3
657
+ },
658
+ "file_extension": ".py",
659
+ "mimetype": "text/x-python",
660
+ "name": "python",
661
+ "nbconvert_exporter": "python",
662
+ "pygments_lexer": "ipython3",
663
+ "version": "3.10.15"
664
+ }
665
+ },
666
+ "nbformat": 4,
667
+ "nbformat_minor": 5
668
+ }
jupyter_notebooks/.ipynb_checkpoints/Section_2-3-4__Figure_8b_sunburst_profession-checkpoint.ipynb ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Prepare *.json for Figure 8b"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 5,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "✅ Sunburst data saved to sunburst_data.json\n"
20
+ ]
21
+ }
22
+ ],
23
+ "source": [
24
+ "import pandas as pd\n",
25
+ "from collections import defaultdict\n",
26
+ "import json\n",
27
+ "from pathlib import Path\n",
28
+ "\n",
29
+ "current_dir = Path.cwd()\n",
30
+ "\n",
31
+ "sunburst_path = current_dir.parent / \"public/json/sunburst_countries_A.json\"\n",
32
+ "\n",
33
+ "\n",
34
+ "aggregated_poi = current_dir.parent / \"data/CSV/Deepseek_annotated_POI_aggregated.csv\"\n",
35
+ "\n",
36
+ "df = pd.read_csv(aggregated_poi)\n",
37
+ "df['country_cleaned'] = df['country'].apply(lambda x: x if x not in ['Unknown', '', None] else 'Other')\n",
38
+ "\n",
39
+ "# Now get top countries excluding what was forced into 'Other'\n",
40
+ "top_countries = df['country_cleaned'].value_counts().nlargest(15).index.tolist()\n",
41
+ "\n",
42
+ "# Final limited country column\n",
43
+ "df['country_limited'] = df['country_cleaned'].apply(lambda x: x if x in top_countries else 'Other')\n",
44
+ "\n",
45
+ "# ---- Step 2: Limit to top 7 professions and combine Unknown and Sports Professional with Other ----\n",
46
+ "top_professions = df['mapped_profession'].value_counts().nlargest(7).index.tolist()\n",
47
+ "\n",
48
+ "# Explicitly remove 'Unknown' and 'Sports Professional' even if they are in the top 7\n",
49
+ "top_professions = [p for p in top_professions if p not in ['Unknown', 'Sports Professional']]\n",
50
+ "\n",
51
+ "# Normalize 'Unknown' and '\n",
52
+ "# Sports Professional' into 'Other'\n",
53
+ "# Normalize 'Unknown' and 'Sports Professional' into 'Other'\n",
54
+ "df['profession_limited'] = df['mapped_profession'].apply(\n",
55
+ " lambda x: 'Other' if x in ['Unknown', 'Sports Professional'] or x not in top_professions else x\n",
56
+ ")\n",
57
+ "\n",
58
+ "\n",
59
+ "\n",
60
+ "# ---- Step 3: Group by the limited country and profession ----\n",
61
+ "sunburst_data = df.groupby(['country_limited', 'profession_limited']).size().reset_index(name='count')\n",
62
+ "\n",
63
+ "# ---- Step 4: Create a nested structure for D3.js ----\n",
64
+ "sunburst_dict = {\"name\": \"root\", \"children\": []}\n",
65
+ "country_map = defaultdict(list)\n",
66
+ "\n",
67
+ "for _, row in sunburst_data.iterrows():\n",
68
+ " country = row['country_limited']\n",
69
+ " profession = row['profession_limited']\n",
70
+ " count = int(row['count'])\n",
71
+ " country_map[country].append({\"name\": profession, \"value\": count})\n",
72
+ "\n",
73
+ "# For each country, sort the profession list so that \"Other\" appears at the end\n",
74
+ "for country, professions in country_map.items():\n",
75
+ " professions_sorted = sorted(professions, key=lambda d: (d[\"name\"] == \"Other\", d[\"name\"]))\n",
76
+ " country_map[country] = professions_sorted\n",
77
+ "\n",
78
+ "for country, professions in country_map.items():\n",
79
+ " sunburst_dict[\"children\"].append({\"name\": country, \"children\": professions})\n",
80
+ "\n",
81
+ "# ---- Step 5: Save to a JSON file ----\n",
82
+ "with open(sunburst_path, \"w\", encoding='utf-8') as f:\n",
83
+ " json.dump(sunburst_dict, f, ensure_ascii=False, indent=2)\n",
84
+ "\n",
85
+ "\n",
86
+ "print(\"✅ Sunburst data saved to sunburst_data.json\")\n"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "metadata": {},
92
+ "source": [
93
+ "the resulting *.json is the input for Figure_8.html"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "markdown",
98
+ "metadata": {},
99
+ "source": []
100
+ }
101
+ ],
102
+ "metadata": {
103
+ "kernelspec": {
104
+ "display_name": "latm",
105
+ "language": "python",
106
+ "name": "python3"
107
+ },
108
+ "language_info": {
109
+ "codemirror_mode": {
110
+ "name": "ipython",
111
+ "version": 3
112
+ },
113
+ "file_extension": ".py",
114
+ "mimetype": "text/x-python",
115
+ "name": "python",
116
+ "nbconvert_exporter": "python",
117
+ "pygments_lexer": "ipython3",
118
+ "version": "3.10.15"
119
+ }
120
+ },
121
+ "nbformat": 4,
122
+ "nbformat_minor": 2
123
+ }
jupyter_notebooks/Section_2-3-4_Figure_8_deepfake_adapters.ipynb CHANGED
@@ -121,29 +121,29 @@
121
  "id": "3c906826",
122
  "metadata": {},
123
  "source": [
124
- "## NER Code"
125
- ]
126
- },
127
- {
128
- "cell_type": "markdown",
129
- "id": "cb529f57",
130
- "metadata": {},
131
- "source": [
132
- "please put NER code here. on the first collumn of the adapter dataset in data/real_person_adapters.csv named entity recognition should be performed to extract the most plausible name."
133
  ]
134
  },
135
  {
136
  "cell_type": "code",
137
- "execution_count": 2,
138
  "id": "ner_code_cell",
139
- "metadata": {},
 
 
 
 
 
 
 
 
140
  "outputs": [
141
  {
142
  "name": "stdout",
143
  "output_type": "stream",
144
  "text": [
145
  "✅ spaCy model loaded: en_core_web_sm\n",
146
- "Loaded 18 rows\n",
147
  "\n",
148
  "🔄 Processing names with spaCy NER...\n",
149
  "\n",
@@ -162,6 +162,13 @@
162
  "Game of Thrones Cast | Game Thrones Cast \n",
163
  "Natalie Portman「LoRa」 | Natalie Portman \n",
164
  "Emma Watson LoRA | Emma Watson \n",
 
 
 
 
 
 
 
165
  "====================================================================================================\n",
166
  "\n",
167
  "🧪 Leetspeak translation examples:\n",
@@ -171,9 +178,9 @@
171
  " L3vi Ackerman -> Levi Ackerman\n",
172
  "\n",
173
  "📈 Statistics:\n",
174
- " Total rows: 18\n",
175
- " Non-empty names: 18\n",
176
- " Empty names: 0\n",
177
  "\n",
178
  "🎯 Sample spaCy NER results:\n",
179
  " 1. IU\n",
@@ -187,8 +194,8 @@
187
  " 9. Emma Watson\n",
188
  " 10. Gal Gadot\n",
189
  "\n",
190
- "✅ Cleaned 18 names using spaCy NER\n",
191
- "💾 Saved to /home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/data/CSV/NER_POI_step01_pre_annotation.csv\n"
192
  ]
193
  }
194
  ],
@@ -214,7 +221,7 @@
214
  "# Set up paths\n",
215
  "current_dir = Path.cwd()\n",
216
  "#input_file = current_dir.parent / \"data/CSV/real_person_adapters.csv\"\n",
217
- "input_file = current_dir.parent / \"data/CSV/samples/real_person_adapters_sample.csv\"\n",
218
  "\n",
219
  "# Load dataset\n",
220
  "df = pd.read_csv(input_file)\n",
@@ -274,7 +281,7 @@
274
  " \n",
275
  " # Remove LoRA-related terms (case insensitive)\n",
276
  " lora_terms = ['lora', 'loha', 'lycoris', 'controlnet', 'textual inversion', \n",
277
- " 'embedding', 'ti', 'checkpoint', 'model', 'adapter']\n",
278
  " for term in lora_terms:\n",
279
  " name = re.sub(rf'\\b{term}\\b', '', name, flags=re.IGNORECASE)\n",
280
  " \n",
@@ -397,7 +404,7 @@
397
  "print(f\"\\n✅ Cleaned {len(df)} names using spaCy NER\")\n",
398
  "\n",
399
  "# Save intermediate result\n",
400
- "output_step1 = current_dir.parent / \"data/CSV/NER_POI_step01_pre_annotation.csv\"\n",
401
  "df.to_csv(output_step1, index=False)\n",
402
  "print(f\"💾 Saved to {output_step1}\")\n"
403
  ]
@@ -407,15 +414,23 @@
407
  "id": "d701d1cb",
408
  "metadata": {},
409
  "source": [
410
- "## nationality to Country\n",
411
  "here tags related to nationality gets converted to the country equivalent. make sure isle of man is shows never up in the country collumn.as well as other uninhabited places."
412
  ]
413
  },
414
  {
415
  "cell_type": "code",
416
- "execution_count": 3,
417
  "id": "36ce71f2",
418
- "metadata": {},
 
 
 
 
 
 
 
 
419
  "outputs": [
420
  {
421
  "name": "stdout",
@@ -423,13 +438,24 @@
423
  "text": [
424
  "Built country/nationality mapping with 781 entries\n",
425
  "\n",
426
- "Processed 18 rows\n",
427
- "Rows with country: 0\n",
428
  "Rows with nationality: 0\n",
429
- "Rows with profession: 14\n",
430
  "\n",
431
  "Top 10 countries:\n",
432
- "Series([], Name: count, dtype: int64)\n",
 
 
 
 
 
 
 
 
 
 
 
433
  "\n",
434
  "Sample results:\n",
435
  " real_name likely_country likely_nationality likely_profession\n",
@@ -449,6 +475,166 @@
449
  " Tags: ['girl', 'photorealistic', 'asian', 'woman', 'beautiful', 'celebrity', 'korean']\n",
450
  " Country: \n",
451
  " Nationality: \n",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  "\n"
453
  ]
454
  }
@@ -461,8 +647,8 @@
461
  "current_dir = Path.cwd()\n",
462
  "countries_file = current_dir.parent / \"misc/lists/countries.csv\"\n",
463
  "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
464
- "input_file = current_dir.parent / \"data/CSV/NER_POI_step01_pre_annotation.csv\"\n",
465
- "output_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
466
  "\n",
467
  "# Load datasets\n",
468
  "poi_df = pd.read_csv(input_file)\n",
@@ -620,12 +806,6 @@
620
  " print()\n"
621
  ]
622
  },
623
- {
624
- "cell_type": "markdown",
625
- "id": "29eec5be",
626
- "metadata": {},
627
- "source": []
628
- },
629
  {
630
  "cell_type": "markdown",
631
  "id": "deepseek_header",
@@ -644,28 +824,121 @@
644
  },
645
  {
646
  "cell_type": "code",
647
- "execution_count": 10,
648
  "id": "deepseek_code",
649
- "metadata": {},
 
 
 
 
 
 
 
 
650
  "outputs": [
651
  {
652
  "name": "stdout",
653
  "output_type": "stream",
654
  "text": [
655
- "❌ Error: deepseek_api_key.txt not found in misc/credentials/\n",
656
- " Please create this file with your API key\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
657
  ]
658
  },
659
  {
660
- "ename": "FileNotFoundError",
661
- "evalue": "[Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/misc/credentials/deepseek_api_key.txt'",
662
- "output_type": "error",
663
- "traceback": [
664
- "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
665
- "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)",
666
- "Cell \u001b[0;32mIn[10], line 37\u001b[0m\n\u001b[1;32m 35\u001b[0m \u001b[38;5;66;03m# === LOAD API KEY ===\u001b[39;00m\n\u001b[1;32m 36\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[0;32m---> 37\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mapi_key_file\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 38\u001b[0m api_key \u001b[38;5;241m=\u001b[39m f\u001b[38;5;241m.\u001b[39mread()\u001b[38;5;241m.\u001b[39mstrip()\n\u001b[1;32m 39\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m✅ Deepseek API key loaded\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
667
- "File \u001b[0;32m~/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:324\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 317\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 318\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 320\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 322\u001b[0m )\n\u001b[0;32m--> 324\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
668
- "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/CODE/pm-paper/misc/credentials/deepseek_api_key.txt'"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  ]
670
  }
671
  ],
@@ -680,7 +953,7 @@
680
  "from tqdm import tqdm\n",
681
  "\n",
682
  "current_dir = Path.cwd()\n",
683
- "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
684
  "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
685
  "professions_mapped_file = current_dir.parent / \"misc/lists/professions_mapped.csv\"\n",
686
  "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\"\n",
@@ -858,8 +1131,8 @@
858
  " return fields\n",
859
  "\n",
860
  "# === PROCESS DATA ===\n",
861
- "output_file = current_dir.parent / f\"data/CSV/deepseek_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
862
- "index_file = current_dir.parent / \"misc/deepseek_query_index.txt\"\n",
863
  "\n",
864
  "# Check for existing progress\n",
865
  "current_index = 0\n",
@@ -952,7 +1225,14 @@
952
  "cell_type": "code",
953
  "execution_count": null,
954
  "id": "qwen_code",
955
- "metadata": {},
 
 
 
 
 
 
 
956
  "outputs": [],
957
  "source": [
958
  "import pandas as pd\n",
@@ -1665,7 +1945,40 @@
1665
  "cell_type": "markdown",
1666
  "id": "01086b35",
1667
  "metadata": {},
1668
- "source": "## Qwen-2.5-32B Local Annotation (Hugging Face Transformers)\n\nRun **Qwen-2.5-32B-Instruct** locally using Hugging Face Transformers - NO API COSTS!\n\n**Requirements:**\n- GPU: NVIDIA A100 80GB or similar (32B model needs ~60-70GB VRAM)\n- RAM: 128GB+ system RAM recommended\n- Model cache: `data/models/` (auto-downloaded on first run)\n- Model size: ~65GB download (BF16 weights)\n\n**Installation:**\n```bash\n# Install required packages\npip install torch transformers accelerate\n```\n\n**Advantages:**\n- ✅ FREE - No API costs\n- ✅ Fast - Local inference with GPU acceleration\n- ✅ Privacy - Data never leaves your machine\n- ✅ Offline - Works without internet (after download)\n- ✅ High quality - 32B parameter instruction-tuned model\n\n**Performance:**\n- Speed: ~10-30 tokens/sec on A100 (depends on batch size)\n- Throughput: ~200-500 samples/hour\n- Cost: $0 (after model download)\n\n**Model Info:**\n- HuggingFace: `Qwen/Qwen2.5-32B-Instruct`\n- License: Apache 2.0 (commercial use allowed)\n- Context: 32K tokens"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1669
  },
1670
  {
1671
  "cell_type": "code",
@@ -1673,14 +1986,263 @@
1673
  "id": "5dd7a803",
1674
  "metadata": {},
1675
  "outputs": [],
1676
- "source": "import pandas as pd\nimport json\nimport time\nimport re\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\ncurrent_dir = Path.cwd()\ninput_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n\n# === CONFIGURATION ===\nTEST_MODE = True\nTEST_SIZE = 10\nMAX_ROWS = 20000\nSAVE_INTERVAL = 10\n\n# Model settings\nMODEL_NAME = \"Qwen/Qwen2.5-32B-Instruct\"\nCACHE_DIR = current_dir.parent / \"data/models\"\nCACHE_DIR.mkdir(parents=True, exist_ok=True)\n\nPROFESSION_CATEGORIES = [\n \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n \"online personality\", \"public figure\", \"voice actor/ASMR\",\n \"sports professional\", \"tv personality\"\n]\n\n# === LOAD MODEL ===\nprint(f\"Loading model: {MODEL_NAME}\")\nprint(f\"Cache directory: {CACHE_DIR}\")\nprint(f\"This may take a while on first run (~65GB download)...\\n\")\n\n# Check GPU availability\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Device: {device}\")\n\nif device == \"cpu\":\n print(\"⚠️ WARNING: No GPU detected! Inference will be VERY slow.\")\n print(\" Consider using a GPU or reducing model size.\")\n\n# Load tokenizer\nprint(\"Loading tokenizer...\")\ntokenizer = AutoTokenizer.from_pretrained(\n MODEL_NAME,\n cache_dir=str(CACHE_DIR),\n trust_remote_code=True\n)\nprint(\"✅ Tokenizer loaded\")\n\n# Load model with optimizations\nprint(\"Loading model (this may take several minutes)...\")\nmodel = AutoModelForCausalLM.from_pretrained(\n MODEL_NAME,\n cache_dir=str(CACHE_DIR),\n torch_dtype=torch.bfloat16, # Use BF16 for efficiency\n device_map=\"auto\", # Automatically distribute across GPUs\n trust_remote_code=True\n)\nmodel.eval() # Set to evaluation mode\nprint(\"✅ Model loaded\")\n\n# Check VRAM usage\nif torch.cuda.is_available():\n vram_gb = torch.cuda.max_memory_allocated() / 1024**3\n print(f\"VRAM used: {vram_gb:.2f} GB\\n\")\n\n# === LOAD DATA ===\ndf = pd.read_csv(input_file)\nprint(f\"Loaded {len(df)} rows\")\n\nif TEST_MODE:\n print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n df = df.head(TEST_SIZE).copy()\nelif MAX_ROWS:\n df = df.head(MAX_ROWS).copy()\n\n# === CREATE PROMPT ===\ndef create_prompt(name: str, tags: list, country: str) -> str:\n tags_str = \", \".join(tags[:5]) if tags else \"none\"\n country_str = country if country else \"unknown\"\n \n return f\"\"\"You are an expert at identifying public figures.\n\nGiven: Name: {name}, Tags: {tags_str}, Country: {country_str}\n\nProvide in JSON format:\n{{\n \"full_name\": \"<full name>\",\n \"gender\": \"<Male/Female/Other/Unknown>\",\n \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n \"country\": \"<country>\"\n}}\n\nChoose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n- Max 3 professions, ordered by relevance\n- \"online personality\" includes streamers, cosplayers, influencers\n- Return \"Unknown\" if uncertain\n\nReturn ONLY JSON, no extra text.\"\"\"\n\n# === QUERY QWEN LOCAL ===\ndef query_qwen_local(prompt: str) -> dict:\n \"\"\"Query Qwen-2.5-32B locally via transformers.\"\"\"\n try:\n # Format as chat message\n messages = [\n {\"role\": \"user\", \"content\": prompt}\n ]\n \n # Tokenize\n text = tokenizer.apply_chat_template(\n messages,\n tokenize=False,\n add_generation_prompt=True\n )\n \n inputs = tokenizer([text], return_tensors=\"pt\").to(device)\n \n # Generate\n with torch.no_grad():\n outputs = model.generate(\n **inputs,\n max_new_tokens=512,\n temperature=0.1,\n do_sample=True,\n top_p=0.9,\n pad_token_id=tokenizer.eos_token_id\n )\n \n # Decode\n generated_ids = outputs[0][inputs['input_ids'].shape[1]:]\n response = tokenizer.decode(generated_ids, skip_special_tokens=True)\n \n # Parse JSON from response\n json_match = re.search(r'\\{[^}]+\\}', response, re.DOTALL)\n if json_match:\n return json.loads(json_match.group())\n \n # Try markdown code block\n code_block = re.search(r'```(?:json)?\\s*(\\{[^}]+\\})\\s*```', response, re.DOTALL)\n if code_block:\n return json.loads(code_block.group(1))\n \n return None\n \n except json.JSONDecodeError as e:\n print(f\"JSON parse error: {e}\")\n return None\n except Exception as e:\n print(f\"Generation error: {e}\")\n return None\n\n# === PROCESS ===\noutput_file = current_dir.parent / f\"data/CSV/qwen_local_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\nindex_file = current_dir.parent / \"misc/qwen_local_query_index.txt\"\n\ncurrent_index = 0\nif index_file.exists():\n with open(index_file) as f:\n current_index = int(f.read().strip())\n print(f\"Resuming from index {current_index}\")\n\nfor col in ['full_name', 'gender', 'profession_llm', 'country']:\n if col not in df.columns:\n df[col] = 'Unknown'\n\nprint(f\"\\nAnnotating with QWEN-2.5-32B LOCAL - rows {current_index} to {len(df)}...\")\nprint(f\"Model: {MODEL_NAME}\")\nprint(f\"This may take a while (~200-500 samples/hour on A100)\\n\")\n\ntry:\n start_time = time.time()\n \n for i in tqdm(range(current_index, len(df)), desc=\"Qwen Local\"):\n row = df.iloc[i]\n \n tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n \n prompt = create_prompt(\n row.get('real_name', row.get('name', '')),\n tags,\n row.get('likely_country', '')\n )\n \n result = query_qwen_local(prompt)\n \n if result:\n df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n df.at[i, 'gender'] = result.get('gender', 'Unknown')\n profs = result.get('professions', [])\n df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n df.at[i, 'country'] = result.get('country', 'Unknown')\n \n current_index = i + 1\n \n if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n df.to_csv(output_file, index=False)\n with open(index_file, 'w') as f:\n f.write(str(current_index))\n \n # Calculate stats\n elapsed = time.time() - start_time\n samples_done = current_index - (0 if not index_file.exists() else int(open(index_file).read().strip()))\n samples_per_hour = (samples_done / elapsed) * 3600 if elapsed > 0 else 0\n \n print(f\"✅ Saved after {i+1} rows (~{samples_per_hour:.1f} samples/hour)\")\n \n elapsed_total = time.time() - start_time\n print(f\"\\n✅ Done! Results: {output_file}\")\n print(f\"Total time: {elapsed_total/60:.1f} minutes\")\n print(f\"Average speed: {len(df)/(elapsed_total/3600):.1f} samples/hour\")\n \n print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n \n # Show VRAM stats\n if torch.cuda.is_available():\n print(f\"\\nFinal VRAM usage: {torch.cuda.max_memory_allocated() / 1024**3:.2f} GB\")\n\nexcept KeyboardInterrupt:\n print(f\"\\n⚠️ Interrupted by user\")\n df.to_csv(output_file, index=False)\n with open(index_file, 'w') as f:\n f.write(str(current_index))\n print(f\"Progress saved up to row {current_index}\")\n\nexcept Exception as e:\n print(f\"⚠️ Error: {e}\")\n df.to_csv(output_file, index=False)\n with open(index_file, 'w') as f:\n f.write(str(current_index))\n print(f\"Progress saved up to row {current_index}\")"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1677
  }
1678
  ],
1679
  "metadata": {
 
 
 
 
 
1680
  "language_info": {
1681
- "name": "python"
 
 
 
 
 
 
 
 
 
1682
  }
1683
  },
1684
  "nbformat": 4,
1685
  "nbformat_minor": 5
1686
- }
 
121
  "id": "3c906826",
122
  "metadata": {},
123
  "source": [
124
+ "## STEP 01 NER"
 
 
 
 
 
 
 
 
125
  ]
126
  },
127
  {
128
  "cell_type": "code",
129
+ "execution_count": 7,
130
  "id": "ner_code_cell",
131
+ "metadata": {
132
+ "execution": {
133
+ "iopub.execute_input": "2025-11-21T10:50:51.775852Z",
134
+ "iopub.status.busy": "2025-11-21T10:50:51.775660Z",
135
+ "iopub.status.idle": "2025-11-21T10:53:16.141998Z",
136
+ "shell.execute_reply": "2025-11-21T10:53:16.141295Z",
137
+ "shell.execute_reply.started": "2025-11-21T10:50:51.775836Z"
138
+ }
139
+ },
140
  "outputs": [
141
  {
142
  "name": "stdout",
143
  "output_type": "stream",
144
  "text": [
145
  "✅ spaCy model loaded: en_core_web_sm\n",
146
+ "Loaded 50861 rows\n",
147
  "\n",
148
  "🔄 Processing names with spaCy NER...\n",
149
  "\n",
 
162
  "Game of Thrones Cast | Game Thrones Cast \n",
163
  "Natalie Portman「LoRa」 | Natalie Portman \n",
164
  "Emma Watson LoRA | Emma Watson \n",
165
+ "Karina Makina Lora | Karina Makina \n",
166
+ "WRAV YUA_三xx亜 | WRAV YUA \n",
167
+ "Dilraba Dilmurat 迪丽热巴 | Dilraba Dilmurat \n",
168
+ "MIMI,大幂幂 | MIMI \n",
169
+ "Chinese Idol - YangMi杨幂 | Chinese Idol YangMi杨幂 \n",
170
+ "Xiaorouseeu / 小柔SeeU - Chinese cosplayer and influencer | Xiaorouseeu \n",
171
+ "Jennifer Connelly (80s/90s) | Jennifer Connelly \n",
172
  "====================================================================================================\n",
173
  "\n",
174
  "🧪 Leetspeak translation examples:\n",
 
178
  " L3vi Ackerman -> Levi Ackerman\n",
179
  "\n",
180
  "📈 Statistics:\n",
181
+ " Total rows: 50861\n",
182
+ " Non-empty names: 50858\n",
183
+ " Empty names: 3\n",
184
  "\n",
185
  "🎯 Sample spaCy NER results:\n",
186
  " 1. IU\n",
 
194
  " 9. Emma Watson\n",
195
  " 10. Gal Gadot\n",
196
  "\n",
197
+ "✅ Cleaned 50861 names using spaCy NER\n",
198
+ "💾 Saved to /shares/weddigen.ki.uzh/laura_wagner/phase_01/pm-paper/data/CSV/model_adapter/real_person_adapter_step_01_NER.csv\n"
199
  ]
200
  }
201
  ],
 
221
  "# Set up paths\n",
222
  "current_dir = Path.cwd()\n",
223
  "#input_file = current_dir.parent / \"data/CSV/real_person_adapters.csv\"\n",
224
+ "input_file = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter.csv\"\n",
225
  "\n",
226
  "# Load dataset\n",
227
  "df = pd.read_csv(input_file)\n",
 
281
  " \n",
282
  " # Remove LoRA-related terms (case insensitive)\n",
283
  " lora_terms = ['lora', 'loha', 'lycoris', 'controlnet', 'textual inversion', \n",
284
+ " 'embedding', 'ti', 'checkpoint', 'model', 'adapter', 'pony', 'sdxl', 'flux', 'illustrious', 'sd14', 'sd14', 'sd2', 'sd3', 'diffusion', 'stable', 'hunyuan']\n",
285
  " for term in lora_terms:\n",
286
  " name = re.sub(rf'\\b{term}\\b', '', name, flags=re.IGNORECASE)\n",
287
  " \n",
 
404
  "print(f\"\\n✅ Cleaned {len(df)} names using spaCy NER\")\n",
405
  "\n",
406
  "# Save intermediate result\n",
407
+ "output_step1 = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_01_NER.csv\"\n",
408
  "df.to_csv(output_step1, index=False)\n",
409
  "print(f\"💾 Saved to {output_step1}\")\n"
410
  ]
 
414
  "id": "d701d1cb",
415
  "metadata": {},
416
  "source": [
417
+ "## STEP 02: Nationality tag to Country\n",
418
  "here tags related to nationality gets converted to the country equivalent. make sure isle of man is shows never up in the country collumn.as well as other uninhabited places."
419
  ]
420
  },
421
  {
422
  "cell_type": "code",
423
+ "execution_count": 11,
424
  "id": "36ce71f2",
425
+ "metadata": {
426
+ "execution": {
427
+ "iopub.execute_input": "2025-11-21T10:57:51.803570Z",
428
+ "iopub.status.busy": "2025-11-21T10:57:51.803378Z",
429
+ "iopub.status.idle": "2025-11-21T10:57:58.819903Z",
430
+ "shell.execute_reply": "2025-11-21T10:57:58.819309Z",
431
+ "shell.execute_reply.started": "2025-11-21T10:57:51.803555Z"
432
+ }
433
+ },
434
  "outputs": [
435
  {
436
  "name": "stdout",
 
438
  "text": [
439
  "Built country/nationality mapping with 781 entries\n",
440
  "\n",
441
+ "Processed 50861 rows\n",
442
+ "Rows with country: 1883\n",
443
  "Rows with nationality: 0\n",
444
+ "Rows with profession: 43674\n",
445
  "\n",
446
  "Top 10 countries:\n",
447
+ "likely_country\n",
448
+ "American Samoa 973\n",
449
+ "Japan 250\n",
450
+ "Indonesia 73\n",
451
+ "Argentina 73\n",
452
+ "Ireland 53\n",
453
+ "France 38\n",
454
+ "Netherlands 38\n",
455
+ "Hong Kong 35\n",
456
+ "Brazil 29\n",
457
+ "Germany 24\n",
458
+ "Name: count, dtype: int64\n",
459
  "\n",
460
  "Sample results:\n",
461
  " real_name likely_country likely_nationality likely_profession\n",
 
475
  " Tags: ['girl', 'photorealistic', 'asian', 'woman', 'beautiful', 'celebrity', 'korean']\n",
476
  " Country: \n",
477
  " Nationality: \n",
478
+ "\n",
479
+ " Name: Irene Makina\n",
480
+ " Tags: ['woman', 'celebrity']\n",
481
+ " Country: \n",
482
+ " Nationality: \n",
483
+ "\n",
484
+ " Name: Red Velvet Irene\n",
485
+ " Tags: ['kpop', 'woman', 'celebrity', 'kpop idol']\n",
486
+ " Country: \n",
487
+ " Nationality: \n",
488
+ "\n",
489
+ " Name: RED VELVET Irene\n",
490
+ " Tags: ['kpop', 'woman', 'celebrity', 'irene', 'redvelvet']\n",
491
+ " Country: \n",
492
+ " Nationality: \n",
493
+ "\n",
494
+ " Name: Irene\n",
495
+ " Tags: ['singer', 'k-pop idol', 'actress', 'celebrity', 'korean', 'irene', 'bae joo-hyun']\n",
496
+ " Country: \n",
497
+ " Nationality: \n",
498
+ "\n",
499
+ " Name: Irene\n",
500
+ " Tags: ['singer', 'photorealistic', 'asian', 'idol', 'dancer', 'woman', 'celebrity']\n",
501
+ " Country: \n",
502
+ " Nationality: \n",
503
+ "\n",
504
+ " Name: Irene Makina\n",
505
+ " Tags: ['kpop', 'celebrity']\n",
506
+ " Country: \n",
507
+ " Nationality: \n",
508
+ "\n",
509
+ " Name: irene주현\n",
510
+ " Tags: ['photorealistic', 'sexy', 'female', 'woman', 'celebrity', 'girls', 'realistic']\n",
511
+ " Country: \n",
512
+ " Nationality: \n",
513
+ "\n",
514
+ " Name: Irene\n",
515
+ " Tags: ['singer', 'woman', 'celebrity']\n",
516
+ " Country: \n",
517
+ " Nationality: \n",
518
+ "\n",
519
+ " Name: Red Velvet Irene\n",
520
+ " Tags: ['kpop', 'celebrity']\n",
521
+ " Country: \n",
522
+ " Nationality: \n",
523
+ "\n",
524
+ " Name: Irene Schouten\n",
525
+ " Tags: ['instagram', 'woman', 'celebrity', 'dutch', 'sporty', 'speed skating']\n",
526
+ " Country: \n",
527
+ " Nationality: \n",
528
+ "\n",
529
+ " Name: Irene\n",
530
+ " Tags: ['photorealistic', 'female', 'woman', 'celebrity', 'realistic']\n",
531
+ " Country: \n",
532
+ " Nationality: \n",
533
+ "\n",
534
+ " Name: Irene\n",
535
+ " Tags: ['photorealistic', 'female', 'kpop', 'idol', 'woman', 'celebrity', 'girls']\n",
536
+ " Country: \n",
537
+ " Nationality: \n",
538
+ "\n",
539
+ " Name: Irene\n",
540
+ " Tags: ['kpop', 'woman', 'celebrity', 'kpop girl']\n",
541
+ " Country: \n",
542
+ " Nationality: \n",
543
+ "\n",
544
+ " Name: Irene Rouse\n",
545
+ " Tags: ['person', 'photorealistic', 'female', 'cosplay', 'woman', 'celebrity', 'girls']\n",
546
+ " Country: \n",
547
+ " Nationality: \n",
548
+ "\n",
549
+ " Name: IreneDiffusion\n",
550
+ " Tags: ['irene', 'irene montero', 'montero']\n",
551
+ " Country: \n",
552
+ " Nationality: \n",
553
+ "\n",
554
+ " Name: Valory Irene\n",
555
+ " Tags: ['female', 'woman', 'celebrity']\n",
556
+ " Country: \n",
557
+ " Nationality: \n",
558
+ "\n",
559
+ " Name: Irene\n",
560
+ " Tags: ['singer', 'asian', 'kpop', 'actress', 'celeb', 'celebrity', 'korean']\n",
561
+ " Country: \n",
562
+ " Nationality: \n",
563
+ "\n",
564
+ " Name: Irene Red Velvet\n",
565
+ " Tags: ['kpop', 'celebrity']\n",
566
+ " Country: \n",
567
+ " Nationality: \n",
568
+ "\n",
569
+ " Name: Irene Rouse - Adult Film Actress\n",
570
+ " Tags: ['photorealistic', 'sexy', 'woman', 'celebrity', 'girls', 'realistic', 'lora']\n",
571
+ " Country: \n",
572
+ " Nationality: \n",
573
+ "\n",
574
+ " Name: Irene Rouse - Adult Film Actress\n",
575
+ " Tags: ['photorealistic', 'sexy', 'woman', 'celebrity', 'girls', 'realistic', 'lora']\n",
576
+ " Country: \n",
577
+ " Nationality: \n",
578
+ "\n",
579
+ " Name: Irene Red Velvet\n",
580
+ " Tags: ['photorealistic', 'kpop', 'woman', 'celebrity', 'korean', 'kpop idol']\n",
581
+ " Country: \n",
582
+ " Nationality: \n",
583
+ "\n",
584
+ " Name: 麦橘复古少女IreneMajicflus\n",
585
+ " Tags: ['style']\n",
586
+ " Country: \n",
587
+ " Nationality: \n",
588
+ "\n",
589
+ " Name: Irene F1\n",
590
+ " Tags: ['woman', 'actor', 'celebrity']\n",
591
+ " Country: \n",
592
+ " Nationality: \n",
593
+ "\n",
594
+ " Name: Irene\n",
595
+ " Tags: ['celebrity']\n",
596
+ " Country: \n",
597
+ " Nationality: \n",
598
+ "\n",
599
+ " Name: IRENE\n",
600
+ " Tags: ['realism', 'kpop', 'idol', 'woman', 'celebrity']\n",
601
+ " Country: \n",
602
+ " Nationality: \n",
603
+ "\n",
604
+ " Name: Irene Rouse\n",
605
+ " Tags: ['photorealistic', 'sexy', 'woman', 'celebrity', 'girls', 'realistic', 'lora']\n",
606
+ " Country: \n",
607
+ " Nationality: \n",
608
+ "\n",
609
+ " Name: Valory Irene\n",
610
+ " Tags: ['girl', 'sexy', 'model', 'woman', 'celebrity']\n",
611
+ " Country: \n",
612
+ " Nationality: \n",
613
+ "\n",
614
+ " Name: Irene Rouse\n",
615
+ " Tags: ['woman', 'celebrity']\n",
616
+ " Country: \n",
617
+ " Nationality: \n",
618
+ "\n",
619
+ " Name: Irene Dirdal\n",
620
+ " Tags: ['photorealistic', 'woman', 'celebrity', 'girls', 'realistic', 'lora']\n",
621
+ " Country: \n",
622
+ " Nationality: \n",
623
+ "\n",
624
+ " Name: Irene Cara\n",
625
+ " Tags: ['singer', 'woman', 'actress', 'celebrity', 'irene cara']\n",
626
+ " Country: \n",
627
+ " Nationality: \n",
628
+ "\n",
629
+ " Name: Irene Rouse\n",
630
+ " Tags: ['colombia', 'instagram', 'latina', 'woman', 'celebrity', 'realistic', 'sd 1.5']\n",
631
+ " Country: Colombia\n",
632
+ " Nationality: \n",
633
+ "\n",
634
+ " Name: Irene Caruso -\n",
635
+ " Tags: ['model', 'celebrity', 'influencer']\n",
636
+ " Country: \n",
637
+ " Nationality: \n",
638
  "\n"
639
  ]
640
  }
 
647
  "current_dir = Path.cwd()\n",
648
  "countries_file = current_dir.parent / \"misc/lists/countries.csv\"\n",
649
  "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
650
+ "input_file = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_01_NER.csv\"\n",
651
+ "output_file = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_02_NER.csv\"\n",
652
  "\n",
653
  "# Load datasets\n",
654
  "poi_df = pd.read_csv(input_file)\n",
 
806
  " print()\n"
807
  ]
808
  },
 
 
 
 
 
 
809
  {
810
  "cell_type": "markdown",
811
  "id": "deepseek_header",
 
824
  },
825
  {
826
  "cell_type": "code",
827
+ "execution_count": 13,
828
  "id": "deepseek_code",
829
+ "metadata": {
830
+ "execution": {
831
+ "iopub.execute_input": "2025-11-21T11:01:40.377178Z",
832
+ "iopub.status.busy": "2025-11-21T11:01:40.376988Z",
833
+ "iopub.status.idle": "2025-11-21T11:02:15.369105Z",
834
+ "shell.execute_reply": "2025-11-21T11:02:15.368626Z",
835
+ "shell.execute_reply.started": "2025-11-21T11:01:40.377164Z"
836
+ }
837
+ },
838
  "outputs": [
839
  {
840
  "name": "stdout",
841
  "output_type": "stream",
842
  "text": [
843
+ "✅ Deepseek API key loaded\n",
844
+ "✅ Loaded profession mapping with 9 categories\n",
845
+ "Loaded 50861 rows\n",
846
+ "\n",
847
+ "Profession categories (9):\n",
848
+ " - actor\n",
849
+ " - adult performer\n",
850
+ " - singer/musician\n",
851
+ " - model\n",
852
+ " - online personality\n",
853
+ " - public figure\n",
854
+ " - voice actor/ASMR\n",
855
+ " - sports professional\n",
856
+ " - tv personality\n",
857
+ "\n",
858
+ "Running in TEST MODE with 10 samples\n",
859
+ "\n",
860
+ "Annotating with DEEPSEEK...\n",
861
+ "Processing rows 0 to 10\n"
862
+ ]
863
+ },
864
+ {
865
+ "name": "stderr",
866
+ "output_type": "stream",
867
+ "text": [
868
+ "Annotating: 90%|█████████ | 9/10 [00:30<00:03, 3.39s/it]"
869
+ ]
870
+ },
871
+ {
872
+ "name": "stdout",
873
+ "output_type": "stream",
874
+ "text": [
875
+ "✅ Progress saved after 10 rows\n"
876
  ]
877
  },
878
  {
879
+ "name": "stderr",
880
+ "output_type": "stream",
881
+ "text": [
882
+ "Annotating: 100%|██████████| 10/10 [00:34<00:00, 3.40s/it]"
883
+ ]
884
+ },
885
+ {
886
+ "name": "stdout",
887
+ "output_type": "stream",
888
+ "text": [
889
+ "\n",
890
+ "✅ Done! Final results saved to /shares/weddigen.ki.uzh/laura_wagner/phase_01/pm-paper/data/CSV/model_adapter/real_person_adapter_step_03_deepseek_test.csv\n",
891
+ "\n",
892
+ "=== Summary Statistics ===\n",
893
+ "Total processed: 10\n",
894
+ "\n",
895
+ "Gender distribution:\n",
896
+ "gender\n",
897
+ "Female 10\n",
898
+ "Name: count, dtype: int64\n",
899
+ "\n",
900
+ "Top 10 profession combinations:\n",
901
+ "profession_llm\n",
902
+ "actor, singer/musician, model 2\n",
903
+ "singer/musician, actor, tv personality 1\n",
904
+ "model, online personality 1\n",
905
+ "singer/musician, actor, model 1\n",
906
+ "singer/musician, voice actor/ASMR, online personality 1\n",
907
+ "singer/musician, model, online personality 1\n",
908
+ "model, online personality, actor 1\n",
909
+ "actor, public figure, model 1\n",
910
+ "actor, model, public figure 1\n",
911
+ "Name: count, dtype: int64\n",
912
+ "\n",
913
+ "Top 10 countries:\n",
914
+ "country\n",
915
+ "South Korea 3\n",
916
+ "China 2\n",
917
+ "Japan 2\n",
918
+ "Unknown 1\n",
919
+ "United Kingdom 1\n",
920
+ "Israel 1\n",
921
+ "Name: count, dtype: int64\n",
922
+ "\n",
923
+ "=== Sample Results ===\n",
924
+ " real_name full_name gender profession_llm country\n",
925
+ " IU Lee Ji-eun Female singer/musician, actor, tv personality South Korea\n",
926
+ "Super Pose Book Unknown Female model, online personality Unknown\n",
927
+ " Liyuu Liyuu Female singer/musician, voice actor/ASMR, online personality China\n",
928
+ " Irene Irene Bae Female singer/musician, actor, model South Korea\n",
929
+ " AESPA Karina Yu Ji-min Female singer/musician, model, online personality South Korea\n",
930
+ " Saika Kawakita Saika Kawakita Female model, online personality, actor Japan\n",
931
+ " Liu Yifei Liu Yifei Female actor, singer/musician, model China\n",
932
+ " HashimotoKanna Kanna Hashimoto Female actor, singer/musician, model Japan\n",
933
+ " Emma Watson Emma Charlotte Duerre Watson Female actor, public figure, model United Kingdom\n",
934
+ " Gal Gadot Gal Gadot Female actor, model, public figure Israel\n"
935
+ ]
936
+ },
937
+ {
938
+ "name": "stderr",
939
+ "output_type": "stream",
940
+ "text": [
941
+ "\n"
942
  ]
943
  }
944
  ],
 
953
  "from tqdm import tqdm\n",
954
  "\n",
955
  "current_dir = Path.cwd()\n",
956
+ "input_file = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_02_NER.csv\"\n",
957
  "professions_file = current_dir.parent / \"misc/lists/professions.csv\"\n",
958
  "professions_mapped_file = current_dir.parent / \"misc/lists/professions_mapped.csv\"\n",
959
  "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\"\n",
 
1131
  " return fields\n",
1132
  "\n",
1133
  "# === PROCESS DATA ===\n",
1134
+ "output_file = current_dir.parent / f\"data/CSV/model_adapter/real_person_adapter_step_03_deepseek{'_test' if TEST_MODE else ''}.csv\"\n",
1135
+ "index_file = current_dir.parent / \"misc/query_indicies/deepseek_query_index.txt\"\n",
1136
  "\n",
1137
  "# Check for existing progress\n",
1138
  "current_index = 0\n",
 
1225
  "cell_type": "code",
1226
  "execution_count": null,
1227
  "id": "qwen_code",
1228
+ "metadata": {
1229
+ "execution": {
1230
+ "iopub.status.busy": "2025-11-20T21:12:14.048643Z",
1231
+ "iopub.status.idle": "2025-11-20T21:12:14.048904Z",
1232
+ "shell.execute_reply": "2025-11-20T21:12:14.048734Z",
1233
+ "shell.execute_reply.started": "2025-11-20T21:12:14.048726Z"
1234
+ }
1235
+ },
1236
  "outputs": [],
1237
  "source": [
1238
  "import pandas as pd\n",
 
1945
  "cell_type": "markdown",
1946
  "id": "01086b35",
1947
  "metadata": {},
1948
+ "source": [
1949
+ "## Qwen-2.5-32B Local Annotation (Hugging Face Transformers)\n",
1950
+ "\n",
1951
+ "Run **Qwen-2.5-32B-Instruct** locally using Hugging Face Transformers - NO API COSTS!\n",
1952
+ "\n",
1953
+ "**Requirements:**\n",
1954
+ "- GPU: NVIDIA A100 80GB or similar (32B model needs ~60-70GB VRAM)\n",
1955
+ "- RAM: 128GB+ system RAM recommended\n",
1956
+ "- Model cache: `data/models/` (auto-downloaded on first run)\n",
1957
+ "- Model size: ~65GB download (BF16 weights)\n",
1958
+ "\n",
1959
+ "**Installation:**\n",
1960
+ "```bash\n",
1961
+ "# Install required packages\n",
1962
+ "pip install torch transformers accelerate\n",
1963
+ "```\n",
1964
+ "\n",
1965
+ "**Advantages:**\n",
1966
+ "- ✅ FREE - No API costs\n",
1967
+ "- ✅ Fast - Local inference with GPU acceleration\n",
1968
+ "- ✅ Privacy - Data never leaves your machine\n",
1969
+ "- ✅ Offline - Works without internet (after download)\n",
1970
+ "- ✅ High quality - 32B parameter instruction-tuned model\n",
1971
+ "\n",
1972
+ "**Performance:**\n",
1973
+ "- Speed: ~10-30 tokens/sec on A100 (depends on batch size)\n",
1974
+ "- Throughput: ~200-500 samples/hour\n",
1975
+ "- Cost: $0 (after model download)\n",
1976
+ "\n",
1977
+ "**Model Info:**\n",
1978
+ "- HuggingFace: `Qwen/Qwen2.5-32B-Instruct`\n",
1979
+ "- License: Apache 2.0 (commercial use allowed)\n",
1980
+ "- Context: 32K tokens"
1981
+ ]
1982
  },
1983
  {
1984
  "cell_type": "code",
 
1986
  "id": "5dd7a803",
1987
  "metadata": {},
1988
  "outputs": [],
1989
+ "source": [
1990
+ "import pandas as pd\n",
1991
+ "import json\n",
1992
+ "import time\n",
1993
+ "import re\n",
1994
+ "from pathlib import Path\n",
1995
+ "from tqdm import tqdm\n",
1996
+ "import torch\n",
1997
+ "from transformers import AutoModelForCausalLM, AutoTokenizer\n",
1998
+ "\n",
1999
+ "current_dir = Path.cwd()\n",
2000
+ "input_file = current_dir.parent / \"data/CSV/NER_POI_step02_annotated.csv\"\n",
2001
+ "\n",
2002
+ "# === CONFIGURATION ===\n",
2003
+ "TEST_MODE = True\n",
2004
+ "TEST_SIZE = 10\n",
2005
+ "MAX_ROWS = 20000\n",
2006
+ "SAVE_INTERVAL = 10\n",
2007
+ "\n",
2008
+ "# Model settings\n",
2009
+ "MODEL_NAME = \"Qwen/Qwen2.5-32B-Instruct\"\n",
2010
+ "CACHE_DIR = current_dir.parent / \"data/models\"\n",
2011
+ "CACHE_DIR.mkdir(parents=True, exist_ok=True)\n",
2012
+ "\n",
2013
+ "PROFESSION_CATEGORIES = [\n",
2014
+ " \"actor\", \"adult performer\", \"singer/musician\", \"model\",\n",
2015
+ " \"online personality\", \"public figure\", \"voice actor/ASMR\",\n",
2016
+ " \"sports professional\", \"tv personality\"\n",
2017
+ "]\n",
2018
+ "\n",
2019
+ "# === LOAD MODEL ===\n",
2020
+ "print(f\"Loading model: {MODEL_NAME}\")\n",
2021
+ "print(f\"Cache directory: {CACHE_DIR}\")\n",
2022
+ "print(f\"This may take a while on first run (~65GB download)...\\n\")\n",
2023
+ "\n",
2024
+ "# Check GPU availability\n",
2025
+ "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
2026
+ "print(f\"Device: {device}\")\n",
2027
+ "\n",
2028
+ "if device == \"cpu\":\n",
2029
+ " print(\"⚠️ WARNING: No GPU detected! Inference will be VERY slow.\")\n",
2030
+ " print(\" Consider using a GPU or reducing model size.\")\n",
2031
+ "\n",
2032
+ "# Load tokenizer\n",
2033
+ "print(\"Loading tokenizer...\")\n",
2034
+ "tokenizer = AutoTokenizer.from_pretrained(\n",
2035
+ " MODEL_NAME,\n",
2036
+ " cache_dir=str(CACHE_DIR),\n",
2037
+ " trust_remote_code=True\n",
2038
+ ")\n",
2039
+ "print(\"✅ Tokenizer loaded\")\n",
2040
+ "\n",
2041
+ "# Load model with optimizations\n",
2042
+ "print(\"Loading model (this may take several minutes)...\")\n",
2043
+ "model = AutoModelForCausalLM.from_pretrained(\n",
2044
+ " MODEL_NAME,\n",
2045
+ " cache_dir=str(CACHE_DIR),\n",
2046
+ " torch_dtype=torch.bfloat16, # Use BF16 for efficiency\n",
2047
+ " device_map=\"auto\", # Automatically distribute across GPUs\n",
2048
+ " trust_remote_code=True\n",
2049
+ ")\n",
2050
+ "model.eval() # Set to evaluation mode\n",
2051
+ "print(\"✅ Model loaded\")\n",
2052
+ "\n",
2053
+ "# Check VRAM usage\n",
2054
+ "if torch.cuda.is_available():\n",
2055
+ " vram_gb = torch.cuda.max_memory_allocated() / 1024**3\n",
2056
+ " print(f\"VRAM used: {vram_gb:.2f} GB\\n\")\n",
2057
+ "\n",
2058
+ "# === LOAD DATA ===\n",
2059
+ "df = pd.read_csv(input_file)\n",
2060
+ "print(f\"Loaded {len(df)} rows\")\n",
2061
+ "\n",
2062
+ "if TEST_MODE:\n",
2063
+ " print(f\"Running in TEST MODE with {TEST_SIZE} samples\")\n",
2064
+ " df = df.head(TEST_SIZE).copy()\n",
2065
+ "elif MAX_ROWS:\n",
2066
+ " df = df.head(MAX_ROWS).copy()\n",
2067
+ "\n",
2068
+ "# === CREATE PROMPT ===\n",
2069
+ "def create_prompt(name: str, tags: list, country: str) -> str:\n",
2070
+ " tags_str = \", \".join(tags[:5]) if tags else \"none\"\n",
2071
+ " country_str = country if country else \"unknown\"\n",
2072
+ " \n",
2073
+ " return f\"\"\"You are an expert at identifying public figures.\n",
2074
+ "\n",
2075
+ "Given: Name: {name}, Tags: {tags_str}, Country: {country_str}\n",
2076
+ "\n",
2077
+ "Provide in JSON format:\n",
2078
+ "{{\n",
2079
+ " \"full_name\": \"<full name>\",\n",
2080
+ " \"gender\": \"<Male/Female/Other/Unknown>\",\n",
2081
+ " \"professions\": [\"<profession 1>\", \"<profession 2>\", \"<profession 3>\"],\n",
2082
+ " \"country\": \"<country>\"\n",
2083
+ "}}\n",
2084
+ "\n",
2085
+ "Choose professions ONLY from: {\", \".join(PROFESSION_CATEGORIES)}\n",
2086
+ "- Max 3 professions, ordered by relevance\n",
2087
+ "- \"online personality\" includes streamers, cosplayers, influencers\n",
2088
+ "- Return \"Unknown\" if uncertain\n",
2089
+ "\n",
2090
+ "Return ONLY JSON, no extra text.\"\"\"\n",
2091
+ "\n",
2092
+ "# === QUERY QWEN LOCAL ===\n",
2093
+ "def query_qwen_local(prompt: str) -> dict:\n",
2094
+ " \"\"\"Query Qwen-2.5-32B locally via transformers.\"\"\"\n",
2095
+ " try:\n",
2096
+ " # Format as chat message\n",
2097
+ " messages = [\n",
2098
+ " {\"role\": \"user\", \"content\": prompt}\n",
2099
+ " ]\n",
2100
+ " \n",
2101
+ " # Tokenize\n",
2102
+ " text = tokenizer.apply_chat_template(\n",
2103
+ " messages,\n",
2104
+ " tokenize=False,\n",
2105
+ " add_generation_prompt=True\n",
2106
+ " )\n",
2107
+ " \n",
2108
+ " inputs = tokenizer([text], return_tensors=\"pt\").to(device)\n",
2109
+ " \n",
2110
+ " # Generate\n",
2111
+ " with torch.no_grad():\n",
2112
+ " outputs = model.generate(\n",
2113
+ " **inputs,\n",
2114
+ " max_new_tokens=512,\n",
2115
+ " temperature=0.1,\n",
2116
+ " do_sample=True,\n",
2117
+ " top_p=0.9,\n",
2118
+ " pad_token_id=tokenizer.eos_token_id\n",
2119
+ " )\n",
2120
+ " \n",
2121
+ " # Decode\n",
2122
+ " generated_ids = outputs[0][inputs['input_ids'].shape[1]:]\n",
2123
+ " response = tokenizer.decode(generated_ids, skip_special_tokens=True)\n",
2124
+ " \n",
2125
+ " # Parse JSON from response\n",
2126
+ " json_match = re.search(r'\\{[^}]+\\}', response, re.DOTALL)\n",
2127
+ " if json_match:\n",
2128
+ " return json.loads(json_match.group())\n",
2129
+ " \n",
2130
+ " # Try markdown code block\n",
2131
+ " code_block = re.search(r'```(?:json)?\\s*(\\{[^}]+\\})\\s*```', response, re.DOTALL)\n",
2132
+ " if code_block:\n",
2133
+ " return json.loads(code_block.group(1))\n",
2134
+ " \n",
2135
+ " return None\n",
2136
+ " \n",
2137
+ " except json.JSONDecodeError as e:\n",
2138
+ " print(f\"JSON parse error: {e}\")\n",
2139
+ " return None\n",
2140
+ " except Exception as e:\n",
2141
+ " print(f\"Generation error: {e}\")\n",
2142
+ " return None\n",
2143
+ "\n",
2144
+ "# === PROCESS ===\n",
2145
+ "output_file = current_dir.parent / f\"data/CSV/qwen_local_annotated_POI{'_test' if TEST_MODE else ''}.csv\"\n",
2146
+ "index_file = current_dir.parent / \"misc/qwen_local_query_index.txt\"\n",
2147
+ "\n",
2148
+ "current_index = 0\n",
2149
+ "if index_file.exists():\n",
2150
+ " with open(index_file) as f:\n",
2151
+ " current_index = int(f.read().strip())\n",
2152
+ " print(f\"Resuming from index {current_index}\")\n",
2153
+ "\n",
2154
+ "for col in ['full_name', 'gender', 'profession_llm', 'country']:\n",
2155
+ " if col not in df.columns:\n",
2156
+ " df[col] = 'Unknown'\n",
2157
+ "\n",
2158
+ "print(f\"\\nAnnotating with QWEN-2.5-32B LOCAL - rows {current_index} to {len(df)}...\")\n",
2159
+ "print(f\"Model: {MODEL_NAME}\")\n",
2160
+ "print(f\"This may take a while (~200-500 samples/hour on A100)\\n\")\n",
2161
+ "\n",
2162
+ "try:\n",
2163
+ " start_time = time.time()\n",
2164
+ " \n",
2165
+ " for i in tqdm(range(current_index, len(df)), desc=\"Qwen Local\"):\n",
2166
+ " row = df.iloc[i]\n",
2167
+ " \n",
2168
+ " tags = [str(row[f'tag_{j}']).lower() for j in range(1, 8)\n",
2169
+ " if f'tag_{j}' in row and pd.notna(row[f'tag_{j}'])]\n",
2170
+ " \n",
2171
+ " prompt = create_prompt(\n",
2172
+ " row.get('real_name', row.get('name', '')),\n",
2173
+ " tags,\n",
2174
+ " row.get('likely_country', '')\n",
2175
+ " )\n",
2176
+ " \n",
2177
+ " result = query_qwen_local(prompt)\n",
2178
+ " \n",
2179
+ " if result:\n",
2180
+ " df.at[i, 'full_name'] = result.get('full_name', 'Unknown')\n",
2181
+ " df.at[i, 'gender'] = result.get('gender', 'Unknown')\n",
2182
+ " profs = result.get('professions', [])\n",
2183
+ " df.at[i, 'profession_llm'] = \", \".join(profs[:3]) if isinstance(profs, list) else str(profs)\n",
2184
+ " df.at[i, 'country'] = result.get('country', 'Unknown')\n",
2185
+ " \n",
2186
+ " current_index = i + 1\n",
2187
+ " \n",
2188
+ " if (i + 1) % SAVE_INTERVAL == 0 or (i + 1) == len(df):\n",
2189
+ " df.to_csv(output_file, index=False)\n",
2190
+ " with open(index_file, 'w') as f:\n",
2191
+ " f.write(str(current_index))\n",
2192
+ " \n",
2193
+ " # Calculate stats\n",
2194
+ " elapsed = time.time() - start_time\n",
2195
+ " samples_done = current_index - (0 if not index_file.exists() else int(open(index_file).read().strip()))\n",
2196
+ " samples_per_hour = (samples_done / elapsed) * 3600 if elapsed > 0 else 0\n",
2197
+ " \n",
2198
+ " print(f\"✅ Saved after {i+1} rows (~{samples_per_hour:.1f} samples/hour)\")\n",
2199
+ " \n",
2200
+ " elapsed_total = time.time() - start_time\n",
2201
+ " print(f\"\\n✅ Done! Results: {output_file}\")\n",
2202
+ " print(f\"Total time: {elapsed_total/60:.1f} minutes\")\n",
2203
+ " print(f\"Average speed: {len(df)/(elapsed_total/3600):.1f} samples/hour\")\n",
2204
+ " \n",
2205
+ " print(f\"\\nTop professions:\\n{df['profession_llm'].value_counts().head(10)}\")\n",
2206
+ " \n",
2207
+ " # Show VRAM stats\n",
2208
+ " if torch.cuda.is_available():\n",
2209
+ " print(f\"\\nFinal VRAM usage: {torch.cuda.max_memory_allocated() / 1024**3:.2f} GB\")\n",
2210
+ "\n",
2211
+ "except KeyboardInterrupt:\n",
2212
+ " print(f\"\\n⚠️ Interrupted by user\")\n",
2213
+ " df.to_csv(output_file, index=False)\n",
2214
+ " with open(index_file, 'w') as f:\n",
2215
+ " f.write(str(current_index))\n",
2216
+ " print(f\"Progress saved up to row {current_index}\")\n",
2217
+ "\n",
2218
+ "except Exception as e:\n",
2219
+ " print(f\"⚠️ Error: {e}\")\n",
2220
+ " df.to_csv(output_file, index=False)\n",
2221
+ " with open(index_file, 'w') as f:\n",
2222
+ " f.write(str(current_index))\n",
2223
+ " print(f\"Progress saved up to row {current_index}\")"
2224
+ ]
2225
  }
2226
  ],
2227
  "metadata": {
2228
+ "kernelspec": {
2229
+ "display_name": "pm-paper",
2230
+ "language": "python",
2231
+ "name": "pm-paper"
2232
+ },
2233
  "language_info": {
2234
+ "codemirror_mode": {
2235
+ "name": "ipython",
2236
+ "version": 3
2237
+ },
2238
+ "file_extension": ".py",
2239
+ "mimetype": "text/x-python",
2240
+ "name": "python",
2241
+ "nbconvert_exporter": "python",
2242
+ "pygments_lexer": "ipython3",
2243
+ "version": "3.11.13"
2244
  }
2245
  },
2246
  "nbformat": 4,
2247
  "nbformat_minor": 5
2248
+ }
jupyter_notebooks/Section_2-3-4__Figure_8_Deepfake_victims.ipynb CHANGED
@@ -26,9 +26,17 @@
26
  },
27
  {
28
  "cell_type": "code",
29
- "execution_count": 1,
30
  "id": "f177df11",
31
- "metadata": {},
 
 
 
 
 
 
 
 
32
  "outputs": [],
33
  "source": [
34
  "import pandas as pd\n",
@@ -41,40 +49,55 @@
41
  },
42
  {
43
  "cell_type": "code",
44
- "execution_count": 2,
45
  "id": "a2383f32",
46
- "metadata": {},
 
 
 
 
 
 
 
 
47
  "outputs": [],
48
  "source": [
49
  "current_dir = Path.cwd()\n",
50
- "poi_models_dir = current_dir.parent / \"data/CSV/model_subsets/POI_models.csv\" ### POI models dataset\n",
51
- "output = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_01.csv\" ### Output file"
52
  ]
53
  },
54
  {
55
  "cell_type": "code",
56
- "execution_count": 3,
57
  "id": "66fc691f",
58
- "metadata": {},
59
- "outputs": [
60
- {
61
- "name": "stderr",
62
- "output_type": "stream",
63
- "text": [
64
- "/home/lauhp/anaconda3/envs/latm/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
65
- " from .autonotebook import tqdm as notebook_tqdm\n"
66
- ]
67
  }
68
- ],
 
69
  "source": [
70
  "nlp = spacy.load(\"en_core_web_sm\") # or another model of your choice"
71
  ]
72
  },
73
  {
74
  "cell_type": "code",
75
- "execution_count": 4,
76
  "id": "8348c4a4",
77
- "metadata": {},
 
 
 
 
 
 
 
 
78
  "outputs": [
79
  {
80
  "name": "stdout",
@@ -174,20 +197,28 @@
174
  },
175
  {
176
  "cell_type": "code",
177
- "execution_count": 5,
178
  "id": "414954ed",
179
- "metadata": {},
 
 
 
 
 
 
 
 
180
  "outputs": [
181
  {
182
  "name": "stdout",
183
  "output_type": "stream",
184
  "text": [
185
- " real_name likely_country likely_nationality likely_profession\n",
186
- "0 ronnie alonte model\n",
187
- "1 zh elena kamperi twitch streamer\n",
188
- "2 sofia vergara model\n",
189
- "3 安然 anran celebrity\n",
190
- "4 zoe kravitz celebrity\n"
191
  ]
192
  }
193
  ],
@@ -199,8 +230,8 @@
199
  "current_dir = Path.cwd()\n",
200
  "countries = current_dir.parent / \"misc/lists/countries.csv\"\n",
201
  "professions = current_dir.parent / \"misc/lists/professions.csv\"\n",
202
- "inputNER = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_01.csv\"\n",
203
- "outfile = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_02.csv\"\n",
204
  "\n",
205
  "# Load datasets\n",
206
  "poi_df = pd.read_csv(inputNER)\n",
@@ -289,9 +320,17 @@
289
  },
290
  {
291
  "cell_type": "code",
292
- "execution_count": 6,
293
  "id": "054f230b",
294
- "metadata": {},
 
 
 
 
 
 
 
 
295
  "outputs": [],
296
  "source": [
297
  "#!pip install transformers torch\n",
@@ -309,9 +348,17 @@
309
  },
310
  {
311
  "cell_type": "code",
312
- "execution_count": 7,
313
  "id": "504b970f",
314
- "metadata": {},
 
 
 
 
 
 
 
 
315
  "outputs": [],
316
  "source": [
317
  "#!pip install openpyxl"
@@ -319,23 +366,38 @@
319
  },
320
  {
321
  "cell_type": "code",
322
- "execution_count": 10,
323
  "id": "7c209115",
324
- "metadata": {},
 
 
 
 
 
 
 
 
325
  "outputs": [
326
  {
327
  "name": "stdout",
328
  "output_type": "stream",
329
  "text": [
330
- "Row 1/4...\n",
331
- "Saved up to row 1\n",
332
- "Row 2/4...\n",
333
- "Saved up to row 2\n",
334
- "Row 3/4...\n",
335
- "Saved up to row 3\n",
336
- "Row 4/4...\n",
337
- "Saved up to row 4\n",
338
- "All done! Files: /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/data/CSV/Deepseek_annotated_POI.csv /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/data/CSV/Deepseek_annotated_POI.xlsx\n"
 
 
 
 
 
 
 
339
  ]
340
  }
341
  ],
@@ -349,13 +411,13 @@
349
  "\n",
350
  "# === PATHS & CONFIG ===\n",
351
  "current_dir = Path.cwd()\n",
352
- "inputCSV = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_02.csv\"\n",
353
  "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\" #store your API key under misc/credentials/deepseek_api_key.txt\n",
354
  "\n",
355
  "# Output both CSV and Excel for compatibility\n",
356
- "OUTPUT_CSV = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.csv\"\n",
357
- "OUTPUT_XLSX = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.xlsx\"\n",
358
- "INDEX_FILE = current_dir.parent / \"misc/deepseek_query_index.txt\"\n",
359
  "SAVE_INTERVAL = 1 # Save every N rows\n",
360
  "START_ROW = 1 # Row index to start from (0-based)\n",
361
  "END_ROW = 5 # Row index to end (exclusive)\n",
@@ -460,12 +522,6 @@
460
  "print(\"All done! Files:\", OUTPUT_CSV, OUTPUT_XLSX)\n"
461
  ]
462
  },
463
- {
464
- "cell_type": "markdown",
465
- "id": "7910e574",
466
- "metadata": {},
467
- "source": []
468
- },
469
  {
470
  "cell_type": "markdown",
471
  "id": "9d377005",
@@ -646,9 +702,9 @@
646
  ],
647
  "metadata": {
648
  "kernelspec": {
649
- "display_name": "latm",
650
  "language": "python",
651
- "name": "python3"
652
  },
653
  "language_info": {
654
  "codemirror_mode": {
@@ -660,7 +716,7 @@
660
  "name": "python",
661
  "nbconvert_exporter": "python",
662
  "pygments_lexer": "ipython3",
663
- "version": "3.10.15"
664
  }
665
  },
666
  "nbformat": 4,
 
26
  },
27
  {
28
  "cell_type": "code",
29
+ "execution_count": 21,
30
  "id": "f177df11",
31
+ "metadata": {
32
+ "execution": {
33
+ "iopub.execute_input": "2025-11-21T10:32:27.999485Z",
34
+ "iopub.status.busy": "2025-11-21T10:32:27.999298Z",
35
+ "iopub.status.idle": "2025-11-21T10:32:28.001776Z",
36
+ "shell.execute_reply": "2025-11-21T10:32:28.001287Z",
37
+ "shell.execute_reply.started": "2025-11-21T10:32:27.999469Z"
38
+ }
39
+ },
40
  "outputs": [],
41
  "source": [
42
  "import pandas as pd\n",
 
49
  },
50
  {
51
  "cell_type": "code",
52
+ "execution_count": 22,
53
  "id": "a2383f32",
54
+ "metadata": {
55
+ "execution": {
56
+ "iopub.execute_input": "2025-11-21T10:32:28.388500Z",
57
+ "iopub.status.busy": "2025-11-21T10:32:28.388376Z",
58
+ "iopub.status.idle": "2025-11-21T10:32:30.176781Z",
59
+ "shell.execute_reply": "2025-11-21T10:32:30.176262Z",
60
+ "shell.execute_reply.started": "2025-11-21T10:32:28.388488Z"
61
+ }
62
+ },
63
  "outputs": [],
64
  "source": [
65
  "current_dir = Path.cwd()\n",
66
+ "poi_models_dir = current_dir.parent / \"data/CSV/model_adapter/real_person_adapters.csv\" ### POI models dataset\n",
67
+ "output = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_01_NER.csv\" ### Output file"
68
  ]
69
  },
70
  {
71
  "cell_type": "code",
72
+ "execution_count": 15,
73
  "id": "66fc691f",
74
+ "metadata": {
75
+ "execution": {
76
+ "iopub.execute_input": "2025-11-21T10:23:43.615765Z",
77
+ "iopub.status.busy": "2025-11-21T10:23:43.615607Z",
78
+ "iopub.status.idle": "2025-11-21T10:23:47.279107Z",
79
+ "shell.execute_reply": "2025-11-21T10:23:47.278503Z",
80
+ "shell.execute_reply.started": "2025-11-21T10:23:43.615751Z"
 
 
81
  }
82
+ },
83
+ "outputs": [],
84
  "source": [
85
  "nlp = spacy.load(\"en_core_web_sm\") # or another model of your choice"
86
  ]
87
  },
88
  {
89
  "cell_type": "code",
90
+ "execution_count": 16,
91
  "id": "8348c4a4",
92
+ "metadata": {
93
+ "execution": {
94
+ "iopub.execute_input": "2025-11-21T10:23:47.279657Z",
95
+ "iopub.status.busy": "2025-11-21T10:23:47.279527Z",
96
+ "iopub.status.idle": "2025-11-21T10:24:34.043631Z",
97
+ "shell.execute_reply": "2025-11-21T10:24:34.042977Z",
98
+ "shell.execute_reply.started": "2025-11-21T10:23:47.279644Z"
99
+ }
100
+ },
101
  "outputs": [
102
  {
103
  "name": "stdout",
 
197
  },
198
  {
199
  "cell_type": "code",
200
+ "execution_count": 17,
201
  "id": "414954ed",
202
+ "metadata": {
203
+ "execution": {
204
+ "iopub.execute_input": "2025-11-21T10:27:41.992098Z",
205
+ "iopub.status.busy": "2025-11-21T10:27:41.991889Z",
206
+ "iopub.status.idle": "2025-11-21T10:27:49.779590Z",
207
+ "shell.execute_reply": "2025-11-21T10:27:49.778889Z",
208
+ "shell.execute_reply.started": "2025-11-21T10:27:41.992081Z"
209
+ }
210
+ },
211
  "outputs": [
212
  {
213
  "name": "stdout",
214
  "output_type": "stream",
215
  "text": [
216
+ " real_name likely_country likely_nationality likely_profession\n",
217
+ "0 iu South Korean celebrity\n",
218
+ "1 super pose \n",
219
+ "2 liyuu \n",
220
+ "3 irene South Korean celebrity\n",
221
+ "4 aespa karina South Korean celebrity\n"
222
  ]
223
  }
224
  ],
 
230
  "current_dir = Path.cwd()\n",
231
  "countries = current_dir.parent / \"misc/lists/countries.csv\"\n",
232
  "professions = current_dir.parent / \"misc/lists/professions.csv\"\n",
233
+ "inputNER = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_01_NER.csv\"\n",
234
+ "outfile = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_02_NER.csv\"\n",
235
  "\n",
236
  "# Load datasets\n",
237
  "poi_df = pd.read_csv(inputNER)\n",
 
320
  },
321
  {
322
  "cell_type": "code",
323
+ "execution_count": 18,
324
  "id": "054f230b",
325
+ "metadata": {
326
+ "execution": {
327
+ "iopub.execute_input": "2025-11-21T10:27:58.250798Z",
328
+ "iopub.status.busy": "2025-11-21T10:27:58.250588Z",
329
+ "iopub.status.idle": "2025-11-21T10:27:58.253063Z",
330
+ "shell.execute_reply": "2025-11-21T10:27:58.252570Z",
331
+ "shell.execute_reply.started": "2025-11-21T10:27:58.250780Z"
332
+ }
333
+ },
334
  "outputs": [],
335
  "source": [
336
  "#!pip install transformers torch\n",
 
348
  },
349
  {
350
  "cell_type": "code",
351
+ "execution_count": 3,
352
  "id": "504b970f",
353
+ "metadata": {
354
+ "execution": {
355
+ "iopub.execute_input": "2025-11-21T10:02:17.101296Z",
356
+ "iopub.status.busy": "2025-11-21T10:02:17.101174Z",
357
+ "iopub.status.idle": "2025-11-21T10:02:17.103118Z",
358
+ "shell.execute_reply": "2025-11-21T10:02:17.102724Z",
359
+ "shell.execute_reply.started": "2025-11-21T10:02:17.101283Z"
360
+ }
361
+ },
362
  "outputs": [],
363
  "source": [
364
  "#!pip install openpyxl"
 
366
  },
367
  {
368
  "cell_type": "code",
369
+ "execution_count": 19,
370
  "id": "7c209115",
371
+ "metadata": {
372
+ "execution": {
373
+ "iopub.execute_input": "2025-11-21T10:29:28.176565Z",
374
+ "iopub.status.busy": "2025-11-21T10:29:28.176373Z",
375
+ "iopub.status.idle": "2025-11-21T10:30:01.404034Z",
376
+ "shell.execute_reply": "2025-11-21T10:30:01.403269Z",
377
+ "shell.execute_reply.started": "2025-11-21T10:29:28.176550Z"
378
+ }
379
+ },
380
  "outputs": [
381
  {
382
  "name": "stdout",
383
  "output_type": "stream",
384
  "text": [
385
+ "Row 1/4...\n"
386
+ ]
387
+ },
388
+ {
389
+ "ename": "ModuleNotFoundError",
390
+ "evalue": "No module named 'openpyxl'",
391
+ "output_type": "error",
392
+ "traceback": [
393
+ "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
394
+ "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)",
395
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[19]\u001b[39m\u001b[32m, line 112\u001b[39m\n\u001b[32m 110\u001b[39m out_df.to_csv(OUTPUT_CSV, index=\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[32m 111\u001b[39m \u001b[38;5;66;03m# Excel\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m112\u001b[39m \u001b[43mout_df\u001b[49m\u001b[43m.\u001b[49m\u001b[43mto_excel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mOUTPUT_XLSX\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mindex\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\n\u001b[32m 113\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(INDEX_FILE, \u001b[33m'\u001b[39m\u001b[33mw\u001b[39m\u001b[33m'\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[32m 114\u001b[39m f.write(\u001b[38;5;28mstr\u001b[39m(current_index))\n",
396
+ "\u001b[36mFile \u001b[39m\u001b[32m/shares/weddigen.ki.uzh/laura_wagner/phase_01/pm-paper/.venv/lib/python3.11/site-packages/pandas/util/_decorators.py:333\u001b[39m, in \u001b[36mdeprecate_nonkeyword_arguments.<locals>.decorate.<locals>.wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 327\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(args) > num_allow_args:\n\u001b[32m 328\u001b[39m warnings.warn(\n\u001b[32m 329\u001b[39m msg.format(arguments=_format_argument_list(allow_args)),\n\u001b[32m 330\u001b[39m \u001b[38;5;167;01mFutureWarning\u001b[39;00m,\n\u001b[32m 331\u001b[39m stacklevel=find_stack_level(),\n\u001b[32m 332\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m333\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
397
+ "\u001b[36mFile \u001b[39m\u001b[32m/shares/weddigen.ki.uzh/laura_wagner/phase_01/pm-paper/.venv/lib/python3.11/site-packages/pandas/core/generic.py:2439\u001b[39m, in \u001b[36mNDFrame.to_excel\u001b[39m\u001b[34m(self, excel_writer, sheet_name, na_rep, float_format, columns, header, index, index_label, startrow, startcol, engine, merge_cells, inf_rep, freeze_panes, storage_options, engine_kwargs)\u001b[39m\n\u001b[32m 2426\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpandas\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mio\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mformats\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mexcel\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m ExcelFormatter\n\u001b[32m 2428\u001b[39m formatter = ExcelFormatter(\n\u001b[32m 2429\u001b[39m df,\n\u001b[32m 2430\u001b[39m na_rep=na_rep,\n\u001b[32m (...)\u001b[39m\u001b[32m 2437\u001b[39m inf_rep=inf_rep,\n\u001b[32m 2438\u001b[39m )\n\u001b[32m-> \u001b[39m\u001b[32m2439\u001b[39m \u001b[43mformatter\u001b[49m\u001b[43m.\u001b[49m\u001b[43mwrite\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2440\u001b[39m \u001b[43m \u001b[49m\u001b[43mexcel_writer\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2441\u001b[39m \u001b[43m \u001b[49m\u001b[43msheet_name\u001b[49m\u001b[43m=\u001b[49m\u001b[43msheet_name\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2442\u001b[39m \u001b[43m \u001b[49m\u001b[43mstartrow\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstartrow\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2443\u001b[39m \u001b[43m \u001b[49m\u001b[43mstartcol\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstartcol\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2444\u001b[39m \u001b[43m \u001b[49m\u001b[43mfreeze_panes\u001b[49m\u001b[43m=\u001b[49m\u001b[43mfreeze_panes\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2445\u001b[39m \u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[43m=\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2446\u001b[39m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2447\u001b[39m \u001b[43m \u001b[49m\u001b[43mengine_kwargs\u001b[49m\u001b[43m=\u001b[49m\u001b[43mengine_kwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2448\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n",
398
+ "\u001b[36mFile \u001b[39m\u001b[32m/shares/weddigen.ki.uzh/laura_wagner/phase_01/pm-paper/.venv/lib/python3.11/site-packages/pandas/io/formats/excel.py:943\u001b[39m, in \u001b[36mExcelFormatter.write\u001b[39m\u001b[34m(self, writer, sheet_name, startrow, startcol, freeze_panes, engine, storage_options, engine_kwargs)\u001b[39m\n\u001b[32m 941\u001b[39m need_save = \u001b[38;5;28;01mFalse\u001b[39;00m\n\u001b[32m 942\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m943\u001b[39m writer = \u001b[43mExcelWriter\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 944\u001b[39m \u001b[43m \u001b[49m\u001b[43mwriter\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 945\u001b[39m \u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[43m=\u001b[49m\u001b[43mengine\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 946\u001b[39m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 947\u001b[39m \u001b[43m \u001b[49m\u001b[43mengine_kwargs\u001b[49m\u001b[43m=\u001b[49m\u001b[43mengine_kwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 948\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 949\u001b[39m need_save = \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[32m 951\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n",
399
+ "\u001b[36mFile \u001b[39m\u001b[32m/shares/weddigen.ki.uzh/laura_wagner/phase_01/pm-paper/.venv/lib/python3.11/site-packages/pandas/io/excel/_openpyxl.py:57\u001b[39m, in \u001b[36mOpenpyxlWriter.__init__\u001b[39m\u001b[34m(self, path, engine, date_format, datetime_format, mode, storage_options, if_sheet_exists, engine_kwargs, **kwargs)\u001b[39m\n\u001b[32m 44\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m__init__\u001b[39m(\n\u001b[32m 45\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 46\u001b[39m path: FilePath | WriteExcelBuffer | ExcelWriter,\n\u001b[32m (...)\u001b[39m\u001b[32m 55\u001b[39m ) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 56\u001b[39m \u001b[38;5;66;03m# Use the openpyxl module as the Excel writer.\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m57\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mopenpyxl\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mworkbook\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Workbook\n\u001b[32m 59\u001b[39m engine_kwargs = combine_kwargs(engine_kwargs, kwargs)\n\u001b[32m 61\u001b[39m \u001b[38;5;28msuper\u001b[39m().\u001b[34m__init__\u001b[39m(\n\u001b[32m 62\u001b[39m path,\n\u001b[32m 63\u001b[39m mode=mode,\n\u001b[32m (...)\u001b[39m\u001b[32m 66\u001b[39m engine_kwargs=engine_kwargs,\n\u001b[32m 67\u001b[39m )\n",
400
+ "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'openpyxl'"
401
  ]
402
  }
403
  ],
 
411
  "\n",
412
  "# === PATHS & CONFIG ===\n",
413
  "current_dir = Path.cwd()\n",
414
+ "inputCSV = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_02_NER.csv\"\n",
415
  "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\" #store your API key under misc/credentials/deepseek_api_key.txt\n",
416
  "\n",
417
  "# Output both CSV and Excel for compatibility\n",
418
+ "OUTPUT_CSV = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_03_deepseek.csv\"\n",
419
+ "OUTPUT_XLSX = current_dir.parent / \"data/CSV/model_adapter/real_person_adapter_step_03_deepseek.xlsx\"\n",
420
+ "INDEX_FILE = current_dir.parent / \"misc/query-indicies/deepseek_query_index.txt\"\n",
421
  "SAVE_INTERVAL = 1 # Save every N rows\n",
422
  "START_ROW = 1 # Row index to start from (0-based)\n",
423
  "END_ROW = 5 # Row index to end (exclusive)\n",
 
522
  "print(\"All done! Files:\", OUTPUT_CSV, OUTPUT_XLSX)\n"
523
  ]
524
  },
 
 
 
 
 
 
525
  {
526
  "cell_type": "markdown",
527
  "id": "9d377005",
 
702
  ],
703
  "metadata": {
704
  "kernelspec": {
705
+ "display_name": "pm-paper",
706
  "language": "python",
707
+ "name": "pm-paper"
708
  },
709
  "language_info": {
710
  "codemirror_mode": {
 
716
  "name": "python",
717
  "nbconvert_exporter": "python",
718
  "pygments_lexer": "ipython3",
719
+ "version": "3.11.13"
720
  }
721
  },
722
  "nbformat": 4,
jupyter_notebooks/Section_2-3-4__Figure_8b_sunburst_profession.ipynb CHANGED
@@ -101,7 +101,7 @@
101
  ],
102
  "metadata": {
103
  "kernelspec": {
104
- "display_name": "latm",
105
  "language": "python",
106
  "name": "python3"
107
  },
@@ -115,9 +115,9 @@
115
  "name": "python",
116
  "nbconvert_exporter": "python",
117
  "pygments_lexer": "ipython3",
118
- "version": "3.10.15"
119
  }
120
  },
121
  "nbformat": 4,
122
- "nbformat_minor": 2
123
  }
 
101
  ],
102
  "metadata": {
103
  "kernelspec": {
104
+ "display_name": "Python 3 (ipykernel)",
105
  "language": "python",
106
  "name": "python3"
107
  },
 
115
  "name": "python",
116
  "nbconvert_exporter": "python",
117
  "pygments_lexer": "ipython3",
118
+ "version": "3.12.10"
119
  }
120
  },
121
  "nbformat": 4,
122
+ "nbformat_minor": 4
123
  }
misc/deleteme.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 10
misc/query_indicies/deepseek_query_index.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 10