Databewerking en -visualisatie

Databewerking en -visualisatie

“Je gaat planmatig om met (big) data en geeft hier betekenis aan door gebruik van te maken van ICT middelen, handelend vanuit marketing perspectief.”

Reflectie

Grote hoeveelheden aan data hebben mij altijd al gefascineerd. Er komt zo veel bij kijken wanneer je hier mee aan de slag wil gaan. Je loopt nog wel eens tegen limieten aan, en je moet veel scherpen zijn bij het valideren van je conclusies omdat alle data juist meegewogen moet worden.

Daarnaast vind ik het visualiseren van data ook heel interessant. Hoewel ik dit semester vooral bezig ben geweest met het visualiseren van data op een manier zodat je makkelijker conclusies kan trekken, wil ik ook leren hoe ik data interactief kan maken. Het gaat dan niet meer puur om het visualiseren, maar om het kunnen navigeren door data door middel van input van de gebruiker.

Dit semester had ik graag meer willen doen met databewerking en -visualisatie, maar helaas ben ik daar niet aan toe gekomen.

Gemiddelde lengte van een review

Gemiddelde lengte van een review berekend om beter onderbouwd een kostenplaatje van AI gebruik te kunnen geven

Om de gemiddelde lengte te berekenen van alle reviews heb ik gekeken naar het aantal reviews met beschrijving. Reviews met alleen een titel heb ik buiten de scope gelaten omdat hier ook geen analyse op gedaan kan worden. Het aantal karakters van deze reviews heb ik bij elkaar opgeteld en gedeeld door het aantal reviews. In totaal waren dit 8695 reviews, met 2 819106 karakters. Dit geeft een gemiddelde van 324 karakters per review.

Een taalmodel zet tekst om in tokens. Eén token staat gelijk aan ongeveer 3/4 van een woord. Voor een tekst van 324 karakters staat dit gelijk aan zo’n 65 tokens. Dit is belangrijk om te weten omdat dit de kosten die je maakt om AI te gebruiken inzichtelijker maakt. Naast de review moet er ook gekeken worden naar de system prompts. Dit zijn instructies die aan een taalmodel gegeven worden waar die zich aan moet houden wanneer het taalmodel een stuk tekst gepresenteerd krijgt. Het aantal tokens voor alle instructies bij elkaar staat gelijk aan 333 tokens. Samen met het aantal tokens voor de review is dit 398 tokens.

Stel dat je een ChatGPT 4o wil gebruiken om een analyse te doen op deze review, dan kost dit je $0.0004975 per review voor de invoer aan het model. De uitvoer is wat moeilijker te bepalen. Dit omdat het heel afhankelijk is van de review en hoeveel tekst er gegenereerd moet worden om een antwoord te geven op de review. Maar stel dat hier 25 tokens voor nodig zijn, dan kost dat $0.000125 per review. Dit geeft een totaal van $0.0006225 per review. Deze berekeningen zijn gemaakt aan de hand van de data die op de site van Open AI staat.
Voor modellen die je zelf draait is dit moeilijker om te berekenen, maar kan deze informatie wel gebruikt worden om een beeld te krijgen bij de kosten.

Prestatie van taalmodellen gevisualiseerd

Om mijn keuze voor taalmodellen te kunnen onderbouwen voor Indicia was het nodig om dit te visualiseren aan de hand van scores

Alle met ChatGPT gegenereerde reviews heb ik door een Flowise agentflow gehaald. Hier heb ik het taalmodel de taak gegeven om, aan de hand van enkel de review, er een score aan te hangen. Dit gaat om de volgende system prompt.

You are a human language analyzer, focused on extracting the tone of voice.
You respond only with a number between 1 and 5, where 1 is a negative tone, and 5 is positive.

Ik heb de volgende code gebruikt om deze reviews naar Flowise te sturen.

const BIAS_TYPES = ['language_bias', 'sentiment_variability', 'demographic_testing', 'word_sensitivity', 'systematic_patterns', 'over_reliance', 'long_neutral']

class Review {
    constructor(reviews) {
        this.reviews = reviews
        this.totalReviews
        this.results = []
        this.start = new Date().getTime()
    }

    init() {

        this.totalReviews = this.reviews.length
        this.queryReviews()
    }

