#Supplementary information file #Author: Maija Ozola-Schade, Technische Universität Ilmenau, #Department of Economic Sciences and Media, Germany #maija.ozola@tu-ilmenau.de #Article: A longitudinal analysis of traditional news media coverage of #immigration in Western Europe: Issue attention cycles and attribute clusters #Journal: Studies in Communication Sciences (SComS) #Vol./Issue 26/02 #DOI: https://doi.org/ 10.24434/j.scoms.2026.02.8700 #Note: The authors of the article are responsible for the layout of the appendix. #Document A2. Python script for multiclass classification model import pandas as pd import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, Dataset from transformers import RobertaTokenizer, RobertaForSequenceClassification from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from torch.optim.lr_scheduler import ReduceLROnPlateau import matplotlib.pyplot as plt import os import nltk nltk.download('punkt_tab') import random import numpy as np # 2. SETUP & DATA LOADING # 2.1 CHECK DEVICE device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("Using device:", device) # 2.2 MOUNT GOOGLE DRIVE (COLAB-SPECIFIC) from google.colab import drive drive.mount('/content/gdrive', force_remount=True) %cd /content/gdrive/MyDrive/Text classification # 2.3 LOAD FIRST CSV WITH FACTORS AND FILTER df1 = pd.read_csv('Coded_with_factors.csv', delimiter=",", encoding="utf-8", engine='python', on_bad_lines='skip') print("Columns in first CSV:", df1.columns.tolist()) df1 = df1[df1['Category'] != "Perspective of reporting"] df1 = df1.dropna(subset=['Factor', 'Text']) df1 = df1[df1['Factor'].isin([1,2,3,4,5,6,7,8,9,10])] df1.loc[df1['Factor'].isin([4,5,6,7,8,9,10]), 'Factor'] = 12 # 2.4 LOAD SECOND CSV WITH ISSUE ATTRIBUTES EXCLUDED FROM FACTOR ANALYSIS df2 = pd.read_csv('Coded_with_factors_12.csv', delimiter=",", encoding="utf-8", engine='python', on_bad_lines='skip') print("Columns in second CSV:", df2.columns.tolist()) df2 = df2[df2['Category'] != "Politics"] df2 = df2[df2['Category'] != "Daily Management / Control"] df2 = df2.dropna(subset=['Factor', 'Text']) # 2.5 COMBINE DATASETS all_data = pd.concat([df1, df2], ignore_index=True) print("Combined dataset shape:", all_data.shape) # 2.6 PREPARE FEATURES AND LABELS X = all_data['Text'].tolist() label_mapping = {label: idx for idx, label in enumerate(sorted(all_data['Factor'].unique()))} y = all_data['Factor'].map(label_mapping).astype(int).tolist() NUM_CLASSES = len(label_mapping) print("Label mapping:", label_mapping) print(f"Total samples after splitting: {len(X)}") # 2.7 SPLIT DATA X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.20, random_state=42, stratify=y) X_dev, X_test, y_dev, y_test = train_test_split(X_temp, y_temp, test_size=0.50, random_state=42, stratify=y_temp) print(f"Training size: {len(X_train)}") print(f"Development (Validation) size: {len(X_dev)}") print(f"Test size: {len(X_test)}") # 3. TOKENIZATION WITH ROBERTA TOKENIZER = RobertaTokenizer.from_pretrained("roberta-base") class TextDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_length=512): self.texts = texts self.labels = labels self.tokenizer = tokenizer self.max_length = max_length def __len__(self): return len(self.texts) def __getitem__(self, idx): encoding = self.tokenizer( self.texts[idx], padding='max_length', truncation=True, max_length=self.max_length, return_tensors='pt' ) return { 'input_ids': encoding['input_ids'][0], 'attention_mask': encoding['attention_mask'][0], 'labels': torch.tensor(self.labels[idx], dtype=torch.long) } train_dataset = TextDataset(X_train, y_train, TOKENIZER) dev_dataset = TextDataset(X_dev, y_dev, TOKENIZER) test_dataset = TextDataset(X_test, y_test, TOKENIZER) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True) dev_loader = DataLoader(dev_dataset, batch_size=32, shuffle=False) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False) # 4. DEFINE ROBERTA CLASSIFIER class ImprovedRoBERTa(nn.Module): def __init__(self, num_labels): super(ImprovedRoBERTa, self).__init__() self.model = RobertaForSequenceClassification.from_pretrained( "roberta-base", num_labels=4, hidden_dropout_prob=0.3, attention_probs_dropout_prob=0.3 ) def forward(self, input_ids, attention_mask): outputs = self.model(input_ids=input_ids, attention_mask=attention_mask) return outputs.logits model = ImprovedRoBERTa(NUM_CLASSES) model.to(device) # 4.1 LOAD PRE-TRAINED CHECKPOINT IF EXISTS ROBERTA_MODEL_PATH = "best_roberta_model.pth" if os.path.exists(ROBERTA_MODEL_PATH): try: model.load_state_dict(torch.load(ROBERTA_MODEL_PATH, map_location=device)) model.eval() print("Loaded pre-trained RoBERTa model.") except RuntimeError as e: print(f"Error loading RoBERTa model: {e}") print("Starting training from scratch.") else: print("Pre-trained RoBERTa model not found. Starting training from scratch.") # 5. CLASS WEIGHTS & LOSS class_counts = torch.tensor(pd.Series(y_train).value_counts().sort_index().values, dtype=torch.float) class_weights = torch.log(1 + class_counts.max() / class_counts) class_weights = class_weights / class_weights.sum() * len(class_counts) class_weights = class_weights.to(device) criterion = nn.CrossEntropyLoss(weight=class_weights) # 5.1 OPTIMIZER & LR SCHEDULER optimizer = optim.AdamW(model.parameters(), lr=3e-5, betas=(0.9, 0.98), eps=1e-8, weight_decay=0.01) scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=5, verbose=True) # 6. TRAINING LOOP WITH EARLY STOPPING EPOCHS = 7 best_val_loss = float("inf") patience = 4 patience_counter = 0 for epoch in range(EPOCHS): model.train() total_loss, total_correct = 0, 0 for batch in train_loader: input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) labels = batch['labels'].to(device) optimizer.zero_grad() outputs = model(input_ids, attention_mask=attention_mask) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() total_correct += (outputs.argmax(dim=1) == labels).sum().item() avg_train_loss = total_loss / len(train_loader) train_accuracy = total_correct / len(train_dataset) # Validation model.eval() val_loss, val_correct = 0, 0 with torch.no_grad(): for batch in dev_loader: input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) labels = batch['labels'].to(device) outputs = model(input_ids, attention_mask=attention_mask) loss = criterion(outputs, labels) val_loss += loss.item() val_correct += (outputs.argmax(dim=1) == labels).sum().item() avg_val_loss = val_loss / len(dev_loader) val_accuracy = val_correct / len(dev_dataset) print(f"Epoch {epoch+1}/{EPOCHS} - " f"Train Loss: {avg_train_loss:.4f} - Train Acc: {train_accuracy:.4f} - " f"Val Loss: {avg_val_loss:.4f} - Val Acc: {val_accuracy:.4f}") scheduler.step(avg_val_loss) # Early Stopping if avg_val_loss < best_val_loss: best_val_loss = avg_val_loss torch.save(model.state_dict(), "Factor_model3.pth") print("Model saved!") patience_counter = 0 else: patience_counter += 1 if patience_counter >= patience: print("Early stopping triggered.") break # 7. LOAD BEST MODEL FOR FINAL EVALUATION model = ImprovedRoBERTa(NUM_CLASSES) model.to(device) model.load_state_dict(torch.load("Factor_model3.pth")) model.eval() # 8. EVALUATE MODEL WITH METRICS import seaborn as sns y_true, y_pred = [], [] with torch.no_grad(): for batch in test_loader: input_ids, attention_mask, labels = batch['input_ids'].to(device), batch['attention_mask'].to(device), batch['labels'].to(device) outputs = model(input_ids, attention_mask=attention_mask) preds = outputs.argmax(dim=1).cpu().tolist() y_pred.extend(preds) y_true.extend(labels.cpu().tolist()) # Classification report report = classification_report( y_true, y_pred, target_names=[str(label) for label in sorted(label_mapping.keys())], digits=4 ) print("Classification Report:") print(report) # Confusion matrix conf_matrix = confusion_matrix(y_true, y_pred) plt.figure(figsize=(8, 6)) sns.heatmap(conf_matrix, annot=True, fmt='d', cmap='Blues', xticklabels=sorted(label_mapping.keys()), yticklabels=sorted(label_mapping.keys())) plt.xlabel("Predicted Label") plt.ylabel("True Label") plt.title("Confusion Matrix") plt.show()