Solución - Workshop 1, Stats for AI - Bloque 2

Variables categóricas, efectos de interacción y efectos cuadráticos en regresión múltiple

Autor/a

Alberto Dorantes

Fecha de publicación

30 de septiembre de 2026

Resumen
Solución detallada del Workshop 1 del Bloque 2. Se diseñan modelos de regresión múltiple para explicar la productividad de empresas de manufactura de EE.UU. Se muestra cómo una variable categórica (tamaño) genera ecuaciones paralelas, cómo un término de interacción permite pendientes distintas por grupo, y cómo un término cuadrático captura efectos no lineales.
NotaCómo renderizar

Este documento usa chunks de Python ejecutados desde RStudio con el paquete reticulate (engine knitr). Se requiere tener instalado en el ambiente de Python: pandas, numpy, requests, statsmodels y matplotlib. Para el PDF se requiere una distribución de LaTeX (por ejemplo, tinytex::install_tinytex()).

1 Preparación de los datos

Cargamos las librerías y el dataset directamente de la liga del workshop.

import pandas as pd
import numpy as np
import requests
import io
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt

pd.set_option("display.width", 120)
pd.set_option("display.max_columns", 20)

# Estilo de gráficas
plt.rcParams.update({
    "figure.dpi": 110, "axes.spines.top": False, "axes.spines.right": False,
    "axes.grid": True, "grid.alpha": 0.25, "axes.titleweight": "bold",
    "font.size": 10})

# Colores y marcadores fijos para cada grupo de tamaño
COL = {"Pequeña": "#2a78d6", "Mediana": "#eb6834", "Grande": "#1baf7a"}
MRK = {"Pequeña": "o", "Mediana": "s", "Grande": "^"}

headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"}

try:
    # Descargando el dataset de empresas:
    url = "http://www.apradie.com/datos/empresas_2023.csv"
    response = requests.get(url, headers=headers)
    response.raise_for_status()  # Error si la respuesta es 4xx o 5xx
    # Leemos el contenido en un DataFrame usando io.StringIO
    df = pd.read_csv(io.StringIO(response.text))
    print("File 'empresas_2023.csv' downloaded successfully.")
except requests.exceptions.HTTPError as e:
    print(f"HTTP Error occurred: {e}")
    raise
except requests.exceptions.RequestException as e:
    print(f"An error occurred during the request: {e}")
    raise
File 'empresas_2023.csv' downloaded successfully.
print(df.shape)
(1072, 9)
df.head()
  empresa  year       ventas      activos  precio_accion   numacciones  retorno_accion     utilidad        deuda
0       A  2023    6833000.0   10763000.0         139.03  2.930041e+05       -0.066370    1350000.0    2735000.0
1      AA  2023   10551000.0   14155000.0          34.00  1.784719e+05       -0.278846    -343000.0    1811000.0
2    AAOI  2023     217646.0     389186.0          19.32  3.553658e+04        2.324564     -41349.0      82394.0
3    AAON  2023    1168518.0     941436.0          73.87  8.124486e+04        0.391070     227494.0      38328.0
4    AAPL  2023  383285000.0  352583000.0         171.21  1.563423e+07        0.221365  114301000.0  111088000.0

El dataset tiene 1,072 empresas. Revisamos estadísticas descriptivas y datos faltantes:

print(df.describe().T.round(2))
                 count        mean          std         min        25%         50%         75%           max
year            1072.0     2023.00         0.00     2023.00    2023.00     2023.00     2023.00  2.023000e+03
ventas          1072.0  6289039.64  22856175.16       10.00  124190.50   791052.50  3558942.75  3.832850e+08
activos         1072.0  9191308.53  29470865.37     2776.59  274854.75  1164608.50  4632700.00  3.763170e+08
precio_accion   1072.0       66.61       115.30        0.18       7.72       27.60       79.55  1.212960e+03
numacciones     1072.0   193430.60    649736.86      966.13   28754.59    57464.06   139505.87  1.563423e+07
retorno_accion  1054.0        0.06         0.48       -2.14      -0.20        0.09        0.33  2.320000e+00
utilidad        1072.0   781319.34   4575892.68 -9128000.00  -28764.50    34307.00   324562.50  1.143010e+08
deuda           1069.0  2834284.15   9838531.45        0.00   21923.00   206295.00  1481000.00  1.492310e+08
print("\nDatos faltantes por columna:")

Datos faltantes por columna:
print(df.isna().sum())
empresa            0
year               0
ventas             0
activos            0
precio_accion      0
numacciones        0
retorno_accion    18
utilidad           0
deuda              3
dtype: int64

Hay 3 empresas sin dato de deuda y 18 sin retorno de la acción. Como no usaremos retorno_accion, sólo perderemos 3 observaciones en los modelos que usen deuda (statsmodels elimina automáticamente las filas con faltantes en las variables del modelo).

1.1 Variables que construimos

A lo largo del workshop necesitamos las siguientes variables. Las creamos de una vez:

Variable Fórmula Qué mide
roa utilidad / activos Productividad: utilidad operativa generada por cada $1 de recursos
valor_mercado precio_accion \times numacciones Tamaño de la empresa según el mercado
upa (utilidad / numacciones) / precio_accion Utilidad por acción como % del precio (earnings yield)
deuda_activos deuda / activos Proporción de los recursos financiada con deuda
rotacion ventas / activos Ventas generadas por cada $1 de activos (eficiencia)
log_activos ln(activos) Tamaño contable en escala logarítmica
df["roa"] = df["utilidad"] / df["activos"]
df["valor_mercado"] = df["precio_accion"] * df["numacciones"]
df["upa"] = (df["utilidad"] / df["numacciones"]) / df["precio_accion"]
df["deuda_activos"] = df["deuda"] / df["activos"]
df["rotacion"] = df["ventas"] / df["activos"]
df["log_activos"] = np.log(df["activos"])

df[["roa", "upa", "deuda_activos", "rotacion"]].describe(
    percentiles=[.01, .25, .5, .75, .99]).T.round(3)
                count   mean    std    min     1%    25%    50%    75%    99%    max
roa            1072.0 -0.065  0.312 -3.011 -1.234 -0.146  0.043  0.099  0.326  1.002
upa            1072.0 -0.052  0.240 -1.700 -0.972 -0.092  0.033  0.071  0.268  0.876
deuda_activos  1069.0  0.261  0.247  0.000  0.000  0.073  0.231  0.376  1.221  3.184
rotacion       1072.0  0.749  0.506  0.000  0.001  0.407  0.685  1.026  2.259  3.718

