editor_options: markdown: wrap: 72 —

Data Envelopment Analysis (DEA) in Python, notebook style

This notebook walks through running and solving DEA models with dea.py. You choose how many DMUs (decision-making units), how many inputs/outputs, the model (CCR or BCC) and the orientation (input or output). Every code cell below was run and the outputs shown are real.

Contents 1. Setup 2. The models in one page 3. Choose your parameters 4. Generate (or load) data 5. CCR input-oriented 6. CCR output-oriented 7. BCC input- and output-oriented 8. Slacks, peers and targets 9. Scale efficiency and returns to scale 10. A textbook check 11. Using your own CSV and the command line

1. Setup

Requirements: numpy, pandas, scipy (the LPs are solved with scipy.optimize.linprog using the HiGHS solver). Put dea.py in your working folder.

pip install numpy pandas scipy
from dea import generate_data, load_csv, solve_dea, scale_efficiency
import numpy as np, pandas as pd

2. The models in one page

For each DMU \(o\) with inputs \(x_o \in \mathbb{R}^m\) and outputs \(y_o \in \mathbb{R}^s\), and intensity weights \(\lambda \ge 0\) over all \(n\) DMUs:

Input-oriented (envelopment form)

\[\min_{\theta,\lambda}\ \theta \quad \text{s.t.}\quad \sum_j \lambda_j x_{ij} \le \theta\, x_{io},\quad \sum_j \lambda_j y_{rj} \ge y_{ro},\quad \lambda_j \ge 0\]

Efficiency \(=\theta^* \in (0,1]\): the proportion to which all inputs could be shrunk.

Output-oriented

\[\max_{\phi,\lambda}\ \phi \quad \text{s.t.}\quad \sum_j \lambda_j x_{ij} \le x_{io},\quad \sum_j \lambda_j y_{rj} \ge \phi\, y_{ro},\quad \lambda_j \ge 0\]

Efficiency \(=1/\phi^*\): \(\phi^*\ge 1\) is the factor by which all outputs could grow.

Model Returns to scale Extra constraint
CCR (Charnes, Cooper & Rhodes 1978) constant (CRS) none
BCC (Banker, Charnes & Cooper 1984) variable (VRS) \(\sum_j \lambda_j = 1\)

Phase 2. After \(\theta^*\) (or \(\phi^*\)) is found, a second LP fixes it and maximizes the sum of input slacks \(s^-\) and output slacks \(s^+\). A DMU is efficient (Pareto-Koopmans) only when the score is 1 and all slacks are 0. Score 1 with a positive slack is reported as weak.

Targets (projection onto the frontier): input-oriented \(\hat x = \theta^* x_o - s^-,\ \hat y = y_o + s^+\); output-oriented \(\hat x = x_o - s^-,\ \hat y = \phi^* y_o + s^+\).

3. Choose your parameters

Change these and re-run the notebook. MODEL is "ccr" or "bcc"; ORIENTATION is "input" or "output". A common rule of thumb is \(n \ge \max\{m \cdot s,\ 3(m+s)\}\) so the model has enough discrimination.

N_DMUS      = 10
N_INPUTS    = 2
N_OUTPUTS   = 2
MODEL       = "ccr"      # "ccr" or "bcc"
ORIENTATION = "input"    # "input" or "output"
SEED        = 7

print("rule of thumb n >=", max(N_INPUTS * N_OUTPUTS, 3 * (N_INPUTS + N_OUTPUTS)))

Output:

rule of thumb n >= 12

4. Generate (or load) data

generate_data creates strictly positive random data, with outputs loosely tied to inputs so the result looks like a real production process. Rows are DMUs.

X, Y, names = generate_data(N_DMUS, N_INPUTS, N_OUTPUTS, seed=SEED)
in_names  = [f"x{i+1}" for i in range(N_INPUTS)]
out_names = [f"y{r+1}" for r in range(N_OUTPUTS)]
data = pd.DataFrame(np.hstack([X, Y]), index=names, columns=in_names + out_names)
print(data)

Output:

         x1    x2    y1    y2
DMU1   66.3  90.7  30.9  74.7
DMU2   79.8  30.3  33.7  43.2
DMU3   37.0  88.6  18.5  91.9
DMU4   10.5  83.9  25.1  96.5
DMU5   81.7  52.1  41.6  97.8
DMU6   37.3  35.1  19.9  39.0
DMU7   32.9  50.1  11.7  41.4
DMU8   55.4  59.8  37.8  58.2
DMU9   99.6  81.3  43.3  65.7
DMU10  66.0  99.0  60.4  77.9

