#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 A1. Python script for binary 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
import os

# Check for GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)

# Load dataset
from google.colab import drive
drive.mount('/content/gdrive')
%cd /content/gdrive/MyDrive/Text classification

all_data = pd.read_csv('All_data_filtered.csv', delimiter=";", encoding="utf-8", engine='python', on_bad_lines='skip')

# Map labels
all_data['label'] = all_data['Code_nr_short'].apply(lambda x: 1 if x == 1 else 2)

# Balance class 2 (randomly sample 200 cases from each of 2,3,4,5)
class_2_data = all_data[all_data['Code_nr_short'].isin([2,3,4,5])]
class_2_sampled = class_2_data.groupby('Code_nr_short').apply(lambda x: x.sample(n=200, random_state=42)).reset_index(drop=True)

# Combine with all class 1 cases
balanced_data = pd.concat([all_data[all_data['Code_nr_short'] == 1], class_2_sampled])

# Define features and labels
X = balanced_data['text'].tolist()
y = balanced_data['label'].tolist()

# Split dataset
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
X_dev, X_test, y_dev, y_test = train_test_split(X_temp, y_temp, test_size=0.5, 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)}")

# Tokenization
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'].squeeze(0),
            'attention_mask': encoding['attention_mask'].squeeze(0),
            'labels': torch.tensor(self.labels[idx] - 1, 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=20, shuffle=True)
dev_loader = DataLoader(dev_dataset, batch_size=20, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=20, shuffle=False)

# Define model
class BinaryRoBERTa(nn.Module):
    def __init__(self):
        super(BinaryRoBERTa, self).__init__()
        self.model = RobertaForSequenceClassification.from_pretrained("roberta-base", num_labels=2)

    def forward(self, input_ids, attention_mask):
        return self.model(input_ids=input_ids, attention_mask=attention_mask).logits

model = BinaryRoBERTa().to(device)

# Loss and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=5e-6, eps=1e-8, weight_decay=0.02)

# Training loop
EPOCHS = 10
best_val_loss = float("inf")

for epoch in range(EPOCHS):
    model.train()
    total_loss = 0
    for batch in train_loader:
        input_ids, attention_mask, labels = batch['input_ids'].to(device), batch['attention_mask'].to(device), 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()

    avg_train_loss = total_loss / len(train_loader)
    print(f"Epoch {epoch+1}: Train Loss = {avg_train_loss:.4f}")

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt

def evaluate(model, test_loader, model_name):
    model.eval()
    all_preds, all_labels = [], []
    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()
            all_preds.extend(preds)
            all_labels.extend(labels.cpu().tolist())

    acc = accuracy_score(all_labels, all_preds)
    precision = precision_score(all_labels, all_preds)
    recall = recall_score(all_labels, all_preds)
    f1 = f1_score(all_labels, all_preds)

    print(f"Evaluation for model: {model_name}")
    print("Overall Statistics:")
    print(f"Accuracy: {acc:.4f}")
    print(f"Precision (Macro): {precision_score(all_labels, all_preds, average='macro'):.4f}")
    print(f"Recall (Macro): {recall_score(all_labels, all_preds, average='macro'):.4f}")
    print(f"F1 Score (Macro): {f1_score(all_labels, all_preds, average='macro'):.4f}")

    print("Per-Class Statistics:")
    per_class_precision = precision_score(all_labels, all_preds, average=None)
    per_class_recall = recall_score(all_labels, all_preds, average=None)
    per_class_f1 = f1_score(all_labels, all_preds, average=None)
    for i, class_label in enumerate(["Class 1", "Class 2"]):
        print(f"{class_label} - Precision: {per_class_precision[i]:.4f}, Recall: {per_class_recall[i]:.4f}, F1: {per_class_f1[i]:.4f}")
    print(f"Precision: {precision:.4f}")
    print(f"Recall: {recall:.4f}")
    print(f"F1 Score: {f1:.4f}")

    cm = confusion_matrix(all_labels, all_preds)
    plt.figure(figsize=(6, 6))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=["Class 1", "Class 2"], yticklabels=["Class 1", "Class 2"])
    plt.xlabel("Predicted")
    plt.ylabel("Actual")
    plt.title("Confusion Matrix")
    plt.savefig("confusion_matrix.png")
    plt.show()

evaluate(model, test_loader, "binary_roberta_model.pth")

# Save final model
torch.save(model.state_dict(), "binary_roberta_model.pth")
print("Model training complete and saved!")

