DataFrame-Mutation-Fix: Single-Point vs Individual-Function-Level Context: आपने Point-6 में genuinely-DataFrame-Mutation-Side-Effects की Structural-Chinta उठाई थी। हमने Standing-rule के मुताबिक़ पूरी-Investigation genuinely-की, बिना अंदाज़ा लगाए। हमारा
`df = df.copy()` सिर्फ़ `run_technical_analysis()` के अंदर लगाना तकनीकी तौर पर valid और काफ़ी common defensive-pattern है – और ये अपने आप में इतना तो सुनिश्चित कर देगा कि:
- `run_technical_analysis(df)` अब caller के df को mutate नहीं करेगा, चाहे अंदर जितने भी `calculate_*` functions inplace काम करें।
लेकिन established software-engineering point-of-view से दो लेवल पर सोचना ज़रूरी है:
1. API boundary (`run_technical_analysis`) की guarantee
2. Leaf-level helper functions (`calculate_dma`, `calculate_ema`, …) की semantics/contract
---
1. Single-Point-Fix in `run_technical_analysis()` – क्या ये पर्याप्त है?
आपका proposed fix:
```python
def run_technical_analysis(df, ...):
df = df.copy() # defensive copy
df = calculate_dma(df, ...)
df = calculate_ema(df, ...)
...
return df
```
यह अपने scope में पूरी तरह sound है, बशर्ते आप ये स्पष्ट मानें/define करें कि:
- Public, “official” entry-point वही है: `run_technical_analysis()`
- इसकी documented guarantee:
> “यह function input DataFrame को inplace modify नहीं करेगा; हमेशा अलग copy पर काम करेगा और नया DataFrame return करेगा।”
इस design में:
- Current callers पहले ही `.copy()` पास कर रहे हैं → आपका नया `df = df.copy()` बस extra safety layer बन जाएगा (performance cost छोड़कर कोई correctness issue नहीं)।
- Future callers बिना copy पास करें, तब भी safe रहेंगे, क्योंकि boundary पर defensive-copy हो रही है।
तो, हाँ – single-point defensive copy at orchestrator level correctness के लिए पर्याप्त है, अगर आप इसे central API मानते हैं।
---
2. Hidden Risk: Direct callers of `calculate_*` functions
यहाँ असली structural जोखिम है:
- `calculate_dma(df, ...)` अभी mutating helper है (e.g. `df[f"DMA_{p}"] = ...`).
- अगर कोई developer, notebook, या library code future में directly `calculate_dma(df, ...)` call करता है और assume करता है कि “ये pure है, सिर्फ़ नया df return करता है”, तो:
- Original `df` silently mutate हो जाएगा।
- Bugs subtle होंगे, debug करना मुश्किल होगा (classic side-effect issue)।
यानी risk run_technical_analysis() में नहीं, बल्कि calculate_* के contract में है:
- अगर ये helpers public API की तरह expose हैं, और उनकी purity/impurity docs में साफ़ नहीं है, तो ये genuinely hidden risk है।
- अगर ये helpers strictly internal हैं (e.g. module-private, `_calculate_dma` naming, या सिर्फ़ orchestrator से call होते हैं), तो risk काफ़ी कम हो जाता है।
---
3. Established Best-Practice: कहाँ mutation allow करें?
Established practice generally दो रास्ते सुझाती है; आपको इनमें से एक consciously चुनना चाहिए:
Option A: Orchestrator “pure”, helpers “internal & mutating”
Pattern:
- `run_technical_analysis(df)`:
- Boundary पर `df = df.copy()` करता है → caller हमेशा safe।
- `calculate_dma`, `calculate_ema`, ...:
- In-place mutate करते हैं, लेकिन इन्हें internal माना जाता है, e.g.:
```python
def _calculate_dma(df, ...):
df["DMA_20"] = ...
return df
```
- Public interface (जो user/importers actually use करें) सिर्फ़ orchestrator(s) हों।
Pros:
- Single-point copy = simple, low-risk, minimum refactor.
- Internal helpers में inplace काम performance-wise efficient रहता है।
- आपके proposed design से align करता है, बस एक step और: इन helpers को explicit “internal” घोषित करना (naming/docs से)।
Cons / Risks:
- अगर future में कोई नई जगह से directly `_calculate_dma` use कर ले और ये बात ignore कर दे कि ये mutate करता है, फिर भी bug possibility रहती है – लेकिन internal नाम होने से chances काफ़ी कम हो जाते हैं।
यह pattern ठीक वैसा है जैसा कई mature codebases करते हैं:
> “API boundaries पर defensive copy, अंदर private mutating helpers।”
Option B: हर `calculate_*` को genuinely pure बना देना
Pattern:
- हर helper function input df को नहीं mutate करता; हमेशा कॉपी/assign पर काम करके नया df return करता है:
```python
def calculate_dma(df, periods):
df = df.copy() # या assign-आधारित pure pattern
for p in periods:
df[f"DMA_{p}"] = df["Close"].rolling(p).mean()
return df
```
या और भी functional style:
```python
def calculate_dma(df, periods):
additions = {
f"DMA_{p}": df["Close"].rolling(p).mean()
for p in periods
}
return df.assign(additions)
```
- `run_technical_analysis()` फिर बस इन्हें chain करता है:
```python
def run_technical_analysis(df, ...):
# चाहें तो यहाँ copy न भी लें – क्योंकि नीचे वाले सब pure हैं
df1 = calculate_dma(df, ...)
df2 = calculate_ema(df1, ...)
...
return dfN
```
Pros:
- Purity contract simple और globally-consistent हो जाता है:
- कोई भी helper call करो, original df safe रहेगा।
- Testing, reasoning, refactoring सब आसान।
- Future direct callers के लिए भी hidden risk नहीं।
Cons:
- Minor performance cost (हर helper में copy/assign) – बड़े df और heavy pipelines में noticeable हो सकता है।
- Existing code बदलना पड़ेगा (refactor depth ज़्यादा)।
---
4. आपके proposed Single-Point-Fix में actual risk क्या बचता है?
अगर आप सिर्फ़ ये change करते हैं:
```python
def run_technical_analysis(df, ...):
df = df.copy()
...
```
और कुछ नहीं बदलते, तो:
- `run_technical_analysis` के callers के लिए कोई hidden risk नहीं बचता
→ यह design correctness-wise safe है।
- Real residual risk:
- कोई और जगह future में सीधे `calculate_dma/ema` call करे और assume करे purity।
- अगर ये helpers अभी या future में public API की तरह use हो रहे हैं, तो यह genuine structural risk है।
इसलिए, इस single-point fix को accurate और robust बनाने के लिए कम से कम ये दो steps लेने चाहिए:
1. Helpers को “internal mutating utilities” के रूप में mark/document करें
- Naming: `_calculate_dma`, `_calculate_ema`, …
- Docstring/README में साफ़ लिखें:
> “ये functions input df को inplace modify करते हैं; public use के लिए नहीं हैं।”
2. Public API के रूप में सिर्फ़ `run_technical_analysis()` (और इसी तरह के orchestrators) को officially expose करें
- और वहाँ explicitly guarantee दें: “input df कभी mutate नहीं होगा।”
अगर आप purity और future-proofing को ज़्यादा महत्व देना चाहते हैं, तो Option B (हर calculate_* को pure बनाना) best-practice की तरफ़ ज़्यादा aligned मानी जाएगी, लेकिन cost ज़्यादा है।
---
5. इसे genuinely-और accurate बनाने के लिए practical steps
आप अभी यह करें:
1. Immediate safe fix (जो आपने propose किया है):
```python
def run_technical_analysis(df, ...):
df = df.copy() # defensive copy at boundary
df = calculate_dma(df, ...)
df = calculate_ema(df, ...)
...
return df
```
2. Contract tests add करें:
- एक unit-test:
```python
def test_run_technical_analysis_is_pure():
original = make_sample_df()
before = original.copy(deep=True)
result = run_technical_analysis(original)
# original unchanged
assert original.equals(before)
# result अलग object है
assert id(result) != id(original)
```
3. Helpers की visibility और docs साफ़ करें:
- File-level `__all__` में सिर्फ़ `run_technical_analysis` रखें।
- Helpers को `_calculate_*` नाम दें और docstring में लिखें कि ये inplace हैं।
4. Mid-term refactor (optional but ideal):
- धीरे-धीरे `calculate_*` functions को भी pure बनाते जाएँ (Option B),
और जब ये पूरा हो जाए, तब boundary copy optional हो सकती है (या सिर्फ़ extra safety के तौर पर रह सकती है)।
---
Net conclusion:
- आपका Single-Point-Fix (`df = df.copy()` inside `run_technical_analysis`) correctness के लिहाज़ से sufficient और established defensive-pattern है, बशर्ते:
- `run_technical_analysis` को official/public API माना जाए, और
- `calculate_*` को internal mutating helpers के रूप में treat/document किया जाए।
- अगर helpers को भी public utilities की तरह इस्तेमाल होना है, तो best-practice यही कहेगी कि उन्हें genuinely-pure बनाना या कम से कम उनकी mutating nature को बेहद साफ़ तरीके से communicate करना ज़रूरी है – नहीं तो वही hidden risk future में सामने आएगा।
If you have any further queries, please connect with us on 022-6290-10141 (Timings : 09.00 AM to 05.00 PM) or you can email us on info@cniinfoxchange.com