Bagian 4 dari seri

Perhitungan TOPSIS dengan Laravel: Normalisasi, Solusi Ideal, dan Nilai Preferensi

Diterbitkan 21 September 2026

Pada bagian ini buat TopsisService. Salin kode berikut apa adanya.

Tahapan Perhitungan

Service akan menangani:

  1. normalisasi bobot;
  2. normalisasi matriks keputusan;
  3. matriks normalisasi terbobot;
  4. solusi ideal positif;
  5. solusi ideal negatif;
  6. jarak terhadap solusi ideal;
  7. nilai preferensi.

Buat Folder Service

Buat folder:

app/Services

Lalu buat:

app/Services/TopsisService.php

TopsisService

Isi:

<?php

namespace App\Services;

use InvalidArgumentException;

class TopsisService
{
    public function calculate(
        array $matrix,
        array $weights,
        array $attributes
    ): array {
        if (empty($matrix)) {
            throw new InvalidArgumentException(
                'Matriks keputusan tidak boleh kosong.'
            );
        }

        $alternativeIds = array_keys($matrix);
        $criteriaIds = array_keys(reset($matrix));

        $normalizedWeights = $this->normalizeWeights(
            $weights,
            $criteriaIds
        );

        [$normalized, $divisors] = $this->normalizeMatrix(
            $matrix,
            $alternativeIds,
            $criteriaIds
        );

        $weighted = $this->weightedMatrix(
            $normalized,
            $normalizedWeights,
            $alternativeIds,
            $criteriaIds
        );

        [$idealPositive, $idealNegative] =
            $this->idealSolutions(
                $weighted,
                $attributes,
                $alternativeIds,
                $criteriaIds
            );

        [$distancePositive, $distanceNegative] =
            $this->distances(
                $weighted,
                $idealPositive,
                $idealNegative,
                $alternativeIds,
                $criteriaIds
            );

        $preferences = $this->preferences(
            $distancePositive,
            $distanceNegative,
            $alternativeIds
        );

        return [
            'weights' => $normalizedWeights,
            'divisors' => $divisors,
            'normalized' => $normalized,
            'weighted' => $weighted,
            'idealPositive' => $idealPositive,
            'idealNegative' => $idealNegative,
            'distancePositive' => $distancePositive,
            'distanceNegative' => $distanceNegative,
            'preferences' => $preferences,
        ];
    }

    public function normalizeWeights(
        array $weights,
        array $criteriaIds
    ): array {
        $total = 0.0;

        foreach ($criteriaIds as $criteriaId) {
            $weight = (float) ($weights[$criteriaId] ?? 0);

            if ($weight <= 0) {
                throw new InvalidArgumentException(
                    'Semua bobot kriteria harus lebih besar dari 0.'
                );
            }

            $total += $weight;
        }

        if ($total <= 0) {
            throw new InvalidArgumentException(
                'Total bobot harus lebih besar dari 0.'
            );
        }

        $result = [];

        foreach ($criteriaIds as $criteriaId) {
            $result[$criteriaId] =
                (float) $weights[$criteriaId] / $total;
        }

        return $result;
    }

    public function normalizeMatrix(
        array $matrix,
        array $alternativeIds,
        array $criteriaIds
    ): array {
        $divisors = [];

        foreach ($criteriaIds as $criteriaId) {
            $sumSquares = 0.0;

            foreach ($alternativeIds as $alternativeId) {
                $value = (float)
                    ($matrix[$alternativeId][$criteriaId] ?? 0);

                $sumSquares += $value ** 2;
            }

            $divisor = sqrt($sumSquares);

            if ($divisor == 0.0) {
                throw new InvalidArgumentException(
                    "Pembagi normalisasi kriteria ID {$criteriaId} bernilai 0."
                );
            }

            $divisors[$criteriaId] = $divisor;
        }

        $normalized = [];

        foreach ($alternativeIds as $alternativeId) {
            foreach ($criteriaIds as $criteriaId) {
                $normalized[$alternativeId][$criteriaId] =
                    (float) $matrix[$alternativeId][$criteriaId]
                    / $divisors[$criteriaId];
            }
        }

        return [$normalized, $divisors];
    }

    public function weightedMatrix(
        array $normalized,
        array $weights,
        array $alternativeIds,
        array $criteriaIds
    ): array {
        $weighted = [];

        foreach ($alternativeIds as $alternativeId) {
            foreach ($criteriaIds as $criteriaId) {
                $weighted[$alternativeId][$criteriaId] =
                    $normalized[$alternativeId][$criteriaId]
                    * $weights[$criteriaId];
            }
        }

        return $weighted;
    }

    public function idealSolutions(
        array $weighted,
        array $attributes,
        array $alternativeIds,
        array $criteriaIds
    ): array {
        $idealPositive = [];
        $idealNegative = [];

        foreach ($criteriaIds as $criteriaId) {
            $values = [];

            foreach ($alternativeIds as $alternativeId) {
                $values[] =
                    $weighted[$alternativeId][$criteriaId];
            }

            $attribute =
                $attributes[$criteriaId] ?? null;

            if ($attribute === 'benefit') {
                $idealPositive[$criteriaId] = max($values);
                $idealNegative[$criteriaId] = min($values);
            } elseif ($attribute === 'cost') {
                $idealPositive[$criteriaId] = min($values);
                $idealNegative[$criteriaId] = max($values);
            } else {
                throw new InvalidArgumentException(
                    "Atribut kriteria ID {$criteriaId} tidak valid."
                );
            }
        }

        return [$idealPositive, $idealNegative];
    }

