1  Formulasi Susu Ibu Hamil: Pendekatan Complete Diet Bertahap

Diadaptasi dari metode formulasi pakan (Chiba, Animal Nutrition Handbook, Section 18)

1.1 Tujuan dan catatan penting

Di materi kuliah, contoh Formulating a Complete Diet memakai kasus untuk pakan babi grower-finisher, dimana protein diseimbangkan terlebih dulu, lalu disusul oleh fosfor, kalsium, dan terakhir protein dikoreksi lagi. Pada perhitungan ini, logika yang sama dipakai untuk merancang susu bubuk ibu hamil.

Hal yang akan dipelajari:

  1. Menyusun spesifikasi (target zat gizi) dan daftar bahan.
  2. Menyeimbangkan zat gizi satu per satu dengan persamaan aljabar satu variabel.
  3. Memahami kenapa perlu koreksi ulang, dan bagaimana iterasi atau sistem persamaan linear menyelesaikannya sekaligus.
  4. Menghitung nilai gizi lengkap, basis kering, takaran saji, dan premix vitamin-mineral.

Penting: Semua angka komposisi bahan dan target di dokumen ini adalah angka hipotetis untuk latihan, bukan resep produk sungguhan. Formulasi nyata memerlukan data analisis laboratorium setiap lot bahan, uji stabilitas, uji sensori, dan kepatuhan terhadap regulasi BPOM.

Cara pakai: jalankan setiap chunk secara berurutan (tombol ▶ di RStudio), baca penjelasannya, lalu cocokkan hasilnya dengan hitungan manual di kertas. Dokumen ini hanya memakai library base R dan library knitr.

1.2 Langkah 0: Data bahan dan target

1.2.1 Komposisi bahan

Masukkan semua angka dalam gram per 100 g bahan, basis basah, kecuali dinyatakan lain. Dalam contoh kasus ini, susu bubuk ibu hamil yang akan dibuat menggunakan 7 buah bahan sesuai dengan data nama.

bahan <- data.frame(
  kode    = c("skim", "malto", "lemak", "caco3", "fos", "dha", "premix"),
  nama    = c("Susu skim bubuk", "Maltodekstrin", "Bubuk lemak nabati (70% lemak)",
              "Kalsium karbonat", "FOS-inulin", "Bubuk DHA (mikroenkapsulasi)",
              "Premix vitamin-mineral"),
  air     = c(4,    5,  2,    0,  5,  3, 0),
  protein = c(34,   0,  3,    0,  0, 10, 0),
  lemak   = c(1,    0,  70,   0,  0, 45, 0),
  karbo   = c(52,   95, 23,   0,  0, 40, 0),   # karbohidrat tercerna (laktosa, dll.)
  serat   = c(0,    0,  0,    0, 95,  0, 0),   # serat pangan (FOS-inulin)
  Ca      = c(1.25, 0,  0.02, 40, 0,  0, 0)    # g Ca per 100 g bahan
)
rownames(bahan) <- bahan$kode
bahan

Penjelasan singkat tiap bahan:

Bahan susu ibu hamil Peran
Susu skim bubuk Sumber protein utama (dan kalsium)
Maltodekstrin Sumber energi dan sebagai bahan “pengisi/penyesuai”
Bubuk lemak nabati Sumber lemak
Kalsium karbonat Sumber kalsium murni
FOS-inulin, bubuk DHA, premix Bahan tetap dengan jumlah yang sudah diputuskan

Maltodekstrin berperan seperti jagung pada pakan babi: setiap kali bahan lain ditambah, maltodekstrin yang dikurangi supaya total tetap 100 g.

Asumsi yang sama seperti di buku: premix dianggap tidak menyumbang protein, lemak, maupun kalsium (kontribusinya terlalu kecil). Isi premix dirancang terpisah di bagian terakhir.

1.2.2 Target spesifikasi

Target per 100 g bubuk (basis basah):

target <- c(protein = 22,   # g/100 g
            lemak   = 8,    # g/100 g
            Ca      = 1.1)  # g/100 g  (= 1.100 mg/100 g)
target
#> protein   lemak      Ca 
#>    22.0     8.0     1.1

1.2.3 Bahan tetap (fixed ingredients)