5. CCR input-oriented

solve_dea solves one LP pair per DMU and returns a DEAResult. summary() gives the efficiency score, efficiency status, reference peers with their \(\lambda\) weights, and rank.

ccr_in = solve_dea(X, Y, model=MODEL, orientation=ORIENTATION,
                   names=names, input_names=in_names, output_names=out_names)
print(ccr_in.summary())

Output:

       efficiency   theta  ...                                   peers rank
DMU                        ...                                             
DMU1       0.6326  0.6326  ...  DMU4(0.294), DMU5(0.363), DMU10(0.140)    9
DMU2       1.0000  1.0000  ...                             DMU2(1.000)    1
DMU3       0.7994  0.7994  ...                DMU4(0.673), DMU5(0.276)    7
DMU4       1.0000  1.0000  ...                             DMU4(1.000)    1
DMU5       1.0000  1.0000  ...                             DMU5(1.000)    1
DMU6       0.8339  0.8339  ...  DMU4(0.023), DMU5(0.269), DMU10(0.135)    6
DMU7       0.5828  0.5828  ...                DMU4(0.220), DMU5(0.206)   10
DMU8       0.9460  0.9460  ...  DMU2(0.077), DMU5(0.214), DMU10(0.435)    5
DMU9       0.7156  0.7156  ...  DMU2(0.367), DMU5(0.225), DMU10(0.357)    8
DMU10      1.0000  1.0000  ...                            DMU10(1.000)    1

[10 rows x 5 columns]

Reading it: a DMU with efficiency 0.80 could produce the same outputs with 80% of each input, by imitating the convex (here: non-negative) combination of its peers.

6. CCR output-oriented

Under CRS the input- and output-oriented scores are reciprocals, so the efficiencies match the previous table; \(\phi\) tells you how much outputs could expand.

ccr_out = solve_dea(X, Y, "ccr", "output", names, in_names, out_names)
print(ccr_out.summary())
print("\nsame efficiency as input-oriented CCR:",
      np.allclose(ccr_out.efficiency, solve_dea(X, Y, "ccr", "input").efficiency))

Output:

       efficiency     phi  ...                                   peers rank
DMU                        ...                                             
DMU1       0.6326  1.5809  ...  DMU4(0.464), DMU5(0.573), DMU10(0.221)    9
DMU2       1.0000  1.0000  ...                             DMU2(1.000)    1
DMU3       0.7994  1.2509  ...                DMU4(0.842), DMU5(0.345)    7
DMU4       1.0000  1.0000  ...                             DMU4(1.000)    1
DMU5       1.0000  1.0000  ...                             DMU5(1.000)    1
DMU6       0.8339  1.1992  ...  DMU4(0.027), DMU5(0.322), DMU10(0.162)    6
DMU7       0.5828  1.7159  ...                DMU4(0.377), DMU5(0.354)   10
DMU8       0.9460  1.0570  ...  DMU2(0.082), DMU5(0.227), DMU10(0.460)    5
DMU9       0.7156  1.3973  ...  DMU2(0.513), DMU5(0.315), DMU10(0.498)    8
DMU10      1.0000  1.0000  ...                            DMU10(1.000)    1

[10 rows x 5 columns]

same efficiency as input-oriented CCR: True

7. BCC input- and output-oriented

BCC adds \(\sum\lambda_j = 1\), so each DMU is compared only with DMUs of a similar size. BCC scores are always \(\ge\) CCR scores, and input/output orientations can now differ.

bcc_in  = solve_dea(X, Y, "bcc", "input",  names, in_names, out_names)
bcc_out = solve_dea(X, Y, "bcc", "output", names, in_names, out_names)
compare = pd.DataFrame({
    "CCR": ccr_in.efficiency.round(4),
    "BCC-in": bcc_in.efficiency.round(4),
    "BCC-out": bcc_out.efficiency.round(4),
}, index=names)
print(compare)

Output:

          CCR  BCC-in  BCC-out
DMU1   0.6326  0.6596   0.7778
DMU2   1.0000  1.0000   1.0000
DMU3   0.7994  0.8128   0.9476
DMU4   1.0000  1.0000   1.0000
DMU5   1.0000  1.0000   1.0000
DMU6   0.8339  1.0000   1.0000
DMU7   0.5828  0.9365   0.6965
DMU8   0.9460  0.9964   0.9955
DMU9   0.7156  0.7217   0.8114
DMU10  1.0000  1.0000   1.0000

