Module 12 · Statistical Computing and Reproducibility Lesson 120 of 120
Reproducible Analysis, Metadata, and Audit Trails
Making a result reproducible without confusing metadata with truth.
Transcript
17 sentences · select one to jump thereCheck your understanding
Does a matching teaching checksum prove the financial analysis is correct?
Code lab
Run it yourself
The lesson source in 7 languages. Edit it, run TypeScript and Python right here, and compare with the expected output.
/**
* Fintech Math Bootcamp · Lesson 120 of 120
* Reproducible Analysis, Metadata, and Audit Trails
* Module 12: Statistical Computing and Reproducibility
*
* Scenario: Making a result reproducible without confusing metadata with truth
* Rule: result + data identity + code version + parameters + information cutoff
*
* Try it: Does a matching teaching checksum prove the financial analysis is correct?
*
* Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
* Free course: https://courses.thefintechbuilder.com
* Synthetic teaching example, not financial advice or a production library.
*/
export function lesson120() {
const record={dataset:"synthetic-five-values",version:"course-1",
unit:"hours",cutoff:"2026-01-01T00:00:00Z",method:"sample-variance"};
const keys=["dataset","version","unit","cutoff","method"];
const canonical=JSON.stringify(Object.fromEntries(Object.entries(record)
.sort(([a],[b])=>a<b?-1:a>b?1:0))); // locale-independent key order
let hash=2166136261;
// Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for ASCII input)
for(let k=0;k<canonical.length;k++){hash=Math.imul(hash^canonical.charCodeAt(k),16777619)>>>0;}
const result={complete:keys.every(k=>k in record),
checksum:hash.toString(16).padStart(8,"0"),canonical};
return result;
}
export const checkedResult = {"complete":true,"checksum":"391bbb44","canonical":"{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"};
// Run this file directly: npx tsx lessons/12-statistical-computing-and-reproducibility/120-reproducible-analysis-metadata-and-audit-trails.ts
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].replace(/\\/g, "/").split("/").pop()!)) {
console.log(JSON.stringify(lesson120(), null, 2));
}
Your output
Press Run to execute the code in your browser.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}# Fintech Math Bootcamp · Lesson 120 of 120
# Reproducible Analysis, Metadata, and Audit Trails
# Module 12: Statistical Computing and Reproducibility
#
# Scenario: Making a result reproducible without confusing metadata with truth
# Rule: result + data identity + code version + parameters + information cutoff
#
# Try it: Does a matching teaching checksum prove the financial analysis is correct?
#
# Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
# Free course: https://courses.thefintechbuilder.com
# Synthetic teaching example, not financial advice or a production library.
#
# Run: python main.py
import json
def lesson_120():
record = {
"dataset": "synthetic-five-values",
"version": "course-1",
"unit": "hours",
"cutoff": "2026-01-01T00:00:00Z",
"method": "sample-variance",
}
keys = ["dataset", "version", "unit", "cutoff", "method"]
# Sorted keys and compact separators reproduce JSON.stringify of the sorted record.
canonical = json.dumps(dict(sorted(record.items())), separators=(",", ":"))
hash_value = 2166136261
# Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for ASCII input)
for ch in canonical:
hash_value = ((hash_value ^ ord(ch)) * 16777619) & 0xFFFFFFFF
return {
"complete": all(k in record for k in keys),
"checksum": format(hash_value, "08x"),
"canonical": canonical,
}
if __name__ == "__main__":
print(json.dumps(lesson_120(), indent=2))
Your output
Press Run to execute the code in your browser.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}/*
* Fintech Math Bootcamp - Lesson 120 of 120
* Reproducible Analysis, Metadata, and Audit Trails
* Module 12: Statistical Computing and Reproducibility
*
* Scenario: Making a result reproducible without confusing metadata with truth
* Rule: result + data identity + code version + parameters + information cutoff
*
* Try it: Does a matching teaching checksum prove the financial analysis is correct?
*
* Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
* Free course: https://courses.thefintechbuilder.com
* Synthetic teaching example, not financial advice or a production library.
*
* Run: javac Main.java && java Main
*/
import java.util.*;
public class Main {
static Map<String, Object> lesson120() {
Map<String, String> record = new LinkedHashMap<String, String>();
record.put("dataset", "synthetic-five-values");
record.put("version", "course-1");
record.put("unit", "hours");
record.put("cutoff", "2026-01-01T00:00:00Z");
record.put("method", "sample-variance");
String[] keys = {"dataset", "version", "unit", "cutoff", "method"};
// TreeMap orders String keys by UTF-16 code units, the same order as JavaScript's < on strings.
StringBuilder canonical = new StringBuilder("{");
for (Map.Entry<String, String> e : new TreeMap<String, String>(record).entrySet()) {
if (canonical.length() > 1) canonical.append(',');
canonical.append(quote(e.getKey())).append(':').append(quote(e.getValue()));
}
canonical.append('}');
int hash = 0x811C9DC5; // 2166136261
// Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for ASCII input)
for (int k = 0; k < canonical.length(); k++) hash = (hash ^ canonical.charAt(k)) * 16777619;
boolean complete = true;
for (String k : keys) complete &= record.containsKey(k);
return obj("complete", complete, "checksum", String.format("%08x", hash), "canonical", canonical.toString());
}
public static void main(String[] args) {
System.out.println(toJson(lesson120(), ""));
}
// Minimal JSON writer: insertion-ordered Map, List, Number, Boolean, String and null.
static String toJson(Object value, String indent) {
if (value == null) return "null";
if (value instanceof String) return quote((String) value);
if (value instanceof Boolean) return value.toString();
if (value instanceof Double) return formatNumber((Double) value);
if (value instanceof Number) return value.toString();
String inner = indent + " ";
StringBuilder sb = new StringBuilder();
if (value instanceof Map) {
Map<?, ?> map = (Map<?, ?>) value;
if (map.isEmpty()) return "{}";
sb.append("{\n");
int i = 0;
for (Map.Entry<?, ?> e : map.entrySet()) {
sb.append(inner).append(quote(e.getKey().toString())).append(": ").append(toJson(e.getValue(), inner));
sb.append(++i < map.size() ? ",\n" : "\n");
}
return sb.append(indent).append("}").toString();
}
List<?> list = (List<?>) value;
if (list.isEmpty()) return "[]";
sb.append("[\n");
for (int i = 0; i < list.size(); i++) {
sb.append(inner).append(toJson(list.get(i), inner)).append(i + 1 < list.size() ? ",\n" : "\n");
}
return sb.append(indent).append("]").toString();
}
// Integral doubles print without ".0", as JavaScript does; others use Java's round-trip form.
static String formatNumber(double x) {
if (x == Math.rint(x) && Math.abs(x) < 1e15) return Long.toString((long) x);
return Double.toString(x);
}
static String quote(String s) {
StringBuilder sb = new StringBuilder("\"");
for (char c : s.toCharArray()) {
if (c == '"' || c == '\\') sb.append('\\').append(c);
else if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
else sb.append(c);
}
return sb.append('"').toString();
}
// Builds an insertion-ordered object from alternating keys and values.
static Map<String, Object> obj(Object... keysAndValues) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
for (int i = 0; i < keysAndValues.length; i += 2) map.put((String) keysAndValues[i], keysAndValues[i + 1]);
return map;
}
static List<Object> list(double... values) {
List<Object> out = new ArrayList<Object>();
for (double v : values) out.add(v);
return out;
}
}
No browser runner for Java yet
Read the code here, then run it in your own toolchain or a ready-made cloud workspace.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}// Fintech Math Bootcamp · Lesson 120 of 120
// Reproducible Analysis, Metadata, and Audit Trails
// Module 12: Statistical Computing and Reproducibility
//
// Scenario: Making a result reproducible without confusing metadata with truth
// Rule: result + data identity + code version + parameters + information cutoff
//
// Try it: Does a matching teaching checksum prove the financial analysis is correct?
//
// Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
// Free course: https://courses.thefintechbuilder.com
// Synthetic teaching example, not financial advice or a production library.
//
// Run: go run main.go
package main
import (
"encoding/json"
"fmt"
"sort"
"unicode/utf16"
)
type Result struct {
Complete bool `json:"complete"`
Checksum string `json:"checksum"`
Canonical string `json:"canonical"`
}
func lesson120() Result {
record := map[string]string{
"dataset": "synthetic-five-values",
"version": "course-1",
"unit": "hours",
"cutoff": "2026-01-01T00:00:00Z",
"method": "sample-variance",
}
keys := []string{"dataset", "version", "unit", "cutoff", "method"}
names := make([]string, 0, len(record))
for k := range record {
names = append(names, k)
}
sort.Strings(names) // byte order equals code-unit order for these ASCII keys
canonical := "{"
for i, k := range names {
if i > 0 {
canonical += ","
}
key, _ := json.Marshal(k)
value, _ := json.Marshal(record[k])
canonical += string(key) + ":" + string(value)
}
canonical += "}"
hash := uint32(2166136261)
// Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for ASCII input)
for _, unit := range utf16.Encode([]rune(canonical)) {
hash = (hash ^ uint32(unit)) * 16777619
}
complete := true
for _, k := range keys {
if _, ok := record[k]; !ok {
complete = false
}
}
return Result{Complete: complete, Checksum: fmt.Sprintf("%08x", hash), Canonical: canonical}
}
func main() {
out, err := json.MarshalIndent(lesson120(), "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
No browser runner for Go yet
Read the code here, then run it in your own toolchain or a ready-made cloud workspace.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}// Fintech Math Bootcamp · Lesson 120 of 120
// Reproducible Analysis, Metadata, and Audit Trails
// Module 12: Statistical Computing and Reproducibility
//
// Scenario: Making a result reproducible without confusing metadata with truth
// Rule: result + data identity + code version + parameters + information cutoff
//
// Try it: Does a matching teaching checksum prove the financial analysis is correct?
//
// Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
// Free course: https://courses.thefintechbuilder.com
// Synthetic teaching example, not financial advice or a production library.
//
// Run: g++ -std=c++17 -O1 -o lesson main.cpp && ./lesson
#include <algorithm>
#include <charconv>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <deque>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <variant>
#include <vector>
// Minimal JSON value: objects keep insertion order; numbers print in shortest round-trip form.
struct Json {
enum class Kind { Null, Bool, Number, String, Array, Object };
Kind kind = Kind::Null;
bool flag = false;
double number = 0.0;
std::string text;
std::vector<std::string> keys; // object keys, parallel to items
std::vector<Json> items; // array items, or object values
};
Json jnull() { return Json{}; }
Json jbool(bool value) { Json j; j.kind = Json::Kind::Bool; j.flag = value; return j; }
Json jnum(double value) { Json j; j.kind = Json::Kind::Number; j.number = value; return j; }
Json jstr(const std::string& value) { Json j; j.kind = Json::Kind::String; j.text = value; return j; }
Json jarr(std::vector<Json> items) { Json j; j.kind = Json::Kind::Array; j.items = std::move(items); return j; }
Json jobj(std::vector<std::pair<std::string, Json>> fields) {
Json j;
j.kind = Json::Kind::Object;
for (auto& field : fields) {
j.keys.push_back(field.first);
j.items.push_back(std::move(field.second));
}
return j;
}
Json jnums(const std::vector<double>& values) {
std::vector<Json> items;
for (double v : values) items.push_back(jnum(v));
return jarr(std::move(items));
}
Json jnums(const std::vector<std::optional<double>>& values) {
std::vector<Json> items;
for (const auto& v : values) items.push_back(v ? jnum(*v) : jnull());
return jarr(std::move(items));
}
std::string formatNumber(double value) {
char buffer[64];
auto result = std::to_chars(buffer, buffer + sizeof buffer, value); // shortest round-trip form
return std::string(buffer, result.ptr);
}
std::string quote(const std::string& text) {
std::string out = "\"";
for (char c : text) {
if (c == '"' || c == '\\') {
out += '\\';
out += c;
} else if (static_cast<unsigned char>(c) < 0x20) {
char buffer[8];
std::snprintf(buffer, sizeof buffer, "\\u%04x", static_cast<unsigned>(c));
out += buffer;
} else {
out += c;
}
}
return out + "\"";
}
void writeJson(std::ostream& out, const Json& value, const std::string& indent = "") {
const std::string inner = indent + " ";
switch (value.kind) {
case Json::Kind::Null: out << "null"; return;
case Json::Kind::Bool: out << (value.flag ? "true" : "false"); return;
case Json::Kind::Number: out << formatNumber(value.number); return;
case Json::Kind::String: out << quote(value.text); return;
case Json::Kind::Array:
case Json::Kind::Object: {
const bool isObject = value.kind == Json::Kind::Object;
if (value.items.empty()) {
out << (isObject ? "{}" : "[]");
return;
}
out << (isObject ? "{\n" : "[\n");
for (std::size_t i = 0; i < value.items.size(); ++i) {
out << inner;
if (isObject) out << quote(value.keys[i]) << ": ";
writeJson(out, value.items[i], inner);
out << (i + 1 < value.items.size() ? ",\n" : "\n");
}
out << indent << (isObject ? "}" : "]");
}
}
}
Json lesson120() {
const std::map<std::string, std::string> record{ // std::map keeps keys sorted
{"dataset", "synthetic-five-values"},
{"version", "course-1"},
{"unit", "hours"},
{"cutoff", "2026-01-01T00:00:00Z"},
{"method", "sample-variance"}};
const std::vector<std::string> keys{"dataset", "version", "unit", "cutoff", "method"};
std::string canonical = "{";
for (const auto& [key, value] : record) {
if (canonical.size() > 1) canonical += ",";
canonical += quote(key) + ":" + quote(value);
}
canonical += "}";
std::uint32_t hash = 2166136261u;
// Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for this ASCII input)
for (unsigned char c : canonical) hash = static_cast<std::uint32_t>((hash ^ c) * 16777619u);
const bool complete = std::all_of(keys.begin(), keys.end(), [&](const std::string& k) { return record.count(k) > 0; });
char checksum[9];
std::snprintf(checksum, sizeof checksum, "%08x", static_cast<unsigned>(hash));
return jobj({{"complete", jbool(complete)}, {"checksum", jstr(checksum)}, {"canonical", jstr(canonical)}});
}
int main() {
writeJson(std::cout, lesson120());
std::cout << "\n";
return 0;
}
No browser runner for C++ yet
Read the code here, then run it in your own toolchain or a ready-made cloud workspace.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}// Fintech Math Bootcamp · Lesson 120 of 120
// Reproducible Analysis, Metadata, and Audit Trails
// Module 12: Statistical Computing and Reproducibility
//
// Scenario: Making a result reproducible without confusing metadata with truth
// Rule: result + data identity + code version + parameters + information cutoff
//
// Try it: Does a matching teaching checksum prove the financial analysis is correct?
//
// Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
// Free course: https://courses.thefintechbuilder.com
// Synthetic teaching example, not financial advice or a production library.
//
// Run: rustc -O main.rs && ./main
#![allow(dead_code)]
use std::collections::BTreeMap;
/// Minimal JSON value; objects keep insertion order.
enum Json {
Null,
Bool(bool),
Num(f64),
Str(String),
Arr(Vec<Json>),
Obj(Vec<(String, Json)>),
}
fn obj(fields: Vec<(&str, Json)>) -> Json {
Json::Obj(fields.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
}
fn nums(values: &[f64]) -> Json {
Json::Arr(values.iter().map(|&v| Json::Num(v)).collect())
}
fn optional_nums(values: &[Option<f64>]) -> Json {
Json::Arr(values.iter().map(|v| v.map_or(Json::Null, Json::Num)).collect())
}
fn quote(text: &str) -> String {
let mut out = String::from("\"");
for c in text.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
// Display for f64 prints the shortest string that round-trips, as JavaScript does.
fn write_json(value: &Json, indent: &str, out: &mut String) {
let inner = format!("{} ", indent);
match value {
Json::Null => out.push_str("null"),
Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Json::Num(n) => out.push_str(&format!("{}", n)),
Json::Str(s) => out.push_str("e(s)),
Json::Arr(items) if items.is_empty() => out.push_str("[]"),
Json::Obj(fields) if fields.is_empty() => out.push_str("{}"),
Json::Arr(items) => {
out.push_str("[\n");
for (i, item) in items.iter().enumerate() {
out.push_str(&inner);
write_json(item, &inner, out);
out.push_str(if i + 1 < items.len() { ",\n" } else { "\n" });
}
out.push_str(indent);
out.push(']');
}
Json::Obj(fields) => {
out.push_str("{\n");
for (i, (key, item)) in fields.iter().enumerate() {
out.push_str(&inner);
out.push_str("e(key));
out.push_str(": ");
write_json(item, &inner, out);
out.push_str(if i + 1 < fields.len() { ",\n" } else { "\n" });
}
out.push_str(indent);
out.push('}');
}
}
}
fn lesson_120() -> Json {
// BTreeMap keeps keys sorted; byte order equals code-unit order for these ASCII keys.
let record: BTreeMap<&str, &str> = [
("dataset", "synthetic-five-values"),
("version", "course-1"),
("unit", "hours"),
("cutoff", "2026-01-01T00:00:00Z"),
("method", "sample-variance"),
]
.into_iter()
.collect();
let keys = ["dataset", "version", "unit", "cutoff", "method"];
let fields: Vec<String> = record.iter().map(|(k, v)| format!("{}:{}", quote(k), quote(v))).collect();
let canonical = format!("{{{}}}", fields.join(","));
let mut hash: u32 = 2166136261;
// Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for ASCII input)
for unit in canonical.encode_utf16() {
hash = (hash ^ unit as u32).wrapping_mul(16777619);
}
obj(vec![
("complete", Json::Bool(keys.iter().all(|k| record.contains_key(k)))),
("checksum", Json::Str(format!("{:08x}", hash))),
("canonical", Json::Str(canonical)),
])
}
fn main() {
let mut out = String::new();
write_json(&lesson_120(), "", &mut out);
println!("{}", out);
}
No browser runner for Rust yet
Read the code here, then run it in your own toolchain or a ready-made cloud workspace.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}// Fintech Math Bootcamp · Lesson 120 of 120
// Reproducible Analysis, Metadata, and Audit Trails
// Module 12: Statistical Computing and Reproducibility
//
// Scenario: Making a result reproducible without confusing metadata with truth
// Rule: result + data identity + code version + parameters + information cutoff
//
// Try it: Does a matching teaching checksum prove the financial analysis is correct?
//
// Lesson article: https://thefintechbuilder.com/financial-mathematics-statistics-and-data-foundations/statistical-computing-and-reproducibility/reproducible-analysis-metadata-and-audit-trails/
// Free course: https://courses.thefintechbuilder.com
// Synthetic teaching example, not financial advice or a production library.
//
// Run: dotnet run
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
var options = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
Console.WriteLine(JsonSerializer.Serialize(Lesson120(), options));
static object Lesson120()
{
var record = new Dictionary<string, string>
{
["dataset"] = "synthetic-five-values",
["version"] = "course-1",
["unit"] = "hours",
["cutoff"] = "2026-01-01T00:00:00Z",
["method"] = "sample-variance",
};
string[] keys = { "dataset", "version", "unit", "cutoff", "method" };
static string Quote(string s) => "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
// Ordinal comparison sorts by UTF-16 code units, like JavaScript's < on strings.
string canonical = "{" + string.Join(",", record.OrderBy(p => p.Key, StringComparer.Ordinal)
.Select(p => Quote(p.Key) + ":" + Quote(p.Value))) + "}";
uint hash = 2166136261;
// Teaching FNV-1a over UTF-16 code units (equals byte-wise FNV-1a for ASCII input)
foreach (char c in canonical) hash = unchecked((hash ^ c) * 16777619u);
return new { complete = keys.All(record.ContainsKey), checksum = hash.ToString("x8"), canonical };
}
No browser runner for C# yet
Read the code here, then run it in your own toolchain or a ready-made cloud workspace.
Expected output
{
"complete": true,
"checksum": "391bbb44",
"canonical": "{\"cutoff\":\"2026-01-01T00:00:00Z\",\"dataset\":\"synthetic-five-values\",\"method\":\"sample-variance\",\"unit\":\"hours\",\"version\":\"course-1\"}"
}Prefer your own machine? Every file is in the course repository · open it in Codespaces.
Lesson notes
The rule
result + data identity + code version + parameters + information cutoff