def _stack_positions(positions, pos_in_dollars=True): """ Convert positions to percentages if necessary, and change them to long format. Parameters ---------- positions: pd.DataFrame Daily holdings (in dollars or percentages), indexed by date. Will be converted to percentages if positions are in dollars. Short positions show up as cash in the 'cash' column. pos_in_dollars : bool Flag indicating whether `positions` are in dollars or percentages If True, positions are in dollars. """ if pos_in_dollars: # convert holdings to percentages positions = get_percent_alloc(positions) # remove cash after normalizing positions positions = positions.drop('cash', axis='columns') # convert positions to long format positions = positions.stack() positions.index = positions.index.set_names(['dt', 'ticker']) return positions
def plot_exposures_by_assets(positions: pd.DataFrame, ax: Optional[Axes] = None) -> Axes: """ plots the exposures of the held positions of all time. """ pos_alloc = pos.get_percent_alloc(positions) pos_alloc_no_cash = pos_alloc.drop('cash', axis=1) if ax is None: ax = plt.gca() pos_alloc_no_cash.plot( title='Portfolio allocation over time', alpha=0.5, ax=ax) box = ax.get_position() ax.set_position([box.x0, box.y0 + box.height * 0.1, box.width, box.height * 0.9]) # Put a legend below current axis ax.legend(loc='upper center', frameon=True, framealpha=0.5, bbox_to_anchor=(0.5, -0.14), ncol=5) ax.set_ylabel('Exposure by holding') ax.set_xlabel('') return ax
def top_positions(self, positions, top=10): """ The exposures of the top 10 and all held positions of all time. Parameters ---------- positions : pd.DataFrame Daily net position values. - See full explanation in create_full_tear_sheet. top : int, optional How many of each to find (default 10). Returns ------- The exposures of the top 10 and all held positions of all time. """ positions_alloc = pos.get_percent_alloc(positions) positions_alloc.columns = positions_alloc.columns.map( utils.format_asset) df_top_long, df_top_short, df_top_abs = pos.get_top_long_short_abs( positions_alloc, top) _, _, df_top_abs_all = pos.get_top_long_short_abs(positions_alloc, top=9999) return { "top_long": df_top_long, "top_short": df_top_short, "top_abs": df_top_abs, "all": df_top_abs_all }
def test_get_percent_alloc(self): raw_data = arange(15, dtype=float).reshape(5, 3) # Make the first column negative to test absolute magnitudes. raw_data[:, 0] *= -1 frame = DataFrame(raw_data, index=date_range("01-01-2015", freq="D", periods=5), columns=["A", "B", "C"]) result = get_percent_alloc(frame) expected_raw = zeros_like(raw_data) for idx, row in enumerate(raw_data): expected_raw[idx] = row / absolute(row).sum() expected = DataFrame(expected_raw, index=frame.index, columns=frame.columns) assert_frame_equal(result, expected)
def plot_interactive_exposures_by_asset(positions: pd.DataFrame) -> Chart: """ plots the exposures of the held positions of all time. """ pos_alloc = pos.get_percent_alloc(positions) pos_alloc_no_cash = pos_alloc.drop('cash', axis=1) attr = pos_alloc_no_cash.index.strftime("%Y-%m-%d") line = Line("Exposures By Asset") for col in pos_alloc_no_cash.columns: line.add(col, attr, np.round(pos_alloc_no_cash[col], 2).tolist(), is_more_utils=True, is_datazoom_show=True, line_width=2, line_opacity=0.7, is_symbol_show=False, datazoom_range=[0, 100], tooltip_trigger="axis") return line
def test_get_percent_alloc(self): raw_data = arange(15, dtype=float).reshape(5, 3) # Make the first column negative to test absolute magnitudes. raw_data[:, 0] *= -1 frame = DataFrame(raw_data, index=date_range('01-01-2015', freq='D', periods=5), columns=['A', 'B', 'C']) result = get_percent_alloc(frame) expected_raw = zeros_like(raw_data) for idx, row in enumerate(raw_data): expected_raw[idx] = row / row.sum() expected = DataFrame( expected_raw, index=frame.index, columns=frame.columns, ) assert_frame_equal(result, expected)
def load_data(self, sid): print('posview load ', sid) for ax in self.fig.axes: ax.clear() datapath = os.path.realpath( os.path.join(os.getcwd(), os.path.dirname(__file__))) datapath = datapath + '/../../pyfolio/tests/test_data/' test_returns = pd.read_csv(gzip.open(datapath + 'test_returns.csv.gz'), index_col=0, parse_dates=True) test_returns = to_series(to_utc(test_returns)) test_txn = to_utc( pd.read_csv(gzip.open(datapath + 'test_txn.csv.gz'), index_col=0, parse_dates=True)) test_pos = to_utc( pd.read_csv(gzip.open(datapath + 'test_pos.csv.gz'), index_col=0, parse_dates=True)) positions = utils.check_intraday('infer', test_returns, test_pos, test_txn) positions_alloc = pos.get_percent_alloc(positions) plotting.plot_exposures(test_returns, positions, ax=self.ax_exposures) plotting.plot_holdings(test_returns, positions_alloc, ax=self.ax_holdings) plotting.plot_long_short_holdings(test_returns, positions_alloc, ax=self.ax_long_short_holdings) plotting.plot_gross_leverage(test_returns, positions, ax=self.ax_gross_leverage) for ax in self.fig.axes: plt.setp(ax.get_xticklabels(), visible=True) self.draw()
def perf_attrib(returns, positions, factor_returns, factor_loadings, pos_in_dollars=True): """ Does performance attribution given risk info. Parameters ---------- returns : pd.Series Returns for each day in the date range. - Example: 2017-01-01 -0.017098 2017-01-02 0.002683 2017-01-03 -0.008669 positions: pd.DataFrame Daily holdings (in dollars or percentages), indexed by date. Will be converted to percentages if positions are in dollars. Short positions show up as cash in the 'cash' column. - Examples: AAPL TLT XOM cash 2017-01-01 34 58 10 0 2017-01-02 22 77 18 0 2017-01-03 -15 27 30 15 AAPL TLT XOM cash 2017-01-01 0.333333 0.568627 0.098039 0.0 2017-01-02 0.188034 0.658120 0.153846 0.0 2017-01-03 0.208333 0.375000 0.416667 0.0 factor_returns : pd.DataFrame Returns by factor, with date as index and factors as columns - Example: momentum reversal 2017-01-01 0.002779 -0.005453 2017-01-02 0.001096 0.010290 factor_loadings : pd.DataFrame Factor loadings for all days in the date range, with date and ticker as index, and factors as columns. - Example: momentum reversal dt ticker 2017-01-01 AAPL -1.592914 0.852830 TLT 0.184864 0.895534 XOM 0.993160 1.149353 2017-01-02 AAPL -0.140009 -0.524952 TLT -1.066978 0.185435 XOM -1.798401 0.761549 pos_in_dollars : bool Flag indicating whether `positions` are in dollars or percentages If True, positions are in dollars. Returns ------- tuple of (risk_exposures_portfolio, perf_attribution) risk_exposures_portfolio : pd.DataFrame df indexed by datetime, with factors as columns - Example: momentum reversal dt 2017-01-01 -0.238655 0.077123 2017-01-02 0.821872 1.520515 perf_attribution : pd.DataFrame df with factors, common returns, and specific returns as columns, and datetimes as index - Example: momentum reversal common_returns specific_returns dt 2017-01-01 0.249087 0.935925 1.185012 1.185012 2017-01-02 -0.003194 -0.400786 -0.403980 -0.403980 """ if pos_in_dollars: # convert holdings to percentages positions = get_percent_alloc(positions) # remove cash after normalizing positions positions = positions.drop('cash', axis='columns') # convert positions to long format positions = positions.stack() positions.index = positions.index.set_names(['dt', 'ticker']) risk_exposures = factor_loadings.multiply(positions, axis='rows') risk_exposures_portfolio = risk_exposures.groupby(level='dt').sum() perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns) common_returns = perf_attrib_by_factor.sum(axis='columns') specific_returns = returns - common_returns returns_df = pd.DataFrame({'total_returns': returns, 'common_returns': common_returns, 'specific_returns': specific_returns}) return (risk_exposures_portfolio, pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))
def perf_attrib(returns, positions, factor_returns, factor_loadings, transactions=None, pos_in_dollars=True): """ Does performance attribution given risk info. Parameters ---------- returns : pd.Series Returns for each day in the date range. - Example: 2017-01-01 -0.017098 2017-01-02 0.002683 2017-01-03 -0.008669 positions: pd.DataFrame Daily holdings (in dollars or percentages), indexed by date. Will be converted to percentages if positions are in dollars. Short positions show up as cash in the 'cash' column. - Examples: AAPL TLT XOM cash 2017-01-01 34 58 10 0 2017-01-02 22 77 18 0 2017-01-03 -15 27 30 15 AAPL TLT XOM cash 2017-01-01 0.333333 0.568627 0.098039 0.0 2017-01-02 0.188034 0.658120 0.153846 0.0 2017-01-03 0.208333 0.375000 0.416667 0.0 factor_returns : pd.DataFrame Returns by factor, with date as index and factors as columns - Example: momentum reversal 2017-01-01 0.002779 -0.005453 2017-01-02 0.001096 0.010290 factor_loadings : pd.DataFrame Factor loadings for all days in the date range, with date and ticker as index, and factors as columns. - Example: momentum reversal dt ticker 2017-01-01 AAPL -1.592914 0.852830 TLT 0.184864 0.895534 XOM 0.993160 1.149353 2017-01-02 AAPL -0.140009 -0.524952 TLT -1.066978 0.185435 XOM -1.798401 0.761549 transactions : pd.DataFrame, optional Executed trade volumes and fill prices. Used to check the turnover of the algorithm. Default is None, in which case the turnover check is skipped. - One row per trade. - Trades on different names that occur at the same time will have identical indicies. - Example: index amount price symbol 2004-01-09 12:18:01 483 324.12 'AAPL' 2004-01-09 12:18:01 122 83.10 'MSFT' 2004-01-13 14:12:23 -75 340.43 'AAPL' pos_in_dollars : bool Flag indicating whether `positions` are in dollars or percentages If True, positions are in dollars. Returns ------- tuple of (risk_exposures_portfolio, perf_attribution) risk_exposures_portfolio : pd.DataFrame df indexed by datetime, with factors as columns - Example: momentum reversal dt 2017-01-01 -0.238655 0.077123 2017-01-02 0.821872 1.520515 perf_attribution : pd.DataFrame df with factors, common returns, and specific returns as columns, and datetimes as index - Example: momentum reversal common_returns specific_returns dt 2017-01-01 0.249087 0.935925 1.185012 1.185012 2017-01-02 -0.003194 -0.400786 -0.403980 -0.403980 """ missing_stocks = positions.columns.difference( factor_loadings.index.get_level_values(1).unique()) # cash will not be in factor_loadings num_stocks = len(positions.columns) - 1 missing_stocks = missing_stocks.drop('cash') num_stocks_covered = num_stocks - len(missing_stocks) missing_ratio = round(len(missing_stocks) / num_stocks, ndigits=3) if num_stocks_covered == 0: raise ValueError("Could not perform performance attribution. " "No factor loadings were available for this " "algorithm's positions.") if len(missing_stocks) > 0: if len(missing_stocks) > 5: missing_stocks_displayed = ( " {} assets were missing factor loadings, including: {}..{}" ).format(len(missing_stocks), ', '.join(missing_stocks[:5].map(str)), missing_stocks[-1]) avg_allocation_msg = "selected missing assets" else: missing_stocks_displayed = ( "The following assets were missing factor loadings: {}." ).format(list(missing_stocks)) avg_allocation_msg = "missing assets" missing_stocks_warning_msg = ( "Could not determine risk exposures for some of this algorithm's " "positions. Returns from the missing assets will not be properly " "accounted for in performance attribution.\n" "\n" "{}. " "Ignoring for exposure calculation and performance attribution. " "Ratio of assets missing: {}. Average allocation of {}:\n" "\n" "{}.\n").format( missing_stocks_displayed, missing_ratio, avg_allocation_msg, positions[missing_stocks[:5].union(missing_stocks[[-1 ]])].mean(), ) warnings.warn(missing_stocks_warning_msg) positions = positions.drop(missing_stocks, axis='columns', errors='ignore') missing_factor_loadings_index = positions.index.difference( factor_loadings.index.get_level_values(0).unique()) if len(missing_factor_loadings_index) > 0: if len(missing_factor_loadings_index) > 5: missing_dates_displayed = ( "(first missing is {}, last missing is {})").format( missing_factor_loadings_index[0], missing_factor_loadings_index[-1]) else: missing_dates_displayed = list(missing_factor_loadings_index) warning_msg = ( "Could not find factor loadings for {} dates: {}. " "Truncating date range for performance attribution. ").format( len(missing_factor_loadings_index), missing_dates_displayed) warnings.warn(warning_msg) positions = positions.drop(missing_factor_loadings_index, errors='ignore') returns = returns.drop(missing_factor_loadings_index, errors='ignore') factor_returns = factor_returns.drop(missing_factor_loadings_index, errors='ignore') if transactions is not None and pos_in_dollars: turnover = get_turnover(positions, transactions).mean() if turnover > PERF_ATTRIB_TURNOVER_THRESHOLD: warning_msg = ( "This algorithm has relatively high turnover of its " "positions. As a result, performance attribution might not be " "fully accurate.\n" "\n" "Performance attribution is calculated based " "on end-of-day holdings and does not account for intraday " "activity. Algorithms that derive a high percentage of " "returns from buying and selling within the same day may " "receive inaccurate performance attribution.\n") warnings.warn(warning_msg) # Note that we convert positions to percentages *after* the checks # above, since get_turnover() expects positions in dollars. if pos_in_dollars: # convert holdings to percentages positions = get_percent_alloc(positions) # remove cash after normalizing positions positions = positions.drop('cash', axis='columns') # convert positions to long format positions = positions.stack() positions.index = positions.index.set_names(['dt', 'ticker']) risk_exposures = factor_loadings.multiply(positions, axis='rows') risk_exposures_portfolio = risk_exposures.groupby(level='dt').sum() perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns) common_returns = perf_attrib_by_factor.sum(axis='columns') specific_returns = returns - common_returns returns_df = pd.DataFrame({ 'total_returns': returns, 'common_returns': common_returns, 'specific_returns': specific_returns }) return (risk_exposures_portfolio, pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))
def perf_attrib(returns, positions, factor_returns, factor_loadings, pos_in_dollars=True): """ Does performance attribution given risk info. Parameters ---------- returns : pd.Series Returns for each day in the date range. - Example: 2017-01-01 -0.017098 2017-01-02 0.002683 2017-01-03 -0.008669 positions: pd.DataFrame Daily holdings (in dollars or percentages), indexed by date. Will be converted to percentages if positions are in dollars. Short positions show up as cash in the 'cash' column. - Examples: AAPL TLT XOM cash 2017-01-01 34 58 10 0 2017-01-02 22 77 18 0 2017-01-03 -15 27 30 15 AAPL TLT XOM cash 2017-01-01 0.333333 0.568627 0.098039 0.0 2017-01-02 0.188034 0.658120 0.153846 0.0 2017-01-03 0.208333 0.375000 0.416667 0.0 factor_returns : pd.DataFrame Returns by factor, with date as index and factors as columns - Example: momentum reversal 2017-01-01 0.002779 -0.005453 2017-01-02 0.001096 0.010290 factor_loadings : pd.DataFrame Factor loadings for all days in the date range, with date and ticker as index, and factors as columns. - Example: momentum reversal dt ticker 2017-01-01 AAPL -1.592914 0.852830 TLT 0.184864 0.895534 XOM 0.993160 1.149353 2017-01-02 AAPL -0.140009 -0.524952 TLT -1.066978 0.185435 XOM -1.798401 0.761549 pos_in_dollars : bool Flag indicating whether `positions` are in dollars or percentages If True, positions are in dollars. Returns ------- tuple of (risk_exposures_portfolio, perf_attribution) risk_exposures_portfolio : pd.DataFrame df indexed by datetime, with factors as columns - Example: momentum reversal dt 2017-01-01 -0.238655 0.077123 2017-01-02 0.821872 1.520515 perf_attribution : pd.DataFrame df with factors, common returns, and specific returns as columns, and datetimes as index - Example: momentum reversal common_returns specific_returns dt 2017-01-01 0.249087 0.935925 1.185012 1.185012 2017-01-02 -0.003194 -0.400786 -0.403980 -0.403980 """ missing_stocks = positions.columns.difference( factor_loadings.index.get_level_values(1).unique()) # cash will not be in factor_loadings num_stocks = len(positions.columns) - 1 missing_stocks = missing_stocks.drop('cash') num_stocks_covered = num_stocks - len(missing_stocks) if num_stocks_covered == 0: raise ValueError("Could not perform performance attribution. " "No factor loadings were available for this " "algorithm's positions.") if len(missing_stocks) > 0: warnings.warn("Could not find factor loadings for the following " "stocks: {}. Ignoring for exposure calculation and " "performance attribution. Coverage ratio: {}/{}. " "Average allocation of missing stocks: {} ".format( list(missing_stocks), num_stocks_covered, num_stocks, positions[missing_stocks].mean())) positions = positions.drop(missing_stocks, axis='columns', errors='ignore') missing_factor_loadings_index = positions.index.difference( factor_loadings.index.get_level_values(0).unique()) if len(missing_factor_loadings_index) > 0: warnings.warn( "Could not find factor loadings for the dates: {}. " "Truncating date range for performance attribution. ".format( list(missing_factor_loadings_index))) positions = positions.drop(missing_factor_loadings_index, errors='ignore') returns = returns.drop(missing_factor_loadings_index, errors='ignore') factor_returns = factor_returns.drop(missing_factor_loadings_index, errors='ignore') if pos_in_dollars: # convert holdings to percentages positions = get_percent_alloc(positions) # remove cash after normalizing positions positions = positions.drop('cash', axis='columns') # convert positions to long format positions = positions.stack() positions.index = positions.index.set_names(['dt', 'ticker']) risk_exposures = factor_loadings.multiply(positions, axis='rows') risk_exposures_portfolio = risk_exposures.groupby(level='dt').sum() perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns) common_returns = perf_attrib_by_factor.sum(axis='columns') specific_returns = returns - common_returns returns_df = pd.DataFrame({ 'total_returns': returns, 'common_returns': common_returns, 'specific_returns': specific_returns }) return (risk_exposures_portfolio, pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))
def perf_attrib(returns, positions, factor_returns, factor_loadings, transactions=None, pos_in_dollars=True): """ Does performance attribution given risk info. Parameters ---------- returns : pd.Series Returns for each day in the date range. - Example: 2017-01-01 -0.017098 2017-01-02 0.002683 2017-01-03 -0.008669 positions: pd.DataFrame Daily holdings (in dollars or percentages), indexed by date. Will be converted to percentages if positions are in dollars. Short positions show up as cash in the 'cash' column. - Examples: AAPL TLT XOM cash 2017-01-01 34 58 10 0 2017-01-02 22 77 18 0 2017-01-03 -15 27 30 15 AAPL TLT XOM cash 2017-01-01 0.333333 0.568627 0.098039 0.0 2017-01-02 0.188034 0.658120 0.153846 0.0 2017-01-03 0.208333 0.375000 0.416667 0.0 factor_returns : pd.DataFrame Returns by factor, with date as index and factors as columns - Example: momentum reversal 2017-01-01 0.002779 -0.005453 2017-01-02 0.001096 0.010290 factor_loadings : pd.DataFrame Factor loadings for all days in the date range, with date and ticker as index, and factors as columns. - Example: momentum reversal dt ticker 2017-01-01 AAPL -1.592914 0.852830 TLT 0.184864 0.895534 XOM 0.993160 1.149353 2017-01-02 AAPL -0.140009 -0.524952 TLT -1.066978 0.185435 XOM -1.798401 0.761549 transactions : pd.DataFrame, optional Executed trade volumes and fill prices. Used to check the turnover of the algorithm. Default is None, in which case the turnover check is skipped. - One row per trade. - Trades on different names that occur at the same time will have identical indicies. - Example: index amount price symbol 2004-01-09 12:18:01 483 324.12 'AAPL' 2004-01-09 12:18:01 122 83.10 'MSFT' 2004-01-13 14:12:23 -75 340.43 'AAPL' pos_in_dollars : bool Flag indicating whether `positions` are in dollars or percentages If True, positions are in dollars. Returns ------- tuple of (risk_exposures_portfolio, perf_attribution) risk_exposures_portfolio : pd.DataFrame df indexed by datetime, with factors as columns - Example: momentum reversal dt 2017-01-01 -0.238655 0.077123 2017-01-02 0.821872 1.520515 perf_attribution : pd.DataFrame df with factors, common returns, and specific returns as columns, and datetimes as index - Example: momentum reversal common_returns specific_returns dt 2017-01-01 0.249087 0.935925 1.185012 1.185012 2017-01-02 -0.003194 -0.400786 -0.403980 -0.403980 """ missing_stocks = positions.columns.difference( factor_loadings.index.get_level_values(1).unique() ) # cash will not be in factor_loadings num_stocks = len(positions.columns) - 1 missing_stocks = missing_stocks.drop('cash') num_stocks_covered = num_stocks - len(missing_stocks) missing_ratio = round(len(missing_stocks) / num_stocks, ndigits=3) if num_stocks_covered == 0: raise ValueError("Could not perform performance attribution. " "No factor loadings were available for this " "algorithm's positions.") if len(missing_stocks) > 0: if len(missing_stocks) > 5: missing_stocks_displayed = ( " {} assets were missing factor loadings, including: {}..{}" ).format(len(missing_stocks), ', '.join(missing_stocks[:5].map(str)), missing_stocks[-1]) avg_allocation_msg = "selected missing assets" else: missing_stocks_displayed = ( "The following assets were missing factor loadings: {}." ).format(list(missing_stocks)) avg_allocation_msg = "missing assets" missing_stocks_warning_msg = ( "Could not determine risk exposures for some of this algorithm's " "positions. Returns from the missing assets will not be properly " "accounted for in performance attribution.\n" "\n" "{}. " "Ignoring for exposure calculation and performance attribution. " "Ratio of assets missing: {}. Average allocation of {}:\n" "\n" "{}.\n" ).format( missing_stocks_displayed, missing_ratio, avg_allocation_msg, positions[missing_stocks[:5].union(missing_stocks[[-1]])].mean(), ) warnings.warn(missing_stocks_warning_msg) positions = positions.drop(missing_stocks, axis='columns', errors='ignore') missing_factor_loadings_index = positions.index.difference( factor_loadings.index.get_level_values(0).unique() ) if len(missing_factor_loadings_index) > 0: if len(missing_factor_loadings_index) > 5: missing_dates_displayed = ( "(first missing is {}, last missing is {})" ).format( missing_factor_loadings_index[0], missing_factor_loadings_index[-1] ) else: missing_dates_displayed = list(missing_factor_loadings_index) warning_msg = ( "Could not find factor loadings for {} dates: {}. " "Truncating date range for performance attribution. " ).format(len(missing_factor_loadings_index), missing_dates_displayed) warnings.warn(warning_msg) positions = positions.drop(missing_factor_loadings_index, errors='ignore') returns = returns.drop(missing_factor_loadings_index, errors='ignore') factor_returns = factor_returns.drop(missing_factor_loadings_index, errors='ignore') if transactions is not None and pos_in_dollars: turnover = get_turnover(positions, transactions).mean() if turnover > PERF_ATTRIB_TURNOVER_THRESHOLD: warning_msg = ( "This algorithm has relatively high turnover of its " "positions. As a result, performance attribution might not be " "fully accurate.\n" "\n" "Performance attribution is calculated based " "on end-of-day holdings and does not account for intraday " "activity. Algorithms that derive a high percentage of " "returns from buying and selling within the same day may " "receive inaccurate performance attribution.\n" ) warnings.warn(warning_msg) # Note that we convert positions to percentages *after* the checks # above, since get_turnover() expects positions in dollars. if pos_in_dollars: # convert holdings to percentages positions = get_percent_alloc(positions) # remove cash after normalizing positions positions = positions.drop('cash', axis='columns') # convert positions to long format positions = positions.stack() positions.index = positions.index.set_names(['dt', 'ticker']) risk_exposures = factor_loadings.multiply(positions, axis='rows') risk_exposures_portfolio = risk_exposures.groupby(level='dt').sum() perf_attrib_by_factor = risk_exposures_portfolio.multiply(factor_returns) common_returns = perf_attrib_by_factor.sum(axis='columns') specific_returns = returns - common_returns returns_df = pd.DataFrame({'total_returns': returns, 'common_returns': common_returns, 'specific_returns': specific_returns}) return (risk_exposures_portfolio, pd.concat([perf_attrib_by_factor, returns_df], axis='columns'))