tetap <- c(fos = 3, dha = 1, premix = 1)   # g per 100 g formula
sisa  <- 100 - sum(tetap)
sisa
#> [1] 95

Maksudnya: jumlah FOS-inulin, bubuk DHA, dan premix sudah diputuskan di awal (misalnya berdasarkan dosis prebiotik dan target DHA). Jadi hanya 95 g yang tersisa untuk dibagi ke susu skim, maltodekstrin, bubuk lemak, dan kalsium karbonat.

Fungsi untuk menghitung isi gizi sebuah resep

# resep: vektor bernama, satuan g bahan per 100 g formula
hitung_gizi <- function(resep, kolom = c("air", "protein", "lemak", "karbo", "serat", "Ca")) {
  komposisi <- as.matrix(bahan[names(resep), kolom])
  colSums(komposisi * resep / 100)   # gram zat gizi per 100 g formula
}

Maksudnya: untuk setiap zat gizi, kontribusi sebuah bahan = jumlah bahan × kadar zat gizi bahan ÷ 100. Semua kontribusi lalu dijumlahkan.

1.3 Langkah 1: Seimbangkan protein

Pertama-tama, hitung kebutuhan susu skim (x) dan maltodekstrin yang akan dipakai untuk susu ibu hamil. Jika 22 gram protein yang dikehendaki, dari DHA dengan kandungan 10% protein dan skim dengan kandungan 34%, maka sisa malto yang dibutuhkan dapat dihitung dengan persamaan ini.

\[\underbrace{0{,}10(1)}_{\text{DHA}} + \underbrace{0{,}34\,x}_{\text{skim}} + \underbrace{0\,(95- x)}_{\text{malto}} = 22\]

protein_tetap <- 0.10 * tetap["dha"]              # protein dari bahan tetap
skim  <- (target["protein"] - protein_tetap) / 0.34
malto <- sisa - skim

resep1 <- c(skim = unname(skim), malto = unname(malto), tetap)
round(resep1, 3)
#>   skim  malto    fos    dha premix 
#> 64.412 30.588  3.000  1.000  1.000
round(hitung_gizi(resep1), 3)
#>     air protein   lemak   karbo   serat      Ca 
#>   4.286  22.000   1.094  62.953   2.850   0.805

Maksudnya: protein diseimbangkan pertama karena susu skim adalah bahan dengan porsi terbesar dan protein adalah zat gizi utama. Maltodekstrin tidak mengandung protein, sehingga persamaannya jadi sangat sederhana.

Hasil check: skim yang dibutuhkan adalah 64.412 g dengan maltodektrin sebanyak 30.588 g untuk mencapai protein 22 g. Meskipun demikian, lemak dan kalsium masih belum mencapai target.

1.3.0.1 Sampingan: cek dengan Pearson Square

kadar_perlu <- (target["protein"] - protein_tetap) / sisa * 100   # % protein campuran
bagian_skim  <- abs(kadar_perlu - 0)    # selisih dengan maltodekstrin (0%)
bagian_malto <- abs(34 - kadar_perlu)   # selisih dengan susu skim (34%)
c(kadar_perlu = unname(kadar_perlu),
  skim_g  = unname(bagian_skim  / (bagian_skim + bagian_malto) * sisa),
  malto_g = unname(bagian_malto / (bagian_skim + bagian_malto) * sisa))
#> kadar_perlu      skim_g     malto_g 
#>     23.0526     64.4118     30.5882

Hasilnya sama dengan langkah 1. Artinya, aljabar satu variabel dan Pearson Square hanyalah dua cara menulis perhitungan yang sama.

1.4 Langkah 2: Seimbangkan lemak

Bubuk lemak (y) ditambahkan dan maltodekstrin dikurangi sebanyak y.

\[0{,}45(1) + 0{,}01(\text{skim}) + 0{,}70\,y = 8\]

lemak_lain <- 0.45 * tetap["dha"] + 0.01 * resep1["skim"]
lemak_bubuk <- (target["lemak"] - lemak_lain) / 0.70
malto <- resep1["malto"] - lemak_bubuk

resep2 <- c(skim = unname(resep1["skim"]), malto = unname(malto),
            lemak = unname(lemak_bubuk), tetap)