La mediana del ROA es 4.3%: la empresa típica genera alrededor de 4 centavos de utilidad operativa por cada dólar de activos. Pero hay empresas con ROA muy negativo (hasta -300%): son empresas pequeñas, muchas de ellas de biotecnología, que todavía no generan utilidades. Esto será importante en el Workshop 2 (diagnóstico de outliers).

2 CHALLENGE 1

Pregunta: ¿Qué factores están relacionados con la productividad de las empresas?

2.1 Diseño del modelo

Una primera idea (intuitiva, pero con un problema que veremos en el Challenge 2) es usar la utilidad como proxy de productividad: una empresa más productiva genera más utilidad. Como variables explicativas usamos:

  • ventas: más ingresos deberían traducirse en más utilidad.
  • activos: los recursos con los que opera la empresa.
  • deuda: el financiamiento con deuda puede afectar la utilidad operativa.

E[utilidad_i] = b_0 + b_1\,ventas_i + b_2\,activos_i + b_3\,deuda_i

2.2 Estimación del modelo

m1 = smf.ols("utilidad ~ ventas + activos + deuda", data=df).fit()
print(m1.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:               utilidad   R-squared:                       0.708
Model:                            OLS   Adj. R-squared:                  0.707
Method:                 Least Squares   F-statistic:                     860.0
Date:                Wed, 30 Sep 2026   Prob (F-statistic):          6.42e-284
Time:                        11:42:25   Log-Likelihood:                -17255.
No. Observations:                1069   AIC:                         3.452e+04
Df Residuals:                    1065   BIC:                         3.454e+04
Df Model:                           3                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
Intercept  -2.131e+05   7.95e+04     -2.680      0.007   -3.69e+05   -5.71e+04
ventas         0.1999      0.008     25.550      0.000       0.185       0.215
activos       -0.0198      0.008     -2.399      0.017      -0.036      -0.004
deuda         -0.0290      0.016     -1.800      0.072      -0.061       0.003
==============================================================================
Omnibus:                     1330.911   Durbin-Watson:                   1.896
Prob(Omnibus):                  0.000   Jarque-Bera (JB):          1038369.068
Skew:                           5.673   Prob(JB):                         0.00
Kurtosis:                     155.262   Cond. No.                     4.10e+07
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 4.1e+07. This might indicate that there are
strong multicollinearity or other numerical problems.

2.3 Interpretación

  • R² = 0.708: las tres variables explican el 70.8% de la variabilidad de la utilidad entre empresas. El estadístico F (p ≈ 0) indica que el modelo en su conjunto es significativo.
  • Ventas (b_1 = 0.1999, t = 25.6, p < 0.001): relación positiva y muy significativa. Manteniendo constantes activos y deuda, por cada $1,000 USD adicionales de ventas la utilidad operativa esperada aumenta en $199.9 USD (un margen operativo marginal de ~20%).
  • Activos (b_2 = -0.0198, t = -2.40, p = 0.017): relación negativa y significativa al 5%. Manteniendo constantes ventas y deuda, $1,000 USD adicionales de activos se asocian con $19.8 USD menos de utilidad. Tiene sentido: si dos empresas venden lo mismo, la que necesita más activos para lograrlo es menos eficiente (más depreciación, más costos de operar esos recursos).
  • Deuda (b_3 = -0.0290, t = -1.80, p = 0.072): relación negativa, pero sólo marginalmente significativa (significativa al 10% pero no al 5%). Por cada $1,000 USD adicionales de deuda la utilidad esperada disminuye $29 USD, ceteris paribus.

2.4 ¿Qué variable tiene mayor poder explicativo?

No podemos comparar los coeficientes directamente porque dependen de la escala de cada variable. Hay dos formas equivalentes de hacerlo:

  1. Comparar el valor absoluto del estadístico t (qué tan lejos de cero está el coeficiente en número de errores estándar).
  2. Estimar coeficientes estandarizados (betas): corremos la regresión con todas las variables convertidas a z-scores. Así cada coeficiente se interpreta como “cuántas desviaciones estándar cambia Y cuando X aumenta 1 desviación estándar”.
def zscore(s):
    return (s - s.mean()) / s.std()

vars1 = ["utilidad", "ventas", "activos", "deuda"]
dz = df[vars1].dropna().apply(zscore)
m1z = smf.ols("utilidad ~ ventas + activos + deuda", data=dz).fit()
pd.DataFrame({"coef. estandarizado": m1z.params[1:], "t": m1.tvalues[1:]}).round(3)
         coef. estandarizado       t
ventas                 0.998  25.550
activos               -0.128  -2.399
deuda                 -0.062  -1.800

Ventas es, por mucho, la variable con mayor poder explicativo: su beta estandarizada (≈ 1.0) es casi 8 veces la de activos, y su estadístico t (25.6) es el más grande.

Advertencia¿Realmente medimos productividad?

La gráfica muestra el problema: unas pocas empresas gigantes (Apple, Exxon, Nvidia y Chevron aparecen arriba a la derecha) dominan la relación. La utilidad refleja sobre todo tamaño, no productividad. Además, ventas, activos y deuda están muy correlacionadas entre sí (correlaciones de 0.75 a 0.90), porque las tres miden “qué tan grande es la empresa”.

fig, ax = plt.subplots(figsize=(7.5, 4.5))
ax.scatter(df["ventas"]/1e6, df["utilidad"]/1e6, s=14, alpha=0.5, color="#2a78d6")
ax.set_xlabel("Ventas (miles de millones USD)")
ax.set_ylabel("Utilidad operativa (miles de millones USD)")
ax.set_title("Las empresas grandes dominan el modelo de utilidad")
for _, r in df.nlargest(4, "utilidad").iterrows():
    ax.annotate(r["empresa"], (r["ventas"]/1e6, r["utilidad"]/1e6),
                xytext=(5, -10), textcoords="offset points", fontsize=8)
plt.tight_layout(); plt.show()

Utilidad vs ventas: la relación está dominada por el tamaño de las empresas

Matriz de correlaciones entre las variables del modelo:

print(df[vars1].corr().round(2))
          utilidad  ventas  activos  deuda
utilidad      1.00    0.84     0.72   0.57
ventas        0.84    1.00     0.90   0.75
activos       0.72    0.90     1.00   0.87
deuda         0.57    0.75     0.87   1.00

3 CHALLENGE 2

3.1 Diseño del modelo

Usamos el ROA (Return on Assets operativo = utilidad / activos) como proxy de productividad. Al ser una proporción, es comparable entre empresas de cualquier tamaño.

Como variables X ya no podemos usar ventas o activos en niveles (tendríamos el mismo problema de escala). Las transformamos en variables comparables:

  • rotacion = ventas / activos: cuántos dólares vende la empresa por cada dólar de activos. Mayor eficiencia en el uso de recursos → esperamos relación positiva con ROA.
  • deuda_activos = deuda / activos: nivel de apalancamiento. Mayor deuda puede significar más riesgo y restricciones → esperamos relación negativa.
  • log_activos = ln(activos): tamaño contable en logaritmo. Usamos logaritmo porque el tamaño tiene una distribución extremadamente sesgada, y el logaritmo convierte diferencias multiplicativas (una empresa 10 veces más grande que otra) en diferencias aditivas.

E[ROA_i] = b_0 + b_1\,rotacion_i + b_2\,deuda\_activos_i + b_3\,\ln(activos_i)

3.2 Estimación del modelo

m2 = smf.ols("roa ~ rotacion + deuda_activos + log_activos", data=df).fit()
print(m2.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    roa   R-squared:                       0.438
Model:                            OLS   Adj. R-squared:                  0.436
Method:                 Least Squares   F-statistic:                     276.7
Date:                Wed, 30 Sep 2026   Prob (F-statistic):          9.43e-133
Time:                        11:42:26   Log-Likelihood:                 37.152
No. Observations:                1069   AIC:                            -66.30
Df Residuals:                    1065   BIC:                            -46.41
Df Model:                           3                                         
Covariance Type:            nonrobust                                         
=================================================================================
                    coef    std err          t      P>|t|      [0.025      0.975]
---------------------------------------------------------------------------------
Intercept        -1.2988      0.048    -26.992      0.000      -1.393      -1.204
rotacion          0.2454      0.014     17.315      0.000       0.218       0.273
deuda_activos    -0.1893      0.030     -6.358      0.000      -0.248      -0.131
log_activos       0.0788      0.003     23.145      0.000       0.072       0.085
==============================================================================
Omnibus:                      748.627   Durbin-Watson:                   2.003
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            25216.464
Skew:                          -2.760   Prob(JB):                         0.00
Kurtosis:                      26.144   Cond. No.                         95.7
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

3.3 Interpretación

  • R² = 0.438: el modelo explica el 43.8% de la variabilidad del ROA. Es menor que el 70.8% del Challenge 1, pero ahora estamos explicando algo mucho más difícil y relevante: por qué algunas empresas son más productivas que otras, ya sin el “efecto tamaño” inflando el R².
  • Rotación (b_1 = 0.2454, p < 0.001): relación positiva y significativa. Si la rotación aumenta en 1 (por ejemplo, de vender $0.80 a vender $1.80 por cada $1 de activos), el ROA esperado aumenta 0.2454, es decir, 24.5 puntos porcentuales, manteniendo constantes deuda y tamaño. Un aumento más realista de 0.1 en rotación se asocia con +2.5 puntos porcentuales de ROA.
  • Deuda/activos (b_2 = -0.1893, p < 0.001): relación negativa y significativa. Si la proporción de deuda aumenta 10 puntos porcentuales (0.10), el ROA esperado disminuye 0.019 (1.9 puntos porcentuales), ceteris paribus.
  • ln(activos) (b_3 = 0.0788, p < 0.001): relación positiva y significativa. Como la X está en logaritmo, si los activos aumentan 1%, el ROA aumenta aproximadamente 0.0788/100 = 0.000788. Una interpretación más intuitiva: si una empresa duplica su tamaño en activos (\ln 2 \approx 0.693), su ROA esperado es 0.0788 \times 0.693 = 0.055 mayor (5.5 puntos porcentuales). Las empresas más grandes son más productivas.

3.4 ¿Qué variable tiene mayor poder explicativo?

vars2 = ["roa", "rotacion", "deuda_activos", "log_activos"]
dz2 = df[vars2].dropna().apply(zscore)
m2z = smf.ols("roa ~ rotacion + deuda_activos + log_activos", data=dz2).fit()
res2 = pd.DataFrame({"coef. estandarizado": m2z.params[1:], "t": m2.tvalues[1:]})
print(res2.round(3))
               coef. estandarizado       t
rotacion                     0.398  17.315
deuda_activos               -0.150  -6.358
log_activos                  0.545  23.145
fig, ax = plt.subplots(figsize=(7, 2.8))
orden = res2["coef. estandarizado"].sort_values()
ax.barh(orden.index, orden.values,
        color=["#eb6834" if v < 0 else "#2a78d6" for v in orden.values], height=0.5)
ax.axvline(0, color="grey", lw=1)
for i, v in enumerate(orden.values):
    ax.text(v + (0.01 if v >= 0 else -0.01), i, f"{v:.3f}",
            va="center", ha="left" if v >= 0 else "right", fontsize=9)
ax.set_xlabel("Cambio en ROA (en desv. estándar) por 1 desv. estándar de X")
ax.set_title("Poder explicativo relativo de cada variable")
ax.set_xlim(-0.3, 0.7)
plt.tight_layout(); plt.show()

Coeficientes estandarizados del modelo de ROA

El tamaño (log de activos) es la variable con mayor poder explicativo (beta estandarizada = 0.545, t = 23.1), seguido de la rotación de activos (0.398). Es decir: aun después de controlar por eficiencia y apalancamiento, el tamaño de la empresa está fuertemente relacionado con su productividad. Este hallazgo motiva el Challenge 3.

4 CHALLENGE 3

4.1 A) Tamaño de la empresa como variable categórica

4.1.1 Construcción de los grupos de tamaño

Clasificamos las empresas en 3 grupos del mismo tamaño usando los terciles del valor de mercado. pd.qcut hace exactamente esto:

df["tamano"] = pd.qcut(df["valor_mercado"], q=3,
                       labels=["Pequeña", "Mediana", "Grande"])

resumen = df.groupby("tamano", observed=True).agg(
    n=("roa", "size"),
    vm_min=("valor_mercado", "min"),
    vm_max=("valor_mercado", "max"),
    roa_promedio=("roa", "mean"))
resumen["vm_min"] = (resumen["vm_min"]/1e3).round(1)   # millones USD
resumen["vm_max"] = (resumen["vm_max"]/1e3).round(1)
print(resumen.rename(columns={"vm_min": "VM mín (mill. USD)",
                              "vm_max": "VM máx (mill. USD)"}).round(4))
           n  VM mín (mill. USD)  VM máx (mill. USD)  roa_promedio
tamano                                                            
Pequeña  357                 4.9               554.9       -0.2426
Mediana  357               559.6              4123.9       -0.0359
Grande   358              4129.9           2676736.9        0.0819

Las empresas pequeñas valen menos de ~$560 millones de USD; las medianas entre $560 millones y $4,130 millones; y las grandes más de $4,160 millones.

4.1.2 ¿Cómo entra una variable categórica a la regresión?

Una variable categórica con 3 categorías se representa con 2 variables dummy (0/1). La categoría que no tiene dummy es la categoría base (en nuestro caso, Pequeña):

E[ROA_i] = b_0 + b_1\,Mediana_i + b_2\,Grande_i

donde Mediana_i = 1 si la empresa es mediana (0 si no) y Grande_i = 1 si es grande (0 si no). Veamos cómo quedan las dummies:

dummies = pd.get_dummies(df["tamano"], prefix="d", dtype=int)
pd.concat([df[["empresa", "tamano"]], dummies], axis=1).sample(6, random_state=1)
    empresa   tamano  d_Pequeña  d_Mediana  d_Grande
120    AZTA  Mediana          0          1         0
552    LAKE  Pequeña          1          0         0
90     ASYS  Pequeña          1          0         0
973     TWI  Mediana          0          1         0
667    MVIS  Pequeña          1          0         0
314     ENR  Mediana          0          1         0

Si no incluimos la dummy de Pequeña es para evitar multicolinealidad perfecta (las 3 dummies siempre suman 1, igual que la columna de unos del intercepto).

4.1.3 Estimación del modelo

smf.ols crea automáticamente las dummies cuando la variable es categórica (con C()), y usa la primera categoría (Pequeña) como base:

m3a = smf.ols("roa ~ C(tamano)", data=df).fit()
print(m3a.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    roa   R-squared:                       0.186
Model:                            OLS   Adj. R-squared:                  0.184
Method:                 Least Squares   F-statistic:                     121.8
Date:                Wed, 30 Sep 2026   Prob (F-statistic):           2.23e-48
Time:                        11:42:26   Log-Likelihood:                -160.46
No. Observations:                1072   AIC:                             326.9
Df Residuals:                    1069   BIC:                             341.9
Df Model:                           2                                         
Covariance Type:            nonrobust                                         
========================================================================================
                           coef    std err          t      P>|t|      [0.025      0.975]
----------------------------------------------------------------------------------------
Intercept               -0.2426      0.015    -16.290      0.000      -0.272      -0.213
C(tamano)[T.Mediana]     0.2067      0.021      9.813      0.000       0.165       0.248
C(tamano)[T.Grande]      0.3246      0.021     15.418      0.000       0.283       0.366
==============================================================================
Omnibus:                      672.666   Durbin-Watson:                   1.952
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            11622.502
Skew:                          -2.579   Prob(JB):                         0.00
Kurtosis:                      18.284   Cond. No.                         3.73
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

4.1.4 Interpretación

Sustituyendo los valores de las dummies obtenemos una ecuación (en este caso, una constante) para cada grupo:

Grupo Mediana Grande E[ROA] Valor
Pequeña 0 0 b_0 -0.2429
Mediana 1 0 b_0 + b_1 = -0.2429 + 0.2083 -0.0346
Grande 0 1 b_0 + b_2 = -0.2429 + 0.3247 0.0818
  • Intercepto (b_0 = -0.2429, p < 0.001): es el ROA promedio de las empresas pequeñas: pierden en promedio 24.3 centavos por cada dólar de activos.
  • Mediana (b_1 = 0.2083, p < 0.001): el ROA promedio de las medianas es 20.8 puntos porcentuales mayor que el de las pequeñas. La diferencia es significativa.
  • Grande (b_2 = 0.3247, p < 0.001): el ROA promedio de las grandes es 32.5 puntos porcentuales mayor que el de las pequeñas. También significativa.

Respuesta: Sí, el tamaño está fuertemente relacionado con la productividad: a mayor tamaño, mayor ROA. El tamaño por sí solo explica el 18.6% de la variabilidad del ROA.

Fíjate que el modelo con sólo una variable categórica reproduce exactamente los promedios de cada grupo (compara con la columna roa_promedio de la tabla anterior). Una regresión con sólo dummies es equivalente a una comparación de medias (ANOVA).

b = m3a.params
pred_a = {"Pequeña": b["Intercept"],
          "Mediana": b["Intercept"] + b["C(tamano)[T.Mediana]"],
          "Grande":  b["Intercept"] + b["C(tamano)[T.Grande]"]}

rng = np.random.default_rng(7)
fig, ax = plt.subplots(figsize=(7.5, 4.8))
for i, g in enumerate(["Pequeña", "Mediana", "Grande"]):
    y = df.loc[df["tamano"] == g, "roa"]
    x = i + rng.uniform(-0.25, 0.25, len(y))
    ax.scatter(x, y, s=10, alpha=0.35, color=COL[g], marker=MRK[g])
    ax.hlines(pred_a[g], i - 0.35, i + 0.35, color="black", lw=2.5)
    ax.text(i + 0.38, pred_a[g], f"{pred_a[g]:.3f}", va="center", fontsize=9,
            bbox=dict(fc="white", ec="none", alpha=0.85))
# Flechas que muestran los coeficientes b1 y b2 como distancias al grupo base
ax.annotate("", xy=(1, pred_a["Mediana"]), xytext=(1, pred_a["Pequeña"]),
            arrowprops=dict(arrowstyle="->", color="black", lw=1.2))
ax.text(1.05, (pred_a["Mediana"] + pred_a["Pequeña"])/2, "b1 = 0.208", fontsize=9,
        bbox=dict(fc="white", ec="none", alpha=0.85))
ax.annotate("", xy=(2, pred_a["Grande"]), xytext=(2, pred_a["Pequeña"]),
            arrowprops=dict(arrowstyle="->", color="black", lw=1.2))
ax.text(2.05, (pred_a["Grande"] + pred_a["Pequeña"])/2, "b2 = 0.325", fontsize=9,
        bbox=dict(fc="white", ec="none", alpha=0.85))
ax.axhline(pred_a["Pequeña"], color="grey", ls=":", lw=1)
ax.set_xticks([0, 1, 2]); ax.set_xticklabels(["Pequeña", "Mediana", "Grande"])
ax.set_ylim(-1.3, 0.5)
ax.set_ylabel("ROA"); ax.set_xlabel("Tamaño de la empresa (terciles de valor de mercado)")
ax.set_title("Los coeficientes de las dummies son diferencias vs. el grupo base")
plt.tight_layout(); plt.show()

ROA por grupo de tamaño: cada línea horizontal es la predicción del modelo (el promedio del grupo)

(Para facilitar la lectura, la gráfica corta el eje Y en -1.3; unas pocas empresas pequeñas con ROA más negativo no se ven, pero sí están en el modelo.)

Ecuaciones de regresión por grupo:

E[ROA \mid \text{Pequeña}] = -0.2429 E[ROA \mid \text{Mediana}] = -0.2429 + 0.2083 = -0.0346 E[ROA \mid \text{Grande}] = -0.2429 + 0.3247 = 0.0818

4.2 B) Agregando la utilidad por acción (upa)

4.2.1 Diseño del modelo

Calculamos la utilidad por acción como proporción del precio (ya la creamos al inicio como upa). El modelo es:

E[ROA_i] = b_0 + b_1\,Mediana_i + b_2\,Grande_i + b_3\,upa_i

m3b = smf.ols("roa ~ C(tamano) + upa", data=df).fit()
print(m3b.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    roa   R-squared:                       0.571
Model:                            OLS   Adj. R-squared:                  0.570
Method:                 Least Squares   F-statistic:                     474.3
Date:                Wed, 30 Sep 2026   Prob (F-statistic):          8.08e-196
Time:                        11:42:27   Log-Likelihood:                 183.40
No. Observations:                1072   AIC:                            -358.8
Df Residuals:                    1068   BIC:                            -338.9
Df Model:                           3                                         
Covariance Type:            nonrobust                                         
========================================================================================
                           coef    std err          t      P>|t|      [0.025      0.975]
----------------------------------------------------------------------------------------
Intercept               -0.0637      0.012     -5.192      0.000      -0.088      -0.040
C(tamano)[T.Mediana]     0.0301      0.016      1.845      0.065      -0.002       0.062
C(tamano)[T.Grande]      0.1039      0.017      6.162      0.000       0.071       0.137
upa                      0.8977      0.029     30.993      0.000       0.841       0.955
==============================================================================
Omnibus:                      894.031   Durbin-Watson:                   1.951
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            49732.087
Skew:                          -3.429   Prob(JB):                         0.00
Kurtosis:                      35.655   Cond. No.                         5.76
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

4.2.2 Las ecuaciones paralelas

Cuando agregamos una variable continua (upa) junto con una variable categórica sin interacción, cada grupo tiene su propia recta, pero todas tienen la misma pendiente b_3. Sólo cambia el intercepto:

Grupo Ecuación Intercepto Pendiente
Pequeña b_0 + b_3\,upa -0.0634 0.8979
Mediana (b_0 + b_1) + b_3\,upa -0.0634 + 0.0300 = -0.0334 0.8979
Grande (b_0 + b_2) + b_3\,upa -0.0634 + 0.1035 = 0.0401 0.8979
b = m3b.params
ecs_b = {"Pequeña": (b["Intercept"], b["upa"]),
         "Mediana": (b["Intercept"] + b["C(tamano)[T.Mediana]"], b["upa"]),
         "Grande":  (b["Intercept"] + b["C(tamano)[T.Grande]"], b["upa"])}
for g, (b0g, b1g) in ecs_b.items():
    print(f"E[ROA | {g:8s}] = {b0g:7.4f} + {b1g:.4f} * upa")
E[ROA | Pequeña ] = -0.0637 + 0.8977 * upa
E[ROA | Mediana ] = -0.0335 + 0.8977 * upa
E[ROA | Grande  ] =  0.0402 + 0.8977 * upa

Gráficamente se ven como rectas paralelas separadas verticalmente por los coeficientes de las dummies:

def base_scatter(ax):
    for g in ["Pequeña", "Mediana", "Grande"]:
        s = df[df["tamano"] == g]
        ax.scatter(s["upa"], s["roa"], s=10, alpha=0.25, color=COL[g],
                   marker=MRK[g], label=f"{g} (datos)")
    ax.set_xlim(-0.6, 0.3); ax.set_ylim(-0.8, 0.45)
    ax.axhline(0, color="grey", lw=0.6); ax.axvline(0, color="grey", lw=0.6)
    ax.set_xlabel("upa (utilidad por acción / precio)"); ax.set_ylabel("ROA")

xg = np.linspace(-0.6, 0.3, 100)
fig, ax = plt.subplots(figsize=(7.5, 5.5))
base_scatter(ax)
for g, (b0g, b1g) in ecs_b.items():
    ax.plot(xg, b0g + b1g * xg, color=COL[g], lw=2.5,
            label=f"{g}: {b0g:.4f} + {b1g:.4f} upa")
# Distancias verticales entre rectas = coeficientes de las dummies
x0, x1 = -0.52, -0.42
yb0 = ecs_b["Pequeña"][0] + b["upa"] * x0
yb1 = ecs_b["Pequeña"][0] + b["upa"] * x1
ax.annotate("", xy=(x0, yb0 + b["C(tamano)[T.Grande]"]), xytext=(x0, yb0),
            arrowprops=dict(arrowstyle="<->", lw=1.2))
ax.text(x0 - 0.005, yb0 - 0.07, "b2 = 0.1035\n(Grande vs Pequeña)", fontsize=8,
        ha="center", va="top", bbox=dict(fc="white", ec="none", alpha=0.85))
ax.annotate("", xy=(x1, yb1 + b["C(tamano)[T.Mediana]"]), xytext=(x1, yb1),
            arrowprops=dict(arrowstyle="<->", lw=1.2))
ax.text(x1 + 0.02, yb1 - 0.02, "b1 = 0.0300\n(Mediana vs Pequeña)", fontsize=8,
        ha="left", va="top", bbox=dict(fc="white", ec="none", alpha=0.85))
ax.set_title("Ecuaciones paralelas: la variable categórica sólo mueve el intercepto")
ax.legend(fontsize=7.5, loc="upper left", ncol=2)
plt.tight_layout(); plt.show()

Modelo sin interacción: tres rectas paralelas (misma pendiente, distinto intercepto)

(La gráfica hace zoom en la región donde está la gran mayoría de las empresas; las observaciones extremas sí se usan en la estimación.)

4.2.3 Interpretación

  • upa (b_3 = 0.8979, p < 0.001): relación positiva y muy significativa. Manteniendo constante el tamaño, si la upa aumenta 0.10 (la empresa genera 10 centavos más de utilidad por cada $1 invertido en sus acciones), el ROA esperado aumenta 0.8979 \times 0.10 = 0.0898, casi 9 puntos porcentuales. Este efecto es el mismo para los tres grupos de tamaño (eso es lo que asume este modelo).
  • Mediana (b_1 = 0.0300, p = 0.067): después de controlar por upa, las empresas medianas tienen un ROA 3.0 puntos porcentuales mayor que las pequeñas con la misma upa. La diferencia sólo es significativa al 10%, no al 5%.
  • Grande (b_2 = 0.1035, p < 0.001): con la misma upa, las empresas grandes tienen un ROA 10.4 puntos porcentuales mayor que las pequeñas. Significativo.
  • R² = 0.571: pasó de 18.6% a 57.1%. La upa aporta mucho poder explicativo.

Respuesta: Además del tamaño, la upa tiene un efecto positivo y muy significativo en la productividad. Al incluirla, las diferencias por tamaño se reducen mucho (de 0.21 a 0.03 para medianas y de 0.32 a 0.10 para grandes): buena parte de la diferencia en ROA entre tamaños se explicaba porque las empresas pequeñas tienen upa más baja (muchas tienen pérdidas).

TipNota para reflexionar

Tanto el ROA como la upa tienen la utilidad en el numerador (upa = utilidad / valor de mercado; ROA = utilidad / activos). Por eso están fuertemente correlacionadas (r = 0.74). La relación no es causal en el sentido estricto: ambas son dos formas de medir qué tanta utilidad genera la empresa, una relativa a su valor en libros y otra relativa a su valor de mercado.

4.3 C) Interacción entre tamaño y upa

4.3.1 Diseño del modelo

Para permitir que el efecto de upa sobre el ROA sea diferente para cada grupo de tamaño, agregamos los productos de cada dummy por upa (términos de interacción):

E[ROA_i] = b_0 + b_1\,Mediana_i + b_2\,Grande_i + b_3\,upa_i + b_4\,(Mediana_i \times upa_i) + b_5\,(Grande_i \times upa_i)

En statsmodels, C(tamano)*upa incluye automáticamente los efectos principales y las interacciones:

m3c = smf.ols("roa ~ C(tamano) * upa", data=df).fit()
print(m3c.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    roa   R-squared:                       0.577
Model:                            OLS   Adj. R-squared:                  0.575
Method:                 Least Squares   F-statistic:                     290.5
Date:                Wed, 30 Sep 2026   Prob (F-statistic):          3.83e-196
Time:                        11:42:28   Log-Likelihood:                 190.36
No. Observations:                1072   AIC:                            -368.7
Df Residuals:                    1066   BIC:                            -338.9
Df Model:                           5                                         
Covariance Type:            nonrobust                                         
============================================================================================
                               coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------
Intercept                   -0.0746      0.013     -5.945      0.000      -0.099      -0.050
C(tamano)[T.Mediana]         0.0417      0.017      2.521      0.012       0.009       0.074
C(tamano)[T.Grande]          0.1096      0.018      6.136      0.000       0.075       0.145
upa                          0.8427      0.032     25.954      0.000       0.779       0.906
C(tamano)[T.Mediana]:upa     0.2784      0.076      3.649      0.000       0.129       0.428
C(tamano)[T.Grande]:upa      0.1683      0.150      1.125      0.261      -0.125       0.462
==============================================================================
Omnibus:                      904.131   Durbin-Watson:                   1.944
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            52305.503
Skew:                          -3.478   Prob(JB):                         0.00
Kurtosis:                      36.506   Cond. No.                         27.0
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

4.3.2 De la ecuación general a las ecuaciones por grupo

Ahora, al sustituir las dummies, cambian tanto el intercepto como la pendiente:

Grupo Intercepto Pendiente de upa
Pequeña b_0 = -0.0745 b_3 = 0.8423
Mediana b_0 + b_1 = -0.0745 + 0.0414 = -0.0331 b_3 + b_4 = 0.8423 + 0.2863 = 1.1286
Grande b_0 + b_2 = -0.0745 + 0.1094 = 0.0349 b_3 + b_5 = 0.8423 + 0.1684 = 1.0107
b = m3c.params
ecs_c = {"Pequeña": (b["Intercept"], b["upa"]),
         "Mediana": (b["Intercept"] + b["C(tamano)[T.Mediana]"],
                     b["upa"] + b["C(tamano)[T.Mediana]:upa"]),
         "Grande":  (b["Intercept"] + b["C(tamano)[T.Grande]"],
                     b["upa"] + b["C(tamano)[T.Grande]:upa"])}
for g, (b0g, b1g) in ecs_c.items():
    print(f"E[ROA | {g:8s}] = {b0g:7.4f} + {b1g:.4f} * upa")
E[ROA | Pequeña ] = -0.0746 + 0.8427 * upa
E[ROA | Mediana ] = -0.0329 + 1.1211 * upa
E[ROA | Grande  ] =  0.0350 + 1.0110 * upa

Ecuaciones de regresión por grupo:

E[ROA \mid \text{Pequeña}] = -0.0745 + 0.8423\,upa E[ROA \mid \text{Mediana}] = -0.0331 + 1.1286\,upa E[ROA \mid \text{Grande}] = 0.0349 + 1.0107\,upa

4.3.3 Visualizando la interacción: de rectas paralelas a pendientes distintas

fig, axes = plt.subplots(1, 2, figsize=(10, 4.8), sharey=True)
for ax, ecs, tit in [(axes[0], ecs_b, "B) Sin interacción: roa ~ tamaño + upa"),
                     (axes[1], ecs_c, "C) Con interacción: roa ~ tamaño * upa")]:
    base_scatter(ax)
    for g, (b0g, b1g) in ecs.items():
        ax.plot(xg, b0g + b1g * xg, color=COL[g], lw=2.5,
                label=f"{g}: pendiente = {b1g:.3f}")
    ax.set_title(tit, fontsize=10)
    handles, labels = ax.get_legend_handles_labels()
    ax.legend(handles[3:], labels[3:], fontsize=8, loc="upper left")
axes[1].set_ylabel("")
plt.tight_layout(); plt.show()

Izquierda: sin interacción (rectas paralelas). Derecha: con interacción (cada grupo tiene su propia pendiente)

En el panel derecho las rectas ya no son paralelas: la recta de las medianas es más inclinada que la de las pequeñas. Esto es exactamente lo que miden los coeficientes de interacción: la diferencia en pendientes respecto al grupo base.

NotaUn modelo con interacción completa = regresiones separadas por grupo

Cuando interactuamos la variable categórica con todas las variables X, los coeficientes por grupo son idénticos a los que obtendríamos corriendo una regresión simple por separado para cada grupo. La ventaja del modelo único es que nos da pruebas de hipótesis de las diferencias (los p-values de b_4 y b_5).

filas = []
for g in ["Pequeña", "Mediana", "Grande"]:
    mg = smf.ols("roa ~ upa", data=df[df["tamano"] == g]).fit()
    filas.append([g, mg.params["Intercept"], mg.params["upa"],
                  ecs_c[g][0], ecs_c[g][1]])
pd.DataFrame(filas, columns=["Grupo", "b0 (reg. separada)", "b1 (reg. separada)",
                             "b0 (modelo interacción)", "b1 (modelo interacción)"]).round(4)
     Grupo  b0 (reg. separada)  b1 (reg. separada)  b0 (modelo interacción)  b1 (modelo interacción)
0  Pequeña             -0.0746              0.8427                  -0.0746                   0.8427
1  Mediana             -0.0329              1.1211                  -0.0329                   1.1211
2   Grande              0.0350              1.0110                   0.0350                   1.0110
fig, axes = plt.subplots(1, 3, figsize=(10, 3.8), sharey=True, sharex=True)
for ax, g in zip(axes, ["Pequeña", "Mediana", "Grande"]):
    s = df[df["tamano"] == g]
    ax.scatter(s["upa"], s["roa"], s=10, alpha=0.3, color=COL[g], marker=MRK[g])
    b0g, b1g = ecs_c[g]
    ax.plot(xg, b0g + b1g * xg, color=COL[g], lw=2.5)
    ax.plot(xg, ecs_c["Pequeña"][0] + ecs_c["Pequeña"][1] * xg,
            color="grey", ls="--", lw=1.2, label="Recta de Pequeña (base)")
    ax.set_title(f"{g}\nROA = {b0g:.3f} + {b1g:.3f} upa", fontsize=9)
    ax.set_xlim(-0.6, 0.3); ax.set_ylim(-0.8, 0.45)
    ax.set_xlabel("upa")
axes[0].set_ylabel("ROA"); axes[1].legend(fontsize=7, loc="lower right")
plt.tight_layout(); plt.show()

La misma interacción vista por grupo: cada panel con su recta ajustada

4.3.4 Interpretación de los coeficientes

  • upa (b_3 = 0.8423, p < 0.001): ahora es la pendiente del grupo base (pequeñas). Para una empresa pequeña, si la upa aumenta 0.10, el ROA esperado aumenta 8.4 puntos porcentuales.
  • Mediana × upa (b_4 = 0.2863, p < 0.001): el efecto de upa en el ROA es 0.2863 mayor para las empresas medianas que para las pequeñas. La diferencia es significativa. Para una empresa mediana, un aumento de 0.10 en upa se asocia con +11.3 puntos porcentuales de ROA (vs. +8.4 en las pequeñas).
  • Grande × upa (b_5 = 0.1684, p = 0.261): la pendiente de las grandes es 0.1684 mayor que la de las pequeñas, pero la diferencia no es estadísticamente significativa. No tenemos evidencia de que el efecto de upa en grandes sea distinto al de pequeñas. Una razón: las empresas grandes tienen una upa muy concentrada (desviación estándar de 0.07 vs. 0.33 en pequeñas), lo que hace que su pendiente se estime con mucha incertidumbre (error estándar de 0.150).
  • Mediana (b_1 = 0.0414, p = 0.012) y Grande (b_2 = 0.1094, p < 0.001): son las diferencias en ROA esperado cuando upa = 0 (una empresa sin utilidad). Con upa = 0, una mediana tiene ROA 4.1 pp mayor y una grande 10.9 pp mayor que una pequeña; ambas diferencias son significativas.
  • R² = 0.577 (vs. 0.571 sin interacción).

¿Las interacciones, en conjunto, mejoran significativamente el modelo? Lo verificamos con una prueba F que compara el modelo B (sin interacción) contra el C (con interacción):

from statsmodels.stats.anova import anova_lm
print(anova_lm(m3b, m3c))
   df_resid        ssr  df_diff   ss_diff         F    Pr(>F)
0    1068.0  44.578120      0.0       NaN       NaN       NaN
1    1066.0  44.002677      2.0  0.575443  6.970286  0.000983

Con F = 7.25 y p = 0.0007, rechazamos la hipótesis nula de que ambas interacciones son cero: el efecto de la upa sí depende del tamaño.

También podemos probar si las pendientes de medianas y grandes son iguales entre sí (H_0: b_4 = b_5):

print(m3c.f_test("C(tamano)[T.Mediana]:upa = C(tamano)[T.Grande]:upa"))
<F test: F=0.4634524010368481, p=0.49616273325745897, df_denom=1.07e+03, df_num=1>

Con p = 0.47 no hay evidencia de que la pendiente de las medianas sea distinta a la de las grandes.

Respuesta: Sí existe interacción entre tamaño y upa. El efecto de upa en la productividad es significativamente mayor en las empresas medianas (1.13) que en las pequeñas (0.84). Para las grandes la pendiente estimada también es mayor (1.01), pero la diferencia con las pequeñas no es significativa.

5 CHALLENGE 4

5.1 Diseño del modelo

Queremos saber el efecto de la proporción de deuda sobre el ROA después de controlar por upa, tamaño y su interacción. Para examinar si hay un efecto no lineal, agregamos la variable lineal y su cuadrado:

E[ROA_i] = b_0 + b_1 Med_i + b_2 Gde_i + b_3 upa_i + b_4 (Med_i \times upa_i) + b_5 (Gde_i \times upa_i) + b_6\, DA_i + b_7\, DA_i^2

donde DA_i = deuda / activos. La idea: un poco de deuda puede ayudar a la empresa a crecer y ser más productiva, pero demasiada deuda puede asfixiarla. Un término cuadrático permite capturar ese “punto óptimo”.

m4 = smf.ols("roa ~ C(tamano) * upa + deuda_activos + I(deuda_activos**2)",
             data=df).fit()
print(m4.summary())
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                    roa   R-squared:                       0.594
Model:                            OLS   Adj. R-squared:                  0.591
Method:                 Least Squares   F-statistic:                     221.5
Date:                Wed, 30 Sep 2026   Prob (F-statistic):          1.54e-202
Time:                        11:42:31   Log-Likelihood:                 210.64
No. Observations:                1069   AIC:                            -405.3
Df Residuals:                    1061   BIC:                            -365.5
Df Model:                           7                                         
Covariance Type:            nonrobust                                         
============================================================================================
                               coef    std err          t      P>|t|      [0.025      0.975]
--------------------------------------------------------------------------------------------
Intercept                   -0.0684      0.014     -4.804      0.000      -0.096      -0.040
C(tamano)[T.Mediana]         0.0470      0.016      2.878      0.004       0.015       0.079
C(tamano)[T.Grande]          0.1180      0.018      6.651      0.000       0.083       0.153
upa                          0.8425      0.032     26.389      0.000       0.780       0.905
C(tamano)[T.Mediana]:upa     0.2485      0.075      3.303      0.001       0.101       0.396
C(tamano)[T.Grande]:upa      0.0821      0.148      0.557      0.578      -0.207       0.372
deuda_activos                0.0207      0.042      0.496      0.620      -0.061       0.102
I(deuda_activos ** 2)       -0.1148      0.026     -4.438      0.000      -0.166      -0.064
==============================================================================
Omnibus:                      894.969   Durbin-Watson:                   1.936
Prob(Omnibus):                  0.000   Jarque-Bera (JB):            54911.298
Skew:                          -3.412   Prob(JB):                         0.00
Kurtosis:                      37.442   Cond. No.                         28.1
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

(En la fórmula usamos I() para que statsmodels calcule el cuadrado aritmético de la variable.)

5.2 Interpretación

  • deuda_activos (b_6 = 0.0208, p = 0.618): el término lineal no es significativo.
  • deuda_activos² (b_7 = -0.1147, p < 0.001): el término cuadrático es negativo y significativo. Hay un efecto no lineal cóncavo (forma de U invertida) de la deuda sobre el ROA.

Mantenemos el término lineal aunque no sea significativo (principio de jerarquía: si incluimos X^2 debemos incluir X; además, el término lineal define dónde está el máximo de la parábola).

Con un término cuadrático, el efecto de la deuda no es constante: depende del nivel de deuda. Lo obtenemos con la derivada:

\frac{\partial E[ROA]}{\partial DA} = b_6 + 2\,b_7\,DA = 0.0208 - 0.2294\,DA

El ROA esperado es máximo donde la derivada es cero:

DA^* = -\frac{b_6}{2\,b_7} = \frac{0.0208}{2 \times 0.1147} \approx 0.09

b6 = m4.params["deuda_activos"]; b7 = m4.params["I(deuda_activos ** 2)"]
da_opt = -b6 / (2 * b7)
print(f"Nivel de deuda/activos donde el ROA esperado es máximo: {da_opt:.4f}")
Nivel de deuda/activos donde el ROA esperado es máximo: 0.0900
print("\nEfecto marginal de un aumento de 0.10 (10 pp) en deuda/activos:")

Efecto marginal de un aumento de 0.10 (10 pp) en deuda/activos:
for da in [0.0, 0.1, 0.231, 0.4, 0.6, 0.8, 1.0]:
    em = b6 + 2 * b7 * da
    print(f"  DA = {da:5.3f}: dROA/dDA = {em:7.4f}  ->  +10pp deuda => {em*0.10*100:6.2f} pp de ROA")
  DA = 0.000: dROA/dDA =  0.0207  ->  +10pp deuda =>   0.21 pp de ROA
  DA = 0.100: dROA/dDA = -0.0023  ->  +10pp deuda =>  -0.02 pp de ROA
  DA = 0.231: dROA/dDA = -0.0324  ->  +10pp deuda =>  -0.32 pp de ROA
  DA = 0.400: dROA/dDA = -0.0712  ->  +10pp deuda =>  -0.71 pp de ROA
  DA = 0.600: dROA/dDA = -0.1171  ->  +10pp deuda =>  -1.17 pp de ROA
  DA = 0.800: dROA/dDA = -0.1631  ->  +10pp deuda =>  -1.63 pp de ROA
  DA = 1.000: dROA/dDA = -0.2090  ->  +10pp deuda =>  -2.09 pp de ROA

Interpretación:

  • Hasta un nivel de deuda de ~9% de los activos, más deuda se asocia con un ROA ligeramente mayor (el efecto es casi nulo).
  • A partir de ese punto, más deuda se asocia con menor ROA, y el efecto negativo se acelera conforme la empresa está más endeudada. Para la empresa típica (mediana de deuda/activos = 23%), aumentar su deuda 10 pp se asocia con una reducción de 0.3 pp de ROA; para una empresa con 60% de deuda, la reducción es de 1.2 pp; y para una con 100% de deuda, de 2.1 pp.
  • Las conclusiones sobre upa y tamaño se mantienen: la pendiente de upa sigue siendo significativamente mayor en medianas (b_4 = 0.2555, p = 0.001) y no significativamente distinta en grandes (b_5 = 0.0823, p = 0.577).
  • El R² aumenta de 0.577 a 0.594.
da_grid = np.linspace(0, 1.3, 200)
upa_med = df["upa"].median()
fig, ax = plt.subplots(figsize=(7.5, 5))
for g in ["Pequeña", "Mediana", "Grande"]:
    s = df[df["tamano"] == g]
    ax.scatter(s["deuda_activos"], s["roa"], s=8, alpha=0.18, color=COL[g], marker=MRK[g])
    nd = pd.DataFrame({"tamano": pd.Categorical([g]*len(da_grid),
                                                 categories=df["tamano"].cat.categories),
                       "upa": upa_med, "deuda_activos": da_grid})
    ax.plot(da_grid, m4.predict(nd), color=COL[g], lw=2.5, label=g)
ax.axvline(da_opt, color="black", ls="--", lw=1)
ax.text(da_opt + 0.02, 0.33, f"Máximo en DA = {da_opt:.2f}", fontsize=9)
ax.set_xlim(0, 1.3); ax.set_ylim(-0.6, 0.4)
ax.set_xlabel("Deuda / Activos"); ax.set_ylabel("ROA")
ax.set_title(f"ROA esperado vs deuda (upa = mediana = {upa_med:.3f})")
ax.legend(title="Tamaño", fontsize=8)
plt.tight_layout(); plt.show()

Efecto cuadrático de la deuda: ROA esperado para cada tamaño, con upa fija en su mediana

Las tres curvas tienen la misma forma (misma parábola) porque en este modelo la deuda no interactúa con el tamaño; sólo están desplazadas verticalmente por el efecto del tamaño en el intercepto y en la pendiente de upa.

fig, ax = plt.subplots(figsize=(7.5, 3.5))
ax.plot(da_grid, (b6 + 2*b7*da_grid) * 0.10 * 100, color="#2a78d6", lw=2.5)
ax.axhline(0, color="grey", lw=1); ax.axvline(da_opt, color="black", ls="--", lw=1)
ax.set_xlabel("Deuda / Activos")
ax.set_ylabel("Cambio en ROA (pp)\npor +10 pp de deuda")
ax.set_title("Efecto marginal de la deuda: b6 + 2·b7·DA")
plt.tight_layout(); plt.show()

El efecto marginal de la deuda sobre el ROA disminuye linealmente conforme aumenta la deuda

Respuesta: Después de controlar por upa, tamaño y su interacción, la deuda financiera sí se relaciona con la productividad, pero de forma no lineal: no hay un efecto lineal directo significativo, pero sí un efecto cuadrático negativo y significativo. Niveles bajos de deuda prácticamente no afectan al ROA, mientras que niveles altos de deuda se asocian con una productividad cada vez menor.

AdvertenciaAntes de confiar en estos resultados…

Hay 15 empresas con deuda mayor que sus activos y varias empresas con ROA y upa extremadamente negativos. Estas observaciones pueden tener mucha influencia en los coeficientes (sobre todo en el término cuadrático). En el Workshop 2 diagnosticaremos observaciones leverage, outliers e influyentes, y corregiremos el modelo.