    public function distances(
        array $weighted,
        array $idealPositive,
        array $idealNegative,
        array $alternativeIds,
        array $criteriaIds
    ): array {
        $distancePositive = [];
        $distanceNegative = [];

        foreach ($alternativeIds as $alternativeId) {
            $sumPositive = 0.0;
            $sumNegative = 0.0;

            foreach ($criteriaIds as $criteriaId) {
                $value =
                    $weighted[$alternativeId][$criteriaId];

                $sumPositive +=
                    ($value - $idealPositive[$criteriaId]) ** 2;

                $sumNegative +=
                    ($value - $idealNegative[$criteriaId]) ** 2;
            }

            $distancePositive[$alternativeId] =
                sqrt($sumPositive);

            $distanceNegative[$alternativeId] =
                sqrt($sumNegative);
        }

        return [
            $distancePositive,
            $distanceNegative,
        ];
    }

    public function preferences(
        array $distancePositive,
        array $distanceNegative,
        array $alternativeIds
    ): array {
        $preferences = [];

        foreach ($alternativeIds as $alternativeId) {
            $dPlus =
                $distancePositive[$alternativeId];

            $dMinus =
                $distanceNegative[$alternativeId];

            $denominator = $dPlus + $dMinus;

            $preferences[$alternativeId] =
                $denominator > 0
                    ? $dMinus / $denominator
                    : 0.0;
        }

        return $preferences;
    }
}

Urutan Perhitungan

Service di atas otomatis menjalankan:

1. Normalisasi bobot
2. Normalisasi matriks
3. Normalisasi terbobot
4. Solusi ideal positif
5. Solusi ideal negatif
6. Jarak D+
7. Jarak D-
8. Nilai preferensi

Aturan penting:

benefit:
A+ = maksimum
A- = minimum

cost:
A+ = minimum
A- = maksimum

Nilai preferensi terbesar mendapat ranking tertinggi.

Data Detail yang Dihasilkan Service

TopsisService mengembalikan data berikut:

weights
divisors
normalized
weighted
idealPositive
idealNegative
distancePositive
distanceNegative
preferences

Data tersebut dipakai pada halaman hasil untuk menampilkan proses TOPSIS secara lengkap.

Menyiapkan Matriks dari Database

Controller hasil nantinya mengambil data:

$kriteria = Kriteria::orderBy('kode')->get();
$alternatif = Alternatif::orderBy('kode')->get();
$penilaian = Penilaian::all();

Kemudian membentuk:

$matrix = [];
$weights = [];
$attributes = [];

foreach ($kriteria as $itemKriteria) {
    $weights[$itemKriteria->id] =
        (float) $itemKriteria->bobot;

    $attributes[$itemKriteria->id] =
        $itemKriteria->atribut;
}

$nilaiMap = $penilaian->keyBy(function ($item) {
    return $item->alternatif_id
        . '-'
        . $item->kriteria_id;
});

foreach ($alternatif as $itemAlternatif) {
    foreach ($kriteria as $itemKriteria) {
        $key =
            $itemAlternatif->id
            . '-'
            . $itemKriteria->id;

        $matrix[$itemAlternatif->id][$itemKriteria->id] =
            (float) $nilaiMap[$key]->nilai;
    }
}

Format akhirnya:

$matrix = [
    1 => [
        1 => 80,
        2 => 8,
        3 => 9,
    ],
    2 => [
        1 => 70,
        2 => 9,
        3 => 8,
    ],
    3 => [
        1 => 75,
        2 => 7,
        3 => 9,
    ],
];

Memanggil Service

$hasil = $topsis->calculate(
    $matrix,
    $weights,
    $attributes
);

Hasil:

[
    'weights' => ...,
    'divisors' => ...,
    'normalized' => ...,
    'weighted' => ...,
    'idealPositive' => ...,
    'idealNegative' => ...,
    'distancePositive' => ...,
    'distanceNegative' => ...,
    'preferences' => ...,
]

Jika Muncul Error Pembagi 0

Periksa nilai pada menu Penilaian. Satu kolom kriteria tidak boleh seluruhnya 0.

Checkpoint

Pastikan:

  • bobot dinormalisasi menjadi total mendekati 1;
  • pembagi normalisasi dihitung per kriteria;
  • matriks normalisasi terbentuk;
  • matriks terbobot terbentuk;
  • benefit menggunakan maksimum untuk A+;
  • cost menggunakan minimum untuk A+;
  • D+ dan D- dihitung untuk setiap alternatif;
  • nilai preferensi berada pada rentang 0 sampai 1;
  • pembagi nol ditangani;
  • kode perhitungan berada di service.

Lanjut: Hasil Nilai Preferensi dan Ranking TOPSIS