def calculate_risk_factor_attribution(portfolio_returns, factors):
    """
    Calculate the risk factor attribution for a given portfolio or benchmark.
    
    Parameters:
    - portfolio_returns (pd.Series): Daily returns of the portfolio or benchmark.
    - factors (pd.DataFrame): Fama-French factors data.
    
    Returns:
    - pd.DataFrame: Attribution results with factors and their contributions.
    - str or None: Error message if any issues occur.
    """
    try:
        # Ensure dates align between returns and factors
        df = pd.concat([portfolio_returns, factors], axis=1).dropna()
        portfolio_returns = df.iloc[:, 0]
        factors = df.iloc[:, 1:]
        
        # Define independent variables (Fama-French factors excluding RF)
        X = factors.drop(columns=['RF'], errors='ignore')
        X = sm.add_constant(X)
        
        # Define dependent variable
        y = portfolio_returns - factors.get('RF', 0)  # Excess returns
        
        # Fit linear regression model
        model = sm.OLS(y, X).fit()
        
        # Get factor exposures (coefficients)
        coefficients = model.params.drop('const', errors='ignore')
        
        # Calculate contribution to variance
        sigma_squared = model.mse_resid
        total_variance = portfolio_returns.var()
        contributions = (coefficients ** 2) * X.var().drop('const', errors='ignore')
        contributions_percentage = (contributions / total_variance) * 100
        
        # Prepare attribution DataFrame
        attribution_df = contributions_percentage.reset_index().rename(columns={
            'index': 'Factor',
            0: 'Contribution (%)'
        })
        
        return attribution_df, None
    except Exception as e:
        return pd.DataFrame(), str(e)

combine the above with the below:
def calculate_risk_factor_attribution(returns, factors, annualization_factor=252):
    """
    Decompose portfolio returns by risk factors using linear regression and provide
    comments on the factor contributions.

    Parameters:
    - returns (pd.Series): Portfolio returns.
    - factors (pd.DataFrame): DataFrame where each column represents a risk factor.
    - annualization_factor (int): Annualization factor (252 for daily, 12 for monthly returns).

    Returns:
    - pd.DataFrame: Factor names, their contribution percentages, and explanatory comments.
    - str: Error message if any exception occurs, else None.
    """
    try:
        # Ensure returns has a name, or assign one if missing
        if returns.name is None:
            returns = returns.rename("Portfolio_Returns")
        
        # Convert returns to a DataFrame
        returns = returns.to_frame()
        
        # Ensure both returns and factors are in decimal format
        if returns['Portfolio_Returns'].max() > 1:
            returns = returns / 100
            
        if factors.max().max() > 1:
            factors = factors / 100
        
        # Adjust returns to excess returns by subtracting RF (if it exists in factors)
        if 'RF' in factors.columns:
            returns['Portfolio_Returns'] = returns['Portfolio_Returns'] - factors['RF']
            factors = factors.drop(columns=['RF'])
        
        # Align the data using an inner join and drop any remaining NaN values
        aligned_data = pd.merge(returns, factors, left_index=True, right_index=True, how='inner').dropna()

        # Check that we have data after merging
        if aligned_data.empty:
            raise ValueError("No overlapping dates between returns and factors after dropping NaNs.")
    
        # Prepare X and y for regression
        X = aligned_data[factors.columns]
        X = sm.add_constant(X)  # Add intercept
        y = aligned_data['Portfolio_Returns']
        
        # Run the regression model
        model = sm.OLS(y, X).fit()
        coefficients = model.params.drop('const', errors='ignore')
        
        # Calculate factor contributions (non-annualized)
        factor_means = X.mean().drop('const', errors='ignore')
        contributions = coefficients * factor_means
        
        # Calculate total return
        total_return = returns['Portfolio_Returns'].mean()

        # Check if total_return is zero to avoid division by zero
        if np.isclose(total_return, 0):
            attribution_df = pd.DataFrame({
                'Factor': contributions.index,
                'Contribution (%)': [0.0] * len(contributions)
            })
        else:
            # Calculate percentage contributions
            attribution_df = pd.DataFrame({
                'Factor': contributions.index,
                'Contribution (%)': (contributions / total_return * 100).round(2)
            })
        
        # Remove the constant term if it exists
        if 'const' in attribution_df['Factor'].values:
            attribution_df = attribution_df[attribution_df['Factor'] != 'const']
        
        # Use full names for factors
        factor_full_names = {
            'Mkt-RF': 'Market Risk Premium',
            'SMB': 'Small Minus Big',
            'HML': 'High Minus Low'
        }
        attribution_df['Factor'] = attribution_df['Factor'].replace(factor_full_names)
        
        # Generate detailed comments based on factor contributions
        comments = []
        for _, row in attribution_df.iterrows():
            factor_name = row['Factor']
            contribution = row['Contribution (%)']

            # Market Risk Premium (Mkt-RF) Analysis
            if factor_name == 'Market Risk Premium':
                if contribution > 75:
                    comments.append(f"{factor_name} = {contribution}%: Extremely high sensitivity to market movements.")
                elif contribution > 50:
                    comments.append(f"{factor_name} = {contribution}%: High sensitivity to overall market movements.")
                elif contribution > 30:
                    comments.append(f"{factor_name} = {contribution}%: Notable sensitivity to market risk.")
                elif contribution > 10:
                    comments.append(f"{factor_name} = {contribution}%: Moderate exposure to market risk.")
                else:
                    comments.append(f"{factor_name} = {contribution}%: Low exposure to market risk.")

            # Small Minus Big (SMB) Analysis
            elif factor_name == 'Small Minus Big':
                if contribution > 5:
                    comments.append(f"{factor_name} = {contribution}%: Strong tilt towards small-cap stocks.")
                elif contribution > 0:
                    comments.append(f"{factor_name} = {contribution}%: Mild preference for small-cap stocks.")
                elif contribution > -5:
                    comments.append(f"{factor_name} = {contribution}%: Slight tilt towards larger-cap stocks.")
                else:
                    comments.append(f"{factor_name} = {contribution}%: Clear preference for large-cap stocks.")

            # High Minus Low (HML) Analysis
            elif factor_name == 'High Minus Low':
                if contribution > 5:
                    comments.append(f"{factor_name} = {contribution}%: Strong tilt toward value stocks.")
                elif contribution > 0:
                    comments.append(f"{factor_name} = {contribution}%: Slight positive tilt toward value stocks.")
                elif contribution > -5:
                    comments.append(f"{factor_name} = {contribution}%: Minor negative tilt toward growth stocks.")
                else:
                    comments.append(f"{factor_name} = {contribution}%: Clear preference for growth stocks.")

        # Add comments to the DataFrame
        attribution_df['Comment'] = comments
    
        return attribution_df, None
    
    except Exception as e:
        error_message = f"Error calculating risk factor attribution: {e}"
        return pd.DataFrame(), error_message