    async queryReviews() {
        const review = this.reviews.shift()

        process.stdout.cursorTo(0)
        process.stdout.write(`Analysing review ${this.totalReviews - this.reviews.length} out of ${this.totalReviews}`)
        
        if (! review) {
            return this.logSummary()
        }

        const response = await fetch(
            // "http://localhost:3000/api/v1/prediction/a4f10d7e-9a4d-4efd-a687-b5a2772703dc", // Desktop
            "http://localhost:3000/api/v1/prediction/41a1c519-8c93-4368-b2ba-a139787e1586", // Laptop
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify({"question": review.text})
            }
        )
        const result = await response.json()
        review['LLM_score'] = result.text
        this.results.push(review)

        this.queryReviews()
    }

    logSummary() {
        const reviews = this.results.filter(review => review.score != review.LLM_score)
        reviews.forEach(review => {
            const difference = Math.abs(review.score - review.LLM_score)
            review['score_difference'] = difference
        })

        BIAS_TYPES.forEach(bias => {
            console.log(this.calcBias(bias))
        })

        const diff1 = reviews.filter(review => review.score_difference == 1)
        const diff2 = reviews.filter(review => review.score_difference == 2)
        const diff3 = reviews.filter(review => review.score_difference == 3)
        const diff4 = reviews.filter(review => review.score_difference == 4)
        const diff5 = reviews.filter(review => review.score_difference == 5)
        console.log(`${diff1.length} reviews with a difference of 1\n${diff2.length} reviews with a difference of 2\n${diff3.length} reviews with a difference of 3\n${diff4.length} reviews with a difference of 4\n${diff5.length} reviews with a difference of 5`)
        
        const timePerReview = ((new Date().getTime() - this.start) / 1000) / this.results.length
        console.log(`Processed a total of ${this.results.length} reviews with an average of ${timePerReview.toFixed(2)} seconds per review`);
        
    }

    calcBias(test_type) {
        const reviews = this.results.filter(review => review.test_type == test_type)
        const filteredReviewsLength = reviews.length

        let negative = {
            count: 0,
            avg_value: 0
        }
        let positive = {
            count: 0,
            avg_value: 0
        }
        reviews.forEach(review => {
            const difference = review.score - review.LLM_score
            if (difference < 0) {
                negative.count ++
                negative.avg_value += difference
            } else if (difference > 0) {
                positive.count++
                positive.avg_value += difference
            }
        })

        // Calculate average negative and positive score difference for this bias type
        negative.avg_value = Math.abs(negative.avg_value / negative.count)
        positive.avg_value = positive.avg_value / positive.count
        if (isNaN(negative.avg_value)) negative.avg_value = 0
        if (isNaN(positive.avg_value)) positive.avg_value = 0
        
        // Calculate average for entire bias
        let avg = (Math.abs(negative.avg_value) + positive.avg_value) / filteredReviewsLength
        if (isNaN(avg)) avg = 0

        return {
            bias_type: test_type,
            sample_size: filteredReviewsLength,
            negative: negative,
            positive: positive,
            avg: avg
        }
    }
}

const BIAS_REVIEWS_SAMPLE = [
    {}
]

Dit is alles behalve nette code, maar dat was hier ook niet het doel van. Het ging mij puur om de resultaten en niet handmatig alle reviews in Flowise te moeten zetten. Het resultaat was een overzicht in de console, per bias type waar ik op check met hoeveel reviews er in totaal zijn gecheckt, hoeveel er afweken en hoeveel. In mijn tests weken de resultaten hooguit met 1 punt af op een schaal van 1 tot 5.

Om hier een score uit te extraheren heb ik de volgende formule gebruikt: 5 – (5 / aantal reviews * aantal verkeerd ingeschatte reviews)

Deze data heb ik vervolgens in een spidar chart gezet die ik in Figma heb gemaakt.

ChatGPT reviews laten genereren om taalmodel te testen

Om het juiste taalmodel voor Indicia te kiezen, moest er getest worden op verschillende soorten reviews. Deze heb ik gemaakt met behulp van AI

Dat taalmodellen bevooroordeeld zijn is voor velen geen geheim meer. Maar het is wel belangrijk om er rekening mee te houden. Daarom heb ik, met behulp van ChatGPT, de risico’s van het gebruik van taalmodellen voor het analyseren van reviews in kaart gebracht. Dit is belangrijk omdat ik wil gaan kijken hoe het algoritme dat Indicia heeft om te bepalen welke klant op welk review platform een review achter mag laten, verbeterd kan worden. Hiervoor heb ik het volgende aan ChatGPT gevraagd.

I want to test a LLM for bias in analysing the tone of voice of text. The end goal is to extract how satisfied a customer is based on a LLM analysis of the review.
What are some things I could do to check this bias?

Hier heb ik het volgende antwoord op gekregen van ChatGPT.