round(resep2, 3)
#>   skim  malto  lemak    fos    dha premix 
#> 64.412 20.723  9.866  3.000  1.000  1.000
round(hitung_gizi(resep2), 3)
#>     air protein   lemak   karbo   serat      Ca 
#>   3.990  22.296   8.000  55.850   2.850   0.807

Maksudnya: susu skim dan bubuk DHA sudah menyumbang sedikit lemak, jadi bubuk lemak hanya perlu menutup kekurangannya. Perhatikan hasil check: lemak sudah tepat 8, tetapi protein naik sedikit di atas 22, karena bubuk lemak juga mengandung 3% protein (dari kaseinat pembawanya). Ini akan dikoreksi di langkah 4.

1.5 Langkah 3: Seimbangkan kalsium

Kalsium karbonat (z) ditambahkan dan maltodekstrin dikurangi lagi.

\[0{,}0125(\text{skim}) + 0{,}0002(\text{bubuk lemak}) + 0{,}40\,z = 1{,}1\]

Ca_lain <- 0.0125 * resep2["skim"] + 0.0002 * resep2["lemak"]
caco3 <- (target["Ca"] - Ca_lain) / 0.40
malto <- resep2["malto"] - caco3

resep3 <- c(skim = unname(resep2["skim"]), malto = unname(malto),
            lemak = unname(resep2["lemak"]), caco3 = unname(caco3), tetap)
round(resep3, 3)
#>   skim  malto  lemak  caco3    fos    dha premix 
#> 64.412 19.990  9.866  0.732  3.000  1.000  1.000
round(hitung_gizi(resep3), 4)
#>     air protein   lemak   karbo   serat      Ca 
#>  3.9533 22.2960  8.0000 55.1542  2.8500  1.1000

Maksudnya: kalsium diseimbangkan terakhir dengan bahan yang hanya mengandung kalsium. Dengan begitu, penambahan CaCO₃ tidak mengganggu protein dan lemak yang sudah pas. Di contoh babi, urutannya fosfor dulu (Dical) baru kalsium (Lime), karena Dical mengandung P dan Ca sehingga ikut menyumbang Ca. Di contoh ini tidak ada bahan yang menyumbang keduanya, jadi cukup satu langkah untuk mineral.

1.6 Langkah 4: Seimbangkan ulang protein

Sekarang jumlah bubuk lemak dan CaCO₃ sudah “dikunci”. Protein dihitung ulang dengan memperhitungkan protein dari bubuk lemak. Ruang untuk susu skim + maltodekstrin menjadi lebih kecil:

ruang <- sisa - resep3["lemak"] - resep3["caco3"]
protein_lain <- 0.10 * tetap["dha"] + 0.03 * resep3["lemak"]
skim  <- (target["protein"] - protein_lain) / 0.34
malto <- ruang - skim

resep4 <- c(skim = unname(skim), malto = unname(malto),
            lemak = unname(resep3["lemak"]), caco3 = unname(resep3["caco3"]), tetap)
round(resep4, 3)
#>   skim  malto  lemak  caco3    fos    dha premix 
#> 63.541 20.861  9.866  0.732  3.000  1.000  1.000

1.7 Cek akhir: kenapa lemak dan kalsium sedikit meleset?

cek <- function(resep) {
  hasil <- hitung_gizi(resep)[names(target)]
  data.frame(zat_gizi = names(target), target = unname(target),
             hasil = round(unname(hasil), 4),
             selisih = round(unname(hasil - target), 4))
}
cek(resep4)

Di langkah 4, susu skim berkurang sedikit. Susu skim juga membawa lemak dan kalsium, jadi lemak dan kalsium ikut turun sedikit di bawah target.

Ini keterbatasan metode bertahap: setiap koreksi mengganggu zat gizi lain yang sudah pas. Di buku Chiba, selisih kecil seperti ini biasanya dibiarkan karena masih dalam batas toleransi. Ada dua cara untuk mendapatkan jawaban yang tepat: mengulang langkah 1–4 sampai stabil (iterasi), atau menyelesaikan semuanya sekaligus sebagai sistem persamaan.

1.7.1 Cara A: iterasi (ulangi langkah sampai stabil)

