edsl 0.1.36.dev5__py3-none-any.whl → 0.1.36.dev6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. edsl/Base.py +303 -303
  2. edsl/BaseDiff.py +260 -260
  3. edsl/TemplateLoader.py +24 -24
  4. edsl/__init__.py +47 -47
  5. edsl/__version__.py +1 -1
  6. edsl/agents/Agent.py +804 -804
  7. edsl/agents/AgentList.py +337 -337
  8. edsl/agents/Invigilator.py +222 -222
  9. edsl/agents/InvigilatorBase.py +294 -294
  10. edsl/agents/PromptConstructor.py +312 -312
  11. edsl/agents/__init__.py +3 -3
  12. edsl/agents/descriptors.py +86 -86
  13. edsl/agents/prompt_helpers.py +129 -129
  14. edsl/auto/AutoStudy.py +117 -117
  15. edsl/auto/StageBase.py +230 -230
  16. edsl/auto/StageGenerateSurvey.py +178 -178
  17. edsl/auto/StageLabelQuestions.py +125 -125
  18. edsl/auto/StagePersona.py +61 -61
  19. edsl/auto/StagePersonaDimensionValueRanges.py +88 -88
  20. edsl/auto/StagePersonaDimensionValues.py +74 -74
  21. edsl/auto/StagePersonaDimensions.py +69 -69
  22. edsl/auto/StageQuestions.py +73 -73
  23. edsl/auto/SurveyCreatorPipeline.py +21 -21
  24. edsl/auto/utilities.py +224 -224
  25. edsl/base/Base.py +289 -289
  26. edsl/config.py +149 -149
  27. edsl/conjure/AgentConstructionMixin.py +152 -152
  28. edsl/conjure/Conjure.py +62 -62
  29. edsl/conjure/InputData.py +659 -659
  30. edsl/conjure/InputDataCSV.py +48 -48
  31. edsl/conjure/InputDataMixinQuestionStats.py +182 -182
  32. edsl/conjure/InputDataPyRead.py +91 -91
  33. edsl/conjure/InputDataSPSS.py +8 -8
  34. edsl/conjure/InputDataStata.py +8 -8
  35. edsl/conjure/QuestionOptionMixin.py +76 -76
  36. edsl/conjure/QuestionTypeMixin.py +23 -23
  37. edsl/conjure/RawQuestion.py +65 -65
  38. edsl/conjure/SurveyResponses.py +7 -7
  39. edsl/conjure/__init__.py +9 -9
  40. edsl/conjure/naming_utilities.py +263 -263
  41. edsl/conjure/utilities.py +201 -201
  42. edsl/conversation/Conversation.py +238 -238
  43. edsl/conversation/car_buying.py +58 -58
  44. edsl/conversation/mug_negotiation.py +81 -81
  45. edsl/conversation/next_speaker_utilities.py +93 -93
  46. edsl/coop/PriceFetcher.py +54 -54
  47. edsl/coop/__init__.py +2 -2
  48. edsl/coop/coop.py +849 -849
  49. edsl/coop/utils.py +131 -131
  50. edsl/data/Cache.py +527 -527
  51. edsl/data/CacheEntry.py +228 -228
  52. edsl/data/CacheHandler.py +149 -149
  53. edsl/data/RemoteCacheSync.py +83 -83
  54. edsl/data/SQLiteDict.py +292 -292
  55. edsl/data/__init__.py +4 -4
  56. edsl/data/orm.py +10 -10
  57. edsl/data_transfer_models.py +73 -73
  58. edsl/enums.py +173 -173
  59. edsl/exceptions/__init__.py +50 -50
  60. edsl/exceptions/agents.py +40 -40
  61. edsl/exceptions/configuration.py +16 -16
  62. edsl/exceptions/coop.py +10 -10
  63. edsl/exceptions/data.py +14 -14
  64. edsl/exceptions/general.py +34 -34
  65. edsl/exceptions/jobs.py +33 -33
  66. edsl/exceptions/language_models.py +63 -63
  67. edsl/exceptions/prompts.py +15 -15
  68. edsl/exceptions/questions.py +91 -91
  69. edsl/exceptions/results.py +26 -26
  70. edsl/exceptions/surveys.py +34 -34
  71. edsl/inference_services/AnthropicService.py +87 -87
  72. edsl/inference_services/AwsBedrock.py +115 -115
  73. edsl/inference_services/AzureAI.py +217 -217
  74. edsl/inference_services/DeepInfraService.py +18 -18
  75. edsl/inference_services/GoogleService.py +156 -156
  76. edsl/inference_services/GroqService.py +20 -20
  77. edsl/inference_services/InferenceServiceABC.py +147 -147
  78. edsl/inference_services/InferenceServicesCollection.py +72 -68
  79. edsl/inference_services/MistralAIService.py +123 -123
  80. edsl/inference_services/OllamaService.py +18 -18
  81. edsl/inference_services/OpenAIService.py +224 -224
  82. edsl/inference_services/TestService.py +89 -89
  83. edsl/inference_services/TogetherAIService.py +170 -170
  84. edsl/inference_services/models_available_cache.py +118 -94
  85. edsl/inference_services/rate_limits_cache.py +25 -25
  86. edsl/inference_services/registry.py +39 -39
  87. edsl/inference_services/write_available.py +10 -10
  88. edsl/jobs/Answers.py +56 -56
  89. edsl/jobs/Jobs.py +1112 -1112
  90. edsl/jobs/__init__.py +1 -1
  91. edsl/jobs/buckets/BucketCollection.py +63 -63
  92. edsl/jobs/buckets/ModelBuckets.py +65 -65
  93. edsl/jobs/buckets/TokenBucket.py +248 -248
  94. edsl/jobs/interviews/Interview.py +651 -651
  95. edsl/jobs/interviews/InterviewExceptionCollection.py +99 -99
  96. edsl/jobs/interviews/InterviewExceptionEntry.py +182 -182
  97. edsl/jobs/interviews/InterviewStatistic.py +63 -63
  98. edsl/jobs/interviews/InterviewStatisticsCollection.py +25 -25
  99. edsl/jobs/interviews/InterviewStatusDictionary.py +78 -78
  100. edsl/jobs/interviews/InterviewStatusLog.py +92 -92
  101. edsl/jobs/interviews/ReportErrors.py +66 -66
  102. edsl/jobs/interviews/interview_status_enum.py +9 -9
  103. edsl/jobs/runners/JobsRunnerAsyncio.py +337 -337
  104. edsl/jobs/runners/JobsRunnerStatus.py +332 -332
  105. edsl/jobs/tasks/QuestionTaskCreator.py +242 -242
  106. edsl/jobs/tasks/TaskCreators.py +64 -64
  107. edsl/jobs/tasks/TaskHistory.py +441 -441
  108. edsl/jobs/tasks/TaskStatusLog.py +23 -23
  109. edsl/jobs/tasks/task_status_enum.py +163 -163
  110. edsl/jobs/tokens/InterviewTokenUsage.py +27 -27
  111. edsl/jobs/tokens/TokenUsage.py +34 -34
  112. edsl/language_models/LanguageModel.py +718 -718
  113. edsl/language_models/ModelList.py +102 -102
  114. edsl/language_models/RegisterLanguageModelsMeta.py +184 -184
  115. edsl/language_models/__init__.py +2 -2
  116. edsl/language_models/fake_openai_call.py +15 -15
  117. edsl/language_models/fake_openai_service.py +61 -61
  118. edsl/language_models/registry.py +137 -137
  119. edsl/language_models/repair.py +156 -156
  120. edsl/language_models/unused/ReplicateBase.py +83 -83
  121. edsl/language_models/utilities.py +64 -64
  122. edsl/notebooks/Notebook.py +259 -259
  123. edsl/notebooks/__init__.py +1 -1
  124. edsl/prompts/Prompt.py +358 -358
  125. edsl/prompts/__init__.py +2 -2
  126. edsl/questions/AnswerValidatorMixin.py +289 -289
  127. edsl/questions/QuestionBase.py +616 -616
  128. edsl/questions/QuestionBaseGenMixin.py +161 -161
  129. edsl/questions/QuestionBasePromptsMixin.py +266 -266
  130. edsl/questions/QuestionBudget.py +227 -227
  131. edsl/questions/QuestionCheckBox.py +359 -359
  132. edsl/questions/QuestionExtract.py +183 -183
  133. edsl/questions/QuestionFreeText.py +113 -113
  134. edsl/questions/QuestionFunctional.py +159 -159
  135. edsl/questions/QuestionList.py +231 -231
  136. edsl/questions/QuestionMultipleChoice.py +286 -286
  137. edsl/questions/QuestionNumerical.py +153 -153
  138. edsl/questions/QuestionRank.py +324 -324
  139. edsl/questions/Quick.py +41 -41
  140. edsl/questions/RegisterQuestionsMeta.py +71 -71
  141. edsl/questions/ResponseValidatorABC.py +174 -174
  142. edsl/questions/SimpleAskMixin.py +73 -73
  143. edsl/questions/__init__.py +26 -26
  144. edsl/questions/compose_questions.py +98 -98
  145. edsl/questions/decorators.py +21 -21
  146. edsl/questions/derived/QuestionLikertFive.py +76 -76
  147. edsl/questions/derived/QuestionLinearScale.py +87 -87
  148. edsl/questions/derived/QuestionTopK.py +91 -91
  149. edsl/questions/derived/QuestionYesNo.py +82 -82
  150. edsl/questions/descriptors.py +418 -418
  151. edsl/questions/prompt_templates/question_budget.jinja +13 -13
  152. edsl/questions/prompt_templates/question_checkbox.jinja +32 -32
  153. edsl/questions/prompt_templates/question_extract.jinja +11 -11
  154. edsl/questions/prompt_templates/question_free_text.jinja +3 -3
  155. edsl/questions/prompt_templates/question_linear_scale.jinja +11 -11
  156. edsl/questions/prompt_templates/question_list.jinja +17 -17
  157. edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -33
  158. edsl/questions/prompt_templates/question_numerical.jinja +36 -36
  159. edsl/questions/question_registry.py +147 -147
  160. edsl/questions/settings.py +12 -12
  161. edsl/questions/templates/budget/answering_instructions.jinja +7 -7
  162. edsl/questions/templates/budget/question_presentation.jinja +7 -7
  163. edsl/questions/templates/checkbox/answering_instructions.jinja +10 -10
  164. edsl/questions/templates/checkbox/question_presentation.jinja +22 -22
  165. edsl/questions/templates/extract/answering_instructions.jinja +7 -7
  166. edsl/questions/templates/likert_five/answering_instructions.jinja +10 -10
  167. edsl/questions/templates/likert_five/question_presentation.jinja +11 -11
  168. edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -5
  169. edsl/questions/templates/linear_scale/question_presentation.jinja +5 -5
  170. edsl/questions/templates/list/answering_instructions.jinja +3 -3
  171. edsl/questions/templates/list/question_presentation.jinja +5 -5
  172. edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -9
  173. edsl/questions/templates/multiple_choice/question_presentation.jinja +11 -11
  174. edsl/questions/templates/numerical/answering_instructions.jinja +6 -6
  175. edsl/questions/templates/numerical/question_presentation.jinja +6 -6
  176. edsl/questions/templates/rank/answering_instructions.jinja +11 -11
  177. edsl/questions/templates/rank/question_presentation.jinja +15 -15
  178. edsl/questions/templates/top_k/answering_instructions.jinja +8 -8
  179. edsl/questions/templates/top_k/question_presentation.jinja +22 -22
  180. edsl/questions/templates/yes_no/answering_instructions.jinja +6 -6
  181. edsl/questions/templates/yes_no/question_presentation.jinja +11 -11
  182. edsl/results/Dataset.py +293 -293
  183. edsl/results/DatasetExportMixin.py +693 -693
  184. edsl/results/DatasetTree.py +145 -145
  185. edsl/results/Result.py +433 -433
  186. edsl/results/Results.py +1158 -1158
  187. edsl/results/ResultsDBMixin.py +238 -238
  188. edsl/results/ResultsExportMixin.py +43 -43
  189. edsl/results/ResultsFetchMixin.py +33 -33
  190. edsl/results/ResultsGGMixin.py +121 -121
  191. edsl/results/ResultsToolsMixin.py +98 -98
  192. edsl/results/Selector.py +118 -118
  193. edsl/results/__init__.py +2 -2
  194. edsl/results/tree_explore.py +115 -115
  195. edsl/scenarios/FileStore.py +443 -443
  196. edsl/scenarios/Scenario.py +507 -507
  197. edsl/scenarios/ScenarioHtmlMixin.py +59 -59
  198. edsl/scenarios/ScenarioList.py +1101 -1101
  199. edsl/scenarios/ScenarioListExportMixin.py +52 -52
  200. edsl/scenarios/ScenarioListPdfMixin.py +261 -261
  201. edsl/scenarios/__init__.py +2 -2
  202. edsl/shared.py +1 -1
  203. edsl/study/ObjectEntry.py +173 -173
  204. edsl/study/ProofOfWork.py +113 -113
  205. edsl/study/SnapShot.py +80 -80
  206. edsl/study/Study.py +528 -528
  207. edsl/study/__init__.py +4 -4
  208. edsl/surveys/DAG.py +148 -148
  209. edsl/surveys/Memory.py +31 -31
  210. edsl/surveys/MemoryPlan.py +244 -244
  211. edsl/surveys/Rule.py +324 -324
  212. edsl/surveys/RuleCollection.py +387 -387
  213. edsl/surveys/Survey.py +1772 -1772
  214. edsl/surveys/SurveyCSS.py +261 -261
  215. edsl/surveys/SurveyExportMixin.py +259 -259
  216. edsl/surveys/SurveyFlowVisualizationMixin.py +121 -121
  217. edsl/surveys/SurveyQualtricsImport.py +284 -284
  218. edsl/surveys/__init__.py +3 -3
  219. edsl/surveys/base.py +53 -53
  220. edsl/surveys/descriptors.py +56 -56
  221. edsl/surveys/instructions/ChangeInstruction.py +47 -47
  222. edsl/surveys/instructions/Instruction.py +51 -51
  223. edsl/surveys/instructions/InstructionCollection.py +77 -77
  224. edsl/templates/error_reporting/base.html +23 -23
  225. edsl/templates/error_reporting/exceptions_by_model.html +34 -34
  226. edsl/templates/error_reporting/exceptions_by_question_name.html +16 -16
  227. edsl/templates/error_reporting/exceptions_by_type.html +16 -16
  228. edsl/templates/error_reporting/interview_details.html +115 -115
  229. edsl/templates/error_reporting/interviews.html +9 -9
  230. edsl/templates/error_reporting/overview.html +4 -4
  231. edsl/templates/error_reporting/performance_plot.html +1 -1
  232. edsl/templates/error_reporting/report.css +73 -73
  233. edsl/templates/error_reporting/report.html +117 -117
  234. edsl/templates/error_reporting/report.js +25 -25
  235. edsl/tools/__init__.py +1 -1
  236. edsl/tools/clusters.py +192 -192
  237. edsl/tools/embeddings.py +27 -27
  238. edsl/tools/embeddings_plotting.py +118 -118
  239. edsl/tools/plotting.py +112 -112
  240. edsl/tools/summarize.py +18 -18
  241. edsl/utilities/SystemInfo.py +28 -28
  242. edsl/utilities/__init__.py +22 -22
  243. edsl/utilities/ast_utilities.py +25 -25
  244. edsl/utilities/data/Registry.py +6 -6
  245. edsl/utilities/data/__init__.py +1 -1
  246. edsl/utilities/data/scooter_results.json +1 -1
  247. edsl/utilities/decorators.py +77 -77
  248. edsl/utilities/gcp_bucket/cloud_storage.py +96 -96
  249. edsl/utilities/interface.py +627 -627
  250. edsl/utilities/repair_functions.py +28 -28
  251. edsl/utilities/restricted_python.py +70 -70
  252. edsl/utilities/utilities.py +391 -391
  253. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev6.dist-info}/LICENSE +21 -21
  254. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev6.dist-info}/METADATA +1 -1
  255. edsl-0.1.36.dev6.dist-info/RECORD +279 -0
  256. edsl-0.1.36.dev5.dist-info/RECORD +0 -279
  257. {edsl-0.1.36.dev5.dist-info → edsl-0.1.36.dev6.dist-info}/WHEEL +0 -0
