= """
prompt You are an AI assistant with years of training in image analysis for political communication. Use the following annotation manual to code the provided image. We're **not** interested in the identity of any person in the image, please anonymize any personal information and concentrate on the objective image analysis framework outlined below.
**Objective**: Perform a content analysis of a given Instagram posts by political candidates during the 2021 German election campaign, identifying the presentation of the self and visual framing as per Goffman (1956) and Grabe and Bucy (2009).
### Annotation Guide
#### 1. **Performance** (Categorical)
- CampaignOrPartyEvent, PrivateEvent, SolidarityEvent, ProtestEvent, MediaEvent, CampaignMaterial, Other (String)
#### 2. **Environment **(Categorical)
- **Environment**: Indoors / Outdoors / NotApplicable / Other
- **Location**: EventHall / StreetOrPlaza / WorkPlace / Parliament / TVStudio / PrivateTransport / PublicTransport / Industry / Commerce / Nature / Home / NotApplicable / Other (String)
#### 3. **Dress Style **(Categorical)
- **DressStyle**: Formal (Identify formal and professional attire, like suits and ties or dresses) / Casual (Look for informal attire, like sportswear, T-shirts, comfortable clothes.)
- **RolledUpSleeves**: Tag shirts/blouses with rolled-up sleeves (Boolean).
**Analytical Process**
- For each Instagram post, identify and record occurrences of the items listed in the provided table.
- Categorize findings under the appropriate theory and visual frame.
**Reporting**: Summarize the findings in a structured JSON format based on the variable names in the Annotation guide. Respond only in JSON, respect the data types indicated in the manual.
"""
In this example we use GPT-4 to classify multiple variables at the same time. We’re using items based on Visual Frames [@Grabe2009-pe], as adopted by @Gordillo-Rodriguez2023-lu. For an actual study based on visual frames we need to add more items!
I found the following sentence to be essential for the classification to work: We’re not interested in the identity of any person in the image, please anonymize any personal information and concentrate on the objective image analysis framework outlined below.
In [14]:
The following methods help with converting the LLM results back into a pandas dataframe.
In [27]:
import pandas as pd
import json
import re
from pandas.io.json import json_normalize
def flatten_json(y):
= {}
out
def flatten(x, name=''):
if type(x) is dict:
for a in x:
+ a + '_')
flatten(x[a], name elif type(x) is list:
= 0
i for a in x:
+ str(i) + '_')
flatten(a, name += 1
i else:
-1]] = x
out[name[:
flatten(y)return out
def parse_response(response, identifier):
try:
if isinstance(response, str):
= json.loads(response)
response = flatten_json(response)
response 'ID'] = identifier
response[return response
except json.JSONDecodeError:
= re.search(r'```json\n([\s\S]+)\n```', response)
match if match:
try:
= json.loads(match.group(1))
json_data = flatten_json(json_data)
json_data 'image_path'] = identifier
json_data[return json_data
except json.JSONDecodeError:
pass
return {'image_path': identifier, 'error': response}
The following cell contains the actual classification loop. As with text classification, we send one image at a time with the same prompt all over again. For our in-class tutorial I added two filters: 1. We sample the data and just classify a part of the dataframe. 2. I filter for one particular account.
Remove these filters for real world applications!
In [28]:
import base64
from tqdm.notebook import tqdm
import openai
from google.colab import userdata
import pandas as pd
import backoff
# Retrieving OpenAI API Key
= userdata.get('openai-lehrstuhl-api')
api_key
# Initialize OpenAI client
= openai.OpenAI(api_key=api_key)
client
# Cost per token
= 0.01 / 1000 # Cost per prompt token
prompt_cost = 0.03 / 1000 # Cost per completion token
completion_cost
# Initialize total cost
= 0.0
total_cost
def encode_image(image_path):
"""
Encodes an image to base64.
:param image_path: Path to the image file.
:return: Base64 encoded string of the image.
"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
@backoff.on_exception(backoff.expo, (openai.RateLimitError, openai.APIError))
def run_request(prompt, base64_image):
"""
Sends a request to OpenAI with given prompt and image.
:param prompt: Text prompt for the request.
:param base64_image: Base64 encoded image.
:return: Response from the API.
"""
= [{
messages "role": "user",
"content": [
"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}
{
]
}]
return client.chat.completions.create(
="gpt-4-vision-preview",
model=0,
temperature=messages,
messages=600
max_tokens
)
= []
responses = []
data
= df[df['Username'] == "afd.bund"]
sample_df = sample_df.sample(5)
sample_df
for index, row in tqdm(sample_df.iterrows(), total=sample_df.shape[0]):
try:
= row['image_path']
image_path = encode_image(image_path)
base64_image = run_request(prompt, base64_image)
response = response.choices[0].message.content
r
= response.usage.prompt_tokens * prompt_cost
current_prompt_cost = response.usage.completion_tokens * completion_cost
current_completion_cost = current_prompt_cost + current_completion_cost
current_cost += current_cost
total_cost
print(f"This round cost ${current_cost:.6f}")
'image_path': row['image_path'], 'classification': r})
responses.append({
= parse_response(r, row['image_path'])
processed_data
data.append(processed_data)
except Exception as e:
print(f"Error processing image {row['image_path']}: {e}")
print(f"Total cost ${total_cost:.6f}")
This round cost $0.017040
This round cost $0.016950
This round cost $0.017010
This round cost $0.017010
This round cost $0.017040
Total cost $0.085050
Once we conver the python list data
to the pandas data_df
, we can display the classification results neatly.
In [29]:
= pd.DataFrame(data) data_df
In [30]:
data_df.head()
Performance | Environment_Environment | Environment_Location | DressStyle_DressStyle | DressStyle_RolledUpSleeves | image_path | |
---|---|---|---|---|---|---|
0 | CampaignOrPartyEvent | Outdoors | StreetOrPlaza | Formal | False | /content/media/images/afd.bund/263854933600008... |
1 | MediaEvent | Indoors | TVStudio | Formal | False | /content/media/images/afd.bund/267144444973600... |
2 | ProtestEvent | Outdoors | StreetOrPlaza | Casual | False | /content/media/images/afd.bund/266546813941933... |
3 | CampaignOrPartyEvent | Indoors | EventHall | Formal | False | /content/media/images/afd.bund/264395151191513... |
4 | CampaignOrPartyEvent | Outdoors | StreetOrPlaza | Formal | False | /content/media/images/afd.bund/264597862229445... |
Using the next cell, we can qualitatively check the classification results.
In [6]:
import pandas as pd
from IPython.display import display, Image
import random
# Assuming your DataFrame is already loaded and named data_df
# data_df = pd.read_csv('your_data_file.csv') # Uncomment if you need to load the DataFrame
def display_random_image_and_classification(df):
# Select a random row from the DataFrame
= df.sample(1).iloc[0]
random_row
# Get the image path and classification from the row
= random_row['image_path'] # Replace 'image_path' with the actual column name
image_path
# Display the image
=image_path))
display(Image(filename
# Display the classification
print(f"Performance: {random_row['Performance']}")
print(f"Environment_Environment: {random_row['Environment_Environment']}")
print(f"Environment_Location: {random_row['Environment_Location']}")
print(f"DressStyle_DressStyle: {random_row['DressStyle_DressStyle']}")
print(f"DressStyle_RolledUpSleeves: {random_row['DressStyle_RolledUpSleeves']}")
# Call the function to display an image and its classification
display_random_image_and_classification(data_df)
And merge the results with the overall dataframe.
In [33]:
= pd.merge(df, data_df, how="left", on="image_path") total_df
In [34]:
~pd.isna(total_df['Performance'])].head() total_df[
Unnamed: 0 | ID | Time of Posting | Type of Content | video_url | image_url | Username | Video Length (s) | Expiration | Caption | Is Verified | Stickers | Accessibility Caption | Attribution URL | image_path | Performance | Environment_Environment | Environment_Location | DressStyle_DressStyle | DressStyle_RolledUpSleeves | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
44 | 44 | 2638549336000085388_1484534097 | 2021-08-12 09:13:30 | Video | NaN | NaN | afd.bund | 5.000 | 2021-08-13 09:13:30 | NaN | True | [] | NaN | https://www.threads.net/t/CSeAZYrotni | /content/media/images/afd.bund/263854933600008... | CampaignOrPartyEvent | Outdoors | StreetOrPlaza | Formal | False |
70 | 70 | 2643951511915139810_1484534097 | 2021-08-19 20:06:41 | Image | NaN | NaN | afd.bund | NaN | 2021-08-20 20:06:41 | NaN | True | [] | Photo by Alternative für Deutschland on August... | https://www.threads.net/t/CSxK2ybDz9D | /content/media/images/afd.bund/264395151191513... | CampaignOrPartyEvent | Indoors | EventHall | Formal | False |
93 | 93 | 2645978622294450871_1484534097 | 2021-08-22 15:14:11 | Video | NaN | NaN | afd.bund | 2.066 | 2021-08-23 15:14:11 | NaN | True | [] | NaN | NaN | /content/media/images/afd.bund/264597862229445... | CampaignOrPartyEvent | Outdoors | StreetOrPlaza | Formal | False |
162 | 162 | 2665468139419335791_1484534097 | 2021-09-18 12:36:23 | Image | NaN | NaN | afd.bund | NaN | 2021-09-19 12:36:23 | NaN | True | [] | Photo by Alternative für Deutschland on Septem... | https://www.threads.net/t/CT9o2o6NZLg | /content/media/images/afd.bund/266546813941933... | ProtestEvent | Outdoors | StreetOrPlaza | Casual | False |
165 | 165 | 2671444449736006853_1484534097 | 2021-09-26 18:30:15 | Image | NaN | NaN | afd.bund | NaN | 2021-09-27 18:30:15 | NaN | True | [{'height': 0.044419695058272, 'rotation': 0, ... | Photo by Alternative für Deutschland on Septem... | NaN | /content/media/images/afd.bund/267144444973600... | MediaEvent | Indoors | TVStudio | Formal | False |