iterasi <- function(maks = 50, toleransi = 1e-8) {
  lemak_b <- 0; caco3_b <- 0
  for (i in 1:maks) {
    # Langkah protein
    skim <- (target["protein"] - 0.10 * tetap["dha"] - 0.03 * lemak_b) / 0.34
    # Langkah lemak
    lemak_baru <- (target["lemak"] - 0.45 * tetap["dha"] - 0.01 * skim) / 0.70
    # Langkah kalsium
    caco3_baru <- (target["Ca"] - 0.0125 * skim - 0.0002 * lemak_baru) / 0.40
    berubah <- abs(lemak_baru - lemak_b) + abs(caco3_baru - caco3_b)
    lemak_b <- lemak_baru; caco3_b <- caco3_baru
    if (berubah < toleransi) break
  }
  malto <- sisa - skim - lemak_b - caco3_b
  list(putaran = i,
       resep = c(skim = unname(skim), malto = unname(malto),
                 lemak = unname(lemak_b), caco3 = unname(caco3_b), tetap))
}
hasil_iterasi <- iterasi()
hasil_iterasi$putaran
#> [1] 5
round(hasil_iterasi$resep, 4)
#>    skim   malto   lemak   caco3     fos     dha  premix 
#> 63.5402 20.8224  9.8780  0.7594  3.0000  1.0000  1.0000
cek(hasil_iterasi$resep)

Maksudnya: langkah protein → lemak → kalsium diulang terus. Karena pengaruh silangnya kecil, hasilnya cepat stabil dalam beberapa putaran saja, dan semua target tercapai tepat.

1.7.2 Cara B: selesaikan sekaligus sebagai sistem persamaan linear

Ada 4 bahan yang belum diketahui (skim, malto, lemak, CaCO₃) dan 4 syarat:

  1. Total keempatnya = 95 g
  2. Protein total = 22 g
  3. Lemak total = 8 g
  4. Kalsium total = 1,1 g

Empat persamaan dengan empat variabel bisa diselesaikan langsung dengan aljabar matriks (A · x = b):

var_bebas <- c("skim", "malto", "lemak", "caco3")
zat <- c("protein", "lemak", "Ca")

A <- rbind(total = rep(1, length(var_bebas)),
           t(as.matrix(bahan[var_bebas, zat])) / 100)
colnames(A) <- var_bebas

kontribusi_tetap <- hitung_gizi(tetap, kolom = zat)
b <- c(total = sisa, target[zat] - kontribusi_tetap)

A
#>           skim malto lemak caco3
#> total   1.0000     1 1e+00   1.0
#> protein 0.3400     0 3e-02   0.0
#> lemak   0.0100     0 7e-01   0.0
#> Ca      0.0125     0 2e-04   0.4
b
#>   total protein   lemak      Ca 
#>   95.00   21.90    7.55    1.10
solusi <- solve(A, b)
resep_final <- c(solusi, tetap)
round(resep_final, 4)
#>    skim   malto   lemak   caco3     fos     dha  premix 
#> 63.5402 20.8224  9.8780  0.7594  3.0000  1.0000  1.0000
cek(resep_final)

Maksudnya: baris pertama matriks A adalah syarat total berat (koefisien 1 untuk setiap bahan). Baris berikutnya adalah kadar protein, lemak, dan Ca setiap bahan (dalam desimal). Vektor b adalah target dikurangi sumbangan bahan tetap. Hasilnya sama dengan cara iterasi. Metode bertahap di slide sebenarnya adalah cara manual untuk menyelesaikan sistem persamaan ini. Software formulasi (seperti Excel Solver) dapat mengerjakan hal yang sama, dan biasanya ditambah tujuan untuk meminimalkan harga (linear programming). Lakukan dengan Excel Solver pada file formulasi_susu_ibu_hamil.xlsx dan bandingkan hasilnya.

1.8 Resep akhir dan nilai gizi lengkap

resep_tabel <- data.frame(
  bahan = bahan[names(resep_final), "nama"],
  g_per_100g = round(unname(resep_final), 2)
)
resep_tabel
sum(resep_final)   # harus 100
#> [1] 100
gizi <- hitung_gizi(resep_final)
energi <- 4 * gizi["protein"] + 9 * gizi["lemak"] + 4 * gizi["karbo"] + 2 * gizi["serat"]

