Get reciprocal allocation working
This commit is contained in:
@@ -50,11 +50,13 @@ pub struct OverheadAllocationRule {
|
||||
to_department_type: DepartmentType,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct TotalDepartmentCost {
|
||||
department: String,
|
||||
value: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct AccountCost {
|
||||
account: String,
|
||||
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>(
|
||||
lines: csv::Reader<Lines>,
|
||||
accounts: csv::Reader<Account>,
|
||||
@@ -103,7 +86,9 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
|
||||
areas: csv::Reader<Area>,
|
||||
cost_centres: csv::Reader<CostCentre>,
|
||||
output: csv::Writer<Output>,
|
||||
) where
|
||||
use_numeric_accounts: bool,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
Lines: Read,
|
||||
Account: Read,
|
||||
AllocationStatistic: Read,
|
||||
@@ -111,11 +96,28 @@ pub fn reciprocal_allocation<Lines, Account, AllocationStatistic, Area, CostCent
|
||||
CostCentre: Read,
|
||||
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')
|
||||
|
||||
// do reciprocal allocation (only for variable portion of accounts), for each account
|
||||
|
||||
// Copy across fixed stuff (if necessary, not sure it is)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Perform the reciprocal allocation (matrix) method to allocate servicing departments (indirect) costs
|
||||
@@ -126,10 +128,7 @@ fn reciprocal_allocation_impl(
|
||||
account_costs: Vec<AccountCost>,
|
||||
// TODO: Throw an appropriate error
|
||||
) -> anyhow::Result<Vec<AccountCost>> {
|
||||
let overhead_department_mappings: HashMap<String, usize> =
|
||||
get_rules_indexes(&allocations, DepartmentType::Overhead);
|
||||
let operating_department_mappings: HashMap<String, usize> =
|
||||
get_rules_indexes(&allocations, DepartmentType::Operating);
|
||||
let overhead_department_mappings = get_rules_indexes(&allocations, DepartmentType::Overhead);
|
||||
|
||||
let mut slice_allocations =
|
||||
vec![0.; overhead_department_mappings.len() * overhead_department_mappings.len()];
|
||||
@@ -138,25 +137,23 @@ fn reciprocal_allocation_impl(
|
||||
.iter()
|
||||
.filter(|allocation| allocation.to_department_type == DepartmentType::Overhead)
|
||||
{
|
||||
// TODO: Check if we need to flp this around
|
||||
let from_index = overhead_department_mappings
|
||||
.get(&allocation.from_overhead_department)
|
||||
.unwrap();
|
||||
let to_index = operating_department_mappings
|
||||
let to_index = overhead_department_mappings
|
||||
.get(&allocation.to_department)
|
||||
.unwrap();
|
||||
let elem = &mut slice_allocations
|
||||
[(*from_index) + (overhead_department_mappings.len() * (*to_index))];
|
||||
*elem = allocation.percent;
|
||||
// TODO: Check if dmatrix is row or column major order, would need to flip if column major
|
||||
slice_allocations[from_index * overhead_department_mappings.len() + to_index] =
|
||||
allocation.percent * -1.;
|
||||
}
|
||||
|
||||
// TODO: Also need ones along the diagonal, and negatives in some places...
|
||||
|
||||
let mat: DMatrix<f64> = DMatrix::from_row_slice(
|
||||
let mut mat: DMatrix<f64> = DMatrix::from_vec(
|
||||
overhead_department_mappings.len(),
|
||||
overhead_department_mappings.len(),
|
||||
&slice_allocations,
|
||||
slice_allocations,
|
||||
);
|
||||
mat.fill_diagonal(1.);
|
||||
|
||||
if mat.determinant() == 0. {
|
||||
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>(
|
||||
solver: T,
|
||||
account_costs: Vec<AccountCost>,
|
||||
department_mappings: HashMap<String, usize>,
|
||||
overhead_department_mappings: HashMap<String, usize>,
|
||||
allocations: Vec<OverheadAllocationRule>,
|
||||
) -> 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);
|
||||
let mut operating_overhead_mappings =
|
||||
vec![0.; overhead_department_mappings.len() * operating_department_mappings.len()];
|
||||
for rule in allocations {
|
||||
if rule.to_department_type == DepartmentType::Operating {
|
||||
let from_index = *overhead_department_mappings
|
||||
.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 {
|
||||
let mut slice_costs = vec![0.; department_mappings.len()];
|
||||
// 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 mut operating_slice_costs = vec![0.; operating_department_mappings.len()];
|
||||
for cost in total_costs.summed_department_costs {
|
||||
let elem = &mut slice_costs[*department_mappings.get(&cost.department).unwrap()];
|
||||
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 costs_vec: DMatrix<f64> =
|
||||
DMatrix::from_row_slice(department_mappings.len(), 1, &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
|
||||
// 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
|
||||
// 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