8. Slacks, peers and targets

Slacks are the extra, non-radial improvements left after the proportional reduction. Targets are where each DMU lands on the frontier.

print(bcc_in.slacks())

Output:

       s-_x1  s-_x2    s+_y1    s+_y2
DMU                                  
DMU1     0.0    0.0   0.0000   0.0000
DMU2     0.0    0.0   0.0000   0.0000
DMU3     0.0    0.0  10.1602   0.0000
DMU4     0.0    0.0   0.0000   0.0000
DMU5     0.0    0.0   0.0000   0.0000
DMU6     0.0    0.0   0.0000   0.0000
DMU7     0.0    0.0   9.4592  11.5243
DMU8     0.0    0.0   0.0000   0.0000
DMU9     0.0    0.0   0.0000   0.0000
DMU10    0.0    0.0   0.0000   0.0000
print(bcc_in.targets())

Output:

          x1*     x2*     y1*     y2*
DMU                                  
DMU1   43.728  59.822  30.900  74.700
DMU2   79.800  30.300  33.700  43.200
DMU3   30.072  72.010  28.660  91.900
DMU4   10.500  83.900  25.100  96.500
DMU5   81.700  52.100  41.600  97.800
DMU6   37.300  35.100  19.900  39.000
DMU7   30.810  46.917  21.159  52.924
DMU8   55.199  59.583  37.800  58.200
DMU9   71.879  58.672  43.300  65.700
DMU10  66.000  99.000  60.400  77.900

The full \(\lambda\) matrix (rows = evaluated DMU, columns = peer) is available too:

print(bcc_in.lambdas_df())

Output:

       DMU1    DMU2  DMU3    DMU4    DMU5    DMU6  DMU7  DMU8  DMU9   DMU10
DMU                                                                        
DMU1    0.0  0.0000   0.0  0.2914  0.2590  0.3541   0.0   0.0   0.0  0.0954
DMU2    0.0  1.0000   0.0  0.0000  0.0000  0.0000   0.0   0.0   0.0  0.0000
DMU3    0.0  0.0000   0.0  0.6718  0.2427  0.0855   0.0   0.0   0.0  0.0000
DMU4    0.0  0.0000   0.0  1.0000  0.0000  0.0000   0.0   0.0   0.0  0.0000
DMU5    0.0  0.0000   0.0  0.0000  1.0000  0.0000   0.0   0.0   0.0  0.0000
DMU6    0.0  0.0000   0.0  0.0000  0.0000  1.0000   0.0   0.0   0.0  0.0000
DMU7    0.0  0.0000   0.0  0.2422  0.0000  0.7578   0.0   0.0   0.0  0.0000
DMU8    0.0  0.0928   0.0  0.0000  0.0750  0.4620   0.0   0.0   0.0  0.3702
DMU9    0.0  0.3725   0.0  0.0000  0.2001  0.0838   0.0   0.0   0.0  0.3436
DMU10   0.0  0.0000   0.0  0.0000  0.0000  0.0000   0.0   0.0   0.0  1.0000
# peers of one DMU as a dict
worst = int(np.argmin(bcc_in.efficiency))
print(names[worst], "->", bcc_in.peers(worst))

Output:

DMU1 -> {'DMU4': 0.2914, 'DMU5': 0.259, 'DMU6': 0.3541, 'DMU10': 0.0954}

9. Scale efficiency and returns to scale

Technical efficiency (CCR) splits into pure technical efficiency (BCC) times scale efficiency: \(SE = \text{CCR} / \text{BCC}\). Returns to scale are read from \(\sum\lambda_j\) in the CCR solution: \(<1\) increasing (IRS), \(=1\) constant (CRS), \(>1\) decreasing (DRS). The CCR \(\lambda\) may not be unique, so treat the RTS label as indicative for DMUs with alternative optima.

print(scale_efficiency(X, Y, "input", names, in_names, out_names))

Output:

       CCR (TE)  BCC (PTE)      SE  sum_lambda  RTS
