Get reciprocal allocation working
This commit is contained in:
@@ -50,11 +50,13 @@ pub struct OverheadAllocationRule {
|
|||||||
to_department_type: DepartmentType,
|
to_department_type: DepartmentType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct TotalDepartmentCost {
|
pub struct TotalDepartmentCost {
|
||||||
department: String,
|
department: String,
|
||||||
value: f64,
|
value: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct AccountCost {
|
pub struct AccountCost {
|
||||||
account: String,
|
account: String,
|
||||||
summed_department_costs: Vec<TotalDepartmentCost>,
|
summed_department_costs: Vec<TotalDepartmentCost>,
|
||||||
@@ -77,25 +79,6 @@ impl ReciprocalAllocationSolver for DMatrix<f64> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rules_indexes(
|
|
||||||
allocations: &Vec<OverheadAllocationRule>,
|
|
||||||
department_type: DepartmentType,
|
|
||||||
) -> HashMap<String, usize> {
|
|
||||||
allocations
|
|
||||||
.iter()
|
|
||||||
.filter(|allocation| allocation.to_department_type == department_type)
|
|
||||||
.flat_map(|department| {
|
|
||||||
[
|
|
||||||
department.from_overhead_department.clone(),
|
|
||||||
department.to_department.clone(),
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.unique()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, department)| (department, index))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCentre, Output>(
|
pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCentre, Output>(
|
||||||
lines: csv::Reader<Lines>,
|
lines: csv::Reader<Lines>,
|
||||||
accounts: csv::Reader<Account>,
|
accounts: csv::Reader<Account>,
|
||||||
@@ -103,7 +86,9 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
|
|||||||
areas: csv::Reader<Area>,
|
areas: csv::Reader<Area>,
|
||||||
cost_centres: csv::Reader<CostCentre>,
|
cost_centres: csv::Reader<CostCentre>,
|
||||||
output: csv::Writer<Output>,
|
output: csv::Writer<Output>,
|
||||||
) where
|
use_numeric_accounts: bool,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
Lines: Read,
|
Lines: Read,
|
||||||
Account: Read,
|
Account: Read,
|
||||||
AllocationStatistic: Read,
|
AllocationStatistic: Read,
|
||||||
@@ -111,11 +96,28 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
|
|||||||
CostCentre: Read,
|
CostCentre: Read,
|
||||||
Output: std::io::Write,
|
Output: std::io::Write,
|
||||||
{
|
{
|
||||||
|
let mut accounts_reader = accounts;
|
||||||
|
let all_accounts_sorted: Result<Vec<CsvAccount>, csv::Error> =
|
||||||
|
accounts_reader.deserialize::<CsvAccount>().collect();
|
||||||
|
let mut accounts_sorted = all_accounts_sorted?;
|
||||||
|
// Sort the accounts, as allocation statistics use account ranges
|
||||||
|
if use_numeric_accounts {
|
||||||
|
accounts_sorted.sort_by(|a, b| {
|
||||||
|
a.code
|
||||||
|
.parse::<i32>()
|
||||||
|
.unwrap()
|
||||||
|
.cmp(&b.code.parse::<i32>().unwrap())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
accounts_sorted.sort_by(|a, b| a.code.cmp(&b.code))
|
||||||
|
};
|
||||||
|
|
||||||
// Build out the the list of allocation rules from areas/allocation statistics (similar to ppm building 'cost drivers')
|
// Build out the the list of allocation rules from areas/allocation statistics (similar to ppm building 'cost drivers')
|
||||||
|
|
||||||
// do reciprocal allocation (only for variable portion of accounts), for each account
|
// do reciprocal allocation (only for variable portion of accounts), for each account
|
||||||
|
|
||||||
// Copy across fixed stuff (if necessary, not sure it is)
|
// Copy across fixed stuff (if necessary, not sure it is)
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
|
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
|
||||||
@@ -126,10 +128,7 @@ fn reciprocal_allocation_impl(
|
|||||||
account_costs: Vec<AccountCost>,
|
account_costs: Vec<AccountCost>,
|
||||||
// TODO: Throw an appropriate error
|
// TODO: Throw an appropriate error
|
||||||
) -> anyhow::Result<Vec<AccountCost>> {
|
) -> anyhow::Result<Vec<AccountCost>> {
|
||||||
let overhead_department_mappings: HashMap<String, usize> =
|
let overhead_department_mappings = get_rules_indexes(&allocations, DepartmentType::Overhead);
|
||||||
get_rules_indexes(&allocations, DepartmentType::Overhead);
|
|
||||||
let operating_department_mappings: HashMap<String, usize> =
|
|
||||||
get_rules_indexes(&allocations, DepartmentType::Operating);
|
|
||||||
|
|
||||||
let mut slice_allocations =
|
let mut slice_allocations =
|
||||||
vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()];
|
vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()];
|
||||||
@@ -138,25 +137,23 @@ fn reciprocal_allocation_impl(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|allocation| allocation.to_department_type == DepartmentType::Overhead)
|
.filter(|allocation| allocation.to_department_type == DepartmentType::Overhead)
|
||||||
{
|
{
|
||||||
// TODO: Check if we need to flp this around
|
|
||||||
let from_index = overhead_department_mappings
|
let from_index = overhead_department_mappings
|
||||||
.get(&allocation.from_overhead_department)
|
.get(&allocation.from_overhead_department)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let to_index = operating_department_mappings
|
let to_index = overhead_department_mappings
|
||||||
.get(&allocation.to_department)
|
.get(&allocation.to_department)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let elem = &mut slice_allocations
|
// TODO: Check if dmatrix is row or column major order, would need to flip if column major
|
||||||
[(*from_index) + (overhead_department_mappings.len() * (*to_index))];
|
slice_allocations[from_index * overhead_department_mappings.len() + to_index] =
|
||||||
*elem = allocation.percent;
|
allocation.percent * -1.;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Also need ones along the diagonal, and negatives in some places...
|
let mut mat: DMatrix<f64> = DMatrix::from_vec(
|
||||||
|
|
||||||
let mat: DMatrix<f64> = DMatrix::from_row_slice(
|
|
||||||
overhead_department_mappings.len(),
|
overhead_department_mappings.len(),
|
||||||
overhead_department_mappings.len(),
|
overhead_department_mappings.len(),
|
||||||
&slice_allocations,
|
slice_allocations,
|
||||||
);
|
);
|
||||||
|
mat.fill_diagonal(1.);
|
||||||
|
|
||||||
if mat.determinant() == 0. {
|
if mat.determinant() == 0. {
|
||||||
let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001);
|
let pseudo_inverse = mat.svd(true, true).pseudo_inverse(0.000001);
|
||||||
@@ -176,31 +173,193 @@ fn reciprocal_allocation_impl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_rules_indexes(
|
||||||
|
allocations: &Vec<OverheadAllocationRule>,
|
||||||
|
department_type: DepartmentType,
|
||||||
|
) -> HashMap<String, usize> {
|
||||||
|
allocations
|
||||||
|
.iter()
|
||||||
|
.filter(|allocation| allocation.to_department_type == department_type)
|
||||||
|
.flat_map(|department| {
|
||||||
|
if department.to_department_type == DepartmentType::Operating {
|
||||||
|
vec![department.to_department.clone()]
|
||||||
|
} else {
|
||||||
|
vec![
|
||||||
|
department.from_overhead_department.clone(),
|
||||||
|
department.to_department.clone(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unique()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, department)| (department, index))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
|
fn do_solve_reciprocal<T: ReciprocalAllocationSolver>(
|
||||||
solver: T,
|
solver: T,
|
||||||
account_costs: Vec<AccountCost>,
|
account_costs: Vec<AccountCost>,
|
||||||
department_mappings: HashMap<String, usize>,
|
overhead_department_mappings: HashMap<String, usize>,
|
||||||
allocations: Vec<OverheadAllocationRule>,
|
allocations: Vec<OverheadAllocationRule>,
|
||||||
) -> anyhow::Result<Vec<AccountCost>> {
|
) -> anyhow::Result<Vec<AccountCost>> {
|
||||||
// TODO: Could batch the accounts, although probably won't see to big a speed increase, compiler should help us out
|
let operating_department_mappings = get_rules_indexes(&allocations, DepartmentType::Operating);
|
||||||
for total_costs in account_costs {
|
let mut operating_overhead_mappings =
|
||||||
let mut slice_costs = vec![0.; department_mappings.len()];
|
vec![0.; overhead_department_mappings.len() * operating_department_mappings.len()];
|
||||||
|
for rule in allocations {
|
||||||
for cost in total_costs.summed_department_costs {
|
if rule.to_department_type == DepartmentType::Operating {
|
||||||
let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()];
|
let from_index = *overhead_department_mappings
|
||||||
*elem = cost.value;
|
.get(&rule.from_overhead_department)
|
||||||
|
.unwrap();
|
||||||
|
let to_index = *operating_department_mappings
|
||||||
|
.get(&rule.to_department)
|
||||||
|
.unwrap();
|
||||||
|
operating_overhead_mappings
|
||||||
|
[from_index * overhead_department_mappings.len() + to_index] = rule.percent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let operating_overhead_mappings_mat: DMatrix<f64> = DMatrix::from_vec(
|
||||||
|
operating_department_mappings.len(),
|
||||||
|
overhead_department_mappings.len(),
|
||||||
|
operating_overhead_mappings,
|
||||||
|
);
|
||||||
|
let mut final_account_costs: Vec<AccountCost> = Vec::with_capacity(account_costs.len());
|
||||||
|
for total_costs in account_costs {
|
||||||
|
// TODO: There has to be a cleaner way to do this, perhaps by presorting things?
|
||||||
|
let mut overhead_slice_costs = vec![0.; overhead_department_mappings.len()];
|
||||||
|
for cost in total_costs.summed_department_costs.iter() {
|
||||||
|
if overhead_department_mappings.contains_key(&cost.department) {
|
||||||
|
overhead_slice_costs[*overhead_department_mappings.get(&cost.department).unwrap()] =
|
||||||
|
cost.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let overhead_costs_vec: DMatrix<f64> =
|
||||||
|
DMatrix::from_row_slice(overhead_department_mappings.len(), 1, &overhead_slice_costs);
|
||||||
|
let calculated_overheads = solver.solve(&overhead_costs_vec);
|
||||||
|
|
||||||
let costs_vec: DMatrix<f64> =
|
let mut operating_slice_costs = vec![0.; operating_department_mappings.len()];
|
||||||
DMatrix::from_row_slice(department_mappings.len(), 1, &slice_costs);
|
for cost in total_costs.summed_department_costs {
|
||||||
|
if operating_department_mappings.contains_key(&cost.department) {
|
||||||
|
let elem = &mut operating_slice_costs
|
||||||
|
[*operating_department_mappings.get(&cost.department).unwrap()];
|
||||||
|
*elem = cost.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let operating_costs_vec: DMatrix<f64> = DMatrix::from_row_slice(
|
||||||
|
operating_department_mappings.len(),
|
||||||
|
1,
|
||||||
|
&operating_slice_costs,
|
||||||
|
);
|
||||||
|
|
||||||
let calculated_overheads = solver.solve(&costs_vec);
|
// Borrow so we don't move between loops
|
||||||
|
let operating_overhead_mappings = &operating_overhead_mappings_mat;
|
||||||
|
let calculated_overheads = &calculated_overheads;
|
||||||
|
|
||||||
// Calculation: operating_overhead_usage . calculated_overheads + initial_totals
|
// Calculation: operating_overhead_usage . calculated_overheads + initial_totals
|
||||||
// Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the
|
// Where operating_overhead_usage is the direct mapping from overhead -> operating department, calculated overheads is the
|
||||||
// solved overheads usages after taking into account usage between departments, and initial_totals is the initial values
|
// solved overheads usages after taking into account usage between departments, and initial_totals is the initial values
|
||||||
// for the operating departments.
|
// for the operating departments.
|
||||||
|
let calculated = operating_overhead_mappings * calculated_overheads + operating_costs_vec;
|
||||||
|
|
||||||
|
let converted_result: Vec<TotalDepartmentCost> = operating_department_mappings
|
||||||
|
.iter()
|
||||||
|
.map(|(department, index)| TotalDepartmentCost {
|
||||||
|
department: department.clone(),
|
||||||
|
value: *calculated.get(*index).unwrap(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
final_account_costs.push(AccountCost {
|
||||||
|
account: total_costs.account,
|
||||||
|
summed_department_costs: converted_result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(final_account_costs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::AccountCost;
|
||||||
|
use crate::DepartmentType;
|
||||||
|
use crate::OverheadAllocationRule;
|
||||||
|
use crate::TotalDepartmentCost;
|
||||||
|
|
||||||
|
use super::reciprocal_allocation_impl;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic() {
|
||||||
|
let allocation_rules = vec![
|
||||||
|
OverheadAllocationRule {
|
||||||
|
from_overhead_department: "Y".to_owned(),
|
||||||
|
to_department: "Z".to_owned(),
|
||||||
|
percent: 0.2,
|
||||||
|
to_department_type: DepartmentType::Overhead,
|
||||||
|
},
|
||||||
|
OverheadAllocationRule {
|
||||||
|
from_overhead_department: "Z".to_owned(),
|
||||||
|
to_department: "Y".to_owned(),
|
||||||
|
percent: 0.3,
|
||||||
|
to_department_type: DepartmentType::Overhead,
|
||||||
|
},
|
||||||
|
OverheadAllocationRule {
|
||||||
|
from_overhead_department: "Y".to_owned(),
|
||||||
|
to_department: "A".to_owned(),
|
||||||
|
percent: 0.4,
|
||||||
|
to_department_type: DepartmentType::Operating,
|
||||||
|
},
|
||||||
|
OverheadAllocationRule {
|
||||||
|
from_overhead_department: "Y".to_owned(),
|
||||||
|
to_department: "B".to_owned(),
|
||||||
|
percent: 0.4,
|
||||||
|
to_department_type: DepartmentType::Operating,
|
||||||
|
},
|
||||||
|
OverheadAllocationRule {
|
||||||
|
from_overhead_department: "Z".to_owned(),
|
||||||
|
to_department: "A".to_owned(),
|
||||||
|
percent: 0.2,
|
||||||
|
to_department_type: DepartmentType::Operating,
|
||||||
|
},
|
||||||
|
OverheadAllocationRule {
|
||||||
|
from_overhead_department: "Z".to_owned(),
|
||||||
|
to_department: "B".to_owned(),
|
||||||
|
percent: 0.5,
|
||||||
|
to_department_type: DepartmentType::Operating,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let initial_totals = vec![AccountCost {
|
||||||
|
account: "Default".to_owned(),
|
||||||
|
summed_department_costs: vec![
|
||||||
|
TotalDepartmentCost {
|
||||||
|
department: "Y".to_owned(),
|
||||||
|
value: 7260.,
|
||||||
|
},
|
||||||
|
TotalDepartmentCost {
|
||||||
|
department: "Z".to_owned(),
|
||||||
|
value: 4000.,
|
||||||
|
},
|
||||||
|
TotalDepartmentCost {
|
||||||
|
department: "A".to_owned(),
|
||||||
|
value: 12000.,
|
||||||
|
},
|
||||||
|
TotalDepartmentCost {
|
||||||
|
department: "B".to_owned(),
|
||||||
|
value: 16000.,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
let expected_final_allocations = vec![AccountCost {
|
||||||
|
account: "Default".to_owned(),
|
||||||
|
summed_department_costs: vec![
|
||||||
|
TotalDepartmentCost {
|
||||||
|
department: "A".to_owned(),
|
||||||
|
value: 16760.,
|
||||||
|
},
|
||||||
|
TotalDepartmentCost {
|
||||||
|
department: "B".to_owned(),
|
||||||
|
value: 22500.,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let result = reciprocal_allocation_impl(allocation_rules, initial_totals).unwrap();
|
||||||
|
assert_eq!(expected_final_allocations, result);
|
||||||
}
|
}
|
||||||
// TODO: return something appropriate
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user