update chicken weight to have chicken_area data as well
This commit is contained in:
1 parent
a6d4ddac65
commit
6bad900cf0
13 files changed
+132
-21
No files matched your search
@@ -283,6 +283,7 @@ class Command(BaseCommand):
|
|||||||
weight = AVG_WEIGHT[i]
|
weight = AVG_WEIGHT[i]
|
||||||
prev_weight = AVG_WEIGHT[i - 1] if i > 0 else DOC_IN_WEIGHT
|
prev_weight = AVG_WEIGHT[i - 1] if i > 0 else DOC_IN_WEIGHT
|
||||||
adg = _round(weight - prev_weight, 2) if weight else 0.0
|
adg = _round(weight - prev_weight, 2) if weight else 0.0
|
||||||
|
area_cm2 = _round(4.8 * (weight ** 0.66), 1) if weight else 0.0
|
||||||
cw, _ = ChickenWeight.objects.update_or_create(
|
cw, _ = ChickenWeight.objects.update_or_create(
|
||||||
cycle=cycle,
|
cycle=cycle,
|
||||||
date=day,
|
date=day,
|
||||||
@@ -293,6 +294,7 @@ class Command(BaseCommand):
|
|||||||
"chicken_count": SAMPLE_COUNT[i],
|
"chicken_count": SAMPLE_COUNT[i],
|
||||||
"uniformity": UNIFORMITY[i],
|
"uniformity": UNIFORMITY[i],
|
||||||
"average_daily_gain": adg,
|
"average_daily_gain": adg,
|
||||||
|
"chicken_area": area_cm2,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
cw_by_date[day] = cw
|
cw_by_date[day] = cw
|
||||||
|
|||||||
@@ -147,16 +147,18 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
cw_by_date = {}
|
cw_by_date = {}
|
||||||
for row in sheets["chicken_weight"]:
|
for row in sheets["chicken_weight"]:
|
||||||
|
avg_wt = float(as_number(row.get("average_weight")))
|
||||||
obj, _ = ChickenWeight.objects.update_or_create(
|
obj, _ = ChickenWeight.objects.update_or_create(
|
||||||
cycle=cycle,
|
cycle=cycle,
|
||||||
date=row["date"],
|
date=row["date"],
|
||||||
defaults={
|
defaults={
|
||||||
"age": int(as_number(row.get("age"))),
|
"age": int(as_number(row.get("age"))),
|
||||||
"doc_weight": int(as_number(row.get("doc_weight"))),
|
"doc_weight": int(as_number(row.get("doc_weight"))),
|
||||||
"average_weight": float(as_number(row.get("average_weight"))),
|
"average_weight": avg_wt,
|
||||||
"chicken_count": int(as_number(row.get("chicken_count"))),
|
"chicken_count": int(as_number(row.get("chicken_count"))),
|
||||||
"uniformity": float(as_number(row.get("uniformity"))),
|
"uniformity": float(as_number(row.get("uniformity"))),
|
||||||
"average_daily_gain": float(as_number(row.get("average_daily_gain"))),
|
"average_daily_gain": float(as_number(row.get("average_daily_gain"))),
|
||||||
|
"chicken_area": round(4.8 * (avg_wt ** 0.66), 1) if avg_wt > 0 else 0.0,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
cw_by_date[row["date"]] = obj
|
cw_by_date[row["date"]] = obj
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 5.2.17 on 2026-10-07 08:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('operations', '0020_drop_orphan_structured_data'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='chickenweight',
|
||||||
|
name='chicken_area',
|
||||||
|
field=models.FloatField(default=0.0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='chickencounting',
|
||||||
|
name='cc_id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='chickenweight',
|
||||||
|
name='cw_id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='feedsacks',
|
||||||
|
name='fs_id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='iotpanel',
|
||||||
|
name='panel_id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='kpi',
|
||||||
|
name='kpi_id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='manualinput',
|
||||||
|
name='manual_id',
|
||||||
|
field=models.BigAutoField(primary_key=True, serialize=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -105,6 +105,7 @@ class ChickenWeight(models.Model):
|
|||||||
chicken_count = models.IntegerField()
|
chicken_count = models.IntegerField()
|
||||||
uniformity = models.FloatField()
|
uniformity = models.FloatField()
|
||||||
average_daily_gain = models.FloatField()
|
average_daily_gain = models.FloatField()
|
||||||
|
chicken_area = models.FloatField(default=0.0)
|
||||||
cycle = models.ForeignKey(Cycle, on_delete=models.CASCADE, related_name="chicken_weights")
|
cycle = models.ForeignKey(Cycle, on_delete=models.CASCADE, related_name="chicken_weights")
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|||||||
@@ -116,10 +116,24 @@ def aggregate_weight_by_date(
|
|||||||
weights = [float(f.get("predicted_weight_g") or 0) for f in floors]
|
weights = [float(f.get("predicted_weight_g") or 0) for f in floors]
|
||||||
unifs = [float(f.get("uniformity") or 0) for f in floors]
|
unifs = [float(f.get("uniformity") or 0) for f in floors]
|
||||||
samples = [int(f.get("n_valid_samples") or 0) for f in floors]
|
samples = [int(f.get("n_valid_samples") or 0) for f in floors]
|
||||||
|
areas = [
|
||||||
|
float(
|
||||||
|
f.get("area_cm2_median")
|
||||||
|
or f.get("area_cm2")
|
||||||
|
or f.get("predicted_area_cm2")
|
||||||
|
or f.get("bbox_area_cm2")
|
||||||
|
or f.get("area")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
for f in floors
|
||||||
|
]
|
||||||
|
valid_areas = [a for a in areas if a > 0]
|
||||||
|
avg_area = sum(valid_areas) / len(valid_areas) if valid_areas else 0.0
|
||||||
by_date[day] = {
|
by_date[day] = {
|
||||||
"average_weight": sum(weights) / len(weights),
|
"average_weight": sum(weights) / len(weights),
|
||||||
"uniformity": sum(unifs) / len(unifs),
|
"uniformity": sum(unifs) / len(unifs),
|
||||||
"chicken_count": max(samples) if samples else 0,
|
"chicken_count": max(samples) if samples else 0,
|
||||||
|
"chicken_area": round(avg_area, 2),
|
||||||
}
|
}
|
||||||
return by_date
|
return by_date
|
||||||
|
|
||||||
@@ -309,6 +323,7 @@ def sync_chicken_weight_for_cycle(
|
|||||||
uniformity = 0.0
|
uniformity = 0.0
|
||||||
chicken_count = int(cycle.doc_in_count)
|
chicken_count = int(cycle.doc_in_count)
|
||||||
adg = 0.0
|
adg = 0.0
|
||||||
|
chicken_area = 0.0
|
||||||
else:
|
else:
|
||||||
metrics = weight_by_date.get(day)
|
metrics = weight_by_date.get(day)
|
||||||
if metrics is None:
|
if metrics is None:
|
||||||
@@ -317,6 +332,7 @@ def sync_chicken_weight_for_cycle(
|
|||||||
uniformity = float(metrics["uniformity"])
|
uniformity = float(metrics["uniformity"])
|
||||||
chicken_count = int(metrics["chicken_count"])
|
chicken_count = int(metrics["chicken_count"])
|
||||||
adg = round(average_weight - prev_weight, 2)
|
adg = round(average_weight - prev_weight, 2)
|
||||||
|
chicken_area = float(metrics.get("chicken_area", 0.0))
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"age": age,
|
"age": age,
|
||||||
@@ -325,6 +341,7 @@ def sync_chicken_weight_for_cycle(
|
|||||||
"chicken_count": chicken_count,
|
"chicken_count": chicken_count,
|
||||||
"uniformity": uniformity,
|
"uniformity": uniformity,
|
||||||
"average_daily_gain": adg,
|
"average_daily_gain": adg,
|
||||||
|
"chicken_area": chicken_area,
|
||||||
}
|
}
|
||||||
ChickenWeight.objects.update_or_create(
|
ChickenWeight.objects.update_or_create(
|
||||||
cycle=cycle,
|
cycle=cycle,
|
||||||
|
|||||||
@@ -175,12 +175,7 @@ erDiagram
|
|||||||
int chicken_count "NOT NULL (n_weight_samples)"
|
int chicken_count "NOT NULL (n_weight_samples)"
|
||||||
float uniformity "NOT NULL (Flock weight uniformity %)"
|
float uniformity "NOT NULL (Flock weight uniformity %)"
|
||||||
float average_daily_gain "NOT NULL (ADG in grams/day)"
|
float average_daily_gain "NOT NULL (ADG in grams/day)"
|
||||||
float area_cm2_median "Median body area in cm^2"
|
float chicken_area "NOT NULL DEFAULT 0.0 (Median chicken area in cm^2)"
|
||||||
float minor_axis_cm_median "Median minor axis / width in cm"
|
|
||||||
float major_axis_cm_median "Median major axis / length in cm"
|
|
||||||
float gompertz_baseline_g "Theoretical Gompertz curve baseline"
|
|
||||||
float vision_gain_g "Allometric vision gain residual"
|
|
||||||
float rejection_rate "MAD outlier filter rejection percentage"
|
|
||||||
timestamp created_at "NOT NULL"
|
timestamp created_at "NOT NULL"
|
||||||
timestamp updated_at "NOT NULL"
|
timestamp updated_at "NOT NULL"
|
||||||
}
|
}
|
||||||
@@ -308,8 +303,7 @@ Our edge pipeline aggregates floor camera runs and pushes clean daily rollups di
|
|||||||
- `chicken_count` $\leftarrow \sum \text{n\_weight\_samples}$ (valid non-outlier tracks).
|
- `chicken_count` $\leftarrow \sum \text{n\_weight\_samples}$ (valid non-outlier tracks).
|
||||||
- `uniformity` $\leftarrow \text{flock size uniformity percentage}$.
|
- `uniformity` $\leftarrow \text{flock size uniformity percentage}$.
|
||||||
- `average_daily_gain` $\leftarrow \text{daily weight gain in grams/day}$.
|
- `average_daily_gain` $\leftarrow \text{daily weight gain in grams/day}$.
|
||||||
- `area_cm2_median`, `minor_axis_cm_median`, `major_axis_cm_median` $\leftarrow \text{calibrated physical dimensions}$.
|
- `chicken_area` $\leftarrow \text{calibrated median chicken body area in cm²}$.
|
||||||
- `gompertz_baseline_g`, `vision_gain_g` $\leftarrow \text{audited growth model residual breakdown}$.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1290,6 +1290,22 @@
|
|||||||
"parent": "PF_pHT-a65sNfpEKlwq7-159",
|
"parent": "PF_pHT-a65sNfpEKlwq7-159",
|
||||||
"label": "average_daily_gain float NOT NULL"
|
"label": "average_daily_gain float NOT NULL"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "PF_pHT-a65sNfpEKlwq7-cw-area",
|
||||||
|
"type": "node",
|
||||||
|
"parent": "PF_pHT-a65sNfpEKlwq7-66"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "PF_pHT-a65sNfpEKlwq7-cw-area-col1",
|
||||||
|
"type": "node",
|
||||||
|
"parent": "PF_pHT-a65sNfpEKlwq7-cw-area"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "PF_pHT-a65sNfpEKlwq7-cw-area-col2",
|
||||||
|
"type": "node",
|
||||||
|
"parent": "PF_pHT-a65sNfpEKlwq7-cw-area",
|
||||||
|
"label": "chicken_area float NOT NULL"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "PF_pHT-a65sNfpEKlwq7-321",
|
"id": "PF_pHT-a65sNfpEKlwq7-321",
|
||||||
"type": "node",
|
"type": "node",
|
||||||
@@ -2537,4 +2553,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 337 KiB |
@@ -147,8 +147,9 @@ CREATE INDEX IF NOT EXISTS idx_cycles_close_requested_by_id ON cycles (close_req
|
|||||||
-- -----------------------------------------------------------------------------
|
-- -----------------------------------------------------------------------------
|
||||||
CREATE TABLE IF NOT EXISTS flock (
|
CREATE TABLE IF NOT EXISTS flock (
|
||||||
flock_id SERIAL PRIMARY KEY,
|
flock_id SERIAL PRIMARY KEY,
|
||||||
flock_name VARCHAR(30) NOT NULL,
|
flock_name VARCHAR(30) NOT NULL,
|
||||||
kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE,
|
iot_api_url VARCHAR(500) NOT NULL DEFAULT '',
|
||||||
|
kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -259,8 +260,9 @@ CREATE TABLE IF NOT EXISTS chicken_weight (
|
|||||||
doc_weight INTEGER NOT NULL,
|
doc_weight INTEGER NOT NULL,
|
||||||
average_weight FLOAT NOT NULL,
|
average_weight FLOAT NOT NULL,
|
||||||
chicken_count INTEGER NOT NULL,
|
chicken_count INTEGER NOT NULL,
|
||||||
uniformity FLOAT NOT NULL,
|
uniformity FLOAT NOT NULL,
|
||||||
average_daily_gain FLOAT NOT NULL,
|
average_daily_gain FLOAT NOT NULL,
|
||||||
|
chicken_area FLOAT NOT NULL DEFAULT 0.0,
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
cycle_id INTEGER NOT NULL REFERENCES cycles (cycle_id) ON DELETE CASCADE
|
cycle_id INTEGER NOT NULL REFERENCES cycles (cycle_id) ON DELETE CASCADE
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
|||||||
age: fallback.age,
|
age: fallback.age,
|
||||||
uniformity: fallback.uniformity,
|
uniformity: fallback.uniformity,
|
||||||
average_daily_gain: fallback.average_daily_gain,
|
average_daily_gain: fallback.average_daily_gain,
|
||||||
|
chicken_area: fallback.chicken_area,
|
||||||
cycle: fallback.cycle,
|
cycle: fallback.cycle,
|
||||||
kandang: fallback.kandang,
|
kandang: fallback.kandang,
|
||||||
};
|
};
|
||||||
@@ -322,7 +323,15 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
|||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
<thead className="bg-gray-50 text-gray-600">
|
<thead className="bg-gray-50 text-gray-600">
|
||||||
<tr>
|
<tr>
|
||||||
{['Tanggal', 'Umur', 'DOC', 'AVG', 'Uniformity', 'ADG'].map((h) => (
|
{[
|
||||||
|
'Tanggal',
|
||||||
|
'Umur',
|
||||||
|
'DOC',
|
||||||
|
'AVG',
|
||||||
|
'Uniformity',
|
||||||
|
'ADG',
|
||||||
|
'Luas Ayam (IOT)',
|
||||||
|
].map((h) => (
|
||||||
<th key={h} className="px-4 py-3 text-left font-semibold">
|
<th key={h} className="px-4 py-3 text-left font-semibold">
|
||||||
{h}
|
{h}
|
||||||
</th>
|
</th>
|
||||||
@@ -332,7 +341,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
|||||||
<tbody>
|
<tbody>
|
||||||
{rows.length === 0 ? (
|
{rows.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="px-4 py-6 text-center text-gray-400">
|
<td colSpan={7} className="px-4 py-6 text-center text-gray-400">
|
||||||
Belum ada data berat untuk siklus ini.
|
Belum ada data berat untuk siklus ini.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -351,6 +360,11 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
|||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
{formatNumber(row.average_daily_gain, 2, 2, true)}
|
{formatNumber(row.average_daily_gain, 2, 2, true)}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-2">
|
||||||
|
{row.chicken_area && row.chicken_area > 0
|
||||||
|
? `${formatNumber(row.chicken_area, 2, 0, true)} cm²`
|
||||||
|
: '-'}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -86,6 +86,15 @@ const DailyDetailPanel: React.FC<DailyDetailPanelProps> = ({ row, docWeight }) =
|
|||||||
value={`${formatNumber(row.chicken_count, 0)} ekor`}
|
value={`${formatNumber(row.chicken_count, 0)} ekor`}
|
||||||
icon={<i className="fa-solid fa-users"></i>}
|
icon={<i className="fa-solid fa-users"></i>}
|
||||||
/>
|
/>
|
||||||
|
<DetailCard
|
||||||
|
label="Luas Ayam (IOT)"
|
||||||
|
value={
|
||||||
|
row.chicken_area && row.chicken_area > 0
|
||||||
|
? `${formatNumber(row.chicken_area, 2, 0, true)} cm²`
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
icon={<i className="fa-solid fa-vector-square"></i>}
|
||||||
|
/>
|
||||||
{deviation != null && (
|
{deviation != null && (
|
||||||
<DetailCard
|
<DetailCard
|
||||||
label="Deviasi vs Target"
|
label="Deviasi vs Target"
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export type ChickenWeight = {
|
|||||||
chicken_count: number;
|
chicken_count: number;
|
||||||
uniformity: number;
|
uniformity: number;
|
||||||
average_daily_gain: number;
|
average_daily_gain: number;
|
||||||
|
chicken_area?: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
@@ -171,6 +172,7 @@ export type LatestAvg = {
|
|||||||
age: number;
|
age: number;
|
||||||
uniformity: number;
|
uniformity: number;
|
||||||
average_daily_gain: number;
|
average_daily_gain: number;
|
||||||
|
chicken_area?: number;
|
||||||
cycle: number;
|
cycle: number;
|
||||||
kandang: number;
|
kandang: number;
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-5
@@ -1,17 +1,21 @@
|
|||||||
export function formatNumber(
|
export function formatNumber(
|
||||||
value: number,
|
value: number | string | null | undefined,
|
||||||
maxDecimals = 2,
|
maxDecimals = 2,
|
||||||
minDecimals = 0,
|
minDecimals = 0,
|
||||||
truncate = false
|
truncate = false
|
||||||
): string {
|
): string {
|
||||||
let display = value;
|
const num = typeof value === 'number' ? value : Number(value);
|
||||||
|
if (!Number.isFinite(num)) return '-';
|
||||||
|
const safeMax = Math.max(0, maxDecimals);
|
||||||
|
const safeMin = Math.min(Math.max(0, minDecimals), safeMax);
|
||||||
|
let display = num;
|
||||||
if (truncate) {
|
if (truncate) {
|
||||||
const factor = 10 ** maxDecimals;
|
const factor = 10 ** safeMax;
|
||||||
display = Math.trunc(display * factor) / factor;
|
display = Math.trunc(display * factor) / factor;
|
||||||
}
|
}
|
||||||
return display.toLocaleString('id-ID', {
|
return display.toLocaleString('id-ID', {
|
||||||
maximumFractionDigits: maxDecimals,
|
maximumFractionDigits: safeMax,
|
||||||
minimumFractionDigits: minDecimals,
|
minimumFractionDigits: safeMin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user