NPS survey in EDSL

This notebook provides sample EDSL code for simulating a Net Promoter Score (NPS) survey with AI agents and large language models. In the steps below we show how to construct an EDSL survey, create personas for AI agents to answer the questions, and then administer the survey to them. We also demonstrate some built-in methods for inspecting and analyzing the dataset of results that is generated when an EDSL survey is run.

The following questions are used in the sample survey:

On a scale from 0-10, how likely are you to recommend our company to a friend or colleague? (0=Not at all likely, 10=Very likely) Please tell us why you gave a rating.

How satisfied are you with the following experience with our company? Product quality Customer support Purchasing experience

Is there anything specific that our company can do to improve your experience?

Technical setup

Before running the code below, ensure that you have (1) installed the EDSL library and (2) created a Coop account to activate remote inference or stored your own API keys for language models that you want to use with EDSL. Please also see our tutorials and documentation page on getting started using the EDSL library.

Constructing questions

We start by selecting appropriate question types for the above questions. EDSL comes with a variety of common question types that we can choose from based on the form of the response that we want to get back from the model. The first quesiton is linear scale; we import the class type and then construct a question in the relevant template:

[1]:
from edsl import QuestionLinearScale
[2]:
q_recommend = QuestionLinearScale(
    question_name = "recommend",
    question_text = "On a scale from 0-10, how likely are you to recommend our company to a friend or colleague?",
    question_options = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    option_labels = {0:"Not at all likely", 10:"Very likely"}
)

Each question type other than free text automatically includes a “comment” field for the model to provide commentary on its response to the main question. When we run the survey, we can check that it has effectively captured the follow-on question from above–Please tell us why you gave a rating–and modify or add questions as needed.

For the next question, we use a {{ placeholder }} for an “experience” that we will insert when repeating the base question:

[3]:
from edsl import QuestionMultipleChoice
[4]:
q_satisfied = QuestionMultipleChoice(
    question_name = "satisfied",
    question_text = "How satisfied are you with the following experience with our company: {{ experience }}",
    question_options = [
        "Extremely satisfied",
        "Moderately satisfied",
        "Neither satisfied nor dissatisfied",
        "Moderately dissatisfied",
        "Extremely dissatisfied"
    ]
)

The third question is a simple free text question that we can choose whether to administer once or individually for each “experience” question. In the steps that follow we show how to apply survey logic to achieve this effect:

[5]:
from edsl import QuestionFreeText
[6]:
q_improve = QuestionFreeText(
    question_name = "improve",
    question_text = "Is there anything specific that our company can do to improve your experience?"
)

Creating variants of questions with scenarios

Next we want to create a version of the “satisfied” question for each “experience”. This can be done with Scenario objects–dictionaries of key/value pairs representing the content to be added to questions. Scenarios can be automatically generated from a variety of data sources (PDFs, CSVs, images, tables, etc.). Here we have import a simple list:

[7]:
from edsl import ScenarioList, Scenario
[8]:
experiences = ["Product quality", "Customer support", "Purchasing experience"]

s = ScenarioList(
    Scenario({"experience":e}) for e in experiences
)

We could also use a specific method for creating scenarios from a list:

[9]:
s = ScenarioList.from_list("experience", experiences)

We can check the scenarios that have been created:

[10]:
s
[10]:

ScenarioList scenarios: 3; keys: ['experience'];

  experience
0 Product quality
1 Customer support
2 Purchasing experience

To create the question variants, we pass the scenario list to the question loop() method, which returns a list of new questions. We can see that each question has a new unique name and a question text with the placeholder replaced with an experience:

