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