DMU                                                
DMU1     0.6326     0.6596  0.9591      0.7961  IRS
DMU2     1.0000     1.0000  1.0000      1.0000  CRS
DMU3     0.7994     0.8128  0.9836      0.9486  IRS
DMU4     1.0000     1.0000  1.0000      1.0000  CRS
DMU5     1.0000     1.0000  1.0000      1.0000  CRS
DMU6     0.8339     1.0000  0.8339      0.4265  IRS
DMU7     0.5828     0.9365  0.6223      0.4262  IRS
DMU8     0.9460     0.9964  0.9495      0.7269  IRS
DMU9     0.7156     0.7217  0.9916      0.9495  IRS
DMU10    1.0000     1.0000  1.0000      1.0000  CRS

10. A textbook check

Cooper, Seiford & Tone (Data Envelopment Analysis, ch. 1) use eight stores with one input (employees) and one output (sales). Their CCR scores are A 0.5, B 1, C 0.667, D 0.75, E 0.8, F 0.4, G 0.5, H 0.625, and BCC input-oriented C 0.833, F 0.5, with A, B, E, H on the VRS frontier. The solver reproduces them:

Xs = [[2],[3],[3],[4],[5],[5],[6],[8]]
Ys = [[1],[3],[2],[3],[4],[2],[3],[5]]
stores = list("ABCDEFGH")
print(pd.DataFrame({
    "CCR":    solve_dea(Xs, Ys, "ccr", "input",  stores).efficiency.round(4),
    "BCC-in": solve_dea(Xs, Ys, "bcc", "input",  stores).efficiency.round(4),
    "BCC-out":solve_dea(Xs, Ys, "bcc", "output", stores).efficiency.round(4),
}, index=stores))

Output:

      CCR  BCC-in  BCC-out
A  0.5000  1.0000   1.0000
B  1.0000  1.0000   1.0000
C  0.6667  0.8333   0.6667
D  0.7500  0.7500   0.8571
E  0.8000  1.0000   1.0000
F  0.4000  0.5000   0.5000
G  0.5000  0.5000   0.6923
H  0.6250  1.0000   1.0000

11. Using your own CSV and the command line

One row per DMU; pick input and output columns by name.

csv = "example_stores.csv"
pd.DataFrame({"Store": list("ABCDEFGH"),
              "Employees": [4, 7, 8, 4, 2, 10, 3, 5],
              "FloorArea": [3, 3, 1, 2, 4, 1, 7, 5],
              "Sales":     [1, 1, 1, 1, 1, 1, 1, 1]}).to_csv(csv, index=False)

X2, Y2, n2 = load_csv(csv, inputs=["Employees", "FloorArea"], outputs=["Sales"], name_col="Store")
res = solve_dea(X2, Y2, "ccr", "input", n2, ["Employees", "FloorArea"], ["Sales"])
print(res.summary())
print()
print(res.slacks())

Output:

     efficiency   theta efficient               peers  rank
DMU                                                        
A        0.8571  0.8571        no  D(0.714), E(0.286)     5
B        0.6316  0.6316        no  C(0.105), D(0.895)     7
C        1.0000  1.0000       yes            C(1.000)     1
D        1.0000  1.0000       yes            D(1.000)     1
E        1.0000  1.0000       yes            E(1.000)     1
F        1.0000  1.0000      weak            C(1.000)     1
G        0.6667  0.6667        no            E(1.000)     6
H        0.6000  0.6000        no  D(0.500), E(0.500)     8

     s-_Employees  s-_FloorArea  s+_Sales
DMU                                      
A             0.0        0.0000       0.0
B             0.0        0.0000       0.0
C             0.0        0.0000       0.0
D             0.0        0.0000       0.0
E             0.0        0.0000       0.0
F             2.0        0.0000       0.0
G             0.0        0.6667       0.0
H             0.0        0.0000       0.0

Store F scores 1 but carries 2 surplus employees, so it is only weakly efficient; G has a floor-area slack on top of its radial inefficiency.

Command-line equivalents:

# you choose the number of DMUs and the model
python dea.py --n-dmus 15 --n-inputs 3 --n-outputs 2 --model bcc --orientation output

# CCR + BCC + scale efficiency in one go, saved to CSV
python dea.py --n-dmus 20 --model both --save results.csv

# your own data
python dea.py --data example_stores.csv --inputs Employees FloorArea --outputs Sales \\
              --name-col Store --model ccr --orientation input

Notes and limits - CCR/BCC need strictly positive inputs and outputs; the solver raises an error otherwise. - Performance: 200 DMUs × 5 variables solves in about a second. - Ties in efficiency share the same rank; efficient DMUs all rank 1 (use super-efficiency if you need to break those ties).