[11]:
satisfied_questions = q_satisfied.loop(s)
satisfied_questions
[11]:
[Question('multiple_choice', question_name = """satisfied_0""", question_text = """How satisfied are you with the following experience with our company: Product quality""", question_options = ['Extremely satisfied', 'Moderately satisfied', 'Neither satisfied nor dissatisfied', 'Moderately dissatisfied', 'Extremely dissatisfied']),
 Question('multiple_choice', question_name = """satisfied_1""", question_text = """How satisfied are you with the following experience with our company: Customer support""", question_options = ['Extremely satisfied', 'Moderately satisfied', 'Neither satisfied nor dissatisfied', 'Moderately dissatisfied', 'Extremely dissatisfied']),
 Question('multiple_choice', question_name = """satisfied_2""", question_text = """How satisfied are you with the following experience with our company: Purchasing experience""", question_options = ['Extremely satisfied', 'Moderately satisfied', 'Neither satisfied nor dissatisfied', 'Moderately dissatisfied', 'Extremely dissatisfied'])]

We can also use the loop() method to create copies of the “improve” question in order to present it as a follow-up question to each of the “satisfied” questions that have been parameterized with experiences. Here, we’re simply duplicating the base question without a scenario {{ placeholder }} because we will instead add a “memory” of the relevant “satisfied” question when administering each copy of it:

[12]:
improve_questions = q_improve.loop(s)
improve_questions
[12]:
[Question('free_text', question_name = """improve_0""", question_text = """Is there anything specific that our company can do to improve your experience?"""),
 Question('free_text', question_name = """improve_1""", question_text = """Is there anything specific that our company can do to improve your experience?"""),
 Question('free_text', question_name = """improve_2""", question_text = """Is there anything specific that our company can do to improve your experience?""")]

Creating a survey

Next we pass a list of all the questions to a Survey in order to administer them together:

[13]:
questions = [q_recommend] + satisfied_questions + improve_questions
[14]:
from edsl import Survey
[15]:
survey = Survey(questions)

Adding survey logic

In the next step we add logic to the survey specifying that each “improve” question should include a “memory” of a “satisfied” question (the question and answer that was provided):

[16]:
for i in range(len(s)):
    survey = survey.add_targeted_memory(f"improve_{i}", f"satisfied_{i}")

We can inspect the survey details:

[17]:
survey
[17]:

Survey # questions: 7; question_name list: ['recommend', 'satisfied_0', 'satisfied_1', 'satisfied_2', 'improve_0', 'improve_1', 'improve_2'];

  question_name question_text option_labels question_type question_options
0 recommend On a scale from 0-10, how likely are you to recommend our company to a friend or colleague? {0: 'Not at all likely', 10: 'Very likely'} linear_scale [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1 satisfied_0 How satisfied are you with the following experience with our company: Product quality nan multiple_choice ['Extremely satisfied', 'Moderately satisfied', 'Neither satisfied nor dissatisfied', 'Moderately dissatisfied', 'Extremely dissatisfied']
2 satisfied_1 How satisfied are you with the following experience with our company: Customer support nan multiple_choice ['Extremely satisfied', 'Moderately satisfied', 'Neither satisfied nor dissatisfied', 'Moderately dissatisfied', 'Extremely dissatisfied']
3 satisfied_2 How satisfied are you with the following experience with our company: Purchasing experience nan multiple_choice ['Extremely satisfied', 'Moderately satisfied', 'Neither satisfied nor dissatisfied', 'Moderately dissatisfied', 'Extremely dissatisfied']
4 improve_0 Is there anything specific that our company can do to improve your experience? nan free_text nan
5 improve_1 Is there anything specific that our company can do to improve your experience? nan free_text nan
6 improve_2 Is there anything specific that our company can do to improve your experience? nan free_text nan

AI agent personas

EDSL comes with a variety of methods for designing AI agents to answer surveys. An Agent is constructed by passing a dictionary of relevant traits with optional additional instructions for the language model to reference in generating responses for the agent. Agents can be constructed from a variety of data sources, including existing survey data (e.g., a dataset of responses that were provided to some other questions). We can also use an EDSL question to draft some personas for agents. Here, we ask for a list of them:

[18]:
from edsl import QuestionList
[19]:
q_personas = QuestionList(
    question_name = "personas",
    question_text = "Draft 5 personas for diverse customers of landscaping business with varying satisfaction levels."
)

We can run this question alone and extract the response list (more on working with results below):

[20]:
personas = q_personas.run().select("personas").to_list()[0]
personas
Job Status (2024-12-28 15:44:51)
Job UUID e11fda90-5a22-4af9-bdfd-8b9235594184
Progress Bar URL https://www.expectedparrot.com/home/remote-job-progress/e11fda90-5a22-4af9-bdfd-8b9235594184
Error Report URL None
Results UUID ccf837e8-b1d3-4ae1-b783-806a293247e4
Results URL None
Current Status: Job completed and Results stored on Coop: https://www.expectedparrot.com/content/ccf837e8-b1d3-4ae1-b783-806a293247e4
[20]:
['John, a retired veteran who loves his garden and is highly satisfied with the personalized service',
 'Emily, a busy professional who is moderately satisfied but wishes for more eco-friendly options',
 'Raj, a young tech entrepreneur who is dissatisfied due to inconsistent appointment scheduling',
 'Maria, a single mother who is very satisfied with the affordable pricing and flexible payment plans',
 'Grace, an elderly woman who is dissatisfied because of slow response times to her queries']

Next we pass the personas to create a set of agents:

[21]:
from edsl import AgentList, Agent
[22]:
a = AgentList(
    Agent(traits = {"persona":p}) for p in personas
)

Selecting language models

EDSL works with many popular large language models that we can select to use with a survey. To see a list of available models:

[23]:
from edsl import Model
[24]:
# Model.available()

To select a model to use with a survey we pass a model name to a Model:

[25]:
m = Model("gemini-1.5-flash")

If we want to compare responses for several models, we can use a ModelList instead:

[26]:
from edsl import ModelList
[27]:
m = ModelList(
    Model(model) for model in ["gemini-1.5-flash", "gpt-4o"]
)

Note: If no model is specified when running a survey, the default model GPT-4o is used (as above when we generated personas).

Running a survey

We administer the survey by adding the agents and models with the by() method and then calling the run() method:

[28]:
results = survey.by(a).by(m).run()
Job Status (2024-12-28 15:47:52)
Job UUID 99b2b457-d27f-4ff5-9b1c-aea15eea2167
Progress Bar URL https://www.expectedparrot.com/home/remote-job-progress/99b2b457-d27f-4ff5-9b1c-aea15eea2167
Error Report URL None
Results UUID 7777e1b5-4832-47bb-b69e-dd98440fd05f
Results URL None
Current Status: Job completed and Results stored on Coop: https://www.expectedparrot.com/content/7777e1b5-4832-47bb-b69e-dd98440fd05f

This generates a dataset of Results that includes a response for each agent/model that was used. We can access the results with built-in methods for analysis. To see a list of all the components of the results:

[29]:
results.columns
[29]:
  0
0 agent.agent_instruction
1 agent.agent_name
2 agent.persona
3 answer.improve_0
4 answer.improve_1
5 answer.improve_2
6 answer.recommend
7 answer.satisfied_0
8 answer.satisfied_1
9 answer.satisfied_2
10 comment.improve_0_comment
11 comment.improve_1_comment
12 comment.improve_2_comment
13 comment.recommend_comment
14 comment.satisfied_0_comment
15 comment.satisfied_1_comment
16 comment.satisfied_2_comment
17 generated_tokens.improve_0_generated_tokens
18 generated_tokens.improve_1_generated_tokens
19 generated_tokens.improve_2_generated_tokens
20 generated_tokens.recommend_generated_tokens
21 generated_tokens.satisfied_0_generated_tokens
22 generated_tokens.satisfied_1_generated_tokens
23 generated_tokens.satisfied_2_generated_tokens
24 iteration.iteration
25 model.frequency_penalty
26 model.logprobs
27 model.maxOutputTokens
28 model.max_tokens
29 model.model
30 model.presence_penalty
31 model.stopSequences
32 model.temperature
33 model.topK
34 model.topP
35 model.top_logprobs
36 model.top_p
37 prompt.improve_0_system_prompt
38 prompt.improve_0_user_prompt
39 prompt.improve_1_system_prompt
40 prompt.improve_1_user_prompt
41 prompt.improve_2_system_prompt
42 prompt.improve_2_user_prompt
43 prompt.recommend_system_prompt
44 prompt.recommend_user_prompt
45 prompt.satisfied_0_system_prompt
46 prompt.satisfied_0_user_prompt
47 prompt.satisfied_1_system_prompt
48 prompt.satisfied_1_user_prompt
49 prompt.satisfied_2_system_prompt
50 prompt.satisfied_2_user_prompt
51 question_options.improve_0_question_options
52 question_options.improve_1_question_options
53 question_options.improve_2_question_options
54 question_options.recommend_question_options
55 question_options.satisfied_0_question_options
56 question_options.satisfied_1_question_options
57 question_options.satisfied_2_question_options
58 question_text.improve_0_question_text
59 question_text.improve_1_question_text
60 question_text.improve_2_question_text
61 question_text.recommend_question_text
62 question_text.satisfied_0_question_text
63 question_text.satisfied_1_question_text
64 question_text.satisfied_2_question_text
65 question_type.improve_0_question_type
66 question_type.improve_1_question_type
67 question_type.improve_2_question_type
68 question_type.recommend_question_type
69 question_type.satisfied_0_question_type
70 question_type.satisfied_1_question_type
71 question_type.satisfied_2_question_type
72 raw_model_response.improve_0_cost
73 raw_model_response.improve_0_one_usd_buys
74 raw_model_response.improve_0_raw_model_response
75 raw_model_response.improve_1_cost
76 raw_model_response.improve_1_one_usd_buys
77 raw_model_response.improve_1_raw_model_response
78 raw_model_response.improve_2_cost
79 raw_model_response.improve_2_one_usd_buys
80 raw_model_response.improve_2_raw_model_response
81 raw_model_response.recommend_cost
82 raw_model_response.recommend_one_usd_buys
83 raw_model_response.recommend_raw_model_response
84 raw_model_response.satisfied_0_cost
85 raw_model_response.satisfied_0_one_usd_buys
86 raw_model_response.satisfied_0_raw_model_response
87 raw_model_response.satisfied_1_cost
88 raw_model_response.satisfied_1_one_usd_buys
89 raw_model_response.satisfied_1_raw_model_response
90 raw_model_response.satisfied_2_cost
91 raw_model_response.satisfied_2_one_usd_buys
92 raw_model_response.satisfied_2_raw_model_response

For example, we can filter, sort and display columns of results in a table:

[31]:
(
    results
    .filter("model.model == 'gemini-1.5-flash'")
    .sort_by("recommend", reverse=True)
    .select("model","persona","recommend", "recommend_comment")
)
[31]:
  model.model agent.persona answer.recommend comment.recommend_comment
0 gemini-1.5-flash John, a retired veteran who loves his garden and is highly satisfied with the personalized service 10 Honestly, I've been so pleased with the personalized attention I've received. It's a breath of fresh air compared to the usual impersonal corporate stuff. Wouldn't hesitate to recommend you to anyone.
1 gemini-1.5-flash Maria, a single mother who is very satisfied with the affordable pricing and flexible payment plans 10 Honestly, I'd give you guys a 10 out of 10! As a single mom, being able to afford things and having flexible payment options is a lifesaver. I really appreciate it.
2 gemini-1.5-flash Emily, a busy professional who is moderately satisfied but wishes for more eco-friendly options 7 Honestly, I've been pretty happy with your services so far. They've gotten the job done, but I do wish there were more sustainable options available. That's my main hesitation.
3 gemini-1.5-flash Raj, a young tech entrepreneur who is dissatisfied due to inconsistent appointment scheduling 3 Honestly, the constant rescheduling is driving me crazy. I *want* to like your company – the product itself is great – but the unreliable appointments are a huge negative. It's just too much hassle.
4 gemini-1.5-flash Grace, an elderly woman who is dissatisfied because of slow response times to her queries 2 Honestly, I'm finding it terribly difficult to even get a simple question answered. The wait times are just inexcusable. I'm not sure I'd want to put a friend through that.
[32]:
(
    results
    .filter("model.model == 'gemini-1.5-flash'")
    .sort_by("satisfied_0")
    .select("satisfied_0", "satisfied_0_comment", "improve_0")
)
[32]:
  answer.satisfied_0 comment.satisfied_0_comment answer.improve_0
0 Extremely satisfied My tomatoes have never been so plump and juicy! The quality of your gardening supplies is top-notch, just like the personalized service I received. Well, honestly, I'm just about as happy as a clam in my garden! The product quality was top-notch, no complaints there at all. You guys really went above and beyond. If I had to nitpick – and I'm really stretching here – maybe a little more detail in the initial instructions could have saved me a phone call. But that's a minor thing. The personalized service I received made all the difference. I felt like a valued customer, not just another number. That's what keeps me coming back, you know? So, keep doing what you're doing!
1 Extremely satisfied Honestly, for the price, the quality is amazing! I'm a single mom, so budgeting is everything, and I've never felt like I'm sacrificing quality for affordability. Honestly? Keeping things affordable and flexible is the biggest thing for me. As a single mom, every penny counts. So, if you could keep those payment plans as straightforward and easy to understand as they are now, that would be amazing. I've never had any trouble, and that peace of mind is worth a lot. Beyond that, I'm really happy with everything!
2 Moderately dissatisfied Honestly, the product itself is okay, but the constant rescheduling of appointments to discuss features and address bugs is a huge headache. It impacts my ability to plan, and that frustration bleeds over into how I feel about the product as a whole. Ugh, look, the product itself is okay, I'll give you that. It's not *bad*, but it's not amazing either. The *real* problem is scheduling. Seriously, it's a nightmare. One minute I'm getting an email saying my appointment's confirmed, the next it's been rescheduled, then cancelled, then rescheduled again... it's a chaotic mess. I'm constantly having to rearrange my whole day because of your scheduling inconsistencies. If you could just get that sorted – make the scheduling process reliable and actually *stick* to the appointments – that would drastically improve my experience. That's way more important to me than minor tweaks to the product itself.
3 Moderately dissatisfied Honestly, the product itself is alright, but it took them *forever* to even get it to me. That's what really soured the experience. Oh, honey, "moderately dissatisfied" is putting it mildly when it comes to the product quality. Frankly, I've had to wait *ages* for even the simplest of issues to be addressed. What I really need is faster response times. I'm not asking for miracles, just a bit of promptness. If a problem arises, I shouldn't have to wait weeks, or even days, to get a response. A simple acknowledgment that my query has been received would be a start! Then, a reasonable timeframe for a solution, not this endless waiting game. Is that too much to ask?
4 Moderately satisfied The product quality is okay; it does what it's supposed to. But I wish there were more sustainable options available. I'm always looking for ways to reduce my environmental impact, and that's a factor I consider when making purchases. Honestly? I'm pretty happy with the product quality, but I'd love to see you guys offer more sustainable packaging options. It's something I'm increasingly thinking about, and it would really make a difference for me. Little things, like less plastic or recycled materials, would go a long way. I know it's probably a bigger undertaking, but it's something I'd really appreciate.
[33]:
(
    results
    .filter("model.model == 'gemini-1.5-flash'")
    .select("satisfied_1", "satisfied_1_comment", "improve_1")
    .print(pretty_labels = {
        "answer.satisfied_1": "Customer service: satisfaction",
        "comment.satisfied_1_comment": "Customer service: comment",
        "answer.improve_1": "Customer service: improvements"
    })
)
[33]:
  Customer service: satisfaction Customer service: comment Customer service: improvements
0 Extremely satisfied Honestly, I've been a customer for years, and the personalized service I've received, especially from customer support, has been top-notch. They always go the extra mile, and it's a breath of fresh air in this day and age. Makes a fella feel appreciated, you know? Well, now that's a thoughtful question. Honestly, I've been so pleased with the personalized attention I've received. It's a breath of fresh air, you know? In this day and age, it feels like you're dealing with robots half the time. So, to be perfectly frank, I can't think of anything specific you need to *improve*.
1 Moderately satisfied Honestly, the customer support was fine. They resolved my issue, but it took a few tries and a bit longer than I'd have liked. I wish they had more eco-friendly options for contacting them, like email instead of just phone. Honestly? It's a bit of a mixed bag. Your customer support was alright – helpful enough when I needed it, but it wasn't exactly *amazing*. What would really bump things up for me would be more eco-conscious options. I'm always looking for companies that are doing their part for the environment, and that includes things like packaging, shipping methods, and even the overall sustainability of your products. If you could offer more choices in that area, it would definitely make me a much happier customer. I know it's a big ask, but it's something I'm increasingly prioritizing.
2 Moderately dissatisfied Ugh, honestly, the appointment scheduling was a nightmare. I had to reschedule twice because of conflicts, and the whole process felt clunky and unprofessional. It's a shame, because the actual support I *did* get was pretty good. But the scheduling hassle really soured the whole experience. Ugh, honestly? The biggest thing is the scheduling. It's a nightmare. I've had appointments moved, cancelled, rescheduled... it's a total mess. I understand things happen, but the lack of consistency and the way it's handled is incredibly frustrating. If you could improve the reliability of appointments and maybe offer more options – like different time slots, or even a wider range of days – that would make a huge difference. I spend way too much time just trying to *get* an appointment, let alone actually *using* your customer support. That's time I could be spending on, you know, *actually running my business*.
3 Extremely satisfied Honestly, as a single mom, I'm always juggling so much. The customer support team was incredibly understanding and helpful – they really went above and beyond to work with my schedule and payment plan. I can't say enough good things! Honestly? Keeping things affordable and flexible is the biggest thing. As a single mom, every penny counts, and knowing I can adjust my payments if something unexpected comes up... that's a lifesaver. So, maybe just keeping a close eye on those things and making sure those options remain available would be amazing. I don't need anything fancy, just reliable and budget-friendly.
4 Extremely dissatisfied Honestly, the wait times were atrocious. I spent more time on hold than I did actually talking to someone! It's simply unacceptable. Oh, honey, "improve my experience"? Where do I even begin? "Extremely dissatisfied" doesn't even begin to cover it. I've been waiting for ages – *ages* – for simple answers. I'm talking days, sometimes weeks, for a response to an email! Days! I'm not asking for the moon, just a prompt and courteous reply.

Posting to the Coop

The Coop is a platform for creating, storing and sharing LLM-based research. It is fully integrated with EDSL, allowing you to access objects from your workspace or Coop account interface. Learn more about creating an account and using the Coop. Here we post the survey and results publicly:

[34]:
survey.push(description = "Example NPS survey", visibility = "public")
[34]:
{'description': 'Example NPS survey',
 'object_type': 'survey',
 'url': 'https://www.expectedparrot.com/content/a9cfac3d-3d7f-414e-b58d-f245f714ecaf',
 'uuid': 'a9cfac3d-3d7f-414e-b58d-f245f714ecaf',
 'version': '0.1.39.dev2',
 'visibility': 'public'}

We can also post a notebook, such as this one:

[35]:
from edsl import Notebook
[36]:
n = Notebook(path = "nps_survey.ipynb")
[37]:
info = n.push(description = "Notebook for simulating an NPS survey")
info
[37]:
{'description': 'Notebook for simulating an NPS survey',
 'object_type': 'notebook',
 'url': 'https://www.expectedparrot.com/content/de151af7-d9a2-40fd-9e74-198a32ca94da',
 'uuid': 'de151af7-d9a2-40fd-9e74-198a32ca94da',
 'version': '0.1.39.dev2',
 'visibility': 'unlisted'}

To update an object at the Coop:

[38]:
n = Notebook(path = "nps_survey.ipynb") # resave
[39]:
n.patch(uuid = info["uuid"], visibility = "public", value = n)
[39]:
{'status': 'success'}