Improve pandas history ergonomics: self-describing KeyErrors, DataFrame.get(symbol), lazy DataFrame on typed history - #9668
Draft
jhonabreul wants to merge 2 commits into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Improves the ergonomics of history results in Python, targeting the most common pandas interop failure modes:
Self-describing
PandasMapperKeyErrors. The former error was opaque and cost a full backtest iteration to decode:The error now keeps the legacy wording (for backwards compatibility) and appends what was requested and what the object actually has:
PandasMapper.wrap_keyerror_functionnow extracts the requested keys (including keys nested in list keys likedf[["symbol", "close"]], which previously reported empty key lists) and appends a description of the indexed object: its columns and index levels.get_level_values/reset_index), requesting a symbol that lives in the index (→df.loc[key]/df.xs), and requesting a known symbol that simply has no data in the frame (→df.get(key)).DataFrame.get(symbol)returns the symbol's sub-frame orNone.PandasMappernow extendsDataFrame.getso that symbols (or cached tickers) are also looked up in the index:history.get(symbol)returns the symbol's sub-frame, or the default (None) when the symbol has no data, instead of raising. Column lookups and non-symbol keys keep the original pandas semantics.Typed history results expose a lazily-built
DataFrame. The typedHistoryoverloads (History<T>(symbol, ...),History<T>(symbols, ...),History<T>(span/periods)and theHistory(symbol, ...)trade bar overloads) now returnDataHistory<T>/DataHistory<DataDictionary<T>>instead of plainIEnumerable:self.history[TradeBar](symbol, 10).data_framenow works instead of requiring manual conversion ('MemoizingEnumerable[TradeBar]' object has no attribute 'iterrows'was a recurring dead end).DataHistory<T>implementsIEnumerable<T>, so existing C# and Python consumers (iteration, LINQ) are unaffected; the pandas conversion is lazy and shares the memoized data with the enumerable, so accessing the data frame does not re-execute the history request and the result remains enumerable afterwards.OptionHistory/FutureHistory/IndicatorHistorypattern, which already subclassDataHistory<T>for exactly this reason.Deferred from the original proposal:
unstacked=True,history_series): changing the default frame shape would be breaking, and an opt-in API deserves its own design discussion. The new KeyError hints teach the existing idioms (get_level_values,reset_index,df.loc) in the meantime..iterrows) requires a pythonnet-side extension point for per-type attribute error hints; out of scope here.Related Issue
N/A
Motivation and Context
MultiIndex/typed-result confusion is the dominant crash class for generated and user Python algorithms: each opaque
PandasMapperKeyError costs a full backtest iteration to decode, andhistory.loc[symbol]on a symbol with no data kills scheduled rebalances. These changes make the errors self-explanatory and provide safe accessors, without changing any default data frame shape.Requires Documentation Change
The typed history return type change (
IEnumerable<T>→DataHistory<T>) andhistory.get(symbol)/.data_frameaccessors could be mentioned in the history documentation.How Has This Been Tested?
Tests/Python/PandasIndexingTests.cs+Tests/Python/PandasTests/PandasIndexingTests.py:KeyErrorDescribesMissingColumn: missing column error names the key, the available columns and the index levels.KeyErrorDescribesIndexLevelKey:df[['symbol', 'lastprice']]error explains 'symbol' is an index level and suggestsreset_index.KeyErrorDescribesSymbolInIndex:df['spy']error points to the 'symbol' index level anddf.loc.KeyErrorDescribesMissingSymbol:df.loc[symbol]for a symbol with no data suggestsdf.get(key).GetWithSymbolReturnsSubFrame/GetWithMissingSymbolReturnsNone/GetWithColumnKeepsPandasSemantics:DataFrame.getsymbol lookup, default handling, and unchanged column semantics.AlgorithmHistoryTests.TypedHistoryResultsExposeADataFrame(C# and Python): single- and multi-symbol typed history results expose a data frame with the expected shape and remain enumerable after the conversion.AlgorithmHistoryTests,PandasConverterTests,PandasIndexingTestsandPythonUtilTestsfixtures pass.CSharp/HistoryAlgorithm,Python/HistoryAlgorithm,CSharp/CustomDataTypeHistoryAlgorithmandPython/CustomDataTypeHistoryAlgorithmpass unchanged (the typed history path is exercised end-to-end).Part of QuantConnect/Agents#305 (improvement #10: history ergonomics)
Types of changes
Checklist:
bug-<issue#>-<description>orfeature-<issue#>-<description>