data.frame(
  komponen = c("Air (g)", "Protein (g)", "Lemak (g)", "Karbohidrat (g)",
               "Serat FOS-inulin (g)", "Kalsium (mg)", "Energi (kkal)"),
  per_100g = round(c(gizi[c("air", "protein", "lemak", "karbo", "serat")],
                     gizi["Ca"] * 1000, energi), 1)
)

Maksudnya: energi dihitung dengan faktor Atwater (protein dan karbohidrat 4 kkal/g, lemak 9 kkal/g). FOS-inulin dihitung 2 kkal/g karena hanya sebagian energinya diperoleh lewat fermentasi di usus besar.

1.8.1 Konversi ke basis kering

BK <- 100 - gizi["air"]                  # % bahan kering formula
gizi_bk <- gizi[c("protein", "lemak", "Ca")] / (BK / 100)
c(bahan_kering_persen = unname(BK))
#> bahan_kering_persen 
#>             96.0397
round(rbind(basis_basah = gizi[c("protein", "lemak", "Ca")],
            basis_kering = gizi_bk), 3)
#>              protein lemak    Ca
#> basis_basah   22.000  8.00 1.100
#> basis_kering  22.907  8.33 1.145

Maksudnya: susu bubuk sangat kering (bahan kering ±96%), jadi basis basah dan basis kering hanya sedikit berbeda. Inilah alasan asumsi 90% bahan kering dari formulasi pakan tidak cocok untuk susu bubuk: kadar air sebaiknya diukur langsung. Sesuai rule of thumb, dari basis basah ke basis kering kadar zat gizi naik.

1.9 Takaran saji

Misalkan satu takaran saji adalah 45 g bubuk (dilarutkan dalam ±200 mL air), diminum 2 kali sehari.

saji_g <- 45
per_saji <- c(gizi[c("protein", "lemak", "karbo")] * saji_g / 100,
              Ca_mg = unname(gizi["Ca"] * 1000 * saji_g / 100),
              energi_kkal = unname(energi * saji_g / 100))
round(per_saji, 1)
#>     protein       lemak       karbo       Ca_mg energi_kkal 
#>         9.9         3.6        25.0       495.0       174.5
round(per_saji * 2, 1)   # per hari (2 saji)
#>     protein       lemak       karbo       Ca_mg energi_kkal 
#>        19.8         7.2        49.9       990.0       348.9

Maksudnya: angka ini dibandingkan dengan Angka Kecukupan Gizi (AKG) ibu hamil untuk melihat berapa persen kebutuhan yang dipenuhi formula. Gunakan tabel AKG terbaru (Permenkes No. 28 Tahun 2019) sesuai trimester. Formula ibu hamil dirancang sebagai pelengkap, bukan untuk memenuhi 100% kebutuhan.

1.10 Merancang premix vitamin-mineral

Premix sudah “dipesan” 1 g per 100 g formula. Sekarang isinya dirancang, mengikuti bagian Formulating a TM or Vit premix di buku Chiba: tentukan kebutuhan, perhitungkan kemurnian sumber, lalu sisanya diisi bahan pembawa (carrier).

Target per 100 g formula (hipotetis):

premix_g <- tetap["premix"]   # 1 g premix per 100 g formula

premix <- data.frame(
  zat      = c("Asam folat", "Besi (Fe)", "Seng (Zn)", "Yodium (I)", "Vitamin D3"),
  target   = c(400, 12, 6, 90, 400),
  satuan   = c("mcg", "mg", "mg", "mcg", "IU"),
  sumber   = c("Asam folat", "Fero fumarat", "Seng sulfat monohidrat",
               "Kalium iodida", "Vitamin D3 bubuk 100.000 IU/g"),
  # kadar zat dalam bahan sumber (fraksi berat, atau IU per g untuk vitamin D)
  kadar    = c(1.00, 0.3287, 0.3643, 0.7645, 100000),
  overage  = c(0.20, 0, 0, 0, 0.20)   # kelebihan untuk mengimbangi kehilangan selama penyimpanan
)

# ubah target ke gram zat (vitamin D tetap dalam IU)
ke_gram <- ifelse(premix$satuan == "mcg", 1e-6, ifelse(premix$satuan == "mg", 1e-3, 1))
premix$target_x_overage <- premix$target * (1 + premix$overage)
premix$g_sumber <- premix$target_x_overage * ke_gram / premix$kadar