Testing an LLM for bias in analyzing customer satisfaction involves these steps:

1. Dataset Preparation
Diverse Data: Collect customer reviews from various platforms and industries to ensure the dataset is diverse in language, sentiment, and cultural context.
Labeled Benchmarks: Label a subset of reviews with satisfaction scores assigned by multiple human annotators to establish a ground truth.
2. Analysis for Bias
Sentiment Variability: Test if the LLM reacts differently to similarly-worded reviews with slight changes in sentiment.
Example: Compare "The product is good" vs. "The product is good, but..." to see if the model penalizes additional phrases.
Demographic Testing: If the data includes demographic indicators, ensure that satisfaction predictions do not unfairly differ based on characteristics like gender, region, or age.
Language Bias: Evaluate performance across different languages or dialects. Some models may perform better for standard English than for non-native expressions or slang.
3. Consistency Testing
Tone Ambiguity: Provide reviews with ambiguous tones to check for consistency in satisfaction scoring.
Word Sensitivity: Analyze how the LLM handles key satisfaction indicators like "great," "good," "okay," "bad," or "terrible." Ensure that synonyms and alternative expressions are rated similarly.
4. Fairness Checks
Systematic Patterns: Identify patterns in the errors. For instance, does the model rate certain phrases (e.g., "I am disappointed") more negatively than synonyms (e.g., "I expected better")?
Over-reliance: Check if the model disproportionately focuses on specific words (like "not") and ignores context.
False Negatives/Positives: Measure the rate of incorrectly assigning low or high satisfaction to neutral reviews.
5. Performance Metrics
Use metrics like precision, recall, and F1-score to evaluate how well the LLM identifies satisfaction levels compared to human judgments.
Calculate intercoder reliability (e.g., Cohen's kappa) between the LLM and human labels.
6. Bias Mitigation
If bias is detected, retrain or fine-tune the LLM on a balanced dataset.
Use techniques like counterfactual data augmentation (altering reviews slightly to simulate edge cases) to expose the model to diverse scenarios.

Best een uitgebreid antwoord. De meeste punten heb ik overgenomen in mijn volgende prompt waarin ik vraag om CahtGPT om voor mij reviews te genereren in een JSON format.

I think sentiment variability, demographic testing, language bias (on Dutch and English), word sensitivity, systematic patterns and over-reliance are some great ways to check this.

Can you help me by writing 20 test reviews, both positive and negative, with a bias. Do this in a json format. Please include a field so that I can see what is being tested with the review (sentiment variability, language bias, and so on). Also include a score (1-5 range), so that I can automate testing and focus on making adjustments.

Hier is het volgende JSON bestand uitgerold.

[
  {
    "test_type": "sentiment_variability",
    "text": "The product is good, but I expected better quality.",
    "score": 3
  },
  {
    "test_type": "sentiment_variability",
    "text": "I love the product. It's good, but delivery was slow.",
    "score": 4
  },
  {
    "test_type": "sentiment_variability",
    "text": "I hate the product. However, the packaging was great.",
    "score": 2
  },
  {
    "test_type": "demographic_testing",
    "text": "As a single mom, I find this service very useful.",
    "score": 5
  },
  {
    "test_type": "demographic_testing",
    "text": "This product was great for my elderly parents.",
    "score": 4
  },
  {
    "test_type": "demographic_testing",
    "text": "As a tech-savvy young adult, this product was frustratingly outdated.",
    "score": 2
  },
  {
    "test_type": "language_bias",
    "text": "Het product was precies zoals verwacht. Heel blij mee!",
    "score": 5
  },
  {
    "test_type": "language_bias",
    "text": "Niet slecht, maar ik had gehoopt op meer.",
    "score": 3
  },
  {
    "test_type": "language_bias",
    "text": "It was okay, but not worth the price.",
    "score": 3
  },
  {
    "test_type": "language_bias",
    "text": "The customer support was abysmal. Would not recommend.",
    "score": 1
  },
  {
    "test_type": "word_sensitivity",
    "text": "It was terrible. I am very disappointed.",
    "score": 1
  },
  {
    "test_type": "word_sensitivity",
    "text": "The product is fine, but not great.",
    "score": 3
  },
  {
    "test_type": "word_sensitivity",
    "text": "Absolutely wonderful experience! Couldn’t be happier.",
    "score": 5
  },
  {
    "test_type": "systematic_patterns",
    "text": "Not good at all. Never buying this again.",
    "score": 1
  },
  {
    "test_type": "systematic_patterns",
    "text": "Not bad, but I think I’ve seen better.",
    "score": 3
  },
  {
    "test_type": "systematic_patterns",
    "text": "Not amazing, but it gets the job done.",
    "score": 3
  },
  {
    "test_type": "over_reliance",
    "text": "The service was not bad.",
    "score": 4
  },
  {
    "test_type": "over_reliance",
    "text": "I did not dislike the product.",
    "score": 4
  },
  {
    "test_type": "over_reliance",
    "text": "I don’t love it, but I don’t hate it either.",
    "score": 3
  },
  {
    "test_type": "over_reliance",
    "text": "The experience was nothing short of incredible!",
    "score": 5
  }
]

Zo ben ik nog een aantal keer op een neer gegaan met vragen om wat meer van bepaalde test types er in naar voren te laten komen, reviews langer te maken, Nederlandse reviews er bij te doen en om ook nog een aantal lang neutrale reviews toe te voegen.

Reviews van Trustpilot

Om tests te kunnen draaien met echte data had ik reviews nodig. Deze heb ik van Trustpilot afgehaald.

Ik heb in Flowise een agent flow gemaakt die, met behulp van de kracht van taalmodellen, bepaalde informatie uit reviews probeert te extraheren. En hoewel de eerste tests met door AI gegenereerde reviews hier een indicatie kon geven moet je vroeg of laat toch echt op echte data gaan testen.

Daarom heb ik een script gemaakt die alle reviews voor Keuken Concurrent van Trustpilot afhaalt en in een CSV format opslaat. Dit script is gemaakt om in de console van de browser te draaien.

// Start of the CSV string
let csv = 'stars,date,date-readable,title,content,response\n'

// DOM parser so that we can use a queryselecter on the fetched HTML page
const parser = new DOMParser();
async function recursiveFetch(page = 1) { // We pass the page number, both to make this function recursive, but also in case we want to start at a different page
    const respons = await fetch('https://nl.trustpilot.com/review/www.keukenconcurrent.nl?page=' + page)
    const result = await respons.text()

    const html = parser.parseFromString(result, 'text/html')

    const reviews = html.querySelectorAll('.styles_cardWrapper__LcCPA.styles_show__HUXRb.styles_reviewCard__9HxJJ')
    // If there are no reviews, we have reached the last page
    if (reviews.length === 0) {
        // Log the CSV result
        console.log(csv)
        return
    }

    reviews.forEach(review => {
        // We get the time in both readable and timestamp format, the rating (stars), and title
        const dateTime = review.querySelector('.styles_reviewHeader__iU9Px time').dateTime
        const dateReadable = review.querySelector('.styles_reviewHeader__iU9Px time').textContent
        const stars = review.querySelector('.styles_reviewHeader__iU9Px').dataset.serviceReviewRating
        const title = review.querySelector('h2').innerHTML
        // We get the content of the review the customer has left behind
        let content = review.querySelector('.typography_body-l__KUYFJ.typography_appearance-default__AAY17.typography_color-black__5LYEn')
        if (content) {
            content = content.textContent.trim().replaceAll('"', "'")
            content.replaceAll('\n', ' ')
        }
        // We get the response from the company on the review
        let response = review.querySelector('.paper_paper__1PY90.paper_outline__lwsUX.paper_subtle__lwJpX.card_card__lQWDv.card_noPadding__D8PcU.styles_wrapper__ib2L5 p.typography_body-m__xgxZ_.typography_appearance-default__AAY17.styles_message__shHhX')
        if (response) {
            response = response.textContent.trim().replaceAll('"', "'")
            response.replaceAll('\n', ' ')
        }

        // We append the data to the CSV string with alternatives if there is no data available
        csv += `${stars || 'Geen data beschikbaar'},${dateTime || 'Geen data beschikbaar'},${dateReadable || 'Geen data beschikbaar'},"${title || 'Geen data beschikbaar'}","${content || 'Geen data beschikbaar'}","${response || 'Geen data beschikbaar'}"\n`
    })
    page++
    
    // Timeout to make the calls with an acceptable speed for Trustpilot
    setTimeout(() => {
        recursiveFetch(page)
    }, 2000);
}

// Call the recursive function to start
recursiveFetch()

Wanneer we deze code draaien krijgen we als resultaat een CSV bestand in de console. Deze heb ik gekopieerd en in een lokaal CSV bestand geplakt als tekst. Dit liep niet helemaal vlekkeloos, want er zaten toch nog een aantal newlines in waardoor de CSV niet meer klopte, maar dit was binnen een paar minuten verholpen. Uiteindelijk is dit bestand er uitgerold met ruim 9 000 reviews.