edsl/results/Results.py CHANGED
@@ -1,1158 +1,1158 @@
1
- """
2
- The Results object is the result of running a survey.
3
- It is not typically instantiated directly, but is returned by the run method of a `Job` object.
4
- """
5
-
6
- from __future__ import annotations
7
- import json
8
- import random
9
- from collections import UserList, defaultdict
10
- from typing import Optional, Callable, Any, Type, Union, List
11
-
12
- from simpleeval import EvalWithCompoundTypes
13
-
14
- from edsl.exceptions.results import (
15
- ResultsBadMutationstringError,
16
- ResultsColumnNotFoundError,
17
- ResultsInvalidNameError,
18
- ResultsMutateError,
19
- ResultsFilterError,
20
- ResultsDeserializationError,
21
- )
22
-
23
- from edsl.results.ResultsExportMixin import ResultsExportMixin
24
- from edsl.results.ResultsToolsMixin import ResultsToolsMixin
25
- from edsl.results.ResultsDBMixin import ResultsDBMixin
26
- from edsl.results.ResultsGGMixin import ResultsGGMixin
27
- from edsl.results.ResultsFetchMixin import ResultsFetchMixin
28
-
29
- from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
30
- from edsl.utilities.utilities import dict_hash
31
-
32
-
33
- from edsl.Base import Base
34
-
35
-
36
- class Mixins(
37
- ResultsExportMixin,
38
- ResultsDBMixin,
39
- ResultsFetchMixin,
40
- ResultsGGMixin,
41
- ResultsToolsMixin,
42
- ):
43
- def print_long(self, max_rows=None) -> None:
44
- """Print the results in long format.
45
-
46
- >>> from edsl.results import Results
47
- >>> r = Results.example()
48
- >>> r.select('how_feeling').print_long(max_rows = 2)
49
- ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┓
50
- ┃ Result index ┃ Key ┃ Value ┃
51
- ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━┩
52
- │ 0 │ how_feeling │ OK │
53
- │ 1 │ how_feeling │ Great │
54
- └──────────────┴─────────────┴───────┘
55
- """
56
- from edsl.utilities.interface import print_results_long
57
-
58
- print_results_long(self, max_rows=max_rows)
59
-
60
-
61
- class Results(UserList, Mixins, Base):
62
- """
63
- This class is a UserList of Result objects.
64
-
65
- It is instantiated with a `Survey` and a list of `Result` objects.
66
- It can be manipulated in various ways with select, filter, mutate, etc.
67
- It also has a list of created_columns, which are columns that have been created with `mutate` and are not part of the original data.
68
- """
69
-
70
- known_data_types = [
71
- "answer",
72
- "scenario",
73
- "agent",
74
- "model",
75
- "prompt",
76
- "raw_model_response",
77
- "iteration",
78
- "question_text",
79
- "question_options",
80
- "question_type",
81
- "comment",
82
- "generated_tokens",
83
- ]
84
-
85
- def __init__(
86
- self,
87
- survey: Optional["Survey"] = None,
88
- data: Optional[list["Result"]] = None,
89
- created_columns: Optional[list[str]] = None,
90
- cache: Optional["Cache"] = None,
91
- job_uuid: Optional[str] = None,
92
- total_results: Optional[int] = None,
93
- task_history: Optional["TaskHistory"] = None,
94
- ):
95
- """Instantiate a `Results` object with a survey and a list of `Result` objects.
96
-
97
- :param survey: A Survey object.
98
- :param data: A list of Result objects.
99
- :param created_columns: A list of strings that are created columns.
100
- :param job_uuid: A string representing the job UUID.
101
- :param total_results: An integer representing the total number of results.
102
- """
103
- super().__init__(data)
104
- from edsl.data.Cache import Cache
105
- from edsl.jobs.tasks.TaskHistory import TaskHistory
106
-
107
- self.survey = survey
108
- self.created_columns = created_columns or []
109
- self._job_uuid = job_uuid
110
- self._total_results = total_results
111
- self.cache = cache or Cache()
112
-
113
- self.task_history = task_history or TaskHistory(interviews = [])
114
-
115
- if hasattr(self, "_add_output_functions"):
116
- self._add_output_functions()
117
-
118
- def leaves(self):
119
- leaves = []
120
- for result in self:
121
- leaves.extend(result.leaves())
122
- return leaves
123
-
124
- def tree(
125
- self,
126
- fold_attributes: Optional[List[str]] = None,
127
- drop: Optional[List[str]] = None,
128
- open_file=True,
129
- ) -> dict:
130
- """Return the results as a tree."""
131
- from edsl.results.tree_explore import FoldableHTMLTableGenerator
132
-
133
- if drop is None:
134
- drop = []
135
-
136
- valid_attributes = [
137
- "model",
138
- "scenario",
139
- "agent",
140
- "answer",
141
- "question",
142
- "iteration",
143
- ]
144
- if fold_attributes is None:
145
- fold_attributes = []
146
-
147
- for attribute in fold_attributes:
148
- if attribute not in valid_attributes:
149
- raise ValueError(
150
- f"Invalid fold attribute: {attribute}; must be in {valid_attributes}"
151
- )
152
- data = self.leaves()
153
- generator = FoldableHTMLTableGenerator(data)
154
- tree = generator.tree(fold_attributes=fold_attributes, drop=drop)
155
- html_content = generator.generate_html(tree, fold_attributes)
156
- import tempfile
157
- from edsl.utilities.utilities import is_notebook
158
-
159
- from IPython.display import display, HTML
160
-
161
- if is_notebook():
162
- import html
163
- from IPython.display import display, HTML
164
-
165
- height = 1000
166
- width = 1000
167
- escaped_output = html.escape(html_content)
168
- # escaped_output = rendered_html
169
- iframe = f""""
170
- <iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
171
- """
172
- display(HTML(iframe))
173
- return None
174
-
175
- with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
176
- f.write(html_content.encode())
177
- print(f"HTML file has been generated: {f.name}")
178
-
179
- if open_file:
180
- import webbrowser
181
- import time
182
-
183
- time.sleep(1) # Wait for 1 second
184
- # webbrowser.open(f.name)
185
- import os
186
-
187
- filename = f.name
188
- webbrowser.open(f"file://{os.path.abspath(filename)}")
189
-
190
- else:
191
- return html_content
192
-
193
- def code(self):
194
- raise NotImplementedError
195
-
196
- def __getitem__(self, i):
197
- if isinstance(i, int):
198
- return self.data[i]
199
-
200
- if isinstance(i, slice):
201
- return self.__class__(survey=self.survey, data=self.data[i])
202
-
203
- if isinstance(i, str):
204
- return self.to_dict()[i]
205
-
206
- raise TypeError("Invalid argument type")
207
-
208
- def _update_results(self) -> None:
209
- from edsl import Agent, Scenario
210
- from edsl.language_models import LanguageModel
211
- from edsl.results import Result
212
-
213
- if self._job_uuid and len(self.data) < self._total_results:
214
- results = [
215
- Result(
216
- agent=Agent.from_dict(json.loads(r.agent)),
217
- scenario=Scenario.from_dict(json.loads(r.scenario)),
218
- model=LanguageModel.from_dict(json.loads(r.model)),
219
- iteration=1,
220
- answer=json.loads(r.answer),
221
- )
222
- for r in CRUD.read_results(self._job_uuid)
223
- ]
224
- self.data = results
225
-
226
- def __add__(self, other: Results) -> Results:
227
- """Add two Results objects together.
228
- They must have the same survey and created columns.
229
- :param other: A Results object.
230
-
231
- Example:
232
-
233
- >>> r = Results.example()
234
- >>> r2 = Results.example()
235
- >>> r3 = r + r2
236
- """
237
- if self.survey != other.survey:
238
- raise Exception(
239
- "The surveys are not the same so they cannot be added together."
240
- )
241
- if self.created_columns != other.created_columns:
242
- raise Exception(
243
- "The created columns are not the same so they cannot be added together."
244
- )
245
-
246
- return Results(
247
- survey=self.survey,
248
- data=self.data + other.data,
249
- created_columns=self.created_columns,
250
- )
251
-
252
- def __repr__(self) -> str:
253
- import reprlib
254
-
255
- return f"Results(data = {reprlib.repr(self.data)}, survey = {repr(self.survey)}, created_columns = {self.created_columns})"
256
-
257
- def _repr_html_(self) -> str:
258
- from IPython.display import HTML
259
-
260
- json_str = json.dumps(self.to_dict()["data"], indent=4)
261
- from pygments import highlight
262
- from pygments.lexers import JsonLexer
263
- from pygments.formatters import HtmlFormatter
264
-
265
- formatted_json = highlight(
266
- json_str,
267
- JsonLexer(),
268
- HtmlFormatter(style="default", full=True, noclasses=True),
269
- )
270
- return HTML(formatted_json).data
271
-
272
- def _to_dict(self, sort=False):
273
- from edsl.data.Cache import Cache
274
-
275
- if sort:
276
- data = sorted([result for result in self.data], key=lambda x: hash(x))
277
- else:
278
- data = [result for result in self.data]
279
- return {
280
- "data": [result.to_dict() for result in data],
281
- "survey": self.survey.to_dict(),
282
- "created_columns": self.created_columns,
283
- "cache": Cache() if not hasattr(self, "cache") else self.cache.to_dict(),
284
- "task_history": self.task_history.to_dict(),
285
- }
286
-
287
- def compare(self, other_results):
288
- """
289
- Compare two Results objects and return the differences.
290
- """
291
- hashes_0 = [hash(result) for result in self]
292
- hashes_1 = [hash(result) for result in other_results]
293
-
294
- in_self_but_not_other = set(hashes_0).difference(set(hashes_1))
295
- in_other_but_not_self = set(hashes_1).difference(set(hashes_0))
296
-
297
- indicies_self = [hashes_0.index(h) for h in in_self_but_not_other]
298
- indices_other = [hashes_1.index(h) for h in in_other_but_not_self]
299
- return {
300
- "a_not_b": [self[i] for i in indicies_self],
301
- "b_not_a": [other_results[i] for i in indices_other],
302
- }
303
-
304
- @property
305
- def has_unfixed_exceptions(self):
306
- return self.task_history.has_unfixed_exceptions
307
-
308
- @add_edsl_version
309
- def to_dict(self) -> dict[str, Any]:
310
- """Convert the Results object to a dictionary.
311
-
312
- The dictionary can be quite large, as it includes all of the data in the Results object.
313
-
314
- Example: Illustrating just the keys of the dictionary.
315
-
316
- >>> r = Results.example()
317
- >>> r.to_dict().keys()
318
- dict_keys(['data', 'survey', 'created_columns', 'cache', 'task_history', 'edsl_version', 'edsl_class_name'])
319
- """
320
- return self._to_dict()
321
-
322
- def __hash__(self) -> int:
323
- return dict_hash(self._to_dict(sort=True))
324
-
325
- @property
326
- def hashes(self) -> set:
327
- return set(hash(result) for result in self.data)
328
-
329
- def sample(self, n: int) -> "Results":
330
- """Return a random sample of the results.
331
-
332
- :param n: The number of samples to return.
333
-
334
- >>> from edsl.results import Results
335
- >>> r = Results.example()
336
- >>> len(r.sample(2))
337
- 2
338
- """
339
- indices = None
340
-
341
- for entry in self:
342
- key, values = list(entry.items())[0]
343
- if indices is None: # gets the indices for the first time
344
- indices = list(range(len(values)))
345
- sampled_indices = random.sample(indices, n)
346
- if n > len(indices):
347
- raise ValueError(
348
- f"Cannot sample {n} items from a list of length {len(indices)}."
349
- )
350
- entry[key] = [values[i] for i in sampled_indices]
351
-
352
- return self
353
-
354
- @classmethod
355
- @remove_edsl_version
356
- def from_dict(cls, data: dict[str, Any]) -> Results:
357
- """Convert a dictionary to a Results object.
358
-
359
- :param data: A dictionary representation of a Results object.
360
-
361
- Example:
362
-
363
- >>> r = Results.example()
364
- >>> d = r.to_dict()
365
- >>> r2 = Results.from_dict(d)
366
- >>> r == r2
367
- True
368
- """
369
- from edsl import Survey, Cache
370
- from edsl.results.Result import Result
371
- from edsl.jobs.tasks.TaskHistory import TaskHistory
372
-
373
- try:
374
- results = cls(
375
- survey=Survey.from_dict(data["survey"]),
376
- data=[Result.from_dict(r) for r in data["data"]],
377
- created_columns=data.get("created_columns", None),
378
- cache=(
379
- Cache.from_dict(data.get("cache")) if "cache" in data else Cache()
380
- ),
381
- task_history=TaskHistory.from_dict(data.get("task_history")),
382
- )
383
- except Exception as e:
384
- raise ResultsDeserializationError(f"Error in Results.from_dict: {e}")
385
- return results
386
-
387
- ######################
388
- ## Convenience methods
389
- ## & Report methods
390
- ######################
391
- @property
392
- def _key_to_data_type(self) -> dict[str, str]:
393
- """
394
- Return a mapping of keys (how_feeling, status, etc.) to strings representing data types.
395
-
396
- Objects such as Agent, Answer, Model, Scenario, etc.
397
- - Uses the key_to_data_type property of the Result class.
398
- - Includes any columns that the user has created with `mutate`
399
- """
400
- d = {}
401
- for result in self.data:
402
- d.update(result.key_to_data_type)
403
- for column in self.created_columns:
404
- d[column] = "answer"
405
- return d
406
-
407
- @property
408
- def _data_type_to_keys(self) -> dict[str, str]:
409
- """
410
- Return a mapping of strings representing data types (objects such as Agent, Answer, Model, Scenario, etc.) to keys (how_feeling, status, etc.)
411
- - Uses the key_to_data_type property of the Result class.
412
- - Includes any columns that the user has created with `mutate`
413
-
414
- Example:
415
-
416
- >>> r = Results.example()
417
- >>> r._data_type_to_keys
418
- defaultdict(...
419
- """
420
- d: dict = defaultdict(set)
421
- for result in self.data:
422
- for key, value in result.key_to_data_type.items():
423
- d[value] = d[value].union(set({key}))
424
- for column in self.created_columns:
425
- d["answer"] = d["answer"].union(set({column}))
426
- return d
427
-
428
- @property
429
- def columns(self) -> list[str]:
430
- """Return a list of all of the columns that are in the Results.
431
-
432
- Example:
433
-
434
- >>> r = Results.example()
435
- >>> r.columns
436
- ['agent.agent_instruction', ...]
437
- """
438
- column_names = [f"{v}.{k}" for k, v in self._key_to_data_type.items()]
439
- return sorted(column_names)
440
-
441
- @property
442
- def answer_keys(self) -> dict[str, str]:
443
- """Return a mapping of answer keys to question text.
444
-
445
- Example:
446
-
447
- >>> r = Results.example()
448
- >>> r.answer_keys
449
- {'how_feeling': 'How are you this {{ period }}?', 'how_feeling_yesterday': 'How were you feeling yesterday {{ period }}?'}
450
- """
451
- from edsl.utilities.utilities import shorten_string
452
-
453
- if not self.survey:
454
- raise Exception("Survey is not defined so no answer keys are available.")
455
-
456
- answer_keys = self._data_type_to_keys["answer"]
457
- answer_keys = {k for k in answer_keys if "_comment" not in k}
458
- questions_text = [
459
- self.survey.get_question(k).question_text for k in answer_keys
460
- ]
461
- short_question_text = [shorten_string(q, 80) for q in questions_text]
462
- initial_dict = dict(zip(answer_keys, short_question_text))
463
- sorted_dict = {key: initial_dict[key] for key in sorted(initial_dict)}
464
- return sorted_dict
465
-
466
- @property
467
- def agents(self) -> "AgentList":
468
- """Return a list of all of the agents in the Results.
469
-
470
- Example:
471
-
472
- >>> r = Results.example()
473
- >>> r.agents
474
- AgentList([Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Sad'}), Agent(traits = {'status': 'Sad'})])
475
- """
476
- from edsl import AgentList
477
-
478
- return AgentList([r.agent for r in self.data])
479
-
480
- @property
481
- def models(self) -> list[Type["LanguageModel"]]:
482
- """Return a list of all of the models in the Results.
483
-
484
- Example:
485
-
486
- >>> r = Results.example()
487
- >>> r.models[0]
488
- Model(model_name = ...)
489
- """
490
- return [r.model for r in self.data]
491
-
492
- @property
493
- def scenarios(self) -> "ScenarioList":
494
- """Return a list of all of the scenarios in the Results.
495
-
496
- Example:
497
-
498
- >>> r = Results.example()
499
- >>> r.scenarios
500
- ScenarioList([Scenario({'period': 'morning'}), Scenario({'period': 'afternoon'}), Scenario({'period': 'morning'}), Scenario({'period': 'afternoon'})])
501
- """
502
- from edsl import ScenarioList
503
-
504
- return ScenarioList([r.scenario for r in self.data])
505
-
506
- @property
507
- def agent_keys(self) -> list[str]:
508
- """Return a set of all of the keys that are in the Agent data.
509
-
510
- Example:
511
-
512
- >>> r = Results.example()
513
- >>> r.agent_keys
514
- ['agent_instruction', 'agent_name', 'status']
515
- """
516
- return sorted(self._data_type_to_keys["agent"])
517
-
518
- @property
519
- def model_keys(self) -> list[str]:
520
- """Return a set of all of the keys that are in the LanguageModel data.
521
-
522
- >>> r = Results.example()
523
- >>> r.model_keys
524
- ['frequency_penalty', 'logprobs', 'max_tokens', 'model', 'presence_penalty', 'temperature', 'top_logprobs', 'top_p']
525
- """
526
- return sorted(self._data_type_to_keys["model"])
527
-
528
- @property
529
- def scenario_keys(self) -> list[str]:
530
- """Return a set of all of the keys that are in the Scenario data.
531
-
532
- >>> r = Results.example()
533
- >>> r.scenario_keys
534
- ['period']
535
- """
536
- return sorted(self._data_type_to_keys["scenario"])
537
-
538
- @property
539
- def question_names(self) -> list[str]:
540
- """Return a list of all of the question names.
541
-
542
- Example:
543
-
544
- >>> r = Results.example()
545
- >>> r.question_names
546
- ['how_feeling', 'how_feeling_yesterday']
547
- """
548
- if self.survey is None:
549
- return []
550
- return sorted(list(self.survey.question_names))
551
-
552
- @property
553
- def all_keys(self) -> list[str]:
554
- """Return a set of all of the keys that are in the Results.
555
-
556
- Example:
557
-
558
- >>> r = Results.example()
559
- >>> r.all_keys
560
- ['agent_instruction', 'agent_name', 'frequency_penalty', 'how_feeling', 'how_feeling_yesterday', 'logprobs', 'max_tokens', 'model', 'period', 'presence_penalty', 'status', 'temperature', 'top_logprobs', 'top_p']
561
- """
562
- answer_keys = set(self.answer_keys)
563
- all_keys = (
564
- answer_keys.union(self.agent_keys)
565
- .union(self.scenario_keys)
566
- .union(self.model_keys)
567
- )
568
- return sorted(list(all_keys))
569
-
570
- def first(self) -> "Result":
571
- """Return the first observation in the results.
572
-
573
- Example:
574
-
575
- >>> r = Results.example()
576
- >>> r.first()
577
- Result(agent...
578
- """
579
- return self.data[0]
580
-
581
- def answer_truncate(self, column: str, top_n=5, new_var_name=None) -> Results:
582
- """Create a new variable that truncates the answers to the top_n.
583
-
584
- :param column: The column to truncate.
585
- :param top_n: The number of top answers to keep.
586
- :param new_var_name: The name of the new variable. If None, it is the original name + '_truncated'.
587
-
588
-
589
-
590
- """
591
- if new_var_name is None:
592
- new_var_name = column + "_truncated"
593
- answers = list(self.select(column).tally().keys())
594
-
595
- def f(x):
596
- if x in answers[:top_n]:
597
- return x
598
- else:
599
- return "Other"
600
-
601
- return self.recode(column, recode_function=f, new_var_name=new_var_name)
602
-
603
- def recode(
604
- self, column: str, recode_function: Optional[Callable], new_var_name=None
605
- ) -> Results:
606
- """
607
- Recode a column in the Results object.
608
-
609
- >>> r = Results.example()
610
- >>> r.recode('how_feeling', recode_function = lambda x: 1 if x == 'Great' else 0).select('how_feeling', 'how_feeling_recoded')
611
- Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling_recoded': [0, 1, 0, 0]}])
612
- """
613
-
614
- if new_var_name is None:
615
- new_var_name = column + "_recoded"
616
- new_data = []
617
- for result in self.data:
618
- new_result = result.copy()
619
- value = new_result.get_value("answer", column)
620
- # breakpoint()
621
- new_result["answer"][new_var_name] = recode_function(value)
622
- new_data.append(new_result)
623
-
624
- # print("Created new variable", new_var_name)
625
- return Results(
626
- survey=self.survey,
627
- data=new_data,
628
- created_columns=self.created_columns + [new_var_name],
629
- )
630
-
631
- def add_column(self, column_name: str, values: list) -> Results:
632
- """Adds columns to Results
633
-
634
- >>> r = Results.example()
635
- >>> r.add_column('a', [1,2,3, 4]).select('a')
636
- Dataset([{'answer.a': [1, 2, 3, 4]}])
637
- """
638
-
639
- assert len(values) == len(
640
- self.data
641
- ), "The number of values must match the number of results."
642
- new_results = self.data.copy()
643
- for i, result in enumerate(new_results):
644
- result["answer"][column_name] = values[i]
645
- return Results(
646
- survey=self.survey,
647
- data=new_results,
648
- created_columns=self.created_columns + [column_name],
649
- )
650
-
651
- def add_columns_from_dict(self, columns: List[dict]) -> Results:
652
- """Adds columns to Results from a list of dictionaries.
653
-
654
- >>> r = Results.example()
655
- >>> r.add_columns_from_dict([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}, {'a':3, 'b':2}, {'a':3, 'b':2}]).select('a', 'b')
656
- Dataset([{'answer.a': [1, 3, 3, 3]}, {'answer.b': [2, 4, 2, 2]}])
657
- """
658
- keys = list(columns[0].keys())
659
- for key in keys:
660
- values = [d[key] for d in columns]
661
- self = self.add_column(key, values)
662
- return self
663
-
664
- @staticmethod
665
- def _create_evaluator(
666
- result: Result, functions_dict: Optional[dict] = None
667
- ) -> EvalWithCompoundTypes:
668
- """Create an evaluator for the expression.
669
-
670
- >>> from unittest.mock import Mock
671
- >>> result = Mock()
672
- >>> result.combined_dict = {'how_feeling': 'OK'}
673
-
674
- >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
675
- >>> evaluator.eval("how_feeling == 'OK'")
676
- True
677
-
678
- >>> result.combined_dict = {'answer': {'how_feeling': 'OK'}}
679
- >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
680
- >>> evaluator.eval("answer.how_feeling== 'OK'")
681
- True
682
-
683
- Note that you need to refer to the answer dictionary in the expression.
684
-
685
- >>> evaluator.eval("how_feeling== 'OK'")
686
- Traceback (most recent call last):
687
- ...
688
- simpleeval.NameNotDefined: 'how_feeling' is not defined for expression 'how_feeling== 'OK''
689
- """
690
- if functions_dict is None:
691
- functions_dict = {}
692
- evaluator = EvalWithCompoundTypes(
693
- names=result.combined_dict, functions=functions_dict
694
- )
695
- evaluator.functions.update(int=int, float=float)
696
- return evaluator
697
-
698
- def mutate(
699
- self, new_var_string: str, functions_dict: Optional[dict] = None
700
- ) -> Results:
701
- """
702
- Creates a value in the Results object as if has been asked as part of the survey.
703
-
704
- :param new_var_string: A string that is a valid Python expression.
705
- :param functions_dict: A dictionary of functions that can be used in the expression. The keys are the function names and the values are the functions themselves.
706
-
707
- It splits the new_var_string at the "=" and uses simple_eval
708
-
709
- Example:
710
-
711
- >>> r = Results.example()
712
- >>> r.mutate('how_feeling_x = how_feeling + "x"').select('how_feeling_x')
713
- Dataset([{'answer.how_feeling_x': ...
714
- """
715
- # extract the variable name and the expression
716
- if "=" not in new_var_string:
717
- raise ResultsBadMutationstringError(
718
- f"Mutate requires an '=' in the string, but '{new_var_string}' doesn't have one."
719
- )
720
- raw_var_name, expression = new_var_string.split("=", 1)
721
- var_name = raw_var_name.strip()
722
- from edsl.utilities.utilities import is_valid_variable_name
723
-
724
- if not is_valid_variable_name(var_name):
725
- raise ResultsInvalidNameError(f"{var_name} is not a valid variable name.")
726
-
727
- # create the evaluator
728
- functions_dict = functions_dict or {}
729
-
730
- def new_result(old_result: "Result", var_name: str) -> "Result":
731
- evaluator = self._create_evaluator(old_result, functions_dict)
732
- value = evaluator.eval(expression)
733
- new_result = old_result.copy()
734
- new_result["answer"][var_name] = value
735
- return new_result
736
-
737
- try:
738
- new_data = [new_result(result, var_name) for result in self.data]
739
- except Exception as e:
740
- raise ResultsMutateError(f"Error in mutate. Exception:{e}")
741
-
742
- return Results(
743
- survey=self.survey,
744
- data=new_data,
745
- created_columns=self.created_columns + [var_name],
746
- )
747
-
748
- def rename(self, old_name: str, new_name: str) -> Results:
749
- """Rename an answer column in a Results object.
750
-
751
- >>> s = Results.example()
752
- >>> s.rename('how_feeling', 'how_feeling_new').select('how_feeling_new')
753
- Dataset([{'answer.how_feeling_new': ['OK', 'Great', 'Terrible', 'OK']}])
754
-
755
- # TODO: Should we allow renaming of scenario fields as well? Probably.
756
-
757
- """
758
-
759
- for obs in self.data:
760
- obs["answer"][new_name] = obs["answer"][old_name]
761
- del obs["answer"][old_name]
762
-
763
- return self
764
-
765
- def shuffle(self, seed: Optional[str] = "edsl") -> Results:
766
- """Shuffle the results.
767
-
768
- Example:
769
-
770
- >>> r = Results.example()
771
- >>> r.shuffle(seed = 1)[0]
772
- Result(...)
773
- """
774
- if seed != "edsl":
775
- seed = random.seed(seed)
776
-
777
- new_data = self.data.copy()
778
- random.shuffle(new_data)
779
- return Results(survey=self.survey, data=new_data, created_columns=None)
780
-
781
- def sample(
782
- self,
783
- n: Optional[int] = None,
784
- frac: Optional[float] = None,
785
- with_replacement: bool = True,
786
- seed: Optional[str] = "edsl",
787
- ) -> Results:
788
- """Sample the results.
789
-
790
- :param n: An integer representing the number of samples to take.
791
- :param frac: A float representing the fraction of samples to take.
792
- :param with_replacement: A boolean representing whether to sample with replacement.
793
- :param seed: An integer representing the seed for the random number generator.
794
-
795
- Example:
796
-
797
- >>> r = Results.example()
798
- >>> len(r.sample(2))
799
- 2
800
- """
801
- if seed != "edsl":
802
- random.seed(seed)
803
-
804
- if n is None and frac is None:
805
- raise Exception("You must specify either n or frac.")
806
-
807
- if n is not None and frac is not None:
808
- raise Exception("You cannot specify both n and frac.")
809
-
810
- if frac is not None and n is None:
811
- n = int(frac * len(self.data))
812
-
813
- if with_replacement:
814
- new_data = random.choices(self.data, k=n)
815
- else:
816
- new_data = random.sample(self.data, n)
817
-
818
- return Results(survey=self.survey, data=new_data, created_columns=None)
819
-
820
- def select(self, *columns: Union[str, list[str]]) -> "Dataset":
821
- """
822
- Select data from the results and format it.
823
-
824
- :param columns: A list of strings, each of which is a column name. The column name can be a single key, e.g. "how_feeling", or a dot-separated string, e.g. "answer.how_feeling".
825
-
826
- Example:
827
-
828
- >>> results = Results.example()
829
- >>> results.select('how_feeling')
830
- Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
831
-
832
- >>> results.select('how_feeling', 'model', 'how_feeling')
833
- Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'model.model': ['...', '...', '...', '...']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
834
-
835
- >>> from edsl import Results; r = Results.example(); r.select('answer.how_feeling_y')
836
- Dataset([{'answer.how_feeling_yesterday': ['Great', 'Good', 'OK', 'Terrible']}])
837
- """
838
-
839
- # if len(self) == 0:
840
- # raise Exception("No data to select from---the Results object is empty.")
841
-
842
- if not columns or columns == ("*",) or columns == (None,):
843
- # is the users passes nothing, then we'll return all the columns
844
- columns = ("*.*",)
845
-
846
- if isinstance(columns[0], list):
847
- columns = tuple(columns[0])
848
-
849
- def get_data_types_to_return(parsed_data_type):
850
- if parsed_data_type == "*": # they want all of the columns
851
- return self.known_data_types
852
- else:
853
- if parsed_data_type not in self.known_data_types:
854
- raise Exception(
855
- f"Data type {parsed_data_type} not found in data. Did you mean one of {self.known_data_types}"
856
- )
857
- return [parsed_data_type]
858
-
859
- # we're doing to populate this with the data we want to fetch
860
- to_fetch = defaultdict(list)
861
-
862
- new_data = []
863
- items_in_order = []
864
- # iterate through the passed columns
865
- for column in columns:
866
- # a user could pass 'result.how_feeling' or just 'how_feeling'
867
- matches = self._matching_columns(column)
868
- if len(matches) > 1:
869
- raise Exception(
870
- f"Column '{column}' is ambiguous. Did you mean one of {matches}?"
871
- )
872
- if len(matches) == 0 and ".*" not in column:
873
- raise Exception(f"Column '{column}' not found in data.")
874
- if len(matches) == 1:
875
- column = matches[0]
876
-
877
- parsed_data_type, parsed_key = self._parse_column(column)
878
- data_types = get_data_types_to_return(parsed_data_type)
879
- found_once = False # we need to track this to make sure we found the key at least once
880
-
881
- for data_type in data_types:
882
- # the keys for that data_type e.g.,# if data_type is 'answer', then the keys are 'how_feeling', 'how_feeling_comment', etc.
883
- relevant_keys = self._data_type_to_keys[data_type]
884
-
885
- for key in relevant_keys:
886
- if key == parsed_key or parsed_key == "*":
887
- found_once = True
888
- to_fetch[data_type].append(key)
889
- items_in_order.append(data_type + "." + key)
890
-
891
- if not found_once:
892
- raise Exception(f"Key {parsed_key} not found in data.")
893
-
894
- for data_type in to_fetch:
895
- for key in to_fetch[data_type]:
896
- entries = self._fetch_list(data_type, key)
897
- new_data.append({data_type + "." + key: entries})
898
-
899
- def sort_by_key_order(dictionary):
900
- # Extract the single key from the dictionary
901
- single_key = next(iter(dictionary))
902
- # Return the index of this key in the list_of_keys
903
- return items_in_order.index(single_key)
904
-
905
- # sorted(new_data, key=sort_by_key_order)
906
- from edsl.results.Dataset import Dataset
907
-
908
- sorted_new_data = []
909
-
910
- # WORKS but slow
911
- for key in items_in_order:
912
- for d in new_data:
913
- if key in d:
914
- sorted_new_data.append(d)
915
- break
916
-
917
- return Dataset(sorted_new_data)
918
-
919
- def select(self, *columns: Union[str, list[str]]) -> "Results":
920
- from edsl.results.Selector import Selector
921
-
922
- if len(self) == 0:
923
- raise Exception("No data to select from---the Results object is empty.")
924
-
925
- selector = Selector(
926
- known_data_types=self.known_data_types,
927
- data_type_to_keys=self._data_type_to_keys,
928
- key_to_data_type=self._key_to_data_type,
929
- fetch_list_func=self._fetch_list,
930
- columns=self.columns,
931
- )
932
- return selector.select(*columns)
933
-
934
- def sort_by(self, *columns: str, reverse: bool = False) -> Results:
935
- import warnings
936
-
937
- warnings.warn(
938
- "sort_by is deprecated. Use order_by instead.", DeprecationWarning
939
- )
940
- return self.order_by(*columns, reverse=reverse)
941
-
942
- def _parse_column(self, column: str) -> tuple[str, str]:
943
- if "." in column:
944
- return column.split(".")
945
- return self._key_to_data_type[column], column
946
-
947
- def order_by(self, *columns: str, reverse: bool = False) -> Results:
948
- """Sort the results by one or more columns.
949
-
950
- :param columns: One or more column names as strings.
951
- :param reverse: A boolean that determines whether to sort in reverse order.
952
-
953
- Each column name can be a single key, e.g. "how_feeling", or a dot-separated string, e.g. "answer.how_feeling".
954
-
955
- Example:
956
-
957
- >>> r = Results.example()
958
- >>> r.sort_by('how_feeling', reverse=False).select('how_feeling').print()
959
- ┏━━━━━━━━━━━━━━┓
960
- ┃ answer ┃
961
- ┃ .how_feeling ┃
962
- ┡━━━━━━━━━━━━━━┩
963
- │ Great │
964
- ├──────────────┤
965
- │ OK │
966
- ├──────────────┤
967
- │ OK │
968
- ├──────────────┤
969
- │ Terrible │
970
- └──────────────┘
971
- >>> r.sort_by('how_feeling', reverse=True).select('how_feeling').print()
972
- ┏━━━━━━━━━━━━━━┓
973
- ┃ answer ┃
974
- ┃ .how_feeling ┃
975
- ┡━━━━━━━━━━━━━━┩
976
- │ Terrible │
977
- ├──────────────┤
978
- │ OK │
979
- ├──────────────┤
980
- │ OK │
981
- ├──────────────┤
982
- │ Great │
983
- └──────────────┘
984
- """
985
-
986
- def to_numeric_if_possible(v):
987
- try:
988
- return float(v)
989
- except:
990
- return v
991
-
992
- def sort_key(item):
993
- key_components = []
994
- for col in columns:
995
- data_type, key = self._parse_column(col)
996
- value = item.get_value(data_type, key)
997
- key_components.append(to_numeric_if_possible(value))
998
- return tuple(key_components)
999
-
1000
- new_data = sorted(self.data, key=sort_key, reverse=reverse)
1001
- return Results(survey=self.survey, data=new_data, created_columns=None)
1002
-
1003
- def filter(self, expression: str) -> Results:
1004
- """
1005
- Filter based on the given expression and returns the filtered `Results`.
1006
-
1007
- :param expression: A string expression that evaluates to a boolean. The expression is applied to each element in `Results` to determine whether it should be included in the filtered results.
1008
-
1009
- The `expression` parameter is a string that must resolve to a boolean value when evaluated against each element in `Results`.
1010
- This expression is used to determine which elements to include in the returned `Results`.
1011
-
1012
- Example usage: Create an example `Results` instance and apply filters to it:
1013
-
1014
- >>> r = Results.example()
1015
- >>> r.filter("how_feeling == 'Great'").select('how_feeling').print()
1016
- ┏━━━━━━━━━━━━━━┓
1017
- ┃ answer ┃
1018
- ┃ .how_feeling ┃
1019
- ┡━━━━━━━━━━━━━━┩
1020
- │ Great │
1021
- └──────────────┘
1022
-
1023
- Example usage: Using an OR operator in the filter expression.
1024
-
1025
- >>> r = Results.example().filter("how_feeling = 'Great'").select('how_feeling').print()
1026
- Traceback (most recent call last):
1027
- ...
1028
- edsl.exceptions.results.ResultsFilterError: You must use '==' instead of '=' in the filter expression.
1029
-
1030
- >>> r.filter("how_feeling == 'Great' or how_feeling == 'Terrible'").select('how_feeling').print()
1031
- ┏━━━━━━━━━━━━━━┓
1032
- ┃ answer ┃
1033
- ┃ .how_feeling ┃
1034
- ┡━━━━━━━━━━━━━━┩
1035
- │ Great │
1036
- ├──────────────┤
1037
- │ Terrible │
1038
- └──────────────┘
1039
- """
1040
-
1041
- def has_single_equals(string):
1042
- if "!=" in string:
1043
- return False
1044
- if "=" in string and not (
1045
- "==" in string or "<=" in string or ">=" in string
1046
- ):
1047
- return True
1048
-
1049
- if has_single_equals(expression):
1050
- raise ResultsFilterError(
1051
- "You must use '==' instead of '=' in the filter expression."
1052
- )
1053
-
1054
- try:
1055
- # iterates through all the results and evaluates the expression
1056
- new_data = []
1057
- for result in self.data:
1058
- evaluator = self._create_evaluator(result)
1059
- result.check_expression(expression) # check expression
1060
- if evaluator.eval(expression):
1061
- new_data.append(result)
1062
-
1063
- except ValueError as e:
1064
- raise ResultsFilterError(
1065
- f"Error in filter. Exception:{e}",
1066
- f"The expression you provided was: {expression}",
1067
- "See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.",
1068
- )
1069
- except Exception as e:
1070
- raise ResultsFilterError(
1071
- f"""Error in filter. Exception:{e}.""",
1072
- f"""The expression you provided was: {expression}.""",
1073
- """Please make sure that the expression is a valid Python expression that evaluates to a boolean.""",
1074
- """For example, 'how_feeling == "Great"' is a valid expression, as is 'how_feeling in ["Great", "Terrible"]'., """,
1075
- """However, 'how_feeling = "Great"' is not a valid expression.""",
1076
- """See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.""",
1077
- )
1078
-
1079
- if len(new_data) == 0:
1080
- import warnings
1081
-
1082
- warnings.warn("No results remain after applying the filter.")
1083
-
1084
- return Results(survey=self.survey, data=new_data, created_columns=None)
1085
-
1086
- @classmethod
1087
- def example(cls, randomize: bool = False) -> Results:
1088
- """Return an example `Results` object.
1089
-
1090
- Example usage:
1091
-
1092
- >>> r = Results.example()
1093
-
1094
- :param debug: if False, uses actual API calls
1095
- """
1096
- from edsl.jobs.Jobs import Jobs
1097
- from edsl.data.Cache import Cache
1098
-
1099
- c = Cache()
1100
- job = Jobs.example(randomize=randomize)
1101
- results = job.run(
1102
- cache=c,
1103
- stop_on_exception=True,
1104
- skip_retry=True,
1105
- raise_validation_errors=True,
1106
- disable_remote_inference=True,
1107
- )
1108
- return results
1109
-
1110
- def rich_print(self):
1111
- """Display an object as a table."""
1112
- pass
1113
- # with io.StringIO() as buf:
1114
- # console = Console(file=buf, record=True)
1115
-
1116
- # for index, result in enumerate(self):
1117
- # console.print(f"Result {index}")
1118
- # console.print(result.rich_print())
1119
-
1120
- # return console.export_text()
1121
-
1122
- def __str__(self):
1123
- data = self.to_dict()["data"]
1124
- return json.dumps(data, indent=4)
1125
-
1126
- def show_exceptions(self, traceback=False):
1127
- """Print the exceptions."""
1128
- if hasattr(self, "task_history"):
1129
- self.task_history.show_exceptions(traceback)
1130
- else:
1131
- print("No exceptions to show.")
1132
-
1133
- def score(self, f: Callable) -> list:
1134
- """Score the results using in a function.
1135
-
1136
- :param f: A function that takes values from a Resul object and returns a score.
1137
-
1138
- >>> r = Results.example()
1139
- >>> def f(status): return 1 if status == 'Joyful' else 0
1140
- >>> r.score(f)
1141
- [1, 1, 0, 0]
1142
- """
1143
- return [r.score(f) for r in self.data]
1144
-
1145
-
1146
- def main(): # pragma: no cover
1147
- """Call the OpenAI API credits."""
1148
- from edsl.results.Results import Results
1149
-
1150
- results = Results.example(debug=True)
1151
- print(results.filter("how_feeling == 'Great'").select("how_feeling"))
1152
- print(results.mutate("how_feeling_x = how_feeling + 'x'").select("how_feeling_x"))
1153
-
1154
-
1155
- if __name__ == "__main__":
1156
- import doctest
1157
-
1158
- doctest.testmod(optionflags=doctest.ELLIPSIS)
1
+ """
2
+ The Results object is the result of running a survey.
3
+ It is not typically instantiated directly, but is returned by the run method of a `Job` object.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import json
8
+ import random
9
+ from collections import UserList, defaultdict
10
+ from typing import Optional, Callable, Any, Type, Union, List
11
+
12
+ from simpleeval import EvalWithCompoundTypes
13
+
14
+ from edsl.exceptions.results import (
15
+ ResultsBadMutationstringError,
16
+ ResultsColumnNotFoundError,
17
+ ResultsInvalidNameError,
18
+ ResultsMutateError,
19
+ ResultsFilterError,
20
+ ResultsDeserializationError,
21
+ )
22
+
23
+ from edsl.results.ResultsExportMixin import ResultsExportMixin
24
+ from edsl.results.ResultsToolsMixin import ResultsToolsMixin
25
+ from edsl.results.ResultsDBMixin import ResultsDBMixin
26
+ from edsl.results.ResultsGGMixin import ResultsGGMixin
27
+ from edsl.results.ResultsFetchMixin import ResultsFetchMixin
28
+
29
+ from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
30
+ from edsl.utilities.utilities import dict_hash
31
+
32
+
33
+ from edsl.Base import Base
34
+
35
+
36
+ class Mixins(
37
+ ResultsExportMixin,
38
+ ResultsDBMixin,
39
+ ResultsFetchMixin,
40
+ ResultsGGMixin,
41
+ ResultsToolsMixin,
42
+ ):
43
+ def print_long(self, max_rows=None) -> None:
44
+ """Print the results in long format.
45
+
46
+ >>> from edsl.results import Results
47
+ >>> r = Results.example()
48
+ >>> r.select('how_feeling').print_long(max_rows = 2)
49
+ ┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━┓
50
+ ┃ Result index ┃ Key ┃ Value ┃
51
+ ┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━┩
52
+ │ 0 │ how_feeling │ OK │
53
+ │ 1 │ how_feeling │ Great │
54
+ └──────────────┴─────────────┴───────┘
55
+ """
56
+ from edsl.utilities.interface import print_results_long
57
+
58
+ print_results_long(self, max_rows=max_rows)
59
+
60
+
61
+ class Results(UserList, Mixins, Base):
62
+ """
63
+ This class is a UserList of Result objects.
64
+
65
+ It is instantiated with a `Survey` and a list of `Result` objects.
66
+ It can be manipulated in various ways with select, filter, mutate, etc.
67
+ It also has a list of created_columns, which are columns that have been created with `mutate` and are not part of the original data.
68
+ """
69
+
70
+ known_data_types = [
71
+ "answer",
72
+ "scenario",
73
+ "agent",
74
+ "model",
75
+ "prompt",
76
+ "raw_model_response",
77
+ "iteration",
78
+ "question_text",
79
+ "question_options",
80
+ "question_type",
81
+ "comment",
82
+ "generated_tokens",
83
+ ]
84
+
85
+ def __init__(
86
+ self,
87
+ survey: Optional["Survey"] = None,
88
+ data: Optional[list["Result"]] = None,
89
+ created_columns: Optional[list[str]] = None,
90
+ cache: Optional["Cache"] = None,
91
+ job_uuid: Optional[str] = None,
92
+ total_results: Optional[int] = None,
93
+ task_history: Optional["TaskHistory"] = None,
94
+ ):
95
+ """Instantiate a `Results` object with a survey and a list of `Result` objects.
96
+
97
+ :param survey: A Survey object.
98
+ :param data: A list of Result objects.
99
+ :param created_columns: A list of strings that are created columns.
100
+ :param job_uuid: A string representing the job UUID.
101
+ :param total_results: An integer representing the total number of results.
102
+ """
103
+ super().__init__(data)
104
+ from edsl.data.Cache import Cache
105
+ from edsl.jobs.tasks.TaskHistory import TaskHistory
106
+
107
+ self.survey = survey
108
+ self.created_columns = created_columns or []
109
+ self._job_uuid = job_uuid
110
+ self._total_results = total_results
111
+ self.cache = cache or Cache()
112
+
113
+ self.task_history = task_history or TaskHistory(interviews = [])
114
+
115
+ if hasattr(self, "_add_output_functions"):
116
+ self._add_output_functions()
117
+
118
+ def leaves(self):
119
+ leaves = []
120
+ for result in self:
121
+ leaves.extend(result.leaves())
122
+ return leaves
123
+
124
+ def tree(
125
+ self,
126
+ fold_attributes: Optional[List[str]] = None,
127
+ drop: Optional[List[str]] = None,
128
+ open_file=True,
129
+ ) -> dict:
130
+ """Return the results as a tree."""
131
+ from edsl.results.tree_explore import FoldableHTMLTableGenerator
132
+
133
+ if drop is None:
134
+ drop = []
135
+
136
+ valid_attributes = [
137
+ "model",
138
+ "scenario",
139
+ "agent",
140
+ "answer",
141
+ "question",
142
+ "iteration",
143
+ ]
144
+ if fold_attributes is None:
145
+ fold_attributes = []
146
+
147
+ for attribute in fold_attributes:
148
+ if attribute not in valid_attributes:
149
+ raise ValueError(
150
+ f"Invalid fold attribute: {attribute}; must be in {valid_attributes}"
151
+ )
152
+ data = self.leaves()
153
+ generator = FoldableHTMLTableGenerator(data)
154
+ tree = generator.tree(fold_attributes=fold_attributes, drop=drop)
155
+ html_content = generator.generate_html(tree, fold_attributes)
156
+ import tempfile
157
+ from edsl.utilities.utilities import is_notebook
158
+
159
+ from IPython.display import display, HTML
160
+
161
+ if is_notebook():
162
+ import html
163
+ from IPython.display import display, HTML
164
+
165
+ height = 1000
166
+ width = 1000
167
+ escaped_output = html.escape(html_content)
168
+ # escaped_output = rendered_html
169
+ iframe = f""""
170
+ <iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
171
+ """
172
+ display(HTML(iframe))
173
+ return None
174
+
175
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
176
+ f.write(html_content.encode())
177
+ print(f"HTML file has been generated: {f.name}")
178
+
179
+ if open_file:
180
+ import webbrowser
181
+ import time
182
+
183
+ time.sleep(1) # Wait for 1 second
184
+ # webbrowser.open(f.name)
185
+ import os
186
+
187
+ filename = f.name
188
+ webbrowser.open(f"file://{os.path.abspath(filename)}")
189
+
190
+ else:
191
+ return html_content
192
+
193
+ def code(self):
194
+ raise NotImplementedError
195
+
196
+ def __getitem__(self, i):
197
+ if isinstance(i, int):
198
+ return self.data[i]
199
+
200
+ if isinstance(i, slice):
201
+ return self.__class__(survey=self.survey, data=self.data[i])
202
+
203
+ if isinstance(i, str):
204
+ return self.to_dict()[i]
205
+
206
+ raise TypeError("Invalid argument type")
207
+
208
+ def _update_results(self) -> None:
209
+ from edsl import Agent, Scenario
210
+ from edsl.language_models import LanguageModel
211
+ from edsl.results import Result
212
+
213
+ if self._job_uuid and len(self.data) < self._total_results:
214
+ results = [
215
+ Result(
216
+ agent=Agent.from_dict(json.loads(r.agent)),
217
+ scenario=Scenario.from_dict(json.loads(r.scenario)),
218
+ model=LanguageModel.from_dict(json.loads(r.model)),
219
+ iteration=1,
220
+ answer=json.loads(r.answer),
221
+ )
222
+ for r in CRUD.read_results(self._job_uuid)
223
+ ]
224
+ self.data = results
225
+
226
+ def __add__(self, other: Results) -> Results:
227
+ """Add two Results objects together.
228
+ They must have the same survey and created columns.
229
+ :param other: A Results object.
230
+
231
+ Example:
232
+
233
+ >>> r = Results.example()
234
+ >>> r2 = Results.example()
235
+ >>> r3 = r + r2
236
+ """
237
+ if self.survey != other.survey:
238
+ raise Exception(
239
+ "The surveys are not the same so they cannot be added together."
240
+ )
241
+ if self.created_columns != other.created_columns:
242
+ raise Exception(
243
+ "The created columns are not the same so they cannot be added together."
244
+ )
245
+
246
+ return Results(
247
+ survey=self.survey,
248
+ data=self.data + other.data,
249
+ created_columns=self.created_columns,
250
+ )
251
+
252
+ def __repr__(self) -> str:
253
+ import reprlib
254
+
255
+ return f"Results(data = {reprlib.repr(self.data)}, survey = {repr(self.survey)}, created_columns = {self.created_columns})"
256
+
257
+ def _repr_html_(self) -> str:
258
+ from IPython.display import HTML
259
+
260
+ json_str = json.dumps(self.to_dict()["data"], indent=4)
261
+ from pygments import highlight
262
+ from pygments.lexers import JsonLexer
263
+ from pygments.formatters import HtmlFormatter
264
+
265
+ formatted_json = highlight(
266
+ json_str,
267
+ JsonLexer(),
268
+ HtmlFormatter(style="default", full=True, noclasses=True),
269
+ )
270
+ return HTML(formatted_json).data
271
+
272
+ def _to_dict(self, sort=False):
273
+ from edsl.data.Cache import Cache
274
+
275
+ if sort:
276
+ data = sorted([result for result in self.data], key=lambda x: hash(x))
277
+ else:
278
+ data = [result for result in self.data]
279
+ return {
280
+ "data": [result.to_dict() for result in data],
281
+ "survey": self.survey.to_dict(),
282
+ "created_columns": self.created_columns,
283
+ "cache": Cache() if not hasattr(self, "cache") else self.cache.to_dict(),
284
+ "task_history": self.task_history.to_dict(),
285
+ }
286
+
287
+ def compare(self, other_results):
288
+ """
289
+ Compare two Results objects and return the differences.
290
+ """
291
+ hashes_0 = [hash(result) for result in self]
292
+ hashes_1 = [hash(result) for result in other_results]
293
+
294
+ in_self_but_not_other = set(hashes_0).difference(set(hashes_1))
295
+ in_other_but_not_self = set(hashes_1).difference(set(hashes_0))
296
+
297
+ indicies_self = [hashes_0.index(h) for h in in_self_but_not_other]
298
+ indices_other = [hashes_1.index(h) for h in in_other_but_not_self]
299
+ return {
300
+ "a_not_b": [self[i] for i in indicies_self],
301
+ "b_not_a": [other_results[i] for i in indices_other],
302
+ }
303
+
304
+ @property
305
+ def has_unfixed_exceptions(self):
306
+ return self.task_history.has_unfixed_exceptions
307
+
308
+ @add_edsl_version
309
+ def to_dict(self) -> dict[str, Any]:
310
+ """Convert the Results object to a dictionary.
311
+
312
+ The dictionary can be quite large, as it includes all of the data in the Results object.
313
+
314
+ Example: Illustrating just the keys of the dictionary.
315
+
316
+ >>> r = Results.example()
317
+ >>> r.to_dict().keys()
318
+ dict_keys(['data', 'survey', 'created_columns', 'cache', 'task_history', 'edsl_version', 'edsl_class_name'])
319
+ """
320
+ return self._to_dict()
321
+
322
+ def __hash__(self) -> int:
323
+ return dict_hash(self._to_dict(sort=True))
324
+
325
+ @property
326
+ def hashes(self) -> set:
327
+ return set(hash(result) for result in self.data)
328
+
329
+ def sample(self, n: int) -> "Results":
330
+ """Return a random sample of the results.
331
+
332
+ :param n: The number of samples to return.
333
+
334
+ >>> from edsl.results import Results
335
+ >>> r = Results.example()
336
+ >>> len(r.sample(2))
337
+ 2
338
+ """
339
+ indices = None
340
+
341
+ for entry in self:
342
+ key, values = list(entry.items())[0]
343
+ if indices is None: # gets the indices for the first time
344
+ indices = list(range(len(values)))
345
+ sampled_indices = random.sample(indices, n)
346
+ if n > len(indices):
347
+ raise ValueError(
348
+ f"Cannot sample {n} items from a list of length {len(indices)}."
349
+ )
350
+ entry[key] = [values[i] for i in sampled_indices]
351
+
352
+ return self
353
+
354
+ @classmethod
355
+ @remove_edsl_version
356
+ def from_dict(cls, data: dict[str, Any]) -> Results:
357
+ """Convert a dictionary to a Results object.
358
+
359
+ :param data: A dictionary representation of a Results object.
360
+
361
+ Example:
362
+
363
+ >>> r = Results.example()
364
+ >>> d = r.to_dict()
365
+ >>> r2 = Results.from_dict(d)
366
+ >>> r == r2
367
+ True
368
+ """
369
+ from edsl import Survey, Cache
370
+ from edsl.results.Result import Result
371
+ from edsl.jobs.tasks.TaskHistory import TaskHistory
372
+
373
+ try:
374
+ results = cls(
375
+ survey=Survey.from_dict(data["survey"]),
376
+ data=[Result.from_dict(r) for r in data["data"]],
377
+ created_columns=data.get("created_columns", None),
378
+ cache=(
379
+ Cache.from_dict(data.get("cache")) if "cache" in data else Cache()
380
+ ),
381
+ task_history=TaskHistory.from_dict(data.get("task_history")),
382
+ )
383
+ except Exception as e:
384
+ raise ResultsDeserializationError(f"Error in Results.from_dict: {e}")
385
+ return results
386
+
387
+ ######################
388
+ ## Convenience methods
389
+ ## & Report methods
390
+ ######################
391
+ @property
392
+ def _key_to_data_type(self) -> dict[str, str]:
393
+ """
394
+ Return a mapping of keys (how_feeling, status, etc.) to strings representing data types.
395
+
396
+ Objects such as Agent, Answer, Model, Scenario, etc.
397
+ - Uses the key_to_data_type property of the Result class.
398
+ - Includes any columns that the user has created with `mutate`
399
+ """
400
+ d = {}
401
+ for result in self.data:
402
+ d.update(result.key_to_data_type)
403
+ for column in self.created_columns:
404
+ d[column] = "answer"
405
+ return d
406
+
407
+ @property
408
+ def _data_type_to_keys(self) -> dict[str, str]:
409
+ """
410
+ Return a mapping of strings representing data types (objects such as Agent, Answer, Model, Scenario, etc.) to keys (how_feeling, status, etc.)
411
+ - Uses the key_to_data_type property of the Result class.
412
+ - Includes any columns that the user has created with `mutate`
413
+
414
+ Example:
415
+
416
+ >>> r = Results.example()
417
+ >>> r._data_type_to_keys
418
+ defaultdict(...
419
+ """
420
+ d: dict = defaultdict(set)
421
+ for result in self.data:
422
+ for key, value in result.key_to_data_type.items():
423
+ d[value] = d[value].union(set({key}))
424
+ for column in self.created_columns:
425
+ d["answer"] = d["answer"].union(set({column}))
426
+ return d
427
+
428
+ @property
429
+ def columns(self) -> list[str]:
430
+ """Return a list of all of the columns that are in the Results.
431
+
432
+ Example:
433
+
434
+ >>> r = Results.example()
435
+ >>> r.columns
436
+ ['agent.agent_instruction', ...]
437
+ """
438
+ column_names = [f"{v}.{k}" for k, v in self._key_to_data_type.items()]
439
+ return sorted(column_names)
440
+
441
+ @property
442
+ def answer_keys(self) -> dict[str, str]:
443
+ """Return a mapping of answer keys to question text.
444
+
445
+ Example:
446
+
447
+ >>> r = Results.example()
448
+ >>> r.answer_keys
449
+ {'how_feeling': 'How are you this {{ period }}?', 'how_feeling_yesterday': 'How were you feeling yesterday {{ period }}?'}
450
+ """
451
+ from edsl.utilities.utilities import shorten_string
452
+
453
+ if not self.survey:
454
+ raise Exception("Survey is not defined so no answer keys are available.")
455
+
456
+ answer_keys = self._data_type_to_keys["answer"]
457
+ answer_keys = {k for k in answer_keys if "_comment" not in k}
458
+ questions_text = [
459
+ self.survey.get_question(k).question_text for k in answer_keys
460
+ ]
461
+ short_question_text = [shorten_string(q, 80) for q in questions_text]
462
+ initial_dict = dict(zip(answer_keys, short_question_text))
463
+ sorted_dict = {key: initial_dict[key] for key in sorted(initial_dict)}
464
+ return sorted_dict
465
+
466
+ @property
467
+ def agents(self) -> "AgentList":
468
+ """Return a list of all of the agents in the Results.
469
+
470
+ Example:
471
+
472
+ >>> r = Results.example()
473
+ >>> r.agents
474
+ AgentList([Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Joyful'}), Agent(traits = {'status': 'Sad'}), Agent(traits = {'status': 'Sad'})])
475
+ """
476
+ from edsl import AgentList
477
+
478
+ return AgentList([r.agent for r in self.data])
479
+
480
+ @property
481
+ def models(self) -> list[Type["LanguageModel"]]:
482
+ """Return a list of all of the models in the Results.
483
+
484
+ Example:
485
+
486
+ >>> r = Results.example()
487
+ >>> r.models[0]
488
+ Model(model_name = ...)
489
+ """
490
+ return [r.model for r in self.data]
491
+
492
+ @property
493
+ def scenarios(self) -> "ScenarioList":
494
+ """Return a list of all of the scenarios in the Results.
495
+
496
+ Example:
497
+
498
+ >>> r = Results.example()
499
+ >>> r.scenarios
500
+ ScenarioList([Scenario({'period': 'morning'}), Scenario({'period': 'afternoon'}), Scenario({'period': 'morning'}), Scenario({'period': 'afternoon'})])
501
+ """
502
+ from edsl import ScenarioList
503
+
504
+ return ScenarioList([r.scenario for r in self.data])
505
+
506
+ @property
507
+ def agent_keys(self) -> list[str]:
508
+ """Return a set of all of the keys that are in the Agent data.
509
+
510
+ Example:
511
+
512
+ >>> r = Results.example()
513
+ >>> r.agent_keys
514
+ ['agent_instruction', 'agent_name', 'status']
515
+ """
516
+ return sorted(self._data_type_to_keys["agent"])
517
+
518
+ @property
519
+ def model_keys(self) -> list[str]:
520
+ """Return a set of all of the keys that are in the LanguageModel data.
521
+
522
+ >>> r = Results.example()
523
+ >>> r.model_keys
524
+ ['frequency_penalty', 'logprobs', 'max_tokens', 'model', 'presence_penalty', 'temperature', 'top_logprobs', 'top_p']
525
+ """
526
+ return sorted(self._data_type_to_keys["model"])
527
+
528
+ @property
529
+ def scenario_keys(self) -> list[str]:
530
+ """Return a set of all of the keys that are in the Scenario data.
531
+
532
+ >>> r = Results.example()
533
+ >>> r.scenario_keys
534
+ ['period']
535
+ """
536
+ return sorted(self._data_type_to_keys["scenario"])
537
+
538
+ @property
539
+ def question_names(self) -> list[str]:
540
+ """Return a list of all of the question names.
541
+
542
+ Example:
543
+
544
+ >>> r = Results.example()
545
+ >>> r.question_names
546
+ ['how_feeling', 'how_feeling_yesterday']
547
+ """
548
+ if self.survey is None:
549
+ return []
550
+ return sorted(list(self.survey.question_names))
551
+
552
+ @property
553
+ def all_keys(self) -> list[str]:
554
+ """Return a set of all of the keys that are in the Results.
555
+
556
+ Example:
557
+
558
+ >>> r = Results.example()
559
+ >>> r.all_keys
560
+ ['agent_instruction', 'agent_name', 'frequency_penalty', 'how_feeling', 'how_feeling_yesterday', 'logprobs', 'max_tokens', 'model', 'period', 'presence_penalty', 'status', 'temperature', 'top_logprobs', 'top_p']
561
+ """
562
+ answer_keys = set(self.answer_keys)
563
+ all_keys = (
564
+ answer_keys.union(self.agent_keys)
565
+ .union(self.scenario_keys)
566
+ .union(self.model_keys)
567
+ )
568
+ return sorted(list(all_keys))
569
+
570
+ def first(self) -> "Result":
571
+ """Return the first observation in the results.
572
+
573
+ Example:
574
+
575
+ >>> r = Results.example()
576
+ >>> r.first()
577
+ Result(agent...
578
+ """
579
+ return self.data[0]
580
+
581
+ def answer_truncate(self, column: str, top_n=5, new_var_name=None) -> Results:
582
+ """Create a new variable that truncates the answers to the top_n.
583
+
584
+ :param column: The column to truncate.
585
+ :param top_n: The number of top answers to keep.
586
+ :param new_var_name: The name of the new variable. If None, it is the original name + '_truncated'.
587
+
588
+
589
+
590
+ """
591
+ if new_var_name is None:
592
+ new_var_name = column + "_truncated"
593
+ answers = list(self.select(column).tally().keys())
594
+
595
+ def f(x):
596
+ if x in answers[:top_n]:
597
+ return x
598
+ else:
599
+ return "Other"
600
+
601
+ return self.recode(column, recode_function=f, new_var_name=new_var_name)
602
+
603
+ def recode(
604
+ self, column: str, recode_function: Optional[Callable], new_var_name=None
605
+ ) -> Results:
606
+ """
607
+ Recode a column in the Results object.
608
+
609
+ >>> r = Results.example()
610
+ >>> r.recode('how_feeling', recode_function = lambda x: 1 if x == 'Great' else 0).select('how_feeling', 'how_feeling_recoded')
611
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'answer.how_feeling_recoded': [0, 1, 0, 0]}])
612
+ """
613
+
614
+ if new_var_name is None:
615
+ new_var_name = column + "_recoded"
616
+ new_data = []
617
+ for result in self.data:
618
+ new_result = result.copy()
619
+ value = new_result.get_value("answer", column)
620
+ # breakpoint()
621
+ new_result["answer"][new_var_name] = recode_function(value)
622
+ new_data.append(new_result)
623
+
624
+ # print("Created new variable", new_var_name)
625
+ return Results(
626
+ survey=self.survey,
627
+ data=new_data,
628
+ created_columns=self.created_columns + [new_var_name],
629
+ )
630
+
631
+ def add_column(self, column_name: str, values: list) -> Results:
632
+ """Adds columns to Results
633
+
634
+ >>> r = Results.example()
635
+ >>> r.add_column('a', [1,2,3, 4]).select('a')
636
+ Dataset([{'answer.a': [1, 2, 3, 4]}])
637
+ """
638
+
639
+ assert len(values) == len(
640
+ self.data
641
+ ), "The number of values must match the number of results."
642
+ new_results = self.data.copy()
643
+ for i, result in enumerate(new_results):
644
+ result["answer"][column_name] = values[i]
645
+ return Results(
646
+ survey=self.survey,
647
+ data=new_results,
648
+ created_columns=self.created_columns + [column_name],
649
+ )
650
+
651
+ def add_columns_from_dict(self, columns: List[dict]) -> Results:
652
+ """Adds columns to Results from a list of dictionaries.
653
+
654
+ >>> r = Results.example()
655
+ >>> r.add_columns_from_dict([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}, {'a':3, 'b':2}, {'a':3, 'b':2}]).select('a', 'b')
656
+ Dataset([{'answer.a': [1, 3, 3, 3]}, {'answer.b': [2, 4, 2, 2]}])
657
+ """
658
+ keys = list(columns[0].keys())
659
+ for key in keys:
660
+ values = [d[key] for d in columns]
661
+ self = self.add_column(key, values)
662
+ return self
663
+
664
+ @staticmethod
665
+ def _create_evaluator(
666
+ result: Result, functions_dict: Optional[dict] = None
667
+ ) -> EvalWithCompoundTypes:
668
+ """Create an evaluator for the expression.
669
+
670
+ >>> from unittest.mock import Mock
671
+ >>> result = Mock()
672
+ >>> result.combined_dict = {'how_feeling': 'OK'}
673
+
674
+ >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
675
+ >>> evaluator.eval("how_feeling == 'OK'")
676
+ True
677
+
678
+ >>> result.combined_dict = {'answer': {'how_feeling': 'OK'}}
679
+ >>> evaluator = Results._create_evaluator(result = result, functions_dict = {})
680
+ >>> evaluator.eval("answer.how_feeling== 'OK'")
681
+ True
682
+
683
+ Note that you need to refer to the answer dictionary in the expression.
684
+
685
+ >>> evaluator.eval("how_feeling== 'OK'")
686
+ Traceback (most recent call last):
687
+ ...
688
+ simpleeval.NameNotDefined: 'how_feeling' is not defined for expression 'how_feeling== 'OK''
689
+ """
690
+ if functions_dict is None:
691
+ functions_dict = {}
692
+ evaluator = EvalWithCompoundTypes(
693
+ names=result.combined_dict, functions=functions_dict
694
+ )
695
+ evaluator.functions.update(int=int, float=float)
696
+ return evaluator
697
+
698
+ def mutate(
699
+ self, new_var_string: str, functions_dict: Optional[dict] = None
700
+ ) -> Results:
701
+ """
702
+ Creates a value in the Results object as if has been asked as part of the survey.
703
+
704
+ :param new_var_string: A string that is a valid Python expression.
705
+ :param functions_dict: A dictionary of functions that can be used in the expression. The keys are the function names and the values are the functions themselves.
706
+
707
+ It splits the new_var_string at the "=" and uses simple_eval
708
+
709
+ Example:
710
+
711
+ >>> r = Results.example()
712
+ >>> r.mutate('how_feeling_x = how_feeling + "x"').select('how_feeling_x')
713
+ Dataset([{'answer.how_feeling_x': ...
714
+ """
715
+ # extract the variable name and the expression
716
+ if "=" not in new_var_string:
717
+ raise ResultsBadMutationstringError(
718
+ f"Mutate requires an '=' in the string, but '{new_var_string}' doesn't have one."
719
+ )
720
+ raw_var_name, expression = new_var_string.split("=", 1)
721
+ var_name = raw_var_name.strip()
722
+ from edsl.utilities.utilities import is_valid_variable_name
723
+
724
+ if not is_valid_variable_name(var_name):
725
+ raise ResultsInvalidNameError(f"{var_name} is not a valid variable name.")
726
+
727
+ # create the evaluator
728
+ functions_dict = functions_dict or {}
729
+
730
+ def new_result(old_result: "Result", var_name: str) -> "Result":
731
+ evaluator = self._create_evaluator(old_result, functions_dict)
732
+ value = evaluator.eval(expression)
733
+ new_result = old_result.copy()
734
+ new_result["answer"][var_name] = value
735
+ return new_result
736
+
737
+ try:
738
+ new_data = [new_result(result, var_name) for result in self.data]
739
+ except Exception as e:
740
+ raise ResultsMutateError(f"Error in mutate. Exception:{e}")
741
+
742
+ return Results(
743
+ survey=self.survey,
744
+ data=new_data,
745
+ created_columns=self.created_columns + [var_name],
746
+ )
747
+
748
+ def rename(self, old_name: str, new_name: str) -> Results:
749
+ """Rename an answer column in a Results object.
750
+
751
+ >>> s = Results.example()
752
+ >>> s.rename('how_feeling', 'how_feeling_new').select('how_feeling_new')
753
+ Dataset([{'answer.how_feeling_new': ['OK', 'Great', 'Terrible', 'OK']}])
754
+
755
+ # TODO: Should we allow renaming of scenario fields as well? Probably.
756
+
757
+ """
758
+
759
+ for obs in self.data:
760
+ obs["answer"][new_name] = obs["answer"][old_name]
761
+ del obs["answer"][old_name]
762
+
763
+ return self
764
+
765
+ def shuffle(self, seed: Optional[str] = "edsl") -> Results:
766
+ """Shuffle the results.
767
+
768
+ Example:
769
+
770
+ >>> r = Results.example()
771
+ >>> r.shuffle(seed = 1)[0]
772
+ Result(...)
773
+ """
774
+ if seed != "edsl":
775
+ seed = random.seed(seed)
776
+
777
+ new_data = self.data.copy()
778
+ random.shuffle(new_data)
779
+ return Results(survey=self.survey, data=new_data, created_columns=None)
780
+
781
+ def sample(
782
+ self,
783
+ n: Optional[int] = None,
784
+ frac: Optional[float] = None,
785
+ with_replacement: bool = True,
786
+ seed: Optional[str] = "edsl",
787
+ ) -> Results:
788
+ """Sample the results.
789
+
790
+ :param n: An integer representing the number of samples to take.
791
+ :param frac: A float representing the fraction of samples to take.
792
+ :param with_replacement: A boolean representing whether to sample with replacement.
793
+ :param seed: An integer representing the seed for the random number generator.
794
+
795
+ Example:
796
+
797
+ >>> r = Results.example()
798
+ >>> len(r.sample(2))
799
+ 2
800
+ """
801
+ if seed != "edsl":
802
+ random.seed(seed)
803
+
804
+ if n is None and frac is None:
805
+ raise Exception("You must specify either n or frac.")
806
+
807
+ if n is not None and frac is not None:
808
+ raise Exception("You cannot specify both n and frac.")
809
+
810
+ if frac is not None and n is None:
811
+ n = int(frac * len(self.data))
812
+
813
+ if with_replacement:
814
+ new_data = random.choices(self.data, k=n)
815
+ else:
816
+ new_data = random.sample(self.data, n)
817
+
818
+ return Results(survey=self.survey, data=new_data, created_columns=None)
819
+
820
+ def select(self, *columns: Union[str, list[str]]) -> "Dataset":
821
+ """
822
+ Select data from the results and format it.
823
+
824
+ :param columns: A list of strings, each of which is a column name. The column name can be a single key, e.g. "how_feeling", or a dot-separated string, e.g. "answer.how_feeling".
825
+
826
+ Example:
827
+
828
+ >>> results = Results.example()
829
+ >>> results.select('how_feeling')
830
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
831
+
832
+ >>> results.select('how_feeling', 'model', 'how_feeling')
833
+ Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'model.model': ['...', '...', '...', '...']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
834
+
835
+ >>> from edsl import Results; r = Results.example(); r.select('answer.how_feeling_y')
836
+ Dataset([{'answer.how_feeling_yesterday': ['Great', 'Good', 'OK', 'Terrible']}])
837
+ """
838
+
839
+ # if len(self) == 0:
840
+ # raise Exception("No data to select from---the Results object is empty.")
841
+
842
+ if not columns or columns == ("*",) or columns == (None,):
843
+ # is the users passes nothing, then we'll return all the columns
844
+ columns = ("*.*",)
845
+
846
+ if isinstance(columns[0], list):
847
+ columns = tuple(columns[0])
848
+
849
+ def get_data_types_to_return(parsed_data_type):
850
+ if parsed_data_type == "*": # they want all of the columns
851
+ return self.known_data_types
852
+ else:
853
+ if parsed_data_type not in self.known_data_types:
854
+ raise Exception(
855
+ f"Data type {parsed_data_type} not found in data. Did you mean one of {self.known_data_types}"
856
+ )
857
+ return [parsed_data_type]
858
+
859
+ # we're doing to populate this with the data we want to fetch
860
+ to_fetch = defaultdict(list)
861
+
862
+ new_data = []
863
+ items_in_order = []
864
+ # iterate through the passed columns
865
+ for column in columns:
866
+ # a user could pass 'result.how_feeling' or just 'how_feeling'
867
+ matches = self._matching_columns(column)
868
+ if len(matches) > 1:
869
+ raise Exception(
870
+ f"Column '{column}' is ambiguous. Did you mean one of {matches}?"
871
+ )
872
+ if len(matches) == 0 and ".*" not in column:
873
+ raise Exception(f"Column '{column}' not found in data.")
874
+ if len(matches) == 1:
875
+ column = matches[0]
876
+
877
+ parsed_data_type, parsed_key = self._parse_column(column)
878
+ data_types = get_data_types_to_return(parsed_data_type)
879
+ found_once = False # we need to track this to make sure we found the key at least once
880
+
881
+ for data_type in data_types:
882
+ # the keys for that data_type e.g.,# if data_type is 'answer', then the keys are 'how_feeling', 'how_feeling_comment', etc.
883
+ relevant_keys = self._data_type_to_keys[data_type]
884
+
885
+ for key in relevant_keys:
886
+ if key == parsed_key or parsed_key == "*":
887
+ found_once = True
888
+ to_fetch[data_type].append(key)
889
+ items_in_order.append(data_type + "." + key)
890
+
891
+ if not found_once:
892
+ raise Exception(f"Key {parsed_key} not found in data.")
893
+
894
+ for data_type in to_fetch:
895
+ for key in to_fetch[data_type]:
896
+ entries = self._fetch_list(data_type, key)
897
+ new_data.append({data_type + "." + key: entries})
898
+
899
+ def sort_by_key_order(dictionary):
900
+ # Extract the single key from the dictionary
901
+ single_key = next(iter(dictionary))
902
+ # Return the index of this key in the list_of_keys
903
+ return items_in_order.index(single_key)
904
+
905
+ # sorted(new_data, key=sort_by_key_order)
906
+ from edsl.results.Dataset import Dataset
907
+
908
+ sorted_new_data = []
909
+
910
+ # WORKS but slow
911
+ for key in items_in_order:
912
+ for d in new_data:
913
+ if key in d:
914
+ sorted_new_data.append(d)
915
+ break
916
+
917
+ return Dataset(sorted_new_data)
918
+
919
+ def select(self, *columns: Union[str, list[str]]) -> "Results":
920
+ from edsl.results.Selector import Selector
921
+
922
+ if len(self) == 0:
923
+ raise Exception("No data to select from---the Results object is empty.")
924
+
925
+ selector = Selector(
926
+ known_data_types=self.known_data_types,
927
+ data_type_to_keys=self._data_type_to_keys,
928
+ key_to_data_type=self._key_to_data_type,
929
+ fetch_list_func=self._fetch_list,
930
+ columns=self.columns,
931
+ )
932
+ return selector.select(*columns)
933
+
934
+ def sort_by(self, *columns: str, reverse: bool = False) -> Results:
935
+ import warnings
936
+
937
+ warnings.warn(
938
+ "sort_by is deprecated. Use order_by instead.", DeprecationWarning
939
+ )
940
+ return self.order_by(*columns, reverse=reverse)
941
+
942
+ def _parse_column(self, column: str) -> tuple[str, str]:
943
+ if "." in column:
944
+ return column.split(".")
945
+ return self._key_to_data_type[column], column
946
+
947
+ def order_by(self, *columns: str, reverse: bool = False) -> Results:
948
+ """Sort the results by one or more columns.
949
+
950
+ :param columns: One or more column names as strings.
951
+ :param reverse: A boolean that determines whether to sort in reverse order.
952
+
953
+ Each column name can be a single key, e.g. "how_feeling", or a dot-separated string, e.g. "answer.how_feeling".
954
+
955
+ Example:
956
+
957
+ >>> r = Results.example()
958
+ >>> r.sort_by('how_feeling', reverse=False).select('how_feeling').print()
959
+ ┏━━━━━━━━━━━━━━┓
960
+ ┃ answer ┃
961
+ ┃ .how_feeling ┃
962
+ ┡━━━━━━━━━━━━━━┩
963
+ │ Great │
964
+ ├──────────────┤
965
+ │ OK │
966
+ ├──────────────┤
967
+ │ OK │
968
+ ├──────────────┤
969
+ │ Terrible │
970
+ └──────────────┘
971
+ >>> r.sort_by('how_feeling', reverse=True).select('how_feeling').print()
972
+ ┏━━━━━━━━━━━━━━┓
973
+ ┃ answer ┃
974
+ ┃ .how_feeling ┃
975
+ ┡━━━━━━━━━━━━━━┩
976
+ │ Terrible │
977
+ ├──────────────┤
978
+ │ OK │
979
+ ├──────────────┤
980
+ │ OK │
981
+ ├──────────────┤
982
+ │ Great │
983
+ └──────────────┘
984
+ """
985
+
986
+ def to_numeric_if_possible(v):
987
+ try:
988
+ return float(v)
989
+ except:
990
+ return v
991
+
992
+ def sort_key(item):
993
+ key_components = []
994
+ for col in columns:
995
+ data_type, key = self._parse_column(col)
996
+ value = item.get_value(data_type, key)
997
+ key_components.append(to_numeric_if_possible(value))
998
+ return tuple(key_components)
999
+
1000
+ new_data = sorted(self.data, key=sort_key, reverse=reverse)
1001
+ return Results(survey=self.survey, data=new_data, created_columns=None)
1002
+
1003
+ def filter(self, expression: str) -> Results:
1004
+ """
1005
+ Filter based on the given expression and returns the filtered `Results`.
1006
+
1007
+ :param expression: A string expression that evaluates to a boolean. The expression is applied to each element in `Results` to determine whether it should be included in the filtered results.
1008
+
1009
+ The `expression` parameter is a string that must resolve to a boolean value when evaluated against each element in `Results`.
1010
+ This expression is used to determine which elements to include in the returned `Results`.
1011
+
1012
+ Example usage: Create an example `Results` instance and apply filters to it:
1013
+
1014
+ >>> r = Results.example()
1015
+ >>> r.filter("how_feeling == 'Great'").select('how_feeling').print()
1016
+ ┏━━━━━━━━━━━━━━┓
1017
+ ┃ answer ┃
1018
+ ┃ .how_feeling ┃
1019
+ ┡━━━━━━━━━━━━━━┩
1020
+ │ Great │
1021
+ └──────────────┘
1022
+
1023
+ Example usage: Using an OR operator in the filter expression.
1024
+
1025
+ >>> r = Results.example().filter("how_feeling = 'Great'").select('how_feeling').print()
1026
+ Traceback (most recent call last):
1027
+ ...
1028
+ edsl.exceptions.results.ResultsFilterError: You must use '==' instead of '=' in the filter expression.
1029
+
1030
+ >>> r.filter("how_feeling == 'Great' or how_feeling == 'Terrible'").select('how_feeling').print()
1031
+ ┏━━━━━━━━━━━━━━┓
1032
+ ┃ answer ┃
1033
+ ┃ .how_feeling ┃
1034
+ ┡━━━━━━━━━━━━━━┩
1035
+ │ Great │
1036
+ ├──────────────┤
1037
+ │ Terrible │
1038
+ └──────────────┘
1039
+ """
1040
+
1041
+ def has_single_equals(string):
1042
+ if "!=" in string:
1043
+ return False
1044
+ if "=" in string and not (
1045
+ "==" in string or "<=" in string or ">=" in string
1046
+ ):
1047
+ return True
1048
+
1049
+ if has_single_equals(expression):
1050
+ raise ResultsFilterError(
1051
+ "You must use '==' instead of '=' in the filter expression."
1052
+ )
1053
+
1054
+ try:
1055
+ # iterates through all the results and evaluates the expression
1056
+ new_data = []
1057
+ for result in self.data:
1058
+ evaluator = self._create_evaluator(result)
1059
+ result.check_expression(expression) # check expression
1060
+ if evaluator.eval(expression):
1061
+ new_data.append(result)
1062
+
1063
+ except ValueError as e:
1064
+ raise ResultsFilterError(
1065
+ f"Error in filter. Exception:{e}",
1066
+ f"The expression you provided was: {expression}",
1067
+ "See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.",
1068
+ )
1069
+ except Exception as e:
1070
+ raise ResultsFilterError(
1071
+ f"""Error in filter. Exception:{e}.""",
1072
+ f"""The expression you provided was: {expression}.""",
1073
+ """Please make sure that the expression is a valid Python expression that evaluates to a boolean.""",
1074
+ """For example, 'how_feeling == "Great"' is a valid expression, as is 'how_feeling in ["Great", "Terrible"]'., """,
1075
+ """However, 'how_feeling = "Great"' is not a valid expression.""",
1076
+ """See https://docs.expectedparrot.com/en/latest/results.html#filtering-results for more details.""",
1077
+ )
1078
+
1079
+ if len(new_data) == 0:
1080
+ import warnings
1081
+
1082
+ warnings.warn("No results remain after applying the filter.")
1083
+
1084
+ return Results(survey=self.survey, data=new_data, created_columns=None)
1085
+
1086
+ @classmethod
1087
+ def example(cls, randomize: bool = False) -> Results:
1088
+ """Return an example `Results` object.
1089
+
1090
+ Example usage:
1091
+
1092
+ >>> r = Results.example()
1093
+
1094
+ :param debug: if False, uses actual API calls
1095
+ """
1096
+ from edsl.jobs.Jobs import Jobs
1097
+ from edsl.data.Cache import Cache
1098
+
1099
+ c = Cache()
1100
+ job = Jobs.example(randomize=randomize)
1101
+ results = job.run(
1102
+ cache=c,
1103
+ stop_on_exception=True,
1104
+ skip_retry=True,
1105
+ raise_validation_errors=True,
1106
+ disable_remote_inference=True,
1107
+ )
1108
+ return results
1109
+
1110
+ def rich_print(self):
1111
+ """Display an object as a table."""
1112
+ pass
1113
+ # with io.StringIO() as buf:
1114
+ # console = Console(file=buf, record=True)
1115
+
1116
+ # for index, result in enumerate(self):
1117
+ # console.print(f"Result {index}")
1118
+ # console.print(result.rich_print())
1119
+
1120
+ # return console.export_text()
1121
+
1122
+ def __str__(self):
1123
+ data = self.to_dict()["data"]
1124
+ return json.dumps(data, indent=4)
1125
+
1126
+ def show_exceptions(self, traceback=False):
1127
+ """Print the exceptions."""
1128
+ if hasattr(self, "task_history"):
1129
+ self.task_history.show_exceptions(traceback)
1130
+ else:
1131
+ print("No exceptions to show.")
1132
+
1133
+ def score(self, f: Callable) -> list:
1134
+ """Score the results using in a function.
1135
+
1136
+ :param f: A function that takes values from a Resul object and returns a score.
1137
+
1138
+ >>> r = Results.example()
1139
+ >>> def f(status): return 1 if status == 'Joyful' else 0
1140
+ >>> r.score(f)
1141
+ [1, 1, 0, 0]
1142
+ """
1143
+ return [r.score(f) for r in self.data]
1144
+
1145
+
1146
+ def main(): # pragma: no cover
1147
+ """Call the OpenAI API credits."""
1148
+ from edsl.results.Results import Results
1149
+
1150
+ results = Results.example(debug=True)
1151
+ print(results.filter("how_feeling == 'Great'").select("how_feeling"))
1152
+ print(results.mutate("how_feeling_x = how_feeling + 'x'").select("how_feeling_x"))
1153
+
1154
+
1155
+ if __name__ == "__main__":
1156
+ import doctest
1157
+
1158
+ doctest.testmod(optionflags=doctest.ELLIPSIS)