carrier <- premix_g - sum(premix$g_sumber)
premix$persen_dalam_premix <- premix$g_sumber / premix_g * 100

knitr::kable(premix[, c("zat", "target", "satuan", "sumber", "overage",
                        "g_sumber", "persen_dalam_premix")], digits = 4)
zat target satuan sumber overage g_sumber persen_dalam_premix
Asam folat 400 mcg Asam folat 0.2 0.0005 0.0480
Besi (Fe) 12 mg Fero fumarat 0.0 0.0365 3.6507
Seng (Zn) 6 mg Seng sulfat monohidrat 0.0 0.0165 1.6470
Yodium (I) 90 mcg Kalium iodida 0.0 0.0001 0.0118
Vitamin D3 400 IU Vitamin D3 bubuk 100.000 IU/g 0.2 0.0048 0.4800
c(carrier_maltodekstrin_g = unname(carrier),
  carrier_persen = unname(carrier / premix_g * 100))
#> carrier_maltodekstrin_g          carrier_persen 
#>                0.941625               94.162488

Maksudnya:

  • Kemurnian sumber. Kita tidak menambahkan “besi murni”, melainkan senyawa seperti fero fumarat, yang hanya mengandung ±32,9% Fe. Jadi bahan sumber yang dibutuhkan = kebutuhan zat ÷ kadar zat dalam sumber.
  • Overage. Vitamin seperti asam folat dan vitamin D bisa rusak selama pengolahan dan penyimpanan, sehingga industri biasanya menambahkan lebih (di sini dimisalkan 20%) agar kadar di akhir masa simpan tetap memenuhi label. Mineral stabil, jadi tidak perlu overage.
  • Carrier. Semua bahan aktif hanya berbobot kecil, jadi sisanya diisi maltodekstrin supaya premix bisa ditimbang dan dicampur merata. Ini sama dengan poin “use a carrier” di buku.
  • Vitamin A sengaja tidak dimasukkan dalam bentuk retinol, karena asupan retinol berlebih saat hamil berisiko teratogenik. Kalau ingin ditambahkan, lebih aman dalam bentuk beta-karoten.

1.11 Latihan untuk mahasiswa

  1. Ubah target protein menjadi 25 g/100 g. Jalankan ulang semua chunk. Bahan apa yang naik dan apa yang turun? Kenapa?
  2. Ganti susu skim dengan isolat protein kedelai (misalnya protein 88%, lemak 1%, Ca 0,2%). Apa yang terjadi pada kebutuhan CaCO₃? Jelaskan.
  3. Hitung ulang langkah 1 secara manual dengan Pearson Square, lalu bandingkan dengan hasil R.
  4. Tambahkan syarat baru: karbohidrat total tidak boleh lebih dari 55 g/100 g. Apakah masih bisa diselesaikan dengan 4 bahan yang ada? Bahan apa yang perlu ditambahkan?
  5. Tambahkan kolom harga per kg ke data bahan, lalu hitung biaya bahan per kg formula. Diskusikan: bagaimana jika ingin formula termurah yang tetap memenuhi semua target?
  6. Jalankan formula yang lebih kompleks dengan 13 bahan baku dengan 12 komponen yang diatur pada file formulasi_susu_ibu_hamil_kompleks.xlsx

1.12 Keterbatasan pendekatan ini

  • Pendekatan ini hanya menjamin angka gizi di atas kertas. Formula sungguhan juga harus mempertimbangkan kelarutan, rasa, aroma, stabilitas oksidatif (terutama DHA), dan interaksi antar-zat (misalnya kalsium menghambat penyerapan besi).
  • Komposisi bahan harus berasal dari analisis laboratorium tiap lot, bukan angka tabel.
  • Batas atas zat gizi (misalnya retinol dan yodium) sama pentingnya dengan batas bawah.
  • Produk akhir harus memenuhi regulasi BPOM untuk pangan olahan keperluan gizi khusus, termasuk batas cemaran dan persyaratan label.

1.13 Rujukan

Chiba, L. I. (2009). Animal Nutrition Handbook, Section 18: Diet Formulation & Feed Ingredients (hlm. 